这两个案例都是仓库内真实可构建的 Manifest v2 小程序(packages/miniapps/curl 与 packages/miniapps/catering),用作最佳实践拆解,而不是虚构协议。红火台自 1.4.0 起采用作者侧目录:capabilities/tools、capabilities/skills、surfaces/cards|widgets,由 @workfly/miniapp-kit 在打包时合并为 contributes;CURL 仍保持薄 manifest + 内联贡献,适合档 0 学习。
| 学习目标 | 先看 |
|---|---|
最小权限、内容带入、一次 wf.request、可选一张摘要卡 |
CURL(约等于 档 0 + 可选档 2 的卡) |
| 业务登录、Hash 深链、只读 query、Skill、MCP、双账户口径 | 红火台(档 1 + 档 2) |
不要从红火台完整清单照抄。先按 CURL 的尺度做出最小闭环,再只增加业务确实需要的能力。概念背景见 设计理念。
先看差异
| CURL 调试 | 红火台 | |
|---|---|---|
| 完整页 | 单页 React Sandbox | 多页面 React Sandbox + Hash 路由 |
| 权限 | net.fetch |
net.fetch、identity.login、vault |
| 数据来源 | 用户输入的任意请求 | 固定的红火台 HTTPS 服务 |
| 宿主入口 | 面板、Spotlight、深链、剪贴板、IM 内容 | 面板、Spotlight、深链 |
| 宿主 UI | 请求摘要会话卡 | 余额卡、订单卡、首页 / Spotlight 动态磁贴 |
| Agent 能力 | 无 | 2 个 Skill、2 个打开 Tool、6 个只读查询 Tool |
| 外部调用 | 不投影业务查询 | 只读查询经 miniapp:read 投影到 MCP |
| 适合学习 | 最小权限、内容带入、wf.request |
鉴权、路由、数据边界、卡片与 Agent 协作 |
共同点只有三条:完整页始终运行在 Sandbox;能力全部由 Manifest 显式声明;组件只通过公开的 wf / host.wf 调宿主。
CURL:从一段命令到可复用请求
CURL 的用户路径很短:
- 用户从剪贴板、IM 文本或 Spotlight 把一段 CURL 带入应用;
- 应用解析方法、URL、Header 与 Body;
- 用户确认后调用
wf.request; - 结果留在应用内,必要时由 React UI Module 渲染一张请求摘要卡。
Manifest 只声明真实入口
{
"id": "com.workfly.curl",
"manifest": 2,
"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": "向你填写的接口发起请求并展示响应" }]
}
它不需要登录、Vault 或设备信息,因此不声明这些权限。clipboardHints 与 contentMatchers 只负责把命中的文本作为 query 带入同一个已声明路由,不在宿主里执行请求。
页面只依赖公开 SDK
const response = await wf.request({
url: spec.url,
method: spec.method,
header: Object.fromEntries(spec.headers.map(({ key, value }) => [key, value])),
data: spec.body,
dataType: 'text'
})
开发态由 @workfly/vite-plugin 注入同名 wf 并代理网络请求;装入客户端后,调用自动切到真实宿主实现。业务组件不需要判断自己运行在哪种环境。
为什么卡片单独编译
CURL 的完整页仍是 Sandbox;只有会话里的 RequestCard 使用高信任 React UI Module。Vite 配置把 React runtime external 出去,把第三方依赖拆成相对 chunk,样式由宿主挂进 Shadow Root。
workfly({
reactUiModules: { entries: { surfaces: 'src/UiSurfaces.tsx' } }
})
这条边界使完整应用可以继续选 React、Vue 或 Vanilla,而注册到宿主 Surface 的组件始终遵循稳定的 workfly-react@1 ABI。
红火台:从业务接口到 Agent 能力
红火台不是“把网页塞进小程序”。它把同一个业务域拆成四层:Sandbox 页面负责完整操作;声明式 query Tool 负责受限只读查询;React UI Module 负责宿主卡片与磁贴;Skill 负责教 Agent 如何分页、区分账户口径并解释结果。
先换自己的业务会话
页面通过 wf.login() 获取一次性 code,再交给红火台服务换取自己的 accessToken + tenantId。业务会话保存在当前 app id 隔离的 Vault,宿主 Token 不会进入小程序。
const { code } = await wf.login()
const session = await exchangeBusinessSession(code)
await wf.setStorage({ key: 'catering.business-session', data: session })
这套会话随后复用于余额、流水、订单、详情与评价接口。Cookie、抓包 Token 和真实身份数据都不写入 Manifest,也不进入示例代码。
路由就是可导航契约
"routing": {
"mode": "hash",
"defaultPath": "/balance",
"routes": [
{ "path": "/balance", "shareable": true },
{ "path": "/accounts/:kind/:id", "shareable": false },
{ "path": "/orders", "shareable": true },
{ "path": "/orders/:orderId", "shareable": true }
]
}
Tool 打开路径、卡片主操作、应用内导航和分享链接都指向这份路由表。账户明细含内部账户 ID,因此明确设为不可分享;订单列表和详情可以被可靠定位。
查询 Tool 固定请求边界
订单列表由宿主执行声明式只读请求,而不是在后台运行第三方 JavaScript:
{
"name": "miniapp_catering_orders_query",
"parameters": [
{ "name": "statuses", "default": "0,1,2" },
{ "name": "current", "default": "1" },
{ "name": "size", "default": "10" }
],
"query": {
"readOnly": true,
"sessionKey": "catering.business-session",
"request": {
"method": "POST",
"url": "https://catering.yonyoucloud.com/catering/order/ctnorderinfo/v1/getUserOrderList",
"header": {
"Authorization": "Bearer {{session.accessToken}}",
"TENANT-ID": "{{session.tenantId}}"
}
},
"result": {
"source": "data.records",
"fields": ["id", "orderNo", "clientOrderStatus", "shopName", "payMoney", "createTime"],
"meta": { "current": "data.current", "total": "data.total", "pages": "data.pages" },
"maxItems": 20
}
},
"expose": true
}
关键不是 POST 或 GET,而是 readOnly: true、固定 HTTPS 主机、明确业务成功条件、字段白名单、分页元数据与结果大小上限。外部 MCP 只拿到这份投影,不拿内部卡片、建议或业务会话。
同一结果服务三种消费者
- 用户在会话里看:
resultCard把当前页最近三笔订单交给OrdersCard;完整列表由卡片主操作进入/orders。 - Agent 理解上下文:
resultCard.summary说明对象、口径、分页与实时性,避免模型只知道“展示了一张卡”。 - 外部工具查询:
expose: true把字段受限结果投影到miniapp:read,不携带内部 UI。
卡片还可以贡献最多 4 条“下一步建议”。查询和分析使用 send;评价、提交、删除等有副作用的动作使用 fill,必须由用户确认后发送。
数据口径写进 Skill
红火台的个人账户和部门账户不能相加,也不能共用消费趋势刻度;订单与流水都是分页结果。Skill 明确要求 Agent 读取 current / pages / total,在分析前判断是否继续翻页。超出 JavaScript 安全整数范围的订单 ID 和账户 ID 全程保持字符串。
这类规则不适合藏在 UI 组件里。UI 负责显示,Tool 负责取数,Skill 负责解释与操作顺序,三者职责分开后才能同时服务会话和外部 AI。
按角色复用的清单
你在做「工具型」小程序(像 CURL)
- 权限能少则少:通常只要
net.fetch。 - 用
clipboardHints/contentMatchers把内容带进已声明路由,不在宿主里执行业务。 - 页面只调公开
wf.*;开发态与安装态同一套 API。 - 若需要会话摘要卡,单独编译 UI Module,完整页仍可保持 Sandbox。
- 不声明用不到的 login / vault / MCP expose。
你在做「业务域」小程序(像红火台)
wf.login→ 自己的业务会话 → 当前 app Vault;禁止宿主 Token 进包。- 路由表先于 Tool / 分享 / 卡片打开路径。
- query Tool:固定主机、
readOnly、成功条件、字段白名单、分页 meta、体积上限。 - 同一查询服务三种消费者:用户卡、Agent summary、MCP 字段投影。
- 业务口径(不可相加的账户、分页未完不算全集、大整数 ID 字符串)写进 Skill,不埋在 CSS。
- Surface 只做摘要;复杂操作
openApp回完整页。
两条共用
- 从 Sandbox 主线开始,没有嵌入需求就不要 UI Module。
- 权限按真实调用声明,写清
reason。 - 用安装包做最终验收,不要只信
npm run dev。
第三条路径 · 会话总结(IM + 宿主 AI)
与 CURL/红火台不同:不依赖你自己的 HTTPS 业务后端,而走宿主 IM 只读 + AI 补全:
| 步骤 | 做法 |
|---|---|
| 入口 | imEntries → 工具栏 popover |
| 数据 | wf.im.getHistory(im.chatContext) |
| 推理 | wf.ai.complete + onChunk(ai.complete) |
| 样板 | 仓库 packages/miniapps/im-context-demo |
适合「客户自写提示词、总结本群精彩片段」类需求;不要为此申请 ACP 或代发消息。→ 开发指南 · 阶梯旁路 · WF API
下一步
- 按档位搭自己的工程 → 上手阶梯
- 建同结构工程 → 开发指南 · 快速开始
- Manifest 字段 → 开发指南 · Manifest
wf.*→ WF API- 会话卡与建议 → 会话与 Surface
- 理念总览 → 设计理念