首页/开发者文档/插件开发指南

插件开发指南

沙箱安全模型、启动时序、window.toolbox API 全览、错误处理与平台限制

插件开发指南

宇宙核的插件是沙箱内运行的 Web 页面(HTML/CSS/JS),由本地静态服务器伺服,通过 window.toolbox 桥接访问系统能力。插件不获得本机文件系统或网络直连权限。

安全模型

  • 插件运行在独立 WebView 沙箱;所有能力经 window.toolbox 显式调用,首次调用弹原生授权框,用户同意后永久授权(可在「权限管理」页随时调整,每次调用记录使用日志)
  • 页面由本地伺服器下发(随机端口 + 会话 Cookie),响应带 Cache-Control: no-store(插件更新即生效)与 CSP(可引用 https 资源;禁止 http 明文、禁止被 iframe 嵌套)
  • 顶层导航仅允许 https(data:/blob: 禁行)
  • 诱饵空间内,非隔离工具拿不到真实数据密钥——相关调用 fail-closed 返回锁定错误

启动时序(必须幂等)

最大的坑:window.toolbox 是宿主在页面加载完成之后才注入的,而插件自己的启动脚本早就跑完了——启动时调用任何宿主能力,那时 window.toolbox 还是 undefined,代码会静悄悄什么都不做。“启动时读存储”会读到空,用户以为数据丢了。

宿主注入 window.toolbox 后会同步广播 yzhToolboxReady 事件(并置 window.__yzhToolboxReady = true),这是唯一可靠的“桥已就绪”信号。老事件 toolboxReady 只在存储确实有数据时派发(现于桥注入时也会补发一次作兼容别名),不要用它做初始化门控。推荐模式:

// 等桥就绪再干活(whenToolbox 见 example-code.md),loadAll 必须可重入
whenToolbox(3000).then(function(){ loadAll(); render(); });
// 存储水合完成后宿主会再发一次 toolboxReady,监听它把已落数据读回来(幂等无害)
window.addEventListener('toolboxReady', loadAll);

数据存储(核心)

插件不做加密,宿主做。 storage.* 数据落盘于 plugin_data/<pluginId>/storage.json,整文件 AES-256-GCM 加密,密钥来自托管密钥链,档位跟随权限页的「密码保护」开关:

密码保护 数据 插件可用性
关(默认) 设备 KEK 档加密 免密可用,没设安全密码也加密
开 密码档加密 进插件需密码;安全中心锁定时 storage 调用返回 {error:'locked', locked:true}

两个历史行为,不处理必然出 bug(完整模板见示例代码):

  1. 读取返回 {value: 数据} 包装,不存在的键是 {value: null}——直接 x?.value ?? x 会把包装对象当数据;
  2. storage.set(key, "合法JSON字符串") 时宿主会自动解析成对象落盘,读回的 value 可能是字符串也可能是对象——只按字符串 JSON.parse 会在二次读取时崩。

window.toolbox API 全览

所有方法返回 Promise,失败返回 {error: '...'}(不 reject,必须检查 res.error 并反馈给用户)。

存储(storage 权限)

API 说明
storage.set(key, value) value 传 JSON 字符串;整文件上限 10MB;串行队列保证并发安全
storage.get(key) / keys() / remove(key) / clear() 读取 / 枚举 / 删除 / 清空;锁定态返回 locked 错误
storage.encrypted.* 遗留加密存储 API(新插件不要使用,见插件数据管理)

文件(file 权限)

API 说明
file.exportFile(name, base64) 导出首选:写入 exports/ 目录并自动弹出系统分享面板,返回 {success, path}
file.share(name) 再次分享 exports 中已导出的文件
file.list(dir?) 列目录;dir='exports' 列导出记录 {files:[{name,size,modified}]}
file.delete(name, dir?) 删除文件;dir='exports' 删导出文件
file.write/read/exists / writeBinary/readBinary 插件根目录小文件(保留名 storage.json 被拒绝)
file.download(url, filename) 下载 https 资源到导出目录
  • 导出文件在 App 沙箱 exports/ 目录——分享面板是用户取出文件的唯一途径(安卓限制)。
  • 文件名禁止 /、\、..、前导 .;单文件写入/导出上限 50MB。

图片 / 分享 / 剪贴板 / UI

API 说明 权限
image.pick(source?) 系统相册/相机选图,返回 {success, data(base64), mimeType, ...};取消时 success:false image_pick
image.save(data, filename) base64 图片存相册 image_save
share.text(text) / share.url(url, title?) 系统分享 share
clipboard.copy(text) / read() 原生 navigator.clipboard 已被禁用并抛错 clipboard_write / read
ui.toast / alert / confirm / prompt / loading.show / hide 原生渲染;confirm/prompt 返回用户选择 ui_dialog

⚠️ App 内 <input type="file"> 无效(WebView 未实现文件选择器)——选图必须用 image.pick;浏览器预览环境可用 window.__yzhPreview 判断后降级为 input。

网络(network 权限)

const r = await window.toolbox.network.fetch({ url, method:'GET', headers:{}, body:'...' });
// r = {statusCode, headers, body} 或 {error}
  • 仅 HTTPS;SSRF 防护(私网/回环拦截);最多 5 跳重定向;响应 20MB 截断;限速 60 次/分钟。
  • window.fetch 已被 polyfill 自动走此通道。

其他

API 说明 权限
device.info() / vibrate(ms) 设备信息 / 震动 vibrate
device.compass(cb) / shake(cb) / motion(cb) 传感器流,返回 {stop()} sensors
notify.schedule({at?, dailyAt?, key?, title?, body?}) / cancel(key) / list() 本地通知(页面关闭后宿主照常触发) notification
workflow.trigger {name, data} 触发用户工作流 workflow
ai.prompt {text} 调用宿主 AI ai
navigate.back {} 请求后退(无历史则关闭页面) 无
security.deriveKey() 遗留:仅用于旧版自行加密数据的迁移,新插件禁止使用 security

底层调用方式

未提供便捷封装的能力,可经桥直接调用:

const result = await window.flutter_inappwebview.callHandler('toolbox', 'navigate.back', {});

错误处理与平台限制

  • 所有 {error} 必须呈现给用户(状态栏/toast),禁止吞掉;{error:'locked', locked:true} 表示“数据已加密、安全中心未解锁”,应显示解锁引导 + 重试按钮
  • 中文输入法组合期间的 Enter 需注意 isComposing
  • html2canvas 不支持 repeating-linear-gradient——纹理背景用 SVG data-URL;截图设 backgroundColor 底色兜底;截图前滚动容器归零
  • 多次启动调用(boot + toolboxReady)保持幂等
  • 详细坑清单与打包发布见插件格式说明与插件数据管理
MATRIX MODE: ON — 再输入一次或按 ESC 退出