
字节笔记本
2026年10月6日 · 约 16 分钟读完
先查接口再写代码:Cordis 动态插件开发全流程
在 Cordis 这类 agent 运行时里,动态插件是一种特别的扩展形态:它不随宿主一起打包发布,而是在运行时由模型写代码、经用户审批后注入进程。开发这类插件有一条铁律放在所有步骤之前:先确定能力该放在 Host 还是 Client,再查询真实接口。永远不要凭 Service 名称、Event 载荷、Slot props、主题 token 或一段示例代码去推断完整 API。
标准工作流:七步走完一个插件

- 调
cordis_inspect_list,一次性拿到 Host 与 Client 两端当前注册的 Provider、方法和 schema; - 用最少数量的
cordis_inspect_query调用,读取即将用到的 Service、Event、Builtin、Slot、主题 token 或 Tool 的精确定义; - 新插件设计首个 Package;改老插件先调
cordis_inspect_self读回基础源码和运行诊断; - 在
code.host、code.client或两者里写普通 JavaScript,然后cordis_define; - 用 define 返回的
pluginId和packageId调cordis_run激活; - 从 Run 卡片、steering 消息或
inspect_self里处理审批、等待、Client 加载与渲染失败; cordis_stop临时停用,cordis_undefine只在确认不再需要时才用。
有一条纪律最容易被忽略:不要在同一个回合里干等审批或异步的浏览器结果。cordis_run 返回 awaiting-approval 或 starting 之后,正确动作是结束当前工具调用流,把最终结果交给系统的状态更新来回报。
七个工具,各管一段
| 工具 | 该用它做什么 | 别拿它做什么 |
|---|---|---|
cordis_inspect_list | 一次调用发现两端 Provider 与方法 schema,能力目录变化后刷新 | 硬编码 Provider 名;把清单当业务数据 |
cordis_inspect_query | 写代码前确认 Service 方法、Event 模式、Slot、token 与 Tool schema | 代替插件真实调 Service;假设 Client 查询必然有页面应答 |
cordis_inspect_self | 列出插件、看版本指针、读 Package 源码与诊断 | 为拼一个列表拉取全部源码 |
cordis_define | 创建首版或给现有插件追加不可变 Package,先让用户预览 | 期待 define 会执行 apply 或请求审批 |
cordis_run | 激活指定 Package:首次、重启、回滚用 run,换版本用 update | 用 run 隐式切版本;把 pending 或 starting 当成功 |
cordis_stop | 暂停当前效果,保留 Package、授权与版本指针 | 把 stop 当永久删除 |
cordis_undefine | 彻底移除插件与全部 Package,并清掉历史业务视图 | 回滚、检查或重启还需要时调用 |
还有一条贯穿性提醒:Provider 名称、方法和入参必须来自当前 list 的结果。Service 与 Event 目录描述的是这个版本允许哪些接口,不保证某个 Service 此刻已挂载;运行时要用真实 Service,而不是缓存或展示目录查询结果。
能力放 Host 还是 Client

判断标准是数据归谁管。文件、命令、进程、网络,agent、持久会话数据和宿主生命周期,以及要在下一步模型调用里生效的动态 Tool,归 Host;页面主题、布局与当前页面状态,会话快照与工作区列表,设置页、侧栏、输入区、覆盖层和工具卡片,归 Client;在 Host 取数、在 Client 展示的组合需求,用 Host Service 加 Client Slot 两头接起来。
原则是选离数据所有者最近的能力:Slot props 已经给出会话快照,就不要再绕道 Host 拉一遍;只改 Package 自己的样式,就不要覆盖全局主题;只需要一个小入口,就不要替换整块产品 UI 区域。
执行环境的硬约束
code.host 和 code.client 都是普通 JavaScript 函数体,返回一个插件对象,不经过 TypeScript、JSX 或任何打包器编译。具体来说:不能写 import、require、TS 类型、as、装饰器和 JSX;不能用未经 Builtin.listBuiltins 确认的全局;不许臆测 window、document、process、Buffer、fetch 和原生定时器的存在。Client 端写 React 必须用 React.createElement。
// 正确:UI 注册进查询过的 Slot
return {
apply(ctx) {
const slots = ctx.get('slots')
if (slots === undefined) return
slots.inject('tool.view.cordis', () => slots.register(
{ name: 'tool.view.cordis', key: 'self' },
() => React.createElement('div', null, 'Hello'),
))
},
}// 错误:apply 里直接返回 JSX 元素
return {
apply(ctx) {
return <div>Hello</div>
},
}后者的错误不只是 JSX 语法:apply() 注册的是生命周期贡献,不能把 React 元素当插件结果返回,UI 必须注册进一个查询过的 Slot。
Service、副作用与定时器
读可选能力默认用 ctx.get(name) 并自己处理缺省;只有硬依赖才声明 inject,声明后 Service 未就绪时插件进入等待,Cordis 会在它恢复后重新激活。不要为了省一个 undefined 判断就滥用 inject;反过来,没声明注入就直接访问 ctx.requiredService,Guard 会拒绝未声明的依赖。
副作用管理的原则:插件被停止、更新或移除后,每个贡献都必须被摘干净。用 ctx.on() 注册事件监听,用 ctx.effect() 持有返回 disposer 的外部订阅,Service、Tool、Slot、定时器和主题 API 返回的 disposer 都要留住;不要在 apply() 之外制造进程级或页面级副作用。如果 Service 的 subscribe() 不返回 disposer,先查询它提供什么清理机制,别假设卸载会自动移除第三方回调。
定时器在两端都是名为 timer 的 Service,接口一致,但不是 Builtin。用之前先在对应平台的 Service.listService 里查询,并声明 inject: ['timer'],然后才能用 ctx.timeout 和 ctx.interval。直接写 setTimeout 没有用,这个全局根本不存在。
事件监听也要先查 Event Provider 确认平台、参数顺序、返回值和 mode。普通 emit 事件直接 ctx.on 收载荷;Waterfall 事件的最后一个参数是 next,除非有意中断下游处理,监听器必须调用并返回它。
在 Client 端长出界面
注册 UI 是固定的三段式:先不带 root 调 Slots.listSubTree,从紧凑的用途与拓扑树里挑目标;再带精确 root 查这个 Slot 的完整契约,包括注册协议是 single、list、keyed 还是 chain、注册选项、标准 props 与业务 owner props、现有占用者和替换风险;最后 ctx.get('slots') 加 slots.inject 等到 Slot 声明,在回调里 slots.register。不要在查询协议前瞎猜 id、key、选择器或 props,也别默认去抢 root、sidebar、conversation、details 这类根级 Slot:整体替换一个占用者,会把它声明的子孙 Slot 一起带走。
几个常用落点:完整设置页走 settings.section 拿完整内容区,settings.general.item 只适合单条紧凑偏好;动态插件是临时的、进程内的,设置 UI 不需要持久化存储,交互状态放在内存里跟随插件生命周期即可。toast 和全局覆盖层先查 shell.overlay,注意它的指针事件与层叠顺序规则;侧栏小动作优先 sidebar.footer.action 这类内嵌 Slot;会话回合后的补充内容查 conversation.chat.turnTail,按返回的 chain 选择器注册。要把交互 UI 放进最新一张 run 卡片,就用 tool.view.cordis 配 key: 'self':运行时 self 绑定 pluginId 加 packageId,同一个 Package 多次运行时最新卡片承载 UI,旧卡片自动降级。自定义普通工具卡片则查 tool.call.toolview,key 是工具名,注册已有 key 可能顶掉产品默认卡片。
主题改动先分清范围:改全局主题,先查 Theme.listTokens,再经 Client 的 Service.listService 查 theme 服务,按查询要求同时提供亮暗两套值并留住 disposer;只改 Package 自己的组件,用 styles.insert(css) 并优先用主题 CSS 变量。不要碰 document.body、window 或硬编码的产品 DOM 选择器,主题服务改 token 但不创建 UI,Slot 创建 UI 但不取代主题系统。
跨端调用与动态 Tool
Client 调 Host 走 Package 私有通道:Host 端用 harness.handle(method, handler) 注册方法,Client 端用 host.call(method, args) 调用。参数与返回值必须是无损 JSON,函数、React 元素、类实例、Context、Service 这些运行时对象一律不能过线,没有返回数据就返回 null。Package 私有通信不要注册公共 Remote Service,也不要用 ctx.remote。
Host 也能用 harness 注册动态模型 Tool,下一步模型调用就能用上。先经 Host 的 Builtin.listBuiltins 查当前 harness 签名,再用 Tool.listTools 看现有工具名和 schema,避免撞名。Tool 的参数和返回值同样必须 JSON 兼容;execute 承载业务结果,render 与 presentation 只承载模型和原生 UI 看得见的内容;注册归属当前插件 Fiber,stop 或 update 后自动移除。
内部活跃数据的三条红线
Service 实例、Event 载荷、Slot props、Session 与会话快照、Tool 状态,这些是运行时的内部活跃数据,有三条红线:不能对它们或其子对象调 JSON.stringify 或 structuredClone;不能递归枚举、整体复制或整对象展示;不能把它们放进插件的长期状态或 RPC 返回值。正确姿势是只读当前功能需要的叶子字段,先抽出最小的字符串、数字、布尔值,再构造自有 JSON。
版本、审批与修复
三个 ID 划清边界:pluginId 是稳定实例;packageId 是不可变代码版本;pluginRunId 属于每一次激活尝试。currentPackageId 是最近一次成功的版本,但不代表插件正在运行;nextPackageId 是正在等审批、激活中、等 Client 激活或最近失败的候选。
run 模式照状态选:没有 current,对插件下任意 Package 用 run;有 current 且目标是同一个 Package,用 run;目标是不同 Package,用 update;update 失败后重试 next 还是 update;要回到 currentPackageId 就用 run 回滚。
审批语义要记牢:未授权的 Client Package 返回 awaiting-approval,勾一次只授权当前 Package,勾两次授权该插件之后的版本;技术性运行失败不会撤销已给的授权;授权过的 Package 返回 starting,在浏览器里异步完成。
技术性失败后的修复是固定四步:用 cordis_inspect_self 读失败版本的源码和精确诊断;报错涉及未知能力,就重新 list 并 query 对应 Provider;在同一插件下 define 一个新 Package,不要覆盖失败的 Package;用新的 packageId 和正确模式再 run。用户拒绝审批后不要自动重试;update 失败也不会自动恢复旧的物理运行,需要回到旧版就显式 run current。
最后是一张故障速查表,出问题时按顺序对号入座:
| 报错或现象 | 先查什么 |
|---|---|
service "x" is not declared | 是否没声明 inject: ['x'] 就用 ctx.x;改用 ctx.get('x') 加缺省检查,或声明真硬依赖 |
cannot get property "timer" without inject | 查 timer Service 并声明 inject: ['timer'] |
| Client 解析失败 | 是否用了 JSX、TS、import 或不存在的全局 |
| Slot 注册失败 | 是否查过实时子树、Slot 是否存在、选项与 key 或选择器是否符合协议 |
| UI 加载但页面报错 | 看 client-render 诊断与堆栈;错误属于某次精确 Run,define 新 Package 修复 |
host.call 失败 | Host 方法名、当前 pluginRunId、JSON 参数与 handler 里的真实 Service 依赖 |
| update 失败 | 守住 current 与 next 语义:修好 next 再 update,或 run current 回滚 |



