实验性项目 WorkFly 完全由 AI 生成,可能存在缺陷或错误,仅供内部测试,非用友官方产品,不代表公司立场。
开发者中心开发指南

开发指南

从选择形态到安装验证的完整路径。先跑通 Sandbox 主线,再按真实需要增加 Tool、Skill 或宿主 Surface。

这份指南带你完成一个闭环:创建工程 → 写一个页面 → 调一次宿主 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/pathwf.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.pathwf.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 和多套风格。常规工具图标建议 16px1.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-smfull--shadow-smpanel--dur-14--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;UI wf.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.fetchvaultocr
runtime 首次调用询问,记住选择,可撤销 clipboardidentity.loginnotificationdevice.identifierim.openChat
operation 每次操作确认 schedule.writedevice.call
restricted 普通第三方不可申请 bip.sessionToken

wf.login() 只返回短期一次性 code,供第三方服务端换自己的业务会话;宿主 Token 不下发给小程序。MTL 兼容层服从同一权限表:用户资料首次询问,设备标识按 app id 隔离,宿主操作逐次确认;Token 兑换与远程代码注入接口直接拒绝。纯本地页内能力(如 wf.fireworks)无需权限。

贡献模型

wf SDK 是你调用宿主;contributes 则是你向宿主贡献 UI——让你的能力出现在会话、磁贴、Spotlight 等宿主表面。在 manifest.jsoncontributes 里声明:

  • 卡片渲染器(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.summaryexpose: 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 则把字段受限的实时投影同时写进 contentWorkflyCardProps.data.result。Agent 用摘要理解后续指代,不应逐字复述用户已经看见的卡片内容。外部 MCP 没有卡片通道,仍只返回查询结果。

cardRenderers[].suggestions 每张卡最多 4 条:label 为 1-14 个 Unicode 字符,text 可选且最多 200 个 Unicode 字符,actsend | 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: 0code: 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 / WorkflyWidgetPropshost.wf 消费公开能力,不得 import WorkFly 私有组件、Context、store 或 IPC。

workfly-react@1 的导出已经冻结:

specifier 公开导出
react defaultActivityChildrenComponentFragmentProfilerPureComponentStrictModeSuspenseactcachecacheSignalcaptureOwnerStackcloneElementcreateContextcreateElementcreateRefforwardRefisValidElementlazymemostartTransitionuseuseActionStateuseCallbackuseContextuseDebugValueuseDeferredValueuseEffectuseEffectEventuseIduseImperativeHandleuseInsertionEffectuseLayoutEffectuseMemouseOptimisticuseReduceruseRefuseStateuseSyncExternalStoreuseTransitionversion
react-dom defaultcreatePortalflushSyncpreconnectprefetchDNSpreinitpreinitModulepreloadpreloadModulerequestFormResetunstable_batchedUpdatesuseFormStateuseFormStatusversion
react-dom/client createRoothydrateRoot
react/jsx-runtime Fragmentjsxjsxs
react/jsx-dev-runtime FragmentjsxDEV
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)。iconStyleauto(默认)/ 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 工具里自己写,标准是一致的。


下一步