第 01 章 先把它跑起来:数字校准与插件树全景
代码基线:commit
8f86b979a1(2026-10-01)|0.2.0-rc.1| 本系列讲次:M0
本章导读
这一章要解决一个很朴素的问题:你在读什么?
代码基线是 commit 8f86b979a1、版本 0.2.0-rc.1。这个仓库有 328 个包、35.6 万行 TypeScript、2 万多次提交。任何人第一次打开它都会晕——不是难,是大。所以第一讲不讲任何机制,只做三件事:把数字校准、把运行时摊开给你看、把最容易混淆的一组概念分开。
具体做三件事。其一,数字校准。 你手上那份《黑鲸出,见全貌》的书稿写于 v0.1.x 时期,它说"219 个包、五十万行"——这是当时的真话,现在既不是真的、也不重要了。真正重要的是建立一条纪律:任何数字主张都必须标注基线版本,否则它会随版本腐坏,而读者无法察觉。
其二,把插件树摊开。 --dump-config 会打印你这台机器此刻真正会启动的东西,每一层都标注了谁 patch 了它。你自己的机器上有 14 层,官方 bundle、第三方插件、你自己写的补丁,在 YAML 里的待遇完全一样。这不是修辞,是可以逐行验证的事实。
其三,分清 profile 与 preset。 这是全书第一个真正的概念陷阱。profile 回答"这个进程由什么组成"(5 个),preset 回答"这一个 Agent 由什么组成"(4 个),二者正交。书稿把它们混成了"四种模式",而这个混淆有实际后果:以为"切 PTC 模式"和"用 headless 启动"是同一层级的操作。
学习要点
- 任何规模数字,先问"哪个版本、怎么算的、能不能复现"
- 亲手 dump 一次运行时配置,比读十页架构文档更有用
- 遇到"官方/第三方/自己地位平等"这种说法,去 YAML 层验证它是不是字面成立
- profile 与 preset 是正交的两层,不是同一个列表
本章目标
- 把"dsh 到底有多大"从印象变成可复算的数字
- 亲眼看见这台机器此刻会启动的那棵插件树
- 分清 profile 与 preset —— 书稿在这里把两个东西混成了一团
一、回读书稿 §1.4 / §1.7 的原文主张
书稿第一章给了三条硬主张,M0 要逐条验:
| # | 书稿主张 | 出处 |
|---|---|---|
| A | "大约 219 个 npm 包" | §1.4 |
| B | "TypeScript 源码规模在 五十万行以上" | §1.4 |
| C | "web 和 headless 是官方随附的两个模板 profile" |
§1.7 |
| D | "Base、web-app、headless 是三个基础 Bundle" | §1.7 |
| E | "官方提供的四种模式:standard / minimal / PTC / creative,本质是四份不同的插件配置清单" | §1.6 |
二、数字校准:实测 vs 书稿
export PATH="/opt/homebrew/bin:$PATH" # 本机 node 26.8.1 / pnpm 11.7.0 不在默认 PATH
find packages -maxdepth 3 -name package.json -not -path '*/node_modules/*' | wc -l
find packages -path '*/src/*.ts' -not -path '*/node_modules/*' | xargs wc -l | tail -1
node -p "require('./package.json').version"
git log -1 --format='%h %ad %s' --date=short
| 指标 | 书稿(v0.1.x 时期) | 实测(0.2.0-rc.1) | 结论 |
|---|---|---|---|
| npm 包数量 | ~219 | 328 | 涨了 109 个,+50% |
| packages 源码 TS 行数 | "五十万行以上" | 356,402 行 | 书稿偏高 |
| 版本 | 未标注 | 0.2.0-rc.1 |
已跨一个大版本 |
| 仓库提交 | — | 20,526 commits | 首提交 2026-06-10 |
读法:书稿 §1.4 的"219 个包 / 五十万行"是当时的真话,现在既不是真的、也不重要了。真正重要的是下面这条规模判断仍然成立——这是一个需要数百人-月维护的严谨工程,而且包数量还在以每版上百的速度增长。
⚠️ 顺带提醒:书稿 §1.4 提到的
code-runtime组、self-modification包名,在 0.2.0-rc.1 里对应的是ptc-runtime与extensions。这类命名漂移后面还会遇到很多次。
三、五个 Profile 实测(本章核心材料)
pnpm dsh --profile <name> --dump-config
| profile | 配置行数 | 唯一插件数 | 定位 |
|---|---|---|---|
web |
214 | 206 | 浏览器应用(含全部 client-ui-*) |
sdk |
98 | 97 | JSON-RPC SDK 服务器 |
headless |
97 | 95 | 一次性 runner,无服务器 |
acp |
96 | 95 | 自动化专用 ACP |
sdk-minimal |
32 | 31 | 刻意不套 dsh-base 的裸树 |
3.1 纠正 C 与 D:profile 和 bundle 都变多了
书稿 §1.7 写"web 和 headless 是官方随附的两个模板 profile""Base、web-app、headless 是三个基础 Bundle"。
0.2.0-rc.1 的 docs/architecture.md 明确写着五个 profile:web、headless、sdk、sdk-minimal、acp;bundle 则是 dsh-base、dsh-web-app、dsh-headless、dsh-sdk-app、dsh-acp-app,外加那个"刻意例外"的 dsh-sdk-minimal。
书稿这一节的结论(Profile/Bundle/Patch 分层组装、patch 按 id 整行替换、层叠顺序)依然完全正确,只是清单落后了一代。
3.2 sdk-minimal 为什么只有 31 个插件 —— 它是全场唯一一次"硬编码"
它的 31 个插件全部列在下面:
dsh-agent-loop dsh-agent dsh-session dsh-session-log-deepseek
dsh-session-persistence-jsonl dsh-session-projection dsh-session-title
dsh-system-prompt dsh-tools dsh-scope/invariant dsh-agent/invariant
dsh-agent-loop/invariant dsh-session/invariant dsh-invariants
dsh-llm dsh-llm-deepseek-api-key dsh-llm-retry dsh-deepseek-llm-api-extensions
dsh-subprocess-local dsh-sandbox-local dsh-sandbox-policy
dsh-terminal dsh-terminal-bash dsh-tool-bash-persistent dsh-tool-pwsh-persistent
dsh-jobs-local dsh-mcp-resources dsh-plugin-package-inventory-deepseek
dsh-sdk-app dsh-sdk-jsonrpc-server cordis-plugin-timer
架构文档的原话是:dsh-sdk-minimal 是刻意的例外——"one bundle owns its complete explicit SDK tree and does not apply dsh-base"。
为什么这是本章最值得记住的一件事:整个系统的主题是"一切皆插件、组合优于硬编码",而 sdk-minimal 是唯一一个把所有依赖逐个写死在 YAML 里的组合。它存在的理由也很硬——它是跑 benchmark 的裸模型环境,你要的就是"除了 agent loop 和会话,别的什么都没有",任何一层隐式继承都会污染基准数据。
教学点:架构里"刻意的例外"往往比"统一的规则"信息量更大。看到 sdk-minimal,就该问一句:为什么这里可以破例?
四、你这台机器此刻的 web 插件树(M0 最值钱的一屏)
--dump-config 打印的不是配置模板,而是你这台机器此刻真正会启动的东西,每一层都标注了谁 patch 了它。
口径说明(2026-10-03 补):原始输出里
# ==分段头会因为同一来源的行组交错而重复出现(dsh-base一个来源就出现 16 次头)。下面是为可读性做的去重版,每来源只列一次。本机实测:52 个分段头 / 20 个不同来源。按 M2 §三的定义,真正的"层"= 参与layers.flat()的一个 patch 列表。
去重后的来源列表:
# == @deepseek-ai/dsh-base ← 地基:模型/工具/持久化/沙箱/审批/设置/凭据/遥测
# == @deepseek-ai/dsh-base, patched by ~/.dsh/profiles/web/cordis.patch.yml
# == @deepseek-ai/dsh-base, patched by @deepseek-ai/dsh-web-app ← 浏览器应用叠上来
# == @deepseek-ai/dsh-host-sysadmin ← 你的
# == @deepseek-ai/dsh-client-ui-settings-sysadmin ← 你的
# == @tt-a1i/archify-dsh ← 第三方:archify 画图
# == @deepseek-ai/dsh-wechat ← 你的
# == dsh-better-sidebar ← 你的
# == dsh-ego-browser ← 你的
# == @captain1275/dsh-client-ui-skin-aurora ← 第三方:皮肤
# == @captain1275/dsh-client-ui-skin-suite ← 第三方:皮肤
# == dshmarket ← 插件市场
# == @deepseek-ai/dsh-experimental-schedule-bundle ← 实验特性
# == @deepseek-ai/dsh-experimental-auto-review
# == @deepseek-ai/dsh-experimental-agent-team-profile
# == dsh-knowly-weclaw ← 你的:knowly/weclaw 桥
# == ~/.dsh/profiles/web/cordis.patch.yml ← 你的补丁,永远在最后
请注意最后三行的顺序:官方 bundle → 第三方插件 → 你自己的 patch。架构文档里那句"官方和第三方地位平等,唯一区别只是层叠的位置更靠下还是更靠上",在这里是字面意义上成立的——@tt-a1i/archify-dsh 和 @deepseek-ai/dsh-wechat 在 YAML 里的待遇没有任何差别。
而你 ygsdoc/开发大坑_dshmarket重复挂载bundle层已注册的包_2026-10-01.md 里记录的那个坑——dshmarket 把 bundle 层已注册的包重复挂成 shim,导致 60+ 个 client 插件全部 pending——根子就在这张表里:dshmarket 是第 12 层,它会去重扫前面 11 层已经挂好的条目。
4.1 一条 patch 长什么样
看 dump 里被 patch 过的行,patch 的粒度是"按 id 锁定一整行,替换它的整个 config":
# == @deepseek-ai/dsh-base, patched by @deepseek-ai/dsh-web-app
- id: workflow-ptc
name: '@deepseek-ai/dsh-workflow-ptc'
config:
provider: spawn
disabled: true # ← 整行被替换,连 disabled 一起
以及你的补丁直接改别人的 config:
# == @deepseek-ai/dsh-base, patched by /Users/ygs/.dsh/profiles/web/cordis.patch.yml
这就是"没有特权内核"最具体的一个演示:你可以把官方 bundle 里的一行整个换掉,它不会反抗。
五、书稿 §1.6"四种模式":数目对,名字错
本节第一版我判成"preset 只剩三个,creative 已消失",这是错的。广山哥当场用 preset 定义把我纠正了。事实是四个,一个不少。下面是核实后的准确版。
5.1 实测:四个 preset,第四个叫 cordis
packages/bundle/web-app/presets/ 下正好四个文件,每个文件定义一个 @deepseek-ai/dsh-agent-preset 行:
| preset 文件 | 行 id | preset id | order |
|---|---|---|---|
standard.patch.yml |
preset-standard |
standard |
1 |
ptc.patch.yml |
preset-ptc |
ptc |
2 |
minimal.patch.yml |
preset-minimal |
minimal |
3 |
cordis.patch.yml |
preset-cordis |
cordis |
4 |
pnpm dsh --profile web --dump-config 里四条都在(本次运行落在第 771 / 953 / 1141 / 1221 行)。
⚠️ 这是活 dump 的绝对行号,会漂移。 同一台机器 2026-10-03 重跑得到的是 774 / 956 / 1144 / 1224(统一 +3),因为 home 级
cordis.patch.yml是你自己的文件,任何一次编辑都会平移后续行号。稳定的锚点是"四条 preset 行按 order 依次为 1/2/3/4",不是行号。
所以书稿 §1.6 说"四种模式"——数目完全正确。唯一的错是第四个的名字:书稿写 creative,代码里叫 cordis。
方法论教训:我第一次 grep 出的 preset id 列表里明明有
cordis,却只数出三个。一手数据的读法比二手结论更可靠 —— 这一条要记进勘误附录。
5.2 "创造模式"没有消失,它只是换了名字——而且它的真身是两组工具
cordis preset 的定义在 packages/bundle/web-app/presets/cordis.patch.yml。它和 standard 的 persona 逐字相同,注释写得很清楚:
# Same persona as `standard`: tool descriptions and the skill catalog carry
# the operating detail, and the skills own the preset-authoring rules.
"创造模式"与"标准模式"的差别,几乎完全落在两行工具声明上:
| 工具 | standard preset |
cordis preset |
|---|---|---|
tool-cordis(@deepseek-ai/dsh-tool-cordis) |
❌ 不挂 | ✅ 挂 —— 运行时 API 勘查(cordis_inspect_list / cordis_inspect_query) |
tool-plugin-manager(@deepseek-ai/dsh-plugin-manager/tools) |
disabled: true(硬关) |
disabled: !!js "!ctx.get('profileContext')" —— 在 preset 作用域内 profileContext 存在,因此它是开的 |
这就是全篇最值得记住的一句:创造模式不是一个模式名,而是一组工具的开关差异。
它也顺手解释了你 开发大坑_cordis只写命名空间服务漏了容器座位 里那类 remote.* 服务为什么会出岔子——cordis preset 里挂的这套勘查工具,背后是 dsh-tool-cordis/host + cordis-host-runner / cordis-client-runner 一整条链(packages/extensions/)。
tool-cordis README 的一句限定值得记住,因为它解释了"为什么单挂 preset 行没用":
Creator mode includes this toolset. Other compositions mount
@deepseek-ai/dsh-tool-cordis/hostonce in the host composition... a preset row alone registers no Host providers.
注意它是"在 web profile 里挂载"(web 命中 3 处,headless / sdk / sdk-minimal / acp 全是 0 处)——所以创造模式事实上只有 Web GUI 有,无头和 SDK 端拿不到这套工具。
5.3 书稿 §1.6 里仍然成立的部分
书稿对创造模式的机制描述基本准确,只有一处需要加限定:
- ✅ "它允许 Agent 检查自己的插件" —— 对,
cordis_inspect_* - ✅ "热加载新插件" —— 对,
tool-plugin-manager在此 preset 内是开启的,Agent 能发起真实的插件安装(经 bundled pnpm 事务) - ⚠️ "甚至自己写一个自定义 preset 然后创作出来" ——
tool-plugin-manager管的是安装 bundle / MCP 配置(README 原话:"Use Plugin Manager to install bundles containing plugin code or MCP configuration");自己写 preset 属于编辑 profile 配置这条路,不是这个工具的职责 - ✅ "目前这种自进化还停留在实验阶段" —— 判断准确
5.4 两个概念的准确划分
| profile | preset | |
|---|---|---|
| 回答的问题 | 这个进程由什么组成? | 这一个 Agent 由什么组成? |
| 定义在 | Harness home 的 profile 目录 | packages/bundle/*/presets/*.patch.yml 里的普通 Cordis YAML 行 |
| 组合单位 | bundle / patch / 第三方插件 | persona / 工具 / 子插件列表 |
| 数量 | 5(web/headless/sdk/sdk-minimal/acp) | 4(standard/ptc/minimal/cordis) |
| 谁选它 | 命令行 --profile |
会话创建时选 |
书稿 §1.6 的真正问题不是"四种模式错了",而是没把 profile 和 preset 这两层分开说。 这个混淆有实际后果:以为"切 PTC 模式"和"用 headless 启动"是同一层级的操作,而它们其实正交——你可以跑 web profile 的界面,选中一个 ptc preset 的 Agent。
本章实验(附录)
六、动手验证(M0 的作业)
export PATH="/opt/homebrew/bin:$PATH"
cd /Users/ygs/ygs/deepseek-harness
# 1. 五个 profile 各导一份,数行数与唯一插件数
for p in web headless sdk sdk-minimal acp; do
pnpm dsh --profile $p --dump-config > /tmp/dump-$p.yml
echo "$p rows=$(grep -c '^- id:' /tmp/dump-$p.yml) plugins=$(grep -oE "name: '[^']+'" /tmp/dump-$p.yml | sort -u | wc -l)"
done
# 2. 看你的 web 树有哪些层(谁 patch 了谁)
grep '^# ==' /tmp/dump-web.yml
# 3. 看 web 比 headless 多了什么
comm -23 <(grep -oE "name: '[^']+'" /tmp/dump-web.yml | sed "s/name: '//;s/'//" | sort -u) \
<(grep -oE "name: '[^']+'" /tmp/dump-headless.yml | sed "s/name: '//;s/'//" | sort -u)
# 4. 亲手改一行 patch,验证它真的生效(M2 §S-12 的反面教材)
# 4a. 先确认目标 id 存在 —— 这是唯一可靠的检查手段
pnpm dsh --profile web --dump-config | grep -n 'dsh-client-ui-goal'
# 4b. 在你的 patch 里按 id 关掉它(先备份)
cp ~/.dsh/profiles/web/cordis.patch.yml /tmp/patch.bak
cat >> ~/.dsh/profiles/web/cordis.patch.yml <<'YAML'
- id: client-ui-goal
disabled: true
YAML
# 4c. 立刻验证(不要靠"感觉界面变了",靠 dump)
pnpm dsh --profile web --dump-config | grep -A2 'id: client-ui-goal'
# 4d. 还原
cp /tmp/patch.bak ~/.dsh/profiles/web/cordis.patch.yml
风险提示:对正在运行的 3080 实例改 client 插件会当场改变界面(这正是 HMR 的意义)。
若 id 拼错,patch 会只 warn 不报错,界面毫无变化——所以 4c 那一步不能省。
先备份再改,出问题用 4d 还原。
通过标准:能指着第 2 步的输出,说出"第 12 层那个 dshmarket 会重扫前 11 层"这句话,并解释它为什么会引发 那 60+ 个插件 pending 的坑。
本章的勘误条目见 附录 A · 勘误总表。