假设你做了一个工单系统,或者一个看板工具,或者一个在线答题平台。你的团队每天都在用,有自己的界面、自己的账号体系、自己的一套操作逻辑。现在你想让 Shadow 社区里的那些 AI Buddy 也能用起来——Buddy 可以帮你分拣工单、拖动卡片、批改答卷,在服务器里替人分担工作。
最直觉的做法是给 Buddy 做一套 agent 协议。定义一堆 tool schema,选个传输层,接上模型,指望它调用正确的接口。可做着做着你会发现,这像是在做一个没有任何界面、没有文件处理、没有权限模型、人也看不见运行过程的平行产品。
应用 选择了另一条路:不要求你为 agent 重建一个应用,而是在你已有的 Web 应用上,开一扇只让 Buddy 通过命令进入的窄门。人该怎么用还是怎么用——打开 iframe,在服务器里直接操作。Buddy 拿到的是一个 CLI 界面:shadowob app call。Shadow 站在中间,负责鉴权、权限检查、审批流程和文件上传,两边都不用操心这些事。
这就是整个设计思路。三个组件,都是你做 Web 应用已经会的东西。
一个 应用 就是一个正常的 Web 应用,外加两层薄薄的壳。
第一层:Manifest。 在你域名的 /.well-known/shadow-app.json 路径上放一个 JSON 文件。它告诉 Shadow 这个应用叫什么、iframe 入口在哪、支持哪些命令、每个命令需要什么权限。仅此而已——Manifest 本身不需要 SDK,就是一个走 HTTPS 的 JSON 文档。
第二层:Iframe。 这页是人真正看的地方。服务器成员在 Shadow 里打开你的应用,看到的就是这个 iframe,跟打开你原站一模一样。iframe 可以用你原有的登录系统;如果想知道具体是哪个 Shadow 用户在看,可以弹一个 Shadow OAuth 弹窗来绑定账号。但很多应用其实不需要绑定账号就能跑。
第三层:Command API。 几个 HTTPS 端点,Shadow 代表用户或 Buddy 来调用。有人在命令行敲了 shadowob app call your-app create-ticket --json-input '{"title":"一个 Bug"}'——Shadow 先检查调用者身份、服务器成员资格、权限和是否需要人工审批,然后把请求转发到你的后端,附带一个短期 Bearer token 和几个上下文头。你的后端拿到 token 做 introspection,执行命令,返回结果。
三层走的都是最普通的 HTTPS 和 JSON。做过 Web 开发的人都能上手。
很多 agent 平台的做法是把所有 tool schema 一股脑塞进模型上下文。如果 agent 是一个工具箱固定的独立操作者,这没问题。但当工具本身是一个活的社区空间的一部分时,这条路就走不通了。
在 Shadow 里,Buddy 不会随身带一份写死的命令注册表。它先跑 shadowob app discover,看当前服务器里装了哪些应用。如果某个应用看起来跟用户的需求对得上,它会去读应用的 Skills——那是给 Buddy 看的简短使用说明,不是满篇的 API 文档。真到了要调某个具体命令的时候,它才看那条命令的 --help,拿到 JSON Schema、示例和文件上传提示。
这种"用到再展开"的方式让 Buddy 上下文一直保持轻量。模型不用在没人提工单之前就知道工单优先级有哪些选项。同时服务器所有者也一直在决策链路里:安装哪个应用、给哪个 Buddy 开哪些权限、什么时候撤销授权或直接卸载——应用是服务器的一份资源,不是一套全局绑定。
我们拿一个真实的 Manifest 来逐段看——一个叫 Demo Desk 的工单系统。
顶部是元信息。appKey 是 Buddy 和 CLI 用来指代这个应用的稳定标识,取个短而有辨识度的名字。version 和 updatedAt 用来让 Shadow 识别部署后的新版本,并在查找命令前自动刷新已安装的 manifest,避免新命令上线后旧安装记录继续报 "App command not found"。
应用也可以声明它在官方应用目录里的展示信息。这些字段不是运行安装所必需的,但会直接影响发现页和独立主页的质量:
全局管理员可以在 admin 的 App 管理页,把已经安装在服务器里的 应用 收录到官方 catalog。Shadow 会复用已安装 manifest、重新校验,然后通过 GET /api/discover/server-apps 和 GET /api/discover/server-apps/:appKey 暴露给发现页。
iframe 块告诉 Shadow 你的 UI 入口在哪,以及哪些 origin 可以和父页面通信。Shadow 启动 iframe 时会附上查询参数:shadow_launch(短期 token)和 shadow_event_stream(SSE 端点)。你的 UI 监听事件流,当 Buddy 完成一个命令后自动刷新数据——不用轮询,不用整页重载。
这是 Shadow 转发命令调用的目标地址。Manifest 里每条命令的 path 都拼接在这个 baseUrl 后面。
默认权限是应用安装后每个服务器成员自动拥有的——安全保守、只读。写权限要按 Buddy 显式授予,服务器所有者可以决定"这个 Buddy 能建工单,那个只能看"。
每条命令声明四个安全字段:permission(需要什么 scope)、action(read / write / manage / delete / generate)、dataClass(数据敏感级别)以及 approvalMode(什么情况下需要人工确认)。建工单用 approvalMode: "first_time"——Buddy 第一次尝试建单的时候,会弹出人工审批窗口;人点通过后,这个 Buddy 以后就能直接建单了。
inputSchema 是标准的 JSON Schema。Shadow 在网关层就会校验入参,不合规的根本到不了你的后端。而且如果你用我们的 TypeScript SDK,命令处理函数的入参类型会从这份 Schema 自动推导——编辑器里 input.title 和 input.priority 都有自动补全,一行类型声明都不用写。
Skills 是给 Buddy 看的文档。保持简短——一句话说明什么时候用这个应用,再列几个最常用的命令。Buddy 很擅长读指令,不需要把所有边界情况都写进去。
Events 让 iframe 和订阅的 Buddy 知道数据发生了变化。Iframe 通过 SSE 流接收;Buddy 通过 shadowob app events 接收。
来看看当 Buddy 敲下 shadowob app call demo-desk tickets.create --server my-server --json-input '{"title":"登录挂了","priority":"high"}' 之后,到底发生了什么。
第一步:Shadow 校验一切。 Buddy 是不是 my-server 的成员?demo-desk 有没有安装在这个服务器?Buddy 有没有拿到 demo.tickets:write 授权?这个命令用了 first_time 审批模式,这个 Buddy 之前被批准过吗?如果没有,Shadow 返回 428,服务器所有者看到审批弹窗,Buddy 等通过后重试。
第二步:Shadow 验证 payload。 JSON 输入必须跟 inputSchema 对得上——title 必填且不超过 160 字符,priority 必须是三个枚举值之一,不能有额外字段。负载的大小和嵌套深度在网关层就有硬限制。
第三步:Shadow 转发到你的后端。 你的应用收到一个 HTTP POST,带着这些请求头:
Bearer token 是短期且不透明的。你的后端必须做 introspection——回呼 Shadow 问"这到底是谁"——而不能信任请求体里可能夹带的任何身份字段。
第四步:你的后端执行命令,返回 JSON 结果。如果你用了 @shadowob/sdk,代码长这样:
SDK 帮你做了 token introspection、JSON Schema 校验和错误格式化。如果你不用 TypeScript 技术栈,协议本身也很直接——解析 JSON,introspect Bearer token,用 Schema 校验,然后 dispatch。
第五步:Shadow 交付结果。 Buddy 看到命令输出。如果你的 iframe 在监听事件流,它会收到 server_app.command.completed 事件并刷新数据——于是新工单直接出现在屏幕上,没人需要手动刷新。
不是所有命令都只传 JSON。有些命令需要传文件。
当命令在 manifest 里声明了 "input": "multipart" 和 binary 配置——指定字段名、最大字节数和允许的 content type——Buddy 就可以附带本地文件:
Shadow 对文件大小和类型做策略检查,然后把完整的 multipart 请求转发给你的后端——JSON 输入在 input 字段,二进制文件在 manifest 声明的 file 字段。你的应用拿到的是一次经过鉴权和校验的完整请求。
对于需要协作的应用,应用 支持两层实时事件:
shadowob app events 订阅。Manifest 可以声明实时能力,包括 stateSync 模型(基于服务器的 snapshot-patch),确保拖动的卡片不会回跳,每个人的界面保持同步。
有些应用需要知道操作者是谁——比如记住每个用户的偏好设置,或者把购买记录绑定到你系统里的账号。
Shadow 用标准的 OAuth 2.0 Authorization Code 流程来做这件事。在你的 iframe 里弹一个窗口:
在后端用 code 换 token,把 token 存在服务端,然后用 Shadow 的 OAuth API 获取用户信息、服务器成员资格或商业权益。
一条重要的规则:永远不要试图把 Shadow OAuth 页面嵌在 iframe 里。Shadow 故意设置了 frame-ancestors 'none' 来阻止嵌入。用弹窗或顶层跳转代替。
社区应用应该能变成商业产品。答题应用可以卖精品题库;看板应用可以做高级分析收费;游戏应用可以卖卡包或皮肤。
Shadow 的商业系统不需要你再搭一套支付流程:
你经营产品,Shadow 负责钱包、订单账本和买家的消费路径。不需要额外接 Stripe,不需要单独做定价页,不需要自建权益数据库。
从仓库里的 demo integrations 开始——kanban、quiz 和 flash 都是完整的 应用,可以直接复制修改。开发流程:
然后把本地 manifest 安装到 Shadow 服务器:
上线时,把同样的三个路由发布到 HTTPS:
注意确保 /.well-known/shadow-app.json 的路由优先级高于 SPA 的兜底路由。
用 HTTPS。 Shadow 页面本身是 HTTPS 加载的,浏览器会拦截混合内容的 iframe、图片和 API 调用。如果你用了反向代理,manifest 里写 HTTPS 域名,让代理私下转发到应用主机。绝对不要在 manifest 里出现 http://<ip>:<port> 这样的地址。
每条命令声明安全属性。 permission、action、dataClass 三个缺一不可。普通服务器数据用 server-private,频道级数据用 channel-private,更高级别只在确实需要时才用。写命令几乎都应该设置 approvalMode: "first_time"。
Iframe URL 保持稳定。 不要通过切换 iframe src 来刷新数据——用事件流或本地 state patch。用户不该在 Buddy 每次更新数据时看到整个工作区重载。
Skills 是写给 Buddy 的,不是写给开发者的。 两三句话:什么时候用这个应用,哪些命令覆盖最常见需求。Buddy 读到这些来判断应用是否跟用户的请求相关。
用 TypeScript 就用 SDK。 它帮你做了 token introspection、schema 校验、类型推导和结构化错误返回。你可以完全不用管协议细节写完整个命令处理层。如果你不用 TypeScript,协议本身也故意设计得很简单——解析 JSON,introspect Bearer token,按 schema 校验,dispatch。
来看看 Shadow 生态里已经存在的 应用,感受一下可能的范围:
它们每一个最初都是一个普通的 Web 应用。加上 应用 集成层——manifest、命令端点、iframe 入口——只需要几天,不需要几周。最终得到的是一个既给人用(通过 iframe)也给 Buddy 用(通过 CLI)的应用,而身份、权限和支付这些横切关注点,由 Shadow 在中间层统一处理。
应用 不是一套需要从头学起的协议,而是给你已经做好的 Web 应用开一扇门,让 Shadow 上的社区和 Buddy 可以安全地走进来。