这份指南带你完成一个闭环:创建工程 → 写一个页面 → 调一次宿主 API → 在浏览器调试 → 打包并装进 WorkFly。跑通主线后,再按真实需要增加路由、登录、Tool、Skill、会话卡或动态磁贴。
WorkFly 小程序通过全局 wf SDK 调用网络、存储、UI、剪贴板与系统能力。新工程统一使用 Manifest v2;完整页运行在隔离 Sandbox 中,构建产物是 .wfapp.zip。React、Vue、Vanilla 和 TypeScript 都可以用。
按需阅读:
| 你想… | 去 |
|---|---|
| 理解为什么这样拆(Tool / Skill / Surface / MCP) | 设计理念 |
| 从最小页升到 AI 查询再升到宿主卡 | 上手阶梯 |
| 对照官方包拆解步骤 | CURL 与红火台最佳实践 |
查 wf.* 签名 |
WF API |
先选开发形态
先判断“页面在哪里运行”,再选框架:
| 你的目标 | 选择 | 说明 |
|---|---|---|
| 做一个可安装的完整应用 | Sandbox | 默认主线;框架不限,权限隔离,支持 Hash 路由 |
| 在会话或首页直接展示一张卡片 | Sandbox + React UI Module | 完整页仍在 Sandbox;宿主 Surface 固定 React 19,属于高信任能力 |
| 让 AI 帮你创建或修改工程 | 上述任一形态 + @workfly/mcp |
AI 可按主题读取同版本权威规范;产物仍遵守同一 Manifest、权限与打包规则 |
没有明确的宿主 Surface 需求,就只做 Sandbox。不要为了“以后可能用”提前申请高信任能力。复杂度应跟随需求出现:先档 0 能装能开,再档 1 贡献 Tool,最后才档 2 嵌宿主——见 上手阶梯。
快速开始
一行命令起一个工程(交互选框架,或用 --template 直选),装好依赖即可本地调试:
# 普通 sandbox:vanilla / vanilla-ts / react-ts / vue-ts
# 高信任宿主 Surface:react-ui-ts
npm create @workfly/app my-app
# 或直选模板:npx @workfly/create-app my-app -- --template react-ts
cd my-app
npm install
npm run dev # 本地调试:注入全局 wf + 网络代理 + 权限门控 + HMR
npm run build # 构建并打包成 my-app.wfapp.zip
生成的是一个普通的 Vite 工程(以 react-ts 为例):
my-app/
manifest.json # 应用元信息与能力声明
icon.svg # 分层占位图标(发布前换成自身品牌)
index.html # 入口(含沙盒 CSP)
vite.config.ts # base:'./' + @workfly/vite-plugin
src/ # 业务代码,直接用全局 wf SDK
业务代码里,通过注入的全局 wf 访问宿主——和微信 wx 同款双模(回调 / Promise):
const res = await wf.request({ url: 'https://api.example.com/data' })
await wf.setStorage({ key: 'last', data: res.data })
await wf.showToast({ title: '已更新' })
需要登录时使用对齐 wx.login 的一次性 code,不读取宿主 Token:
const { code } = await wf.login()
await wf.request({
url: 'https://api.example.com/v1/workfly/session',
method: 'POST',
data: { code }
})
完整签名见 WF API,机器可读版
/api/wf-api.json;TypeScript 工程经@workfly/wf-types拿到类型。
本地调试与安装测试
开发分两层循环,日常都在内循环、不必每次打包:
- 内循环 ·
npm run dev(不打包):@workfly/vite-plugin注入与客户端内同名同接口的wf,网络与 RSA 经本地 server 代理(绕 CORS、复刻主进程语义),未声明的权限直接拒绝,右下角调试面板看每次wf.*调用,改码即热更。开发态是近似实现:加密只做 base64、UI 为简化版——最终行为以客户端内为准。
- 装进客户端测真实形态:
npm run build产出my-app.wfapp.zip后,把它**拖进客户端的「小程序面板」**即安装运行——这才是 webview 进程级隔离、真实权限门控、真实加密存储的运行环境。也可在 IM 会话把.wfapp.zip发给自己,在文件卡点「安装小程序」。
Manifest
包根目录的 manifest.json 描述身份、版本族、能力与入口。核心字段如下:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string |
✅ | v2 全小写 reverse-domain,各段不以 - 开头/结尾 |
name |
string |
✅ | 展示名 |
version |
SemVer |
✅ | ① 产品版本,面向用户、决定升级 |
manifest |
2 |
✅ | ② 清单格式版本;缺省只用于兼容历史 v1 包 |
wfSdk |
SemVer |
— | ③ 面向的 wf SDK / 平台能力版本,宿主判兼容、降级 |
minClient |
SemVer |
— | ④ 最低宿主版本门槛,低于则拒装、拒跑 |
scope |
'private' | 'team' | 'org_market' |
✅ | 可见性 / 分发范围 |
contains |
('ui' | 'skill' | ...)[] |
✅ | 本包含哪些资产;当前第三方至少含 'ui' |
ui.standalone |
AppUiStandalone |
✅ | 完整页入口 + 显式 routing |
ui.modules |
AppUiModule[] |
— | 高信任 React 宿主组件,固定 workfly-react@1 |
launchers |
AppLauncher[] |
✅ | 可从哪些入口启动(panel / spotlight / deeplink…) |
permissions |
AppPermission[] |
— | 所需能力 + 用途说明:{ id, reason } |
contributes |
ManifestContributes |
— | 卡片、磁贴、Skill 与 launch/query Tool 等宿主贡献 |
icon |
string |
— | 安全的包内相对路径,优先分层 SVG,缺省回退占位图 |
一个最小可用示例:
{
"id": "com.acme.approval",
"name": "审批助手",
"version": "1.0.0",
"manifest": 2,
"wfSdk": "1.0.0",
"minClient": "0.16.27",
"scope": "team",
"contains": ["ui"],
"icon": "icon.svg",
"iconPlaceholder": { "initial": "审", "color": "#3461ff" },
"ui": {
"standalone": {
"enabled": true,
"entry": "dist/index.html",
"render": "sandbox",
"layout": "full",
"routing": {
"mode": "hash",
"defaultPath": "/",
"routes": [{ "path": "/", "shareable": true }]
}
}
},
"launchers": ["miniprogram_panel", "spotlight", "deeplink"],
"permissions": [
{ "id": "net.fetch", "reason": "调用你填写的接口获取数据" },
{ "id": "vault", "reason": "加密保存访问令牌" }
]
}
workfly.* 整段由平台保留;workfly.gen.* 也只允许 WorkFly 端内生成流程使用。第三方发布包和开发服务必须使用自己的 reverse-domain ID。
TypeScript 工程可用 import type { MiniAppManifestV2 } from '@workfly/wf-types' 获取完整清单类型;路由、ui.modules、权限、卡片、磁贴、Skill 与 Tool 都在同一契约中。LegacyMiniAppManifest 只用于旧包迁移,类型检查不能替代构建与安装时的运行时校验。
版本族是四件不同的事:version(产品)、manifest(清单格式)、wfSdk(面向 SDK)、minClient(最低宿主)。minClient 要求客户端 >= 目标;wfSdk 要求与宿主同主版本且目标 <= 宿主,安装和运行前都会复查。缺省表示不启用对应检查,只用于历史兼容;v2 新包应两项都写。版本字符串使用规范 SemVer,不接受数值前导零或空 prerelease/build 段。v2 字段不改变既有含义;破坏性清单变化必须升 Manifest 主版本,React ABI 破坏性变化必须发布新的 workfly-react@N。
同一份 ui.standalone.entry 有两种宿主呈现:它可以在「小程序」面板里作为多标签页运行;用户把应用钉到主侧边栏后,也可以作为一级工作区占满主内容区。钉选只增加入口,不会搬走或复制当前面板实例;首次点击侧栏入口时才创建独立实例,因此两处可以同时打开并分别保留页面状态。侧栏始终只高亮当前所在的一处。普通切换会保活,但侧栏的后台独立实例受有限 LRU 约束;持久状态请使用 wf 存储或业务后端,不要只放在页面内存里。
wf.getLaunchOptionsSync() 固定返回当前实例首次创建的 scene/query/path;wf.getEnterOptionsSync() 返回最近一次宿主进入参数,首次二者相同。两者不会在面板与侧栏实例间串用。当前 scene 有 panel/standalone/spotlight/im/agent/deeplink/miniapp/mcp;该字段是开放字符串,应用必须兼容未知新值。location.hash 是当前页面路由,应用内部 Hash 导航和前进/后退不会改写 Options API。关闭面板标签后从启动器手动重开属于新的空 panel 启动。
路由与分享
Manifest v2 的页面地址必须显式声明:
"routing": {
"mode": "hash",
"defaultPath": "/orders",
"routes": [
{ "path": "/orders", "shareable": true },
{ "path": "/orders/:orderId", "shareable": true },
{ "path": "/settings", "shareable": false }
]
}
页面 URL 会真实呈现 #/orders/208?from=share,刷新和前进/后退都保留定位。Tool launch.path 与 wf.openApp({ path }) 只能进入已声明 route;wf.createAppLink({ path, query }) 还要求目标 shareable: true。标准链接为 workfly://app/<appId>/<route>?query。静态段优先于动态段;相同结构的动态 route 会被打包守门人拒绝。完整路径段 . / .. 及其百分号编码等价形式一律拒绝。
不需要页面地址时显式写 routing: { "mode": "none" },此时不能携带 path,但仍可生成应用级链接。workfly://app/<id> 表示省略 route 并使用 defaultPath,workfly://app/<id>/ 才表示显式根路由 /。分享 query 不得包含 Token、Cookie、一次性 code 或个人敏感字段。
应用图标
宿主在小程序面板、主侧边栏、标签栏、Spotlight、权限设置和会话卡来源行中统一使用 manifest.icon,加载失败后回退 iconPlaceholder,再回退应用名首字。
开发者只需交付一张真彩图:主侧边栏未激活时,宿主自动做灰度和透明度降强,并按亮暗主题调整亮度;选中、悬停或键盘聚焦时恢复真彩。不需要准备“选中/未选中 × 亮/暗”四套图标。
- 优先使用正方形 SVG(建议
viewBox="0 0 1024 1024");背景层标为id="bg",前景图形放在id="fg"分组。 - SVG 会以独立的 HTML 图片元素加载,不继承外层
currentColor或 WorkFly CSS 变量;颜色应在文件内闭合,不依赖外部字体、图片、样式和脚本。 - 未激活侧边栏保留图标亮度结构,而不是把整张图当 alpha mask 染成实心方块。请确保前景/背景在灰度下仍有足够明度差,不要只靠色相区分形状。
- PNG 仅作兜底,至少
512×512且带透明通道。iconPlaceholder仍应保留,便于文件缺失或解码失败时降级。 manifest.icon不会自动改写小程序内部 DOM。完整页、会话卡或磁贴内需要 logo 时,显式复用同一文件(例如dist/入口使用<img src="../icon.svg" alt="">),不要另画首字占位块。manifest.icon是应用身份,不是页面操作图标集。导航、刷新、返回、关闭、分页和业务对象应统一使用一套成熟图标库(脚手架可选 Lucide),不要混用 emoji、Unicode 符号、手画 SVG 和多套风格。常规工具图标建议16px、1.75-1.8线宽;纯图标按钮必须提供aria-label和桌面端 tooltip。
主题与容器变量
宿主会把当前明暗模式、租户品牌色和设计 token 的已解析值注入 sandbox 完整页、home/Spotlight widget 与会话卡片,并在主题变化时实时更新。React UI Module 的 Shadow Root 从宿主节点继承同一组变量。优先直接使用这些变量,让小程序自然融入当前工作台:
按页面层级控制品牌强度
- 真正的欢迎页、活动页和品牌落地页可以用自己的配色、排版、实质内容与一次性签名动效建立辨识度;不要默认加入装饰性光球、网格、渐变文字、玻璃叠层或持续 3D 交互。仍需保证窄宽可用、键盘焦点、文本对比度,并为
prefers-reduced-motion提供静态降级。 - 账户主页、列表、搜索、订单、流水、详情、表单和设置等任务页面优先采用 WorkFly 注入的颜色、间距、圆角、阴影与动效 token。品牌收敛到 logo、业务对象自身的识别色、选中态和少量强调色,让信息保持可扫描、可比较。
- 首页 / Spotlight 磁贴、会话卡和启动器条目属于宿主 Surface,必须保持 WorkFly 的表面与控件风格。不要把完整页的自定义背景或导航缩进磁贴。
若同一小程序确有品牌页与任务页,两类样式应通过路由或根节点 class 隔离,离开后停止相关动效。红火台是任务型参考实现:统一复用根目录 icon.svg,餐卡用克制的红 / 蓝 / 绿识别业务类型,其余账户、订单、流水、详情、磁贴和会话卡遵循宿主表面与交互语汇。
| 类别 | 常用变量 |
|---|---|
| 画布 / 表面 | --color-background、--color-background-soft、--color-surface-page、--color-surface-card、--color-surface-card-strong |
| 文本 | --color-text、--color-text-secondary、--color-text-dimmed、--color-text-light、--color-text-inverse |
| 边框 | --color-border、--color-border-heavy、--color-border-strong |
| 品牌 | --brand-color、--brand-color-hover、--brand-color-soft、--brand-color-border |
| 语义 | --color-warning-surface / text / strong、--color-danger-surface / text / strong |
| 形状 / 阴影 / 动效 | --radius-sm … full、--shadow-sm … panel、--dur-1 … 4、--ease-standard |
.page {
color: var(--color-text, #1f2937);
background: var(--color-background, #edf2f7);
}
.panel {
border: 0.5px solid var(--color-border, rgba(15, 23, 42, 0.08));
border-radius: var(--radius-lg, 12px);
background: var(--color-surface-card, #fff);
box-shadow: var(--shadow-sm, 0 2px 6px rgba(15, 23, 42, 0.08));
}
- 每个
var()都带接近宿主的浏览器预览 fallback;普通 Vite 页面不会注入宿主 token,不能把预览空白误判为客户端主题失效。 - 不要重定义宿主同名变量。应用自己的变量使用业务前缀,例如
--catering-accent。 - CSS 明暗差异可用
prefers-color-scheme;JS 侧读取wf.getSystemInfoSync().theme。能用 token 自动跟随的颜色不要再维护平行主题表。 - 任务页面的画布、正文、边框和普通表面使用宿主 token;品牌页样式不得泄漏到任务页面或宿主 Surface。
wf SDK
全局对象 wf(对标微信 wx)。异步方法接受单个对象入参,含可选 success / fail / complete 回调;都不传时返回 Promise。同步方法以 Sync 结尾。命名按三档清晰分区:
wf.<同名>(对标 wx):凡 wx 有的,方法名 / 入参 / 返回 / 错误约定逐字对齐——拿 wx 文档即是我们的文档。- 登录
wf.login;网络wf.request;存储wf.setStorage/getStorageSync/encrypt;UIwf.showToast/showModal/showLoading/showActionSheet/previewImage;剪贴板wf.setClipboardData/getClipboardData;系统 / 启动wf.getSystemInfoSync/getLaunchOptionsSync/openApp/createAppLink。
- 登录
wf.<新名>(WorkFly 原生标准面 · 建设中):wx 没有、但与 BIP 无关、对任意小程序通用的原生能力——通知wf.notify、日程wf.schedule.*、放烟花wf.fireworks(演示)等。服务「看完数据顺手提醒 / 写代办」这类原生闭环。wf.bip.*(用友 BIP 生态扩展):只放确实与 BIP 绑定的能力(如wf.bip.rsaEncrypt),前缀围栏、不污染标准面。身份登录统一使用wf.login(),不另造wf.bip.auth。
完整签名、参数表与状态见 WF API(自源码自动生成,永远与实现同步)。
权限模型
manifest 只声明「需要什么、为什么需要」,风险等级由平台固定;未声明的能力调用会直接失败:
{
"permissions": [
{ "id": "net.fetch", "reason": "调用你填写的接口获取数据" },
{ "id": "vault", "reason": "加密保存你的访问令牌" }
]
}
| 等级 | 用户交互 | 当前权限 |
|---|---|---|
normal |
声明后直接使用 | net.fetch、vault、ocr |
runtime |
首次调用询问,记住选择,可撤销 | clipboard、identity.login、notification、device.identifier、im.openChat |
operation |
每次操作确认 | schedule.write、device.call |
restricted |
普通第三方不可申请 | bip.sessionToken |
wf.login() 只返回短期一次性 code,供第三方服务端换自己的业务会话;宿主 Token 不下发给小程序。MTL 兼容层服从同一权限表:用户资料首次询问,设备标识按 app id 隔离,宿主操作逐次确认;Token 兑换与远程代码注入接口直接拒绝。纯本地页内能力(如 wf.fireworks)无需权限。
贡献模型
wf SDK 是你调用宿主;contributes 则是你向宿主贡献 UI——让你的能力出现在会话、磁贴、Spotlight 等宿主表面。在 manifest.json 的 contributes 里声明:
- 卡片渲染器(cardRenderers):Manifest v2 固定使用
mode: react + module + component,从WorkflyCardProps取数据与host.wf。可声明160-600px高度与最多 4 条下一步建议。sandbox + entry仅供历史 v1 包读取兼容。 - 磁贴(widgets):Manifest v2 固定使用
mode: react + module + component,从WorkflyWidgetProps.span取当前跨度,并通过host.wf.openApp()进入 sandbox 完整页。 - Skill(skills):v2 每项声明应用内小写
id和展示name;稳定身份是<appId>.<skillId>,名称重合时由宿主限定应用作用域。Markdown 工作流程随安装进入索引,卸载后消失。 - Tool(tools):要求小程序主入口为 sandbox。launch 与声明式只读 query 都可声明业务
resultCard;这意味着同包必须提供高信任 React cardRenderer。不接受高信任时省略resultCard:launch Tool 使用平台入口卡,query Tool 仍返回字段受限的纯数据结果。声明业务卡时必须提供resultCard.summary。expose: true时可投影到外部 MCP,当前不在后台执行第三方 JavaScript。 - 详情渲染器(details)· 暂仅内置 App:按
kind注册业务对象的详情视图——目前只接内置 App 的代码贡献,第三方下载小程序的 manifest 暂不消费details(平台待接)。 - 剪贴板提示 / 内容匹配(clipboardHints / contentMatchers):前者在 Spotlight 读取剪贴板并展示命中入口;后者在 IM 文本消息的“打开方式”菜单中展示匹配应用。两者都通过受约束的
workfly://app/<id>/<shareable-route>模板投递命中内容。
会话与 Surface 集成就是把卡片、引用和下一步建议接进主会话——让数据既能被 Agent 理解,也能由应用可信呈现。详见 会话与 Surface 集成。
会话内遵循“卡片优先、打开应用是渐进升级”:能在卡片完成就不导航,需要完整页面时由用户点击卡片主操作;内部 Agent 不自动打开应用,也不弹 modal 打断对话。卡片宽度由平台统一控制,应用应保持单一主题、一个主操作、亮暗主题和窄宽可用。
卡片和 Agent 上下文是两个通道:details.card 给用户渲染,Tool 的 content 给 Agent 阅读。第三方 Tool 由平台把 resultCard.summary 写入 content;摘要应说明业务对象、关键口径和缓存/实时状态,不能只写“已展示卡片”,也不得包含 Token、Cookie 或一次性 code。launch Tool 的摘要是静态语义概要;query Tool 则把字段受限的实时投影同时写进 content 与 WorkflyCardProps.data.result。Agent 用摘要理解后续指代,不应逐字复述用户已经看见的卡片内容。外部 MCP 没有卡片通道,仍只返回查询结果。
cardRenderers[].suggestions 每张卡最多 4 条:label 为 1-14 个 Unicode 字符,text 可选且最多 200 个 Unicode 字符,act 为 send | fill。查询、查看、分析类可用 send;提交、发送、删除、提交评价等有副作用的动作使用 fill,由用户确认后再发。宿主会裁剪、去重并复核风险;本地候选优先与辅助模型建议融合,辅助模型不可用时也可直接展示。用户关闭“下一步建议”或处于角色态时不展示,候选不会投影到外部 MCP。
卡片来源行由平台根据 manifest 自动显示为“卡片来自于 [类型标识] 小程序 [应用图标] 应用名”。归属在注册 manifest 时记录,不从 card kind 首段猜 App id;应用图标和名称是同一可点击区,点击后进入对应小程序。第三方卡片不要在内部重复绘制来源或应用名称。
Manifest v2 的 card kind 固定为 <appId>.<slug>,slug 是单段小写字母、数字和连字符,不能再含点。clipboardHints/contentMatchers.match 只接受内建 curl | url | json,不接受自定义正则。
声明式 query 必须写 readOnly: true;即使请求方法是 POST,也必须没有业务副作用。所有查询声明 net.fetch;需要鉴权时再声明 vault + sessionKey 并使用 {{session.x}},公开无鉴权查询可以省略。request.url 必须固定 HTTPS 主机,宿主不跟随 HTTP 重定向,任何 3xx 都按查询失败返回。
默认只以 HTTP 2xx 判定成功,宿主不会把 code: 0 或 code: 200 当成通用规则。后端另有业务状态字段时,在 query.response 显式写 success: { path, values },可用 message 指定失败消息路径。expose: true 的 query 必须声明非空 result.fields,且 result.source 必须实际指向对象或对象数组,裸标量不能绕过字段白名单。原始响应在读取流时执行 4 MiB 上限,Agent、MCP 与实时卡片共用的字段投影最多 256 KiB;应用会话长字符串会从成功和错误输出统一脱敏。
Surface 动态磁贴
首页与 Spotlight 的 Surface 是用户可拖拽改形状的动态磁贴,不是固定尺寸卡片。宿主按当前可用宽度自动生成方格列,widget 以 col×row 占格;size: bar|compact|square|wide|tall 只决定初始跨度和拖拽边界。用户可以把同一磁贴调整成横条、方形、竖条或不对称矩形。
widget 应把自身 Shadow Root 当作响应式容器,同时根据宽度、高度和纵横比重排:矮横条保留单行核心摘要,方形/宽矩形可用双列,窄竖条改为纵向分组。不要写固定外部宽高,也不要只按 max-width 隐藏内容。核心业务信息在任何允许形状下都必须保留,次要说明才可渐进披露;复杂内容通过 host.wf.openApp() 进入完整页面。
根节点应填满外框并避免内部滚动;长文本需换行或省略,支持亮暗主题。宿主右下角有拖拽手柄,不要把唯一操作贴在该区域。红火台 BalanceWidget React UI Module 是官方响应式示例。
React UI Module 构建
完整页框架不限;直接挂入宿主的高信任组件固定使用 React:
workfly({
reactUiModules: { entries: { surfaces: 'src/UiSurfaces.tsx' } }
})
源码按 src/card/、src/surface/ 分类,薄 src/UiSurfaces.tsx 只做稳定命名导出;目录不触发隐式注册,manifest 的 module + component 是唯一真相源。
插件会 external react、JSX runtime 与 react-dom,由宿主 workfly-react@1 提供同一 React 19 runtime;每个 external 同时受固定命名导出白名单约束。第三方 npm 依赖拆为相对 dist/ui/chunks/vendor-*.js,CSS 汇总后按组件挂入 Shadow Root。组件只通过 WorkflyCardProps / WorkflyWidgetProps 和 host.wf 消费公开能力,不得 import WorkFly 私有组件、Context、store 或 IPC。
workfly-react@1 的导出已经冻结:
| specifier | 公开导出 |
|---|---|
react |
default;Activity、Children、Component、Fragment、Profiler、PureComponent、StrictMode、Suspense、act、cache、cacheSignal、captureOwnerStack、cloneElement、createContext、createElement、createRef、forwardRef、isValidElement、lazy、memo、startTransition、use、useActionState、useCallback、useContext、useDebugValue、useDeferredValue、useEffect、useEffectEvent、useId、useImperativeHandle、useInsertionEffect、useLayoutEffect、useMemo、useOptimistic、useReducer、useRef、useState、useSyncExternalStore、useTransition、version |
react-dom |
default;createPortal、flushSync、preconnect、prefetchDNS、preinit、preinitModule、preload、preloadModule、requestFormReset、unstable_batchedUpdates、useFormState、useFormStatus、version |
react-dom/client |
createRoot、hydrateRoot |
react/jsx-runtime |
Fragment、jsx、jsxs |
react/jsx-dev-runtime |
Fragment、jsxDEV |
react/compiler-runtime |
c |
未列出的成员、私有字段和其它子路径不属于 ABI。react / react-dom 默认导出只是冻结的公开成员对象;默认或 namespace import 只能静态访问表内成员,动态下标、整体转交和绕过式解构会构建失败。推荐使用命名导入。
声明任意 ui.modules 都会把整个包提升为无沙箱高信任安装,并显示明确警告。host 是唯一受支持、可版本化的公共 ABI,但不是安全隔离边界。Manifest v2 不接受 sandbox HTML card/widget;普通未审核第三方省略自定义宿主 Surface。未来即使宿主改用 Vue,注册到 WorkFly Surface 的组件仍采用这套 React ABI。开发服务只预览 standalone;卡片和磁贴需 build 后安装验证,外部 React 19 宿主可自行映射同一 external 复用产物。@workfly/wf-types 是纯类型包,只用 import type;manifest 的 runtime id 直接写字面值 "workfly-react@1"。
外部 MCP / CLI 调用 launch Tool 需要两道独立授权:连接先获授 miniapp:launch scope,每次调用再由宿主用户确认,不能设为永久允许。声明式只读 query Tool 使用 miniapp:read,连接授权后返回字段受限结果,不逐次弹确认,也不携带内部会话卡或建议。页面内调用 wf.login() 等 API 时仍走小程序自身权限,连接 scope 不会继承或绕过它。
打包与分发
npm run build 经 Vite 构建后,由 @workfly/vite-plugin 校验 manifest 并打包出 .wfapp.zip:标准 ZIP,复合后缀过 IM 服务端白名单;manifest.json 为第一条目,EOCD 注释区写魔数,供零解包识别。文件顺序稳定、ZIP 时间戳固定为 1980-01-01,同一输入必须产生字节完全一致的包。源码、依赖、测试目录、coverage、构建配置与工程根 .workfly/(本地 build 计数)默认不进入发布包。
默认产物文件名是 {manifest.name}-{version}({build}).wfapp.zip(名称做路径安全化)。build 是工程根 .workfly/build-number 里的整数,每次 pack 自增,方便反复打包时区分产物,而不必每次改 manifest.version。包内身份仍只认 id + version(魔数 / 安装路径),括号里的 build 只出现在文件名。脚手架 .gitignore 已忽略 .workfly/,序号按本机维护。
分发渠道:
- 本地拖拽
.wfapp.zip进小程序面板; - IM 会话发送
.wfapp.zip,对端在文件卡点「安装小程序」; - 集团应用市场(
org_market)规划中。
安装与卸载
安装:解包 → 校验 manifest → 计算整包与逐文件 SHA-256 → 落盘 userData/miniapps/<id>@<version>(保留当前版 + 上一版用于回滚)。UI Module 的 JS、CSS、chunk 与资产每次读取都会复验安装时摘要;任何 React UI Module 安装前都会弹高信任风险确认,v2 standalone 始终留在 sandbox。
卸载:删包目录 + 摘除登记 + 清理该 App 私有数据。包落盘不受「清除 Web 缓存」影响。
IM 入口与宿主 AI
把「自定义业务脑」挂在消息场景:用户在会话工具栏点图标 → 小程序拿到当前会话 → 本地处理正文 → 调 WorkFly 纯补全流式输出。完整设计与分期见仓库 docs/miniapp-host-ai-apis.md;上手旁路见 阶梯 · IM + 宿主 AI。
1. 注册入口
"contributes": {
"imEntries": [
{
"id": "summary",
"placement": "composer.toolbar.end",
"label": "会话助手",
"tooltip": "总结当前会话精彩内容",
"icon": "icons/entry.svg",
"order": 30,
"when": { "chatTypes": ["chat", "groupchat"] },
"launch": { "path": "/summary", "presentation": "bubble" }
}
]
}
也可写在 surfaces/im-entries.json,build 时合并。launch.path 必须落在已声明 route。
呈现 launch.presentation:
| 值 | 含义 |
|---|---|
bubble(默认) |
锚定按钮的气泡;可用 size |
slide |
聊天区右侧侧滑 |
modal |
应用内弹层;标题=小程序图标+名称 |
window |
独立 BrowserWindow,不挡主窗口 |
standalone |
主窗口小程序面板 |
消息右键 message.context 无稳定锚点时建议 standalone。
图标亮暗:
"icon": "icons/entry.svg"
单色 SVG 默认由宿主 mask 染色。若要用全彩或 PNG,可:
"icon": "icons/entry-light.png",
"iconDark": "icons/entry-dark.png",
"iconStyle": "image"
iconLight 可选(缺省等于 icon)。iconStyle:auto(默认)/ glyph(强制染色)/ image(强制原图切换)。
2. 权限
"permissions": [
{ "id": "im.chatContext", "reason": "读取当前会话历史以便本地筛选" },
{ "id": "ai.complete", "reason": "用宿主 AI 按用户提示词流式总结" }
]
均为 runtime:首次询问,可在权限设置撤销。
3. 页内闭环(示意)
const { query } = wf.getLaunchOptionsSync()
// query.chatId / chatType / peerName / entryId …
const page = await wf.im.getHistory({ limit: 80 })
// 本地筛选、截断、脱敏后再拼 prompt
const transcript = page.messages.map((m) => `${m.isSelf ? '我' : m.from}:${m.text}`).join('\n')
const el = document.getElementById('out')!
el.textContent = ''
const { text } = await wf.ai.complete({
system: '你是中文会话精要助手,只根据给定记录提炼要点,不要编造。',
prompt: `会话「${query.peerName ?? ''}」记录:\n\n${transcript}`,
onChunk: (d) => {
el.textContent += d
}
})
getChatContext:轻量摘要 + 最近若干条(适合首屏)。getHistory:分页原料(before游标);chatId 必须与 IM 入口会话一致,禁止扫其它会话。ai.complete:无工具、无主 Agent、无 ACP;模型用用户已配置的「消息回复助手」(缺省回退主模型)。完整签名见 WF API。
4. 边界(对客户也可原话使用)
| 开放 | 不开放 |
|---|---|
| 当前会话只读历史(脱敏) | 任意 chatId 全库扫描 |
| 宿主代推理 + 流式 | 裸 wf.acp / 本机 CLI |
| 后续文库/邮件等写操作(规划中,operation 确认) | 代发 IM、改群资料 |
官方样板包:packages/miniapps/im-context-demo(安装后进任意会话点工具栏图标)。
用你熟悉的 AI 工具开发(接 @workfly/mcp)
你不必在 WorkFly 里才能开发小程序——把 WorkFly 的开发规范接进你常用的 AI 编程工具(Claude Code / Cursor / Claude Desktop 等),让它一边写代码一边查我们的权威规范,照标准产出 .wfapp。
WorkFly 把这些规范以标准 MCP 工具 read_app_dev_doc 对外暴露(公开只读、不涉及任何个人数据),你的 AI 工具连上后即可按主题查询:
| 主题 | 内容 |
|---|---|
overview / workflow |
选形态、脚手架到 .wfapp 的标准工程流程 |
manifest |
manifest.json 每个字段的权威契约(以打包守门人实际校验为准,照此写即能打包) |
routing |
Hash 路由、Tool path、分享深链与冷启动规则 |
ui-modules |
React external、split chunks、固定 Props、Shadow CSS 与高信任边界 |
contributes |
向宿主贡献 UI 的真实能力边界(卡片渲染器等) |
sdk / api |
wf.* 可调能力清单与完整签名 |
permissions |
权限声明与门控 |
theming |
宿主设计 token、明暗主题、图标与容器视觉规范 |
接入(以 Claude Code 为例):在你的 AI 工具里把 @workfly/mcp 配成一个 MCP server(npx @workfly/mcp,连接名用 --name 标识)。首次用到时 WorkFly 弹「连接授权」卡片,你勾选授权即可——配置里不放任何 token,授权后自动配对、之后免打扰。
你得到的:
- 照规范开发,不靠猜:AI 拿到的是与客户端同版本的权威字段契约与 API 签名,避免「写出来装不上 / 打包失败」。
- 规范永远跟随客户端版本:能力与文档死绑同一版本,不会让 AI 用到你这台客户端还不支持的能力。
- 同一座桥,更多语境:同一个 MCP 连接还能按授权访问 WorkFly 的其它对外能力(研发缺陷、文库笔记…),详见 会话与 Surface 集成。
提示:WorkFly 内置助手与它派遣的专家,背后查的是同一份
read_app_dev_doc规范——所以无论你在 WorkFly 里让它造小程序,还是在外部 AI 工具里自己写,标准是一致的。
下一步
- 能力层级怎么选(零代码 / sandbox / 高信任 React Surface)→ 开发者中心总览
- IM 入口 + 会话历史 + AI 总结 → 上文 IM 入口与宿主 AI · 阶梯旁路
- 把数据接进主会话(引用 / 渲染面)→ 会话与 Surface 集成
- 查
wf.*完整签名与参数 → WF API - 脚手架与本地调试细节 → 开发者工具