DSH 插件开发
把「DSH 插件」从选型做到发布:选形态 → 写代码 → 排错 → 打包 → 发布。入口只放执行时必需的东西(闸门 / 选型 / 路由 / 工作流 / 红线 / 合同 / 验收),细节一律在 references/,按需读取,不要一次全读。
定位:这是一个「用来开发 DSH 插件的技能」,它本身不是能装进 DSH 的插件。 它不会被
dsh plugin add安装,也不进入任何 DSH profile;本技能产出的那个dsh.bundle包才是能装进 DSH 的东西。找现成插件请去社区插件市场(references/12-community-plugins.md)。 已知边界与维护守则见references/15-skill-scope-and-maintenance.md(此处不重复)。
何时使用
- 用户要求开发 / 修改 / 调试 / 打包 / 发布 DSH(DeepSeek Harness)插件
- 出现这些词:DSH 插件、cordis 插件、
cordis.patch.yml、dsh.bundle、defineTool、ctx.slots、ctx.tools.register、PENDING - 用户要求审查现有 DSH 插件代码、判断该用哪种插件形态
- 用户问「为什么我的插件装上了但没反应」
- 用户报这些症状:插件不生效 / slot 崩溃 / 服务注入失败 / 改了代码不生效 / 样式被冲掉
不适用:要的是「装进 DSH 就能直接用的现成插件」——本技能不提供任何运行时能力、也不负责插件分发。
第 0 步(强制,不可跳过)· 版本闸门
references 是某一时刻的快照,会腐化。 DSH 处于 rc 阶段(基线 dsh-v0.2.1-alpha.3 / commit 2fbf4359b8,2026-10-14),tag 间隔中位数约 1.1 天、出现过破坏性变更。所以不要承诺「不过时」,要保证「过时会响」。
① 权威顺序(源码永远压过本技能)
1. 当前 DSH 源码 ← 唯一权威
2. 探针脚本对当前源码的核验结果 ← 可执行的证据
3. 本技能的 references(快照) ← 脚手架,不是权威
4. 模型记忆 / 常识 ← 禁止作为唯一依据
② 三问:① 我在对哪个版本写代码?(拿不到就先要版本,不知道版本不得断言 API)② 我要用的 API 属于 S/M/V 哪一级?③ 我能核验吗?(不能 → 走降级规则)
③ 跑探针(两个版本,优先 Python 版)
python <skill>/scripts/dsh-api-probe.py <DSH 仓库路径> # 推荐:零 coreutils 依赖,Windows 最稳
bash <skill>/scripts/dsh-api-probe.sh <DSH 仓库路径> # 备选:仅当 shell 有完整 coreutils
# 0 = 断言全部成立 1 = 有 STALE(不要照抄)
# 2 = 仓库路径不对 3 = 断言表为空(结果无效)
⚠️ shell 缺
cat/grep/dirname时,bash 版会报几十条假 STALE。拿不准就用 Python 版;两者不一致以 Python 版为准。 S/M/V:问「这条事实明天变了,我的代码会崩吗?」——会崩 → M/V,必须核验;不会崩 → S,可直接用。 降级规则(拿不到源码时):① 显式声明「以下 API 未对当前版本核验」;② S 级骨架照给,M/V 级符号单列成「需你确认」;③ 禁止把未核验事实写成肯定句。绝不编造 API 名填补空白。 目标版本高于基线时不要猜:dsh-sync.sh拉源码(先经确认)→ 探针核验 →dsh-version-diff.sh出差异。 完整机制(腐化推导、三级全表、五种核验手段、网络不可达时的降级路径)见references/00-version-gate.md。
基线 dsh-v0.2.1-alpha.3 的硬变化(照抄旧资料前先对照)
DSH 每次发版的破坏性变更集中在少数几类。本节只展开基线所属的最新一个区间;更早的 8 个区间(dsh-v0.1.5-rc.2 → 0.2.1-alpha.2)见 references/13-version-history.md §2。
0.2.1-alpha.2 → 0.2.1-alpha.3(852 个提交、1 个新 tag)—— 🔴 有破坏性变更,手上是 alpha.2 时代资料的人必须看完下面 15 条(🔴 硬失效 10 条 + ⚠️ 语义/行为收紧 5 条)。
🔴 本轮换判据了 —— 记住这条方法论:本区间
!:自报破坏性提交是 0 条(--all口径也是 0),实测却有 15 条破坏性项。所以本技能起改用上游自带的docs/upgrade-guide/v<版本>/<topic>/guide.md当主判据(每篇只讲一处破坏性变更,含## Change/## Migration;英文上限 500 词)。⚠️ 但目录名不可全信:上游约定「目录名 = 升级起点版本」(.agents/skills/dsh-doc/SKILL.md:71),而本区间新增的 14 个主题目录里有 4 个挂在更早的版本名下、描述的变化其实落在 alpha.3(作者先写指南、代码后合入发布线)。判「属不属于本区间」必须回到两端源码树验证 —— 只信目录名会把这 4 条整批漏掉。实测对照表见references/13-version-history.md§2.5。
- 🔴
llm-deepseek-account移除了配置键models:残留该键会让插件在加载期直接失败(报llm-deepseek-account: models is no longer configurable),Desktop / Web 在删掉它之前不显示任何账号模型。模型改由账号的GET /models列出(未加载前先给内置目录),逐模型微调只走新键modelOverrides(判据docs/upgrade-guide/v0.2.1-alpha.2/deepseek-account-model-catalog/guide.md)。 - 🔴
ctx.connection.operator改名browserPeer(packages/extensions/tool-cordis/src/api-catalog.ts:757,同一行号位置对照);OperatorPeer类型整族消失,读ctx.connection.operator的插件编译期即报错。admit()保留,另新增admitConnection()与createPeer(options)。 - 🔴
pluginManager.installBundle(spec)装了已装的包会硬失败:在跑 HMR 的 profile(官方 profile 都跑)里,改为在跑 pnpm 之前就失败 ——application: 'failed'+error.code: 'already-installed',且不改任何 profile 文件;重复装同一 spec 也不再返回changed: false。影响 Host API 调用方、@deepseek-ai/dsh-api-remotes/client与 agent 侧plugin_manager的install_bundle。 - 🔴 非 page 的 Connection 安装默认
interactions: 'none':该模式下不打开$events,$on()与世代恢复辅助$stream()不可用(生成的一元方法与裸流方法仍可用;浏览器无需重新认证)。 - 🔴 pi-ai 的 Azure provider 路由键
azure-openai-responses→azure:写死旧键的配置与模型选择不再命中(AZURE_OPENAI_*环境变量名不变)。 - 🔴
dsh-tool-subagent在加载期拒绝persona/toolFilter(含空值):packages/subagent/tool-subagent/src/index.ts:147-149直接抛tool-subagent: <field> is no longer supported; configure persona and tool plugins in an Agent Preset and select it with agentPreset (spawn only)。同批新增src/preset-selection.ts,Config改extends PresetSelectionConfig,由它提供agentPreset/allowedAgentPresets;fork 继承父 preset、不能再独立改 persona 或收窄工具。⚠️ 这不是「schemastery 静默合并未知键」 —— 上游专门加了显式拒绝,理由见.agents/notes/implemented/simplification/2026-10-09-preset-owned-subagent-composition.md:15(「仅删 schemastery 字段不会拒绝未知配置」)。 - 🔴
@deepseek-ai/dsh-session-persistence-jsonl取消了 default export:改为具名name/Config/apply的命名空间插件形式。默认导入、直接构造服务类、继承、instanceof、类类型标注全部失效;sessionPersistence服务 API、YAML 配置与已存文件不变。 - 🔴 JobId 换格式并删掉
executionId:注册表由<kind>-N(序号)改为<kind>-<short-uuid>(16 位小写 UUID hex,packages/jobs/jobs-local/src/index.ts:231);生命周期记录不再携带executionId,JobExecutionId品牌类型整族移除(docs/persistence-schema.json里成对的字段被删)。→ 用 kind + 序号自行拼 id、或按executionId配对 SDK 事件的代码失效(改按data.jobId配对)。 - 🔴
dsh-app-boot移除了HARNESS_SOURCE_SECTION/addHarnessSourceSection(旧packages/boot/app-boot/src/index.ts:1061;该文件区间 +266 / −69):checkout 路径与 Web GUI URL 这两个进程相关值从系统提示词移入运行时上下文,改由新官方包@deepseek-ai/dsh-harness-dev-context承担;dsh-host-webserver新增WebServer.getOrigin()/bindUrlHost(host)、dsh-web-app的webApp.getAdvertisedUrl()、dsh-system-prompt新增CONTEXT_ORDERS.HARNESS_SOURCE = 200/WEB_SURFACE = 210。动因值得记住:这两个值每次 Desktop 启动(--port 0随机端口)都会变,从而改写提示词头部;对非systemPromptUpdate: 'in-history'的路由(如手工声明的 pi-aianthropic-messages),Claude 会把历史 thinking 签名绑到会话前缀,于是上游回 400Invalid signature … bound to a different conversation、会话卡死。 - 🔴 客户端 UI 契约改名(5 个导出符号消失 → 4 个插槽增删):
ui-chat的RenderMessageImages/TURN_PROCESS_INDEPENDENT_KINDS/projectUserText、ui-primitives的UserTextReferences、api/terminal-controller的TerminalAttachmentId全树 0 命中;替代名为MessageImagesOwnerProps/MessageImageLoader/MessageImageSource/UserTextFacts/UserTextInput/TurnProcessChatData(packages/client/ui-chat/src/client/contract/slots.ts:11)。同批插槽:新增conversation.chat.message.images、conversation.chat.user.images、conversation.hero.options、conversation.user.text.node、workspace.toast(+5),旧键conversation.message.images改名走掉(−1)。 - ⚠️
DeepSeekAccount.watch(signal)语义收窄:原先「profile 首次给出或改变缓存 Platform 用户 ID」也会发快照,现在默认只发账号状态变更 —— 用它刷新getPlatformSession()的插件必须显式订阅身份变更(新增可选参includeIdentityChanges)。凭证替换与登录态变更仍通知全部订阅者。 - ⚠️ 凭证优先级反转 + headless 默认不再用存储凭证:
$DSH_HOME/.credentials.yaml的refs条目现在优先于同名导出环境变量(导出变量只在文件里没有该名字时才生效;$cwd/.env与$DSH_HOME/.env仍排在两者之后)。且随包的headlessprofile 关闭了凭证存储 / 授权 / DeepSeek 账号 provider → 只靠已保存密钥或登录态认证的 headless 作业会失败(Web / SDK / ACP 仍保留存储)。 - ⚠️
dsh --profile <name> --from-default-profile <template>由「拒绝」改为「删除并替换」:目标 profile 已存在时不再报profile "<name>" already exists,而是每次都把目标目录换成模板的全新副本,旧目录连同package.json依赖、已装node_modules、cordis.patch.yml一起被删除(官方 profile 名仍被拒绝作为目标)。⚠️ 该选项在 alpha.2 就已存在,本轮只改语义。 - ⚠️ Desktop 占用了字号快捷键:
Cmd/Ctrl+=(含Shift+=)与Cmd/Ctrl+-被桌面端征用调字号(文本、代码、终端一起),用这些组合(含重叠双键和弦)的自定义绑定仍保存但不再执行;Web 行为不变。 - ⚠️ 自定义 primary-runtime payload 的 python 解释器别名必须可重定位(macOS / Linux):
dependencies/python/bin/python与python3必须直接指向同级的、与runtime.json的 Python 主次版本同名的正则文件(如python3.12);旧式指向 payload 之外或经其它链接跳转的别名会在 payload 校验 / 工具调用期失败(Windows 不变;正则命令文件含 wheel 分发的仍受支持)。
其余为加法,未变项逐条实测:官方包 340 → 345(+5 / −0,移除数 0) —— 新增 dsh-api-remote-access、dsh-client-ui-secret-mask、dsh-cua、dsh-experimental-cua-macos、dsh-harness-dev-context(第 9 条的载体)。插槽计数由 129 / 131 / 约 94 改为 139 / 141 / 约 98,客户端权威目录 slot-catalog.ts 94 → 98。未变:SESSION_FORMAT_VERSION 仍 4;模型 ID 仍 deepseek-flash;参数 DSL packages/core/tools/src/schema.ts blob 2cc6f0cb 两端逐字相同;CLI 选项集合未变(--from-default-profile 早已存在);docs/postmortem/ 仍 4 篇;上游 skill 树 22 文件 / 4 个 skill 两端相同。⚠️ 两处「有内容但不动六维度」的改动:① 包内 Config 键 406 → 412(+10 / −4) —— 新增 collapseTiming、tool-fs.{read,readImage,write,edit}、ptc-runtime-node.executionMode、session-persistence-jsonl.listConcurrency、llm-deepseek-account.{modelsTimeoutMs,modelsMaxAgeMs,modelOverrides};移除 tool-subagent.{persona,toolFilter,allow,deny};② base 补丁 blob 9e3f03ee → ee2f8f28(+remote-access 一行、凭证来源注释改写);③ 包内非代码资源 43 → 45。
更早的 8 个区间(dsh-v0.1.5-rc.2 → 0.2.1-alpha.2)不在此处重复。 完整破坏性变更总表(按区间逐行)见 references/13-version-history.md §2;作者自报渠道 !: 的覆盖率实测与反例见同文件 §2.5。手上资料早于 0.2.1-alpha.2 时,先去读那两节再动手。
判据:三条来源都要看 —— 探针(M33M37 / N06N08 / P06P07 / V01 / X07 / U11U12 / X08~X09 就是这几批)、dsh-version-diff.sh 的六维度、以及上游自带的 docs/upgrade-guide/。
⚠️ 别指望 !::八个区间里非 merge 提交带 !: 的分别是 0 / 0 / 1 / 0 / 0 / 0 / 0 / 1 条,而最狠的整族改名一条都没标 —— ! 既非充分也非必要,判影响只看两端文件内容的集合 diff(完整实测与反例见 13-version-history.md §2.5)。
三条基础纪律
- 引用任何字段名 / 事件名 / 服务名 / 插槽名前,先查
references/08-cheatsheet.md或references/api-claims.md。 查不到就 grep 源码,不要凭印象写。 - 代码跑通前不要声称完成。「装上了」不等于「生效了」——必须看到实际输出或界面变化。
- 每条结论尽量附出处(文件路径 + 行号)。
决策流程
第 1 步 · 选形态(选错形态 = 后面全部返工)
| 形态 | 何时选 | 必须这样写 | 禁止 |
|---|---|---|---|
| 函数式插件 | 绝大多数情况:加工具 / 命令 / 事件监听 | 具名导出 name / inject / Config / apply |
绝对不能有 export default(会被当成服务类解析而失败) |
| 服务插件 | 要给别的插件提供能力,让别人 inject 你 |
export default class extends Service,构造函数首行 super(ctx, '服务名') |
写成普通函数导出 |
| 组合包 bundle | 只是把已有插件装到一起,纯配置 | 只需 package.json + cordis.patch.yml,声明 "dsh": {"bundle": {"patch": "./cordis.patch.yml"}} |
不要写 JS / TS,不要有 src/ |
UI 插件是函数式插件的特例,不是第四种形态——它只是多了一个「浏览器半侧」和若干登记点。判定流程见
references/01-mental-model.md。
第 2 步 · 按需求路由到模板
用户不会说「T3」或 defineTool,他说的是人话。照抄用户的说法在左列定位,再往右走:
| 用户这么说 | 模板 | 形态 | 注册在哪 | 怎么算跑通(冒烟判定) |
|---|---|---|---|---|
| 「把某个现成能力 / MCP server 挂进来」 | T1 | 组合包 | cordis.patch.yml(零代码) |
--dump-config 里出现 insert 行 |
| 「改一下某个现成插件的行为」 | T2 | 组合包 + 覆盖层 | 同上 + 补丁字段 | 同上,且被改的行为确实变了 |
| 「让模型会用某个能力」「加个工具」 | T3(首选)/ T4 | 函数式插件 | 宿主半侧 ctx.tools.register(defineTool({...})) |
真实会话里模型调用它并拿到结果 |
| 「加个 /命令」 | T5 | 函数式插件 | ctx.commands.register({...}) |
敲一次 /命令,有返回 |
| 「让用户能调参 / 改配置」 | T6 | 函数式插件 | export const Config |
改配置后行为变化;非法配置被拒 |
| 「给别的插件提供能力」「当基础设施」 | T7 | 服务插件 | extends Service / ctx.provide |
另一个插件能 inject 并调用成功 |
| 「网页界面上加个东西」 | T8 | 函数式插件 + 浏览器半侧 | ctx.slots.inject(key, () => ctx.slots.register({...}, 组件)) |
界面出现你的东西,无 slot 崩溃 |
| 「设置界面里加一张卡」 | T9 | 同上 | 设置命名空间 + settings.* 插槽 |
卡片可见、改动生效 |
| 「管住 Agent / 拦消息 / 改请求 / 加提示词」 | T10 | 函数式插件 | agent/pre-step、agent/request(waterfall) |
跑一次会话,行为确实被改 |
| 「接一个外部系统(HTTP / CLI / 协议)」 | T11(零代码优先 → T1) | 函数式插件 或 组合包 | 工具 + 事件钩子 | 真调一次外部系统,拿到真数据 |
| 「暴露一个接口给外面调」「加个健康检查」 | — | 函数式插件 | ctx.webServer.register({kind, path, handler}) |
curl 真打一次,看到 200 与预期响应体 |
| 「每天定时跑」「周期任务」 | — | 函数式插件 | ctx.interval(cb, delay)(不是 ctx.setInterval) |
等一个周期,看到任务真的跑了 |
| 「建一个规范的完整工程 / 准备发布」 | T12 | 工程骨架 | 全套约定 | 门禁脚本通过 + 真 profile 安装成功 |
| 「托管静态文件」「发布一个 DSH skill」 | ⚠️ 范围外 | — | — | 见 references/15-skill-scope-and-maintenance.md |
没在表里? 先按「实现位置」归属——改宿主行为 = 工具/命令/服务/事件;改界面 = 插槽;只是组装 = 补丁。归属清楚后照最近的模板改。
带 — 的两行(入站 HTTP / 定时任务)没有独立模板,完整写法见 references/14-inbound-http-and-timers.md;更细的路由展开见 references/01-mental-model.md §1.1 与 references/08-cheatsheet.md A.4,冲突时以 references 的逐字源码为准。
第 3 步 · 按难度阶梯选模板
不要从最难的开始。 先做能跑通的最小闭环,再逐级加复杂度:零代码组合包(T1/T2,低)→ 斜杠命令(T5,低)→ 工具插件(T3/T4,中)→ 可配置插件(T6,中)→ 服务插件(T7,中高)→ UI 与设置卡片(T8/T9,高)→ Agent 流程拦截(T10,高)→ 外部集成 / 完整工程骨架(T11/T12,高)。
12 个模板的完整代码、常用 UI 插槽名速查(节选,非全清单)在 references/02-templates.md;官方 7 类模板的逐字源码在 references/02b-official-templates.md。
标准工作流
按顺序执行,每一步都能独立验证。不要跳步,也不要在上一阶段未确认时抢跑下一阶段。
| # | 阶段 | 做什么 | 完成标志(未达到不得进入下一步) | 读哪个文件 |
|---|---|---|---|---|
| 0 | 版本闸门 | 强制:答三问 + 跑探针 | 探针退出码 0;或已显式声明哪些 API 未核验 | 00-version-gate.md |
| 1 | 选型 | 判定三种形态之一 + 路由到模板 | 形态与模板编号都写下来了,且没有 export default |
01-mental-model.md → 02-templates.md |
| 2 | 环境 | 版本对照、DSH_HOME 位置、从源码跑起来 |
dsh --profile <名字> --dump-config 能打出组合树 |
06-workflow.md ① |
| 3 | 最小闭环 | 先做一个能加载、能看到的空插件 | 装进真 profile 后有可见结果(哪怕只是一条命令返回) | 02-templates.md(T1 或 T3) |
| 4 | 冒烟 | 给每个能力面立起冒烟点并跑通 | 对照本文件「冒烟与验收」的判定表,逐项为真 | 本文件「冒烟与验收」 |
| 5 | 写功能 | 按 API 写工具 / 命令 / 配置 / 服务 | inject 覆盖全部用到的服务;需清理的副作用在 ctx.effect() 内 |
03-api-cookbook.md |
| 6 | UI(如需) | 两个半侧 + 三个登记点 + 插槽 | 界面出现你的东西,且控制台无 slot 崩溃 | 04-ui-and-slots.md |
| 7 | 调试 | --dump-config 查组合树;PENDING 审计查注入失败;HMR 免重启 |
没有 PENDING 条目、没有静默失败 | 06-workflow.md ④ |
| 8 | 打包 | 组合包结构、package.json、patch 引用、分发方式对比 |
合同不变量 5 条全过 | 06-workflow.md ③ |
| 9 | 安装验证 | dsh plugin --profile <名字> add <包>(--profile 必填) |
从最终分发源重装后仍通过冒烟 | 06-workflow.md ② |
| 10 | 工程化 | 命名、README 强制节、测试、门禁、提交规范 | 过 07-conventions.md 的门禁清单 |
07-conventions.md |
决策门的回退规则(失败先分类,别一律推倒重来):
- 合同级 / 形态级错误(导出形式、
inject、patch 声明、client 声明、名称三处不一致)→ 回第 1~2 步重新选型。在错形态上打补丁只会越补越乱。 - 业务级错误(逻辑、返回值、样式、报错信息)→ 留在第 5~7 步原地修。
环境降级:本机没有 Node / pnpm 时,仍要产出等价的完整文件,并在 docs/plan.md 里显式标注跳过了哪些可执行验证。不许把「跳过」写成「通过」。
冒烟与验收
原则:每个能力面,先配一个「一眼能看出真假」的冒烟点,再写业务。 新建插件时先立起三件套:一条命令(总能成功、输出可辨认)+ 一个真实调用(工具/集成类要让模型真调一次,看它拿到真数据)+ 一个可见标记(UI 类要在界面上一眼能看到)。
| 能力面 | 冒烟通过 = |
|---|---|
| 组合包 / 补丁 | --dump-config 里出现你的行,且改配置确实生效 |
| 命令 | 敲一次 /命令,有返回 |
| 工具 | 真实会话里模型调用它,拿到预期结果 |
| 服务 | 另一个插件 inject 后能调用成功 |
| 事件 / 拦截 | 触发一次事件,行为确实随之改变(不是静默无响应) |
| HTTP 路由 | curl 真打一次你注册的 {kind, path},看到 200 与预期响应体 |
| 定时任务 | 等过至少一个周期,看到任务真的执行 |
| UI / 设置卡片 | 组件可见可交互,控制台无 slot entry crashed、无重复 React 报错 |
| 配置 | 改配置后行为变化;给非法值能被明确拒绝 |
⚠️ 三条反假通过的纪律:① 必须在真实组合(真 profile、真加载)里验,本地另一份配置跑通不算;② 验收要指向用户实际访问的那个 URL / 那个 profile;③ 部分成功不能当成功返回——操作了一半要如实报错。
改完代码怎么生效(改错地方 = 白改)
| 改了什么 | 怎么生效 |
|---|---|
profile / home 下 cordis.patch.yml 里的 config |
自动热替换(框架卸载旧实例、加载新实例) |
--patch 指定的 overlay 文件 |
不会被热重载——改它必须重启进程 |
插件代码(src/) |
默认必须重启;想免重启要先开启 HMR(默认 disabled: true,且依赖 timer 插件,缺了会永久 PENDING) |
| 浏览器半侧(client) | 重新构建 → 重新安装 / 更新插件 → 刷新页面(client bundle 按 id 缓存) |
任何时候都只改
src/,不要手改lib/——lib/是构建产物,手改会在下次构建被覆盖。
合同不变量(写 package.json / patch / client 前必看)
DSH 里最常见的「我明明装了啊」有五种成因,全部落在这 5 条上:
package.json:type: "module"、main: "lib/index.js";exports["."]与exports["./package.json"]必备。- 组合包形态再加:
dsh.bundle.patch指向./cordis.patch.yml,且exports["./cordis.patch.yml"]与files都要有。 ⚠️ 少了dsh.bundle.patch,dsh plugin add只打一条declares no dsh.bundle警告然后什么都不做——新手第一大坑。 - UI 形态再加:
dsh.client.platform: "web"与exports["./client"]必须成对存在,缺一个客户端半侧根本不组装。 - 名称三处一致(铁律):
package.json#name= patch 的 insertid/name= 客户端 ModuleLoaderid。 - 禁止声明
@deepseek-ai/*依赖(DSH profile 已提供);客户端 bundle 里react、react/jsx-runtime、react-dom、react-dom/client必须 external(打进第二份 React 会导致 hooks 直接崩)。
逐字对照表、官方不变式原文、各形态 files 清单见 07-conventions.md 与 08-cheatsheet.md;客户端 bundle 的产物格式契约见 14-inbound-http-and-timers.md §17.3。
写代码前必读的八条红线
每条都来自官方事故复盘或社区真实提交,违反任何一条都会被用户打回:
- 绝不写
export default—— 函数式插件的头号杀手。 - 可选服务一律用
ctx.get('x'),不用ctx.x—— 后者仅限已声明inject的服务;未声明就访问会 PENDING 或直接崩。 - 手搓测试不算数 —— 必须用真实组合(真 profile、真加载)验证,不能只调函数单测。
!!js表达式只在config:与disabled:内求值 —— 写错位置会被当普通字符串。- 快照刷新 ≠ 正确性验证 —— 更新快照只是让测试通过,不代表行为正确。
- 验收要指向「用户实际访问的那个 URL / 那个 profile」 —— 本地另一份配置跑通不算。
- 别用 shell
&起后台进程 —— 用工具自己的后台机制,否则进程会随会话退出被杀。 - 部分成功不能当成功返回 —— 操作了一半要如实报错,不能返回 success。
最容易写错的一处(required 有两条路径,规则相反)
这是本套手册调研中发现并纠正的真实错误:
defineTool路径(DSH 自有 DSL):required写在属性级,值为字面量true。parameters: { questions: { type: 'array', required: true, description: '...' }, // ← 属性级 }- 裸
ctx.tools.register路径(标准 JSON Schema):required写在对象级,值为字符串数组required: ['a', 'b']。
两条路径规则相反,套错就静默失效。 完整源码证据见 05-pitfalls.md 的坑 P7。
排查顺序
只问局部问题(如「为什么我的 slot 崩溃」)时不要重跑全流程——直接按症状定位,改完只重跑受影响的验证。
- 插件装上了但完全没反应 →
06-workflow.md第 ④ 部分(--dump-config+ PENDING 审计) - 报错信息看不懂 →
05-pitfalls.md的「报错信息 / 现象对照表」 - 已知症状检索 →
05-pitfalls.md(23 条坑,按症状编号 P1~P22b) - 追根究底(原始社区提交、行号级出处)→
10-community-casebook.md(总索引,再进10a~10d对应方向分册)
端到端示例(一次完整的装配)
需求:「做一个插件,能敲一条命令查当前时间,也让模型能调用它。」
| 步 | 做什么 | 结果 |
|---|---|---|
| 0 | 版本闸门:确认目标版本 + 跑探针 | 探针退出码 0 |
| 1 | 选型:要「给人用」又要「给模型用」→ 都是函数式插件 | 不是服务插件、不是组合包 |
| 2 | 路由:T5(命令)+ T3(工具) | 两者可以并存于同一个 apply |
| 3 | 最小闭环:先只写命令,让它能加载、能敲 | export const inject = ['commands'] + apply 内注册 |
| 4 | 冒烟:装进真 profile,敲一次命令 | 有返回 = 链路通了 |
| 5 | 加第二个能力:工具走 ctx.tools.register(defineTool({...})),inject 补 'tools' |
两个能力共用一个 apply |
| 6 | 可选:加 export const Config,让用户能调时区 |
改配置后行为变化 |
| 7 | 打包 → 安装 → 从最终分发源重装验收 | dsh plugin --profile <名字> add <包> |
| 8 | 留痕:把上述决策与验证结果写进 docs/plan.md |
可回放、可交接 |
⚠️ 合并多个模板时只有两条规则:inject 取并集;需要清理的副作用包进 ctx.effect() 并返回 disposer(普通注册本身即 effect、会自动回收——见 01-mental-model.md §3.6,但显式 effect 更稳)。
交付前自检清单
逐项打勾;打不了勾的项就是还没做完。
- 形态选对了(函数式 / 服务 / 组合包),且没有任何
export default - 版本闸门已过:三问答完 + 探针退出码 0(或已按降级规则声明未核验项)
-
inject覆盖所有用到的服务;可选服务走ctx.get('x') - 需要清理的副作用在
ctx.effect()内并返回 disposer - 合同不变量 5 条全部满足(含名称三处一致)
- 每个能力面都有冒烟点,且在真 profile 里看到了实际输出 / 界面变化
- 没有手改
lib/(改的是src/,且跑过构建) - 打包产物齐全(
files含 patch 与 client;有静态资源也含) - 失败路径试过:给坏输入 / 缺服务时报错能指向原因,而不是静默成功
-
docs/plan.md已按实际决策勾选完成(模板:assets/plan-template.md)
与 AI Agent 结对开发
用户常让 Agent 代写插件代码。此时:给 Agent 的提示词模板见 09-agent-pairing.md(两套来源共 10 个可直接复制的模板);要求 Agent 每次附出处(文件 + 行号),无出处不予采纳;交付后按 07-conventions.md 的门禁清单验收。
资源索引与加载条件
默认只读本文件。 上面 10 个步骤全部可以只靠 SKILL.md 完成;references/ 是触发才读的加分项,不是必读项。
加载纪律(三条,先读再看表)
- 不要为「保险」通读
references/。 全量 1.6 万行 / 24 篇.md(口径:本SKILL.md+references/下全部.md,实测 15,994 行 / 24 篇(复现:wc -l SKILL.md references/*.md);中文字符 14.9 万 + 英文及数字词 8.4 万,后者 = 剥离 CJK 后的[^\W_]+词数;整个技能目录含 README 与assets/则为 29 篇 / 1.7 万行),通读既装不下也不必要——这是本技能最贵的误用方式。 - 按「读法」列执行,只有三种:
- 整读 —— 文件不大,直接整份读;
- 定点读 —— 先
grep -n '<锚点>' <文件>拿行号,再只读那一节(Read带offset/limit); - 禁止整读 —— 只允许 grep 命中后定点读,整读会挤爆上下文。
references/里有两类内容,别把第二类当第一类读:- 前段 = 面向任务的整理稿(编号小节,可直接照做);
- 后段 = 一手素材档案 —— 标题带「(原始素材)」或紧跟
<!-- ↓ 源:… -->标记,是整理稿的上游原始记录,比整理稿更细也更粗糙。只在你需要追查某条结论的原始出处时才读它,平时整段跳过。每个大文件头部都插了导航块,标出这段从哪开始。
references/(按主题组织,共 23 篇:编号 00~15 + api-claims + 6 个上游素材分册)
编号带字母后缀的(
02b、02c、10a~10d)是上游素材分册 —— 由超大引用文件按来源拆出, 内容为原文的逐行搬迁。它们不是整理稿:性质不一(既有官方文档逐字摘录,也有调研期粗笔记)。 ⚠️ 例外:10b-casebook-tools.md不是逐行搬迁(其余各册是)—— 它按来源政策定点改写:私有实现类条目不收录,通用教训以「自撰建议」形式保留。读它请连带读该册文件头的许可警示。
| 触发条件(满足才读) | 文件 | 读法 | 成本 |
|---|---|---|---|
| 写任何 DSH 代码之前,回答版本三问;探针报 STALE 后 | references/00-version-gate.md |
整读 | 中 |
| 拿不准某条事实属 S/M/V/N 哪一级、该怎么核验 | references/api-claims.md |
定点:grep -n '^### ' |
中 |
| 开始任何插件前;搞不清形态 / profile / 组合包加载机制 / 该选哪个模板 | references/01-mental-model.md |
整读 | 中 |
| 要抄代码时 | references/02-templates.md |
定点:grep -n '^## T[0-9]' 取 T1~T12(纯整理稿,1.2k 行) |
中 |
| 要逐字照抄官方 7 类模板的完整源码 | references/02b-official-templates.md |
定点:册内 grep -n '^## ' |
高 |
| 要一个社区零代码组合包全文范本 | references/02c-community-bundle.md |
整读(仅 255 行) | 低 |
| 写工具 / 配置 / 命令 / 事件 / 服务 / 终端功能 | references/03-api-cookbook.md |
定点:grep -n '^## ' |
高 |
| 做 UI 插件或设置卡片 | references/04-ui-and-slots.md |
定点:grep -n '^## ' |
中 |
| 写代码前通读整理稿;出问题按症状检索 | references/05-pitfalls.md |
整读整理稿段;grep -n '^### 坑 P' 定位单条 |
中 |
| 环境准备 / 安装与 CLI 机制 / 打包分发 / 调试排错(四部分按需选读) | references/06-workflow.md |
定点:grep -n '^## ' |
中 |
| 需要像官方包一样规范:README 强制节 / 门禁清单 / 命名 / 测试 | references/07-conventions.md |
定点:grep -n '^## ' |
中 |
| 随手查任何符号名(服务名 / 事件名 / 插槽名 / 命令 / CLI / 路径 / 参数 DSL) | references/08-cheatsheet.md |
定点:grep -n |
中 |
| 与 AI Agent 结对开发,需要可直接复制的提示词模板 | references/09-agent-pairing.md |
整读 | 低 |
| 追查某条结论的原始社区出处(先在此定位方向) | references/10-community-casebook.md |
整读索引(仅 38 行) | 低 |
| 已定位到 UI 方向,要读该方向原始素材 | references/10a-casebook-ui.md |
定点:grep -n '^# [ABCD] · ' 定位小节 |
高 |
| 已定位到工具 / 外部调用方向 | references/10b-casebook-tools.md |
定点:同上 | 高 |
| 已定位到服务 / 状态内核方向 | references/10c-casebook-services.md |
定点:同上(该册用二级标题) | 高 |
| 已定位到宿主 / 组合包方向 | references/10d-casebook-host-bundle.md |
定点:同上 | 高 |
| 查术语 / 素材代号对照(§A.2)/ 已知边界与官方文档矛盾 | references/11-glossary-and-provenance.md |
整读 | 低 |
| 选型参考;找可借鉴的社区高星插件 | references/12-community-plugins.md |
整读 | 中 |
| 目标 DSH 版本高于本技能基线时(否则不读);查 tag 版本史、破坏性变更 | references/13-version-history.md |
定点:grep -n '^## ' |
中 |
| 要暴露 HTTP 接口 / 写定时任务 / 搞清客户端产物格式 | references/14-inbound-http-and-timers.md |
整读 | 中 |
| 评估本技能是否适用 / 要修改本技能(已知边界 + 维护守则) | references/15-skill-scope-and-maintenance.md |
整读 | 低 |
「成本」= 一次装载的上下文代价,粗分低 / 中 / 高 / 极高。只有标「禁止整读」的那个才真的不能整读,其余按需选读即可。
assets/(可直接复制 / 改名,不进上下文)
| 何时复制 | 目录 | 形态 |
|---|---|---|
| 只是把已有插件装到一起 | assets/minimal-bundle/ |
零代码组合包(T1),0 行 JS |
| 要加工具 / 命令 / 事件(绝大多数情况) | assets/minimal-tool-plugin/ |
最小工具插件(T3),defineTool + 参数 Schema |
| 每次开发都建议做(决策留痕与交接) | assets/plan-template.md |
复制到插件项目的 docs/plan.md |
assets/是产出物不是文档:它们会被复制进用户工程,所以不要改成「说明本技能」的口吻,要写成「说明这个插件」的口吻。改动后必须过 WSL 真机核验(它们要被真实 DSH 组合器接受)。
scripts/(确定性工具,跑它而不是读它)
「必须跑的时机」列是硬要求——跳过对应核验会直接产出坏代码:
| 必须跑的时机 | 脚本 | 用法与退出码 |
|---|---|---|
| 写任何 DSH 代码之前(强制,与版本闸门是同一件事) | dsh-api-probe.py <仓库路径> |
核验 48 正向 + 8 反向断言。0=全成立 / 1=有 STALE(不要照抄)/ 2=路径错 / 3=断言表空(结果无效)。纯标准库、零外部命令依赖,首选 |
| 同上,但仅在 shell 有完整 coreutils 时 | dsh-api-probe.sh <仓库路径> |
断言表须与 .py 版同步;缺 grep 会报几十条假 STALE,拿不准就用 Python 版 |
| 用到入站 HTTP / 定时器 / 客户端产物 / 插槽任一项 | verify_absorbed_claims.py <仓库路径> |
49 条第二批断言。先跑 --selftest 证明它能失败,再跑正向 |
| 要写插槽名之前(不要凭表抄) | extract_slots.py <仓库路径> <skill目录> |
从源码抽权威插槽清单并双向 diff |
| 改过本技能的任何文档或资源之后 | check_refs.py <skill目录> |
悬空引用 / 生成期素材路径 / 本机绝对路径;0=全部可在技能内解析 |
改动任何来源标注 / 许可声明 / NOTICE / licenses/ 之后(发版前必跑) |
check_oss_license.sh(在技能根目录跑) |
5 项:copyleft 关键词扫描(只对未声明的文件判失败)、五个声明文件齐备(LICENSE · NOTICE · licenses/ 下 Apache-2.0 / MIT / BSD-3-Clause 三份正文)、LICENSE 来源表与 NOTICE 条目数一致、无「待逐字核取」占位符、NOTICE 行尾为 LF。0=PASS 可对外分发。它是「已逐文件核实」这句话的可复现证据 |
| 目标版本高于基线,且需要本地源码 | dsh-sync.sh |
把 DSH 源码拉进技能目录(vendor/dsh-src/)。首次约 200 MB,必须先经开发者确认;绝不执行 pnpm install |
| 目标版本高于基线,要看上游到底改了什么 | dsh-version-diff.sh <仓库路径> |
六维度差异 + 作者自报的 !: 破坏性提交;输出可追加到 13-version-history.md |
| 质疑「API 名是否稳定」这类历史结论时 | dsh-tag-matrix.sh <仓库路径> [out.tsv] |
对全部 tag 逐条核验 API 面 |
检索配方(不要整读,先检索)
grep -rn "ctx.slots.inject" references/ # 某个 API 名出现在哪
grep -n '^## T[0-9]' references/02-templates.md # 定位某个模板(T1~T12)
grep -n -A 40 "^### 坑 P7" references/05-pitfalls.md # 某个坑的全文(按编号)
grep -rn '^#\{1,2\} [ABCD] · 一、' references/ # 四个素材方向各有哪一册(档案册)
grep -n '(原始素材)' references/05-pitfalls.md # 找「整理稿 → 上游素材」的分界
