agentleFS
Practice019Practice019/agent-skills/dsh-plugin-development/SKILL.md

dsh-plugin-development

Skill0 starsChanged 2 months agoMITView on GitHub

在 DeepSeek Harness (DSH) 上创建、修改、调试动态 Cordis 插件的完整指南。涵盖插件的协议与生命周期(Plugin/Package/Run、Host 半与 Client 半、apply(ctx)/inject、服务与事件、Slot UI、主题、Client→Host 私有 RPC、动态 Tool、版本与审批)。当用户想给 DSH 写一个插件、扩展运行时能力、注册 UI 或动态工具、或修复/升级已有插件时,使用本技能。 Build, modify, debug, and extend dynamic Cordis plugins on DeepSeek Harness (DSH): plugin protocol and lifecycle (Plugin/Package/Run, Host and Client halves, apply(ctx)/inject, services and events, Slot UI, theme, Client-to-Host private RPC, dynamic Tools, versions and approval). Use when the user wants to write a DSH plugin, extend runtime capabilities, register UI or dynamic tools, or repair/upgrade an existing plugin.

  • Installs packages

5 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Files
SKILL.md

DSH 动态插件开发(Dynamic Cordis Plugin Development)

本技能教你如何在 DeepSeek Harness 上正确、完整地创建和使用动态 Cordis 插件。核心原则:先查实时接口,再写代码;写纯 JavaScript;每一个副作用都可逆。

动态插件只存在于当前 DSH 进程内存中,可在后续轮次保持活跃,但会在 cordis_stop/cordis_undefine、工具集卸载或 DSH 重启后消失。它不写磁盘、不产生插件文件、不改 cordis.yml,也不跨重启存续;要固化为正式插件,需走常规开发流程。

插件统一存放与文件夹规范(强制约定)

所有本地开发/固化到磁盘的 DSH 插件,统一存放在:

C:\Users\<用户名>\.dsh\profiles\web\plugins\

目录规范

每个插件一个独立文件夹,命名使用小写字母 + 中划线,例如:

C:\Users\<用户名>\.dsh\profiles\web\plugins\
├── doubao-dsh-plugin\
│   ├── package.json
│   ├── cordis.patch.yml
│   ├── index.mjs
│   └── README.md
└── <other-plugin>\
    ├── package.json
    ├── cordis.patch.yml
    ├── index.mjs
    └── README.md

要求:

  • 一个插件一个文件夹,禁止把多个插件混在同一个目录。
  • 禁止把本地插件源码直接放在 DSH profile 根目录、node_modules 或任意外部路径。
  • 插件文件夹内必须包含 package.json;如果作为 bundle 插件,还必须有 cordis.patch.yml。
  • 入口文件建议统一为 index.mjs(ESM),或按实际需要命名为 lib/index.js。

安装方式

在 profile 的 package.json 中,依赖统一写成:

"<plugin-name>": "file:./plugins/<plugin-name>"

然后执行:

cd C:\Users\<用户名>\.dsh\profiles\web
pnpm install

也可以使用 DSH 命令安装:

dsh plugin --profile web add ./plugins/<plugin-name>

开发与修改

  • 源码统一修改 plugins/<plugin-name> 内的文件。
  • node_modules/<plugin-name> 只是安装后的运行副本,不要把它当作长期开发目录。
  • 修改源码后需要重新同步:
cd C:\Users\<用户名>\.dsh\profiles\web
pnpm install

或:

dsh plugin --profile web add ./plugins/<plugin-name>

卸载/清理

  1. 从 C:\Users\<用户名>\.dsh\profiles\web\plugins\ 删除对应插件文件夹。
  2. 从 profile 的 package.json 中移除:
    • dependencies 里的 <plugin-name>
    • dsh.profile.bundles 里的 <plugin-name>
  3. 执行:
cd C:\Users\<用户名>\.dsh\profiles\web
pnpm install

动态插件与正式插件的关系

  • 通过 cordis_define 创建的动态插件只存在于内存,不写盘,不需要放到上面的 plugins 目录。
  • 当用户要求“固化/正式安装/重启后仍然存在”时,必须把插件落到:
C:\Users\<用户名>\.dsh\profiles\web\plugins\<plugin-name>\

并按照上面的规范安装。

核心概念

概念含义
Plugin(插件)稳定的实例,由 pluginId 标识(形如 dyn-<n>,或含你提交的 idPrefix)。可跨版本演进。
Package(包)一个不可变代码版本,由 packageId 标识。每次改代码 = 追加一个新 Package,绝不覆盖旧版本。
Run(运行)一次激活尝试,由 pluginRunId 标识,连接审批、加载、Run 卡片与错误。
Host 半在 DSH 的 Node.js 进程里运行。适合文件、命令、网络、Agent/Session、Host 服务/事件、动态 Tool、供 Client 调用的 JSON 方法。
Client 半在浏览器页面里运行。适合主题、布局、页面状态、Tool 卡片、Slot UI。
apply(ctx)插件的入口。Cordis 在其中注册生命周期贡献(服务、事件监听、Tool、Slot、样式等),并返回可被整体 dispose 的对象。

currentPackageId 是最新一次完全成功的版本(不代表正在运行);nextPackageId 是待审批/正在激活/最近失败的版本。

标准工作流

  1. 调用 cordis_inspect_list 获取当前 Host/Client 上注册的 Inspect Provider 及其方法、schema。
  2. 用最少的 cordis_inspect_query 读取你将用到的精确 Service、Event、Builtin、Slot、Theme token 或 Tool 契约。
  3. 新插件:设计第一个 Package。改现有插件:先用 cordis_inspect_self(pluginId, packageId) 读原代码与诊断。
  4. 在 code.host、code.client(或两者)中写纯 JavaScript,然后 cordis_define。
  5. 用 define 返回的 pluginId + packageId 调 cordis_run。
  6. 从 Run 卡片、steering 消息或 cordis_inspect_self 处理审批、等待、Client 加载与渲染失败。
  7. 临时停用用 cordis_stop;确认不再需要才用 cordis_undefine 永久删除。

同一轮里不要等待用户审批或异步浏览器结果。 cordis_run 返回 awaiting-approval 或 starting 后,结束当前工具流,等系统通过状态更新报告最终结果。

工具速查

工具用途不要
cordis_inspect_list一次列出当前 Host/Client Provider 与方法 schema硬编码 Provider 名、把清单当业务数据
cordis_inspect_query写码前确认精确的 Service/Event/Builtin/Slot/token/Tool 契约用它替代调用真实 Service;假设 Client 查询无需响应页面也能完成
cordis_inspect_self列出我的插件、读版本指针、读精确 Package 源码与诊断只为列清单就取全部源码;用它去修改/启动插件
cordis_define创建第一个版本,或给已有插件追加不可变 Package期望 define 会执行 apply、请求审批或更新 current
cordis_run激活一个精确 Package;run=首次/重启/回滚,update=切换版本用 run 隐式切换版本;把 pending/starting 当成成功
cordis_stop暂停当前副作用,保留 Package、授权与版本指针用 stop 表达永久删除
cordis_undefine永久删除插件及全部 Package、授权与历史视图在还需要回滚/检查/重启时调用

平台选择

需求平台先查
文件、命令、进程、网络Hostfs、shell、subprocess、web 于 Service.listService
Agent、会话数据、Host 生命周期Host相关 Service 与 Event.listEvents
注册下一步可被模型调用的动态 ToolHostharness 于 Builtin.listBuiltins,加 Tool.listTools
页面主题/布局/页面状态ClientTheme.listTokens 与 Client Service.listService
会话快照或 session/workspace 列表Client目标 Slot 的标准 props 与 owner props
设置页、侧边栏、输入区、覆盖层、Tool 卡片ClientSlots.listSubTree
Host 取数据、Client 展示两者Host Service + harness.handle;Client Slot + host.call

优先选择最靠近数据所有者的能力。Slot props 已给会话快照就别再经 Host 取;只改自身样式就别覆盖全局主题;只需一个小入口就别替换整块产品 UI。

执行环境

code.host 与 code.client 都是返回 Cordis 插件的普通 JavaScript 函数体,不经 TypeScript/JSX/bundler 转换。

禁止:

  • import、require、TypeScript 类型、as、装饰器、JSX;
  • 未经 Builtin.listBuiltins 确认的全局变量;
  • 臆测 window、document、process、Buffer、fetch、原生定时器。

Client 的 React 代码必须用 React.createElement(...),不能写 <div/>。

Host 半内建符号

  • ctx — 受限 Cordis 上下文:ctx.get(name)、ctx.on(name, listener)、ctx.provide(name, value)、ctx.effect(callback, label?)。
  • harness — Host 助手:
    • harness.handle(method, handler) — 注册包私有 Client→Host 方法。
    • harness.defineTool(definition) / harness.registerTool(ctx, tool) — 注册动态模型 Tool。
  • console(包标签日志)、btoa/atob(base64)、TextEncoder/TextDecoder。

Client 半内建符号

  • ctx — 同上(get/on/provide/effect)。
  • React — React.createElement / React.useState / React.useEffect(无 JSX)。
  • host — host.call(method, args?):包私有 JSON RPC,调用本包 Host 半。
  • styles — styles.insert(css):包私有样式表,随 Client run 清理。
  • console。

Client guard 门面:apply 收到的是真 fiber ctx 之上的白名单代理,只能拿到「返回的 plugin 自己在 inject 里声明」的服务。因此要用对象形态 { inject: ['slots'], apply(ctx) {} };裸函数 (ctx) => {} 没有声明位,拿不到任何服务。

Cordis 插件模型

读服务:默认用 ctx.get

可选能力用 ctx.get(name) 读取并处理缺失:

return {
  apply(ctx) {
    const service = ctx.get('serviceName')
    if (service === undefined) return
    service.someMethod()
  },
}

硬依赖:用 inject 声明

只有当一个服务是硬依赖、且插件应在服务出现后由 Cordis 重新激活时,才在插件对象上声明 inject:

return {
  inject: ['requiredService'],
  apply(ctx) {
    ctx.requiredService.someMethod()
  },
}

不要为了省 undefined 判断而滥用 inject;也不要未声明就访问 ctx.xxx(guard 会拒绝未声明依赖)。

生命周期:副作用必须可逆

每个贡献都要在 stop/update/remove 后被移除。优先用 Cordis 生命周期 API:

  • ctx.on(name, listener) 注册事件监听(返回 disposer)。
  • ctx.effect(callback) 持有返回 disposer 的外部订阅。
  • 保留 Service/Tool/Slot/timer/theme API 返回的 disposer。
  • 不要在 apply() 之外或模块作用域制造进程级/页面级副作用。
return {
  apply(ctx) {
    const service = ctx.get('serviceName')
    if (service === undefined) return
    ctx.effect(() => service.subscribe((value) => console.log(value)))
  },
}

定时器

两个平台的定时器都是名为 timer 的服务(不是内建符号)。先查对应平台的 Service.listService({ service: 'timer' }),并 inject: ['timer'] 后使用混入方法:ctx.timeout、ctx.interval、ctx.throttle、ctx.debounce。不要用原生 setTimeout/setInterval(全局不存在或被遮蔽)。

监听事件

先查 Event Provider 确认平台、参数顺序、返回值和 mode。

  • 普通 emit 事件:ctx.on('some/event', (payload) => {...})。
  • waterfall 事件的最后一个参数是 next,除非有意截断下游,必须调用并返回它:
return {
  apply(ctx) {
    ctx.on('some/waterfall', (payload, next) => {
      console.log(payload)
      return next()
    })
  },
}

提供服务

ctx.provide(name, value) 向运行时的 service 注册表发布一个服务(返回 disposer)。注意:动态包发布服务受同一注册规则约束,重复注册同名服务会抛错;除非你确认该服务归本包独占,否则不要轻易 provide。

Client UI:Slot 注册

先用 Slots.listSubTree(不带 root)选目标,再带 root 查精确 Slot 的完整契约再写注册。精确结果决定:

  • Slot 在布局中的用途;
  • 注册协议是 single / list / keyed / chain;
  • 注册选项(id/order/label,或 key,或 select);
  • scope 标准 props 与业务 owner props;
  • 当前占用者、替换风险与后代 Slot。

用 ctx.get('slots') 并处理缺失,再用 slots.inject 等 Slot 声明、在回调里 slots.register:

return {
  apply(ctx) {
    const slots = ctx.get('slots')
    if (slots === undefined) return
    slots.inject('target.slot', () => slots.register(
      { name: 'target.slot', id: 'my-view' },
      (props) => React.createElement('div', null, String(props.someValue)),
    ))
  },
}

不要臆测 id/key/selector/props;不要默认替换 root 级 root/sidebar/conversation/details(替换整个占用者会连它声明的后代 Slot 一起移除)。ctx.get('slots') 不需要注入;但 ctx.slots 只有在声明 inject: ['slots'] 时才能用。

常用 Slot 定位

  • 设置页:完整设置页通常注册 settings.section;单个通用偏好才用 settings.general.item。查实际 subtree、选项与 props,选最窄够用的入口。动态插件是临时的,设置 UI 无需持久化存储。
  • 会话/页面数据:会话作用域 Slot 常通过标准 props 提供 useSession/useSessions/useWorkspaces 等。优先直接用,别为已有数据加 Host RPC。
  • Cordis Run 专属面板:把交互 UI 放进最新 cordis_run 卡片,注册 tool.view.cordis 且 key: 'self':
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' },
      (props) => React.createElement('div', null, `Package ${props.packageId}`),
    ))
  },
}

self 在运行时绑定为 pluginId + packageId;不要在 key 里放 pluginRunId。

  • 普通 Tool 卡片:定制普通模型 Tool 的调用卡,查 tool.call.toolview,其 key 是 Tool 名。
  • 覆盖层/本地入口:toast/通知用 shell.overlay;侧边栏小按钮优先加法式内部 Slot 如 sidebar.footer.action;对话轮次后附加内容查 conversation.chat.turnTail(chain 协议)。

主题与样式

先定范围:

  1. 全局主题:先查 Theme.listTokens,再经 Client Service.listService 查 { service: 'theme' };按查询要求给每个覆盖提供 light/dark 两套值,保留返回的 disposer。
  2. 包自身组件:用 styles.insert(css),颜色优先用主题 CSS 变量。
  3. 新可见内容:先选 Slot,再决定本地 CSS 还是全局 token。

不要动 document.body、window 或硬编码产品 DOM 选择器。

Client → Host 私有 RPC

Host 用 harness.handle(method, handler) 注册包私有方法,Client 用 host.call(method, args) 调用。方向只有 Client→Host,且只能过 lossless JSON。

// Host 半
return {
  apply(ctx) {
    harness.handle('read-state', async (args) => ({ value: args.key }))
  },
}
// Client 半
return {
  async apply(ctx) {
    const result = await host.call('read-state', { key: 'demo' })
    console.log(result.value)
  },
}

不要跨线传函数、React 元素、类实例、Context、Service 等运行时对象;无返回数据时返回 null。不要注册公开 Remote Service,也不用 ctx.remote 做包私有通信。

注册动态模型 Tool

Host 用 harness 注册下一步可被模型调用的 Tool。先查 Host Builtin.listBuiltins 拿 harness 签名,再用 Tool.listTools 检查已有 Tool 名避免冲突。Tool 参数与返回值必须 JSON 兼容;execute 持有业务结果。Tool 注册必须归属当前插件 fiber,stop/update 后自动移除。

内部实时数据

Service 实例、Event 载荷、Slot props、Session/会话快照、Tool 状态等都是内部实时数据。

不要:

  • 对它们(或其子孙)JSON.stringify / structuredClone;
  • 递归枚举、整体复制或整体展示;
  • 把 Host 对象放进包的长时状态或 RPC 返回值。

只读当前功能需要的叶子字段,抽取最小标量值再构造自有 JSON。

版本、审批与修复

选择 cordis_run 的 mode:

当前状态目标mode
无 current该插件下任意 Packagerun
有 current同一个 Packagerun
有 current不同的 Packageupdate
update 失败nextPackageIdupdate 重试
update 失败currentPackageIdrun 回滚
  • 未授权的 Client Package 返回 awaiting-approval;单个勾只授权当前 Package,双勾授权该插件未来版本;授权在技术失败后仍保留。
  • 已授权返回 starting,在浏览器异步完成;starting 不等于成功。
  • 技术失败后:① cordis_inspect_self(pluginId, packageId) 读失败版本源码与诊断;② 若涉及未知能力,重新 list/query 对应 Provider;③ 在同一插件下定义新 Package(不覆盖失败版本);④ 用新 packageId + 正确 mode 再 run。
  • 用户拒绝审批后不要自动重试。失败的 update 不会自动恢复旧 Run,需要时显式 run current 回滚。

修改 @pluginId

用户用 @pluginId 指向某插件时,不要另建同名插件(注入上下文只含身份、版本指针、默认基础 Package,不含源码):

  1. 用 cordis_inspect_self(pluginId, packageId) 读基础 Package。
  2. 保留不需改的 Host/Client 半,只改目标代码。
  3. cordis_define 用 plugin.kind: 'existing' 与原 pluginId 追加 Package。
  4. 用返回的 packageId;有 current 时通常用 update 激活。

若引用不可用,说明插件已被移除、属于别的会话或进程重启后丢失;不要重建同名替代。

常见错误检查

失败首先检查
service "x" is not declared是否未声明 inject 就用了 ctx.x;改 ctx.get('x') + 缺失判断,或声明真正的硬依赖
cannot get property "timer" without inject查 timer 服务并声明 inject: ['timer']
Client 解析失败是否用了 JSX、TypeScript、import 或不可用全局
Slot 注册失败是否查了实时 subtree、Slot 存在、选项/key/selector 满足返回协议
UI 已加载但页面报错读 client-render 诊断与堆栈;错误归属某次 Run,定义新 Package 修复
host.call 失败Host handler 名、当前 pluginRunId、JSON 参数、handler 内真实服务依赖
update 失败保持 current/next 语义;修复 next 后 update,或 run current 回滚

关键服务/事件/Slot 索引(速查,写码前仍需实时查询)

这些名字来自当前运行时,写码前务必用 cordis_inspect_query 确认签名:

  • Host 服务(常用):fs(文件)、shell(bash 执行)、subprocess、web(搜索/抓取)、timer、tools(工具注册表)、agents、sessions、agentPresets、goals、jobs、skills、settings、llm、subagents、workflowEngine、workspaceRegistry、sandboxPolicy。
  • Client 服务:slots、theme、timer、layout、locale、sessions、workspaces。
  • Host 事件(类别):agent/*(created/disposed/error/pre-step/request/status/turn-stopping…)、tools/*(pre-execute/execute/post-execute/result…)、session/*、settings/*、llm/*、subagent/*、workflow/*、approval/request、fs/*、goal/changed、skills/change。多数含 waterfall 模式,需调 next()。
  • Client 事件:connection/reset、locale/change、slots/changed、theme/change。
  • Slot(常用):root、shell.overlay、sidebar、sidebar.footer.action、sidebar.workspaces、settings.section、settings.general.item、conversation、conversation.session、conversation.session.header.actions、conversation.composer、conversation.input.left/right/dock/overlay、conversation.chat.node、conversation.chat.turnTail、conversation.chat.assistant-actions、tool.call.toolview、tool.view.cordis、details。
  • 主题 token:--dsw-alias-*(bg/border/brand/label/state…)与 --dsw-specific-*,大多需 light+dark 两套值。

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.