原生能力入口
MiniApp 常用原生能力入口,不是完整模块总表。
主文档已合并:入口模型、Python 模块表、AppUI 原生组件入口表与侧边栏导航均由 schema 生成,请优先阅读 **iOS 原生能力**。本文仅保留历史深链,不再维护独立能力表。
MiniApp 场景下的原生能力配方:按任务选模块、组合调用、状态写回界面。
边界:这是 MiniApp 配方索引,不是可import的模块。完整 API 以各模块文档为准;系统调用放在按钮回调,不要写在body()里。
#模块概览
| 项 | 说明 |
|---|---|
| 导入 | 按场景 import 对应模块(如 photos、storage) |
| 适合做什么 | 选图、定位、通知、持久化、网络列表等 MiniApp 常见流 |
| 调用时机 | 按钮、刷新、选择器触发;body() 只读 State |
| 数据分工 | 设置 storage、记录 database、密钥 keychain、ZIP/IPA 用 archive |
| 反馈 | 成功/取消/失败都写回 State 或 HUD |
#快速开始
保存主题并触发触觉反馈:
import haptics
import storage
storage.set("demo.theme", "Dark")
if haptics.is_supported():
haptics.notification("success")
print("已保存:", storage.get("demo.theme"))
#每个 MiniApp 的能力声明
新建或准备分发的 MiniApp 应在 miniapp.json 声明最小能力。字段缺失表示旧应用兼容 profile;字段存在且为 [] 表示明确不允许任何受控能力。已有 MiniApp 不需要改代码,新能力只做增量声明。
下面的字段、能力名和外部输入选择器直接来自 MiniApp contract;不要自行改成蛇形命名。
{
"name": "External Input Tool",
"entry": "main.py",
"runtime": "appui",
"capabilities": [
"network",
"file_system"
],
"allowedNetworkHosts": [
"api.example.com",
"*.cdn.example.com"
],
"externalInputs": {
"kinds": [
"file",
"url"
],
"fileExtensions": [
"ipa"
],
"urlSchemes": [
"https"
],
"urlHosts": [
"apps.apple.com"
],
"acceptsMultiple": true,
"acceptsMixedKinds": false
},
"intentEntry": "handle_external.py"
}
#Manifest 字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
capabilities | array<string> | omitted | 否 | 最小原生能力集合;缺失时使用旧应用兼容 profile,空数组表示不允许受控能力。 |
allowedNetworkHosts | array<string> | 否 | 声明式 MiniApp 可访问的 HTTPS/WSS 主机;*.example.com 仅匹配子域;最多 128 条且 UTF-8 总计不超过 32768 字节。 |
externalInputs | object | 否 | 声明此 MiniApp 可从系统分享页接收的内容;缺失或没有任何选择器时不会出现在可运行 MiniApp 列表。 |
intentEntry | string | 否 | 处理外部启动请求的可选 Python 入口;缺失时由常规 entry 接收。 |
#合法能力名
installed_apps、external_navigation、network、media_preview、media、haptics、clipboard_read、clipboard_write、camera、device_info、storage、shared_storage、file_system、photos、health、contacts、location、motion、bluetooth、nfc、microphone、notifications、calendar、alarms、biometric、unsafe_native
#externalInputs 字段
可用 kinds:file、url、text、image、audio、video、data。
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
kinds | array<externalInputKind> | [] | 允许的输入类别;file 可匹配除 URL 外的文件型内容。 |
contentTypes | array<string> | [] | 允许的 Uniform Type Identifier,例如 public.image。 |
fileExtensions | array<string> | [] | 允许的文件扩展名,不区分大小写,可写 ipa 或 .ipa。 |
urlSchemes | array<string> | [] | 允许的 URL scheme,例如 https。 |
urlHosts | array<string> | [] | 允许的 URL 主机;*.example.com 仅匹配子域。 |
urlPathPrefixes | array<string> | [] | 允许的 URL 路径前缀。 |
acceptsMultiple | boolean | false | 是否允许一次接收多个项目。 |
acceptsMixedKinds | boolean | false | 多项目时是否允许混合不同输入类别。 |
外部启动时用 miniapp_runtime.launch_request() 读取当前运行的只读请求;普通启动返回 None。 LaunchItem.open() 只接受只读模式,授权路径会在该次 MiniApp 运行结束时失效。读取这一条 host-issued 输入不要求 file_system;写输出或访问其他普通路径仍按模块能力规则检查。
#模块能力要求
| 模块 | 必须全部声明 | 至少声明一个 | 本次外部输入只读例外 |
|---|---|---|---|
_ctypes | unsafe_native | - | - |
alarm | alarms | - | - |
archive | file_system | - | 可读取准确的 LaunchItem.path |
audio_recorder | microphone | - | - |
background_download | network、file_system | - | 可读取准确的 LaunchItem.path |
biometric | biometric | - | - |
ble_peripheral | bluetooth | - | - |
bluetooth | bluetooth | - | - |
calendar_events | calendar | - | - |
camera | camera | - | - |
clipboard | - | clipboard_read、clipboard_write | - |
contacts | contacts | - | - |
ctypes | unsafe_native | - | - |
database | storage | - | - |
file_picker | file_system | - | - |
health | health | - | - |
http_server | network、file_system | - | - |
keychain | storage | - | - |
location | location | - | - |
mail | external_navigation | - | - |
message | external_navigation | - | - |
motion | motion | - | - |
music | media | - | - |
music_player | media | - | - |
native_image | file_system | - | 可读取准确的 LaunchItem.path |
network | network | - | - |
nfc | nfc | - | - |
notification | notifications | - | - |
photos | - | photos、camera | - |
shazam | microphone、media | - | - |
shortcuts | external_navigation | - | - |
speech | microphone | - | - |
speech_recognition | microphone | - | - |
ssh | network | - | - |
storage | storage | - | - |
video_recorder | camera、microphone | - | - |
vision | - | photos、camera | - |
vision_helper | - | photos、camera | - |
weather | network | - | - |
websocket | network | - | - |
#分享页如何选择 MiniApp
系统分享页不会在多个候选中“随便打开第一个”。它会用每个已安装 MiniApp 的 externalInputs 对本次全部输入做匹配,并把所有匹配项显示为 MiniApp 卡片;用户点了哪一张卡片,请求就只绑定到哪一个 MiniApp。没有匹配项时仍可按普通导入/预览处理。
匹配规则是确定的:
kinds未填写时由更具体的类型、扩展名或 URL 选择器决定;file可接收除 URL 以外的文件型输入。- 对文件输入,
contentTypes使用 UTI 一致性/继承关系匹配,和fileExtensions之间是“任一匹配”;图片、音频和视频会按实际文件类型识别,不按分享传输层的泛型public.file-url误判。 - 对 URL 输入,已填写的
urlSchemes、urlHosts、urlPathPrefixes必须同时满足;*.example.com不包含裸域example.com。 - 每个输入都必须匹配;多项目还需
acceptsMultiple=true,混合类别还需acceptsMixedKinds=true。
分享扩展只负责暂存输入和记录用户选择。主 App 打开后会再次校验目标 MiniApp 仍存在、工作区没有切换、运行时支持外部请求且当前声明仍匹配;如果安装状态或声明已经变化,输入会安全回退为普通导入,不会误交给另一个 MiniApp,也不会因为目标失效直接丢失。
#miniapp_runtime 启动请求
普通启动时 launch_request() 返回 None;外部启动时返回非空、只读的 LaunchRequest。如果当前业务必须由外部输入启动,可使用 launch_request(required=True),缺少请求时会抛 LaunchRequestError。
import miniapp_runtime
request = miniapp_runtime.launch_request(required=True)
item = request.first
print(request.id, request.source, request.target_miniapp_id)
print(item.kind, item.name, item.content_type, item.size)
if item.url:
print("原始 URL:", item.url)
else:
with item.open("rb") as stream:
header = stream.read(64)
print(header)
| 对象 | 字段/方法 | 说明 |
|---|---|---|
LaunchRequest | id / source / created_at / target_miniapp_id | 本次请求、来源、创建时间与已选目标;对象不可修改。 |
LaunchRequest | items / first | 非空输入元组与第一项。 |
LaunchRequest | files / urls | 本次受管文件路径与原始 URL 的只读元组。 |
LaunchItem | id / kind / name / content_type / size | 输入的稳定元数据。 |
LaunchItem | path / url | 宿主暂存路径;URL 输入还提供规范化原始 URL。 |
LaunchItem | open("rb") / open("r") / open("rt") | 只读打开;写入、追加和更新模式会被拒绝。 |
LaunchItem | read_bytes(max_bytes=...) / read_text(...) | 有明确大小上限的便捷读取;大文件应使用 open() 流式处理。 |
| 模块函数 | has_launch_request() | 判断本次运行是否带外部请求。 |
intentEntry 只是在外部请求存在时选择另一份 Python 入口文件;请求内容仍统一从 miniapp_runtime 读取。未填写、文件名为空或普通启动时继续运行原 entry,因此不会改变旧 MiniApp 的启动写法。请求授权与当前运行票据绑定:运行结束后不要保存 LaunchItem.path 供下次使用;需要跨运行继续使用时,应在当前授权有效期间导入工作区,或交给会立即接管文件的宿主持久任务(例如 background_download.upload())。
#Server MiniApp 启动就绪握手
这组 API 只用于自己启动 HTTP 监听器的 Server MiniApp;AppUI、console、scene、ui 和 widget 不需要调用。旧 Server MiniApp 仍保留兼容启动路径,新代码建议在正常路由之前响应宿主为本次运行生成的健康请求,让宿主确认“当前端口确实属于这一代 Python 运行”,而不是误连上一次遗留的监听器。
import miniapp_runtime
def handle_http_request(path, headers):
if miniapp_runtime.is_health_request(path, headers):
# 返回 (JSON 字符串, 200, no-store + 本次运行 token header)
return miniapp_runtime.health_response()
return route_application_request(path, headers)
| API | 语义 |
|---|---|
health_config() | 返回只读配置:enabled、protocol、path、header、token。仅受管 Server 运行会启用。 |
is_health_request(path, headers=None) | 判断请求路径以及可用时的 token header 是否属于本次运行。框架能提供 header 时应一并传入。 |
health_payload() | 返回宿主期望的 JSON 可序列化字典。未启用时明确失败。 |
health_headers() | 返回 no-store、JSON content type 与本次 token header。 |
health_response() | 返回适合 Flask 类接口的 (body, status, headers) 三元组。 |
健康 token 只用于本次启动证明,不是业务 API 密钥;不要打印、持久化、返回给普通业务路由,也不要自己固定 path/header/token。运行结束后这些值失效。
其中 storage 只允许当前 MiniApp 的私有数据;使用 storage.shared 还需显式声明 shared_storage。
宿主持久任务与普通运行资源是两类生命周期:Timer、普通网络请求、传感器和订阅在当前 session 停止时取消;后台文件传输由宿主任务中心继续持有。用 miniapp_runtime.durable_tasks() / durable_task() 查询自己的任务;provider="background_transfer" 标识当前 provider,记录中的 origin_session_id 指向创建运行。一旦换成新运行,origin_run_state 明确为 interrupted,表示旧 Python 调用栈没有恢复。传输的暂停、恢复、重试和取消使用 background_download。
声明式 profile 的网络规则:
network只表示允许发起网络操作,还必须匹配allowedNetworkHosts。- 只允许 HTTPS / WSS;
*.example.com匹配子域,不匹配根域example.com。 WebView.allowed_hosts可以缩小 manifest 范围,不能扩大;Swift 会把 Python node/IR 当作不可信输入,用 native lease 中不可变的 manifest 重新求有效 host 集合,无效或越权输入 fail closed。导航、HTTPS/WSS 子资源、Cookie 与网站数据共用该集合,FTP/FTPS/自定义 hierarchical network scheme 默认阻断。- 导入受控模块、AppUI 系统组件和真正调用原生能力时都会经过同一策略并留下审计记录。
- 每次运行有独立 session 与 generation;停止时会自动取消计时器、网络、WebSocket、传感器和订阅资源,并拒绝旧会话迟到事件。
权限声明不是系统授权的替代品:先通过 MiniApp 自身能力策略后,iOS 仍会在需要时显示系统权限界面。用户拒绝、设备不支持和主动取消都必须有可见失败路径。
#AppUI 示例
设备信息、快照、通知组合;所有原生调用在按钮回调里。
import appui
import device
import haptics
import notification
import storage
SNAPSHOT_KEY = "native.snapshot"
REMINDER_ID = "native.snapshot.reminder"
state = appui.State(
message="就绪",
rows=[
{"id": "model", "title": "型号", "value": "点击刷新"},
{"id": "battery", "title": "电量", "value": "—"},
],
)
def row_key(row):
return row["id"]
def row_view(row):
return appui.LabeledContent(row["title"], value=row["value"])
def refresh_device_info():
state.rows = [
{"id": "model", "title": "型号", "value": device.model()},
{"id": "battery", "title": "电量", "value": f"{device.battery_level():.0%}"},
]
state.message = "设备信息已刷新"
def save_snapshot():
storage.set_json(SNAPSHOT_KEY, state.rows)
if haptics.is_supported():
haptics.notification("success")
state.message = "快照已保存"
def remind_later():
perm = notification.request_permission()
if not perm.get("granted"):
state.message = "未获得通知权限"
return
result = notification.schedule(
REMINDER_ID,
"原生快照",
"查看已保存的设备快照",
delay=60,
)
state.message = f"已调度提醒: {result}"
def body():
return appui.NavigationStack(
appui.List([
appui.Section("设备", [
appui.ForEach(state.rows, row_builder=row_view, key=row_key),
]),
appui.Section("操作", [
appui.Button("刷新设备", action=refresh_device_info),
appui.Button("保存快照", action=save_snapshot)
.button_style("bordered_prominent"),
appui.Button("1 分钟后提醒", action=remind_later),
], footer=state.message),
]).navigation_title("原生能力")
)
appui.run(body, state=state)
#API 参考
#能力入口
| 能力 | 主文档 | 典型用途 |
|---|---|---|
| 定位与地图 | location | 坐标、地理编码、指南针 |
| 相册与相机 | photos | 选图、拍照、保存 |
| 通知与实时活动 | notification、live_activity | 提醒、角标、锁屏状态 |
| 持久化 | storage、database、keychain | 设置、记录、密钥 |
| ZIP / IPA | archive | 原生检查、列表、事务化解压、确定性创建 |
| 设备与传感器 | device、motion | 屏幕、电池、传感器 |
| 触觉与声音 | haptics、sound | 反馈、提示音 |
| 网络 | network、websocket | HTTP、实时连接 |
| 编辑器工具栏 | keyboard | 插入代码片段 |
#场景配方
| 目标 | 推荐组合 | 说明 |
|---|---|---|
| 选图并保存设置 | appui + photos + storage | 按钮触发 picker,路径写入 State |
| 下载并存相册 | network + photos | 下载到本地再 save_video |
| 地图展示位置 | permission + location + MapView | 先查权限再定位 |
| 健康面板 | permission + health + Chart | 拒绝时显示空状态 |
| 提醒任务 | notification + storage | 稳定 identifier 调度/取消 |
| 播放历史 | database + List | 大列表勿塞 storage |
| 安全配置 | biometric + keychain + Form | 密钥进 keychain |
| 传感器面板 | motion + haptics | 高频数据避免每帧重建 UI |
| 远端列表 | network + List + .refreshable | 请求放刷新回调 |
| IPA / ZIP 工具 | FileImporter + archive + List | 先检查/列表,再由按钮触发解压 |
#使用规则
- 权限类能力先查状态,再请求。
- 触觉、通知、声音不要在循环里连续触发。
- 写代码前打开对应模块文档,按公开 Python API 调用。
#常见错误
| 错误写法 | 后果 | 修正 |
|---|---|---|
在 body() 里请求权限/发通知 | 刷新时反复弹窗 | 放进按钮回调 |
token 写入 storage 或日志 | 泄露风险 | 用 keychain |
| 权限拒绝后空白页 | 用户不知原因 | 显示说明与重试按钮 |
| 用户取消选择未处理 | 崩溃或脏状态 | 判断 None 并保留原状态 |
| 网络失败清空旧数据 | 体验差 | 保留缓存并显示错误 |
#相关文档
| 文档 | 用途 |
|---|---|
| iOS 原生能力 | 入口模型、模块表与权限矩阵 |
| 全部模块总览 | 按分类浏览模块 |
| 运行时选择 | appui / scene / widget 选型 |
| permission | 统一权限查询与申请 |
| photos | 相册与相机 |
| notification | 本地通知 |
#使用原则
- 原生能力放在用户触发的回调里,不要写在
body()构建期 - 先查最小模块,再组合;避免一次 import 多个无关模块
- 返回值、取消和拒绝都要给用户可见反馈
#失败路径
| 情况 | 处理 |
|---|---|
| 权限拒绝 | 提示去设置开启,并保留当前页面 |
| 设备不支持 | 用 is_available() 或模块返回值判断 |
| 用户取消 | 不要当作成功继续写数据 |
#预期效果
运行示例后,界面应出现文档描述的目标结果;若与预期不符,请按「失败路径」排查。