L10 · 工程规范与毕业
学习指南 / 实战课程 / 第 10 课
Lesson 10 · 模块 C · 会改

工程规范与毕业实战:
发布你的插件。

会写插件只是半场,按工程规范交付才是全场。这一课收拢全课程的纪律: 分层测试怎么跑、文档与 Agent Note 怎么跟、高频坑怎么避开, 然后完成毕业项目——写一个完整插件,挂上 dsh-plugin topic 发布给全世界。

精读 20 分钟 练习 10 分钟 · 6 题 前置 见课程首页
学完你将能够
  • 说清四层测试的分工,记住 CI 覆盖率门禁是 pnpm run test:coverage 而非 test
  • 讲出「模型可见行为变更必须同 PR 补 keyless snapshot」的原因,以及双 SDK 投影的同步要求
  • 按规范同步文档、为非琐碎改动撰写 Agent Note,并尊重 archived notes 的冻结状态
  • 照 cookbook 完成毕业插件:选型、编写、测试、打包,挂 dsh-plugin topic 发布
01 · Testing Policy

测试政策:四层防线,各管一段

仓库的测试不是一堵墙,而是四层防线(docs/testing.zh.md 是权威文本)。 每一层回答一个不同的问题,互相不能替代:

  • 单元测试pnpm run test:vitest 跑各包 tests/** 与仓库脚本的 *.spec.ts,测试与所覆盖的代码放在一起。优先覆盖边界、错误路径、事件顺序与并发竞态。
  • 覆盖率门禁pnpm run test:coverage这才是 CI 的覆盖率门禁,对 packages/*/*/src 逐文件要求 100% 覆盖。未覆盖的行往往是门禁正确标记出的死代码——优先删除而不是补测试。行覆盖是必要条件,不是充分条件:它证明行被执行过,不证明功能按预期工作。
  • 真实 API e2epnpm run test:e2e:带 key 调用真实提供方。缺少密钥时套件自动跳过,keyless CI 保持绿色——这是设计,不是测试坏了。价值最高的是冒烟测试:启动真实示例、发一条提示词、检查外部世界。
  • 快照pnpm run test:snapshot:无密钥回放,固定对外行为。模型 transcript 变化时用 test:snapshot:record 重录,每处 diff 都要评审。

铁律:模型可见 ⟺ 必须有快照

每项非平凡的模型可见、协议可见或人类可见变更,都必须在同一个 PR 里, 通过真实可运行示例所属的快照套件添加或更新无密钥场景。 包测试、e2e 断言、mock fixture、PR 文字说明,都不能替代组装后应用的真实输出 transcript。 原因在 L6 已经学过:模型可见的一切都必须能从会话日志重建,快照套件就是这条铁律在 CI 上的化身。

还有一条容易漏的同步义务:TypeScript 与 Python 两个 SDK 各自独立投影 agent loop、 会话生命周期与 SessionEventMap——改动其中任何一项,就要同时更新 snapshots/sdk/(TS 客户端)和 scripts/snapshots/python-sdk-single-exe/(Python 客户端)两侧的预期输出。

动手试 · 5 分钟

在没有 API key 的状态下验证四层防线中 keyless 的部分全部绿(e2e 会自动跳过):

pnpm run typecheck && pnpm run test && pnpm run test:snapshot

有 key 时再补一刀真实 API:

pnpm run test:e2e
记忆锚点

pnpm run test 是日常开发循环;pnpm run test:coverage 才是 CI 覆盖率门禁。面试和 PR review 里把两者说混,是应届生式的错误。

02 · Docs & Agent Notes

文档与决策记录:代码改了,纸面也要改

这个仓库把文档当作代码的一部分:docs 随代码同改——受影响的 README、JSDoc 契约、 cookbook 步骤,与代码在同一个 PR 里更新。每个事实只有一个家园(one home per fact), 其余地方只放链接,这是 docs/AGENTS.md 文档标准的核心。

Agent Note:决策的存档

每项非琐碎改动 MUST 在同一 PR 附带至少一篇 Agent Note.agents/notes/README.md)。 「非琐碎」指改变了行为、架构、跨文件契约、流程工具、测试策略、磁盘/线上/配置格式—— 换句话说,任何维护者日后可能重新审视的决定。纯机械或局部修改才豁免。

  • 路径即状态:{lifecycle}/{class}/yyyy-mm-dd-topic-title.md。lifecycle 是 proposed/(评审中的提案)、implemented/(已落地的决定,用现在时描述现实并保持同步)、rejected/(被否决的提案)。
  • 每篇 note 必须记录 Alternatives considered——决策若不记下它击败了什么,就会引来重复争论。
  • archived/ 下的 notes 永久冻结:永不编辑、永不移动、不作现行行为的依据;文档门禁会跳过它们。想引用历史可以,想拿它当规范不行。

postmortem:用别人的事故长自己的判断

docs/postmortem/ 是唯一允许「战报叙事」的层级。强烈建议通读—— 例如 0001-acp-default-export-drops-inject.md:单元测试全绿、产品却是坏的, 因为 Loader 组合下默认导出替换掉了必需的具名导出而没有任何守卫能拦住。 正是这类事故催生了「测试真实入口路径」「冒烟测试优先」等规则。 读 postmortem 学的不只是这个仓库的历史,更是什么样的缺陷会穿过什么样的测试这种工程直觉。

社区现状

CONTRIBUTING.md 写明:项目早期暂不接受外部 PR,但生态完全敞开——写插件、挂 dsh-plugin topic、写博客、在社区答疑,都是正式的贡献方式。这正是第 04 节毕业项目的出口。

03 · Defensive Patterns

防御式模式与全课程高频坑复盘

docs/defensive-patterns.zh.md 是「差一点发布出去的缺陷」固化成的规则清单, 写生命周期、并发、子进程、清理代码前必读。五条要点速览:

  • 正交结果独立上报:进程可能超时却以退出码 0 结束。timedOutsignalexitCode 各自单独上报,绝不把一个标志嵌在另一个的分支里。
  • 异步状态不是同步状态agent.followup() 没有逐消息完成态;别把 agent/statuswhenIdle() 当作某次 followup 的结果——多条排队消息共享同一个 running 区间。自动化调用方要显式定义自己的区间(如:持久回执 → 下一次 idle)。
  • dispose 必须达到完全停稳:只发终止信号就返回会留下孤儿进程。清理要异步等待子进程真正退出,并在终止前关闭监听器注册表,让迟到的事件保持静默。
  • 在分发器中隔离回调异常:一个行为不当的监听器绝不能破坏核心生命周期或饿死排它后面的监听器。
  • 绝不把环境变量暴露给不可信输出:启动命令用清理过的环境,剔除 *KEY*/*SECRET*/*TOKEN*/*PASSWORD*;临时文件放 0700 私有目录、随机文件名、独占打开。

全课程三大高频坑,毕业前再钉一次

  • 跳过 Cordis 直接读码(L4 的坑):不懂插件生命周期、inject、四种事件派发,读任何子系统都会迷路。先补微内核,再读业务。
  • 直接改 loop(L7/L8 的坑):新行为挂在文档化扩展点上——extension-cookbook 的「功能→机制映射」证明每个产品功能都只是某个扩展点上的监听器,没有一行修改循环本身。改 agent-loop 是最后手段,且必须同步更新 docs/architecture.md
  • 写死应可配置的参数(本课新坑):部署相关的选择(端口、路径、阈值、开关)必须是带校验的 Config 字段,让用户能从 cordis.yml 改。在代码里写一个 DEFAULT_* 常量、或留一个测试钩子,都不算可配置性
陷阱 · DEFAULT_* 不算可配置

「我把所有默认值都集中成常量了,很干净」——评审不会买账。用户改不了的东西就不是配置。 正确姿势:声明带 schema 校验的 Config,给出用户大概率会保留的默认值, 其余交给用户在 profile 的 cordis.patch.yml 里覆盖你的整行 config(patch 按行替换整个 config 值,不是深度合并)。

04 · Graduation Project

毕业项目:写一个完整插件并发布

把模块 C 学到的东西一次用光。路线图:选型 → 编写 → 测试 → 打包 → 发布

第一步 · 选型

打开 docs/cookbook/extension-cookbook.zh.md 挑一种形态: 工具插件(ctx.tools.register())、钩子插件(tools/pre-execute 等 waterfall 上返回类型化决策)、 UI 插件(监听 session/event 渲染)、协议驱动(把协议对端接入 ctx.agents)、 模型适配器(registerAdapter,L9 已练过)。拿不准就选工具插件——最小、最完整、最容易测。

第二步 · 编写与测试

若要进官方仓库,照 docs/cookbook/adding-a-package.zh.md 的逐文件清单: package.json 不变式(private: truetype: module、cordis 同时进 peer/dev 依赖)、 tsconfig 注册、README 必备章节(Model Experience + Known Limitations)。 独立分发(推荐路径)则照 docs/user/develop/basic/publish.zh.md 打成组合包

hello-plugin/package.json —— dsh.bundle manifest 回答「这个包贡献什么」 { "name": "dsh-hello-plugin", "version": "0.1.0", "type": "module", "main": "index.js", "files": ["index.js", "cordis.patch.yml"], "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } }
# hello-plugin/cordis.patch.yml —— 插件行按包名引用,Node 才能解析到已安装的代码 - insert: - id: hello name: dsh-hello-plugin

测试按第 01 节的纪律来:包内单元测试打底;模型可见的行为补 keyless 快照场景; 再用真实入口路径验证——装进 profile 跑起来,而不是只在 ctx.plugin(...) 手工组装里绿。

动手试 · 10 分钟

把插件装进一个 profile 并验证层确实生效(在源码 checkout 里把 dsh 换成 pnpm dsh):

dsh plugin --profile demo add ./hello-plugin

先不启动,只看组合结果里出现了你的层:

dsh --profile demo --dump-config

输出里看到 # == dsh-hello-plugin 这一段,就说明组合包被正确加载了——然后再 dsh --profile demo 启动验证行为。

第三步 · 发布

  • 推到 GitHub,给仓库挂上 dsh-plugin topic——这是社区插件的集散地(github.com/topics/dsh-plugin),也是被社区认识最快的方式。
  • 用户可以直接 dsh plugin add github:you/hello-plugin 安装,但 git 安装拉的是源码,没有任何环节替你跑 build:TypeScript 包要么提供自包含的 prepare 脚本(pnpm 在 git 安装后运行;用户侧需在 profile 的 pnpm-workspace.yaml 里做 allowBuilds 授权),要么改发预构建产物——发布到 npm,或用 pnpm pack 交付 tarball,两者都不需要构建授权。
① 改代码 插件 + cordis.yml 组合 ② typecheck + test pnpm run typecheck / test ③ snapshot 模型可见变更同 PR 补快照 ④ 文档 + Agent Note README / JSDoc / note 同 PR ⑤ 发布 dsh-plugin GitHub topic 让社区发现 毕业开发循环 每一圈 = 一个完整交付 没有捷径的一圈:跳过任何一站,下一圈都会加倍偿还。
图 10-1 · 毕业开发循环:改代码 → typecheck/test → snapshot → 文档 + Agent Note → 发布 dsh-plugin,周而复始。
05 · What's Next

课程回顾与下一步

十课走完,回看这条路线:模块 A 会用(跑起来、配明白、接模型), 模块 B 读懂(微内核、事件、会话日志、turn 主线), 模块 C 会改(工具、适配器、工程规范)。 你现在已经拥有了这套框架最重要的世界观:一切皆插件,新行为挂在扩展点上,组合优于修改

终面:42 道深度问答

回到 deep-dive.html 做那场「终面」—— 42 道面试式问答覆盖全部课程知识点,先自己作答再展开对照。全部答得上来,就可以自信地说:熟练使用。

加入社区

  • GitHub Discussions(github.com/deepseek-ai/deepseek-harness/discussions):官方指定的反馈、bug 报告与讨论渠道,潜水的最佳起点。
  • Discord(discord.gg/Ycq5dCaS4):英文实时交流社区,适合快速提问、跟踪日常迭代。
  • 企微群:中文圈主阵地——到学习指南首页的「加入社区」一节扫企微小助手二维码、填问卷入群。
  • dsh-plugin topic:发布插件时挂上它,让全世界能找到你的作品。

建议的参与节奏:先潜水两周摸清大家关心的问题 → 用 cookbook 写一个小插件 → 挂 dsh-plugin topic 发布 → 想深入贡献时读根目录 CONTRIBUTING.md。 课程到此结束,仓库刚刚开始——Into the unknown.

06 · Quiz · 10 分钟

课堂练习

2 单选 + 2 多选 + 2 问答。选择即时批改,问答先写下你的答案再对照。成绩保存在本地浏览器。
单选
CI 的覆盖率门禁命令是哪一条?

正文第 01 节:覆盖率门禁是 pnpm run test:coverage,对 packages/*/*/src 逐文件 100% 覆盖。test 只是日常单元测试循环;test:e2e 是真实 API 测试(无 key 自动跳过);test:snapshot 是无密钥回放。四条命令四个用途,不能互相替代。

单选
你做了一个模型可见的行为变更(比如改了系统提示词的一个 section),测试上的硬性要求是?

正文第 01 节铁律:非平凡的模型可见、协议可见或人类可见变更,必须在同一 PR 里补/更新 keyless 快照。包测试、e2e 断言、mock fixture、PR 文字说明都不能替代组装后应用的真实输出 transcript——A、B、D 正好是政策点名不接受的替代品。

多选
向这套 harness 贡献一个插件时,哪些动作是合规的?(选出所有正确项)

B 错:DEFAULT_* 常量用户改不了,正文第 03 节明确说它不算可配置性——可配置性意味着 cordis.yml 里能改。E 错:文档必须与代码同 PR 更新(第 02 节),「之后补」正是文档腐烂的起点。A、C、D 分别是第 03、02 节写明的纪律。

多选
发布你的插件时,哪些姿势是正确的?(选出所有正确项)

D 错:publish.zh.md 明确「git 安装拉取的是源码,不是构建产物,没有任何环节运行你的 build 脚本」——所以才有 C 的两条出路(prepare 脚本 + 用户 allowBuilds 授权,或干脆发预构建产物)。A 是社区发现机制,B 是组合包的定义本身。

问答总结这门课教你的「扩展点优先」世界观。写下答案后点击对照

参考要点:① 一切皆插件——loop、工具、模型适配器、UI 都是可替换的插件,框架本身只是微内核加组合配置;② 新行为挂在文档化扩展点上(事件监听器、服务注册、waterfall 决策),而不是修改 loop——extension-cookbook 的「功能→机制映射」证明每个产品功能都只是扩展点上的监听器;③ 组合优于修改:cordis.yml + patch 就能替换插件树里的任何一行,用户无需 fork 源码;④ 改 agent-loop 是最后手段,且必须同步更新 architecture.md。这套世界观让你既能定制框架,又不失去跟随上游演进的能力。

问答描述你的毕业插件计划:做什么、挂哪个扩展点、如何测试、如何发布?(本课最后一道问答,作为你的毕业设计提交)写下答案后点击对照

参考要点:① 做什么:一个具体、小而完整的价值点(如一个领域工具、一个权限门禁钩子、一个状态栏 UI 插件),从 extension-cookbook 的五种形态中选定一种;② 扩展点:指明机制——ctx.tools.register()tools/pre-executesession/eventctx.agentsregisterAdapter,并说明部署相关取值走带校验的 Config 字段;③ 测试:包内单元测试打底、真实入口路径(装进 profile 跑起来)验证、模型可见行为补 keyless 快照;④ 发布:dsh.bundle manifest + cordis.patch.yml 打成组合包,推 GitHub 挂 dsh-plugin topic,TS 包提供自包含 prepare 或发 npm/tarball 预构建产物。能把四步说圆,你就毕业了。

本课小结

毕业课交付了四样东西:四层测试防线(记住 CI 门禁是 test:coverage)、 文档与 Agent Note 的同 PR 纪律(archived notes 冻结,不作现行依据)、 防御式模式与三大高频坑(尤其是 DEFAULT_* 不算可配置), 以及一条完整的发布路径(组合包 + dsh-plugin topic)。 接下来先去 第 11 课:维护者视角 看看上游如何运转——Remote API、双 aggregate、发版流水线都在那里; 然后到 deep-dive.html 完成 42 题终面,在社区里亮出你的插件。