深色模式
源码拆解 01 · 仓库地图与模块依赖关系
从这一篇开始,我们带着前 8 章的心智模型进入真实仓库。本篇解决"第一次 clone 下来该看哪里"的问题。以下内容基于 deepseek-ai/deepseek-harness 仓库的 master 分支。
1.1 仓库顶层结构
text
deepseek-harness/
├── apps/ # 产品入口(薄壳)
│ ├── cli/ # dsh 命令行启动器
│ └── web/ # Web 前端的 Vite 壳(@deepseek-ai/dsh-web-frontend)
├── packages/ # 全部插件包(仓库的主体)
│ ├── boot/ # 启动胶水:app-boot、cmdline
│ ├── bundle/ # 三个内置 bundle:base / web-app / headless
│ ├── client/ # 浏览器侧:web 壳库、UI 组件、client runtime
│ ├── code-runtime/ # 代码模式运行时(worker thread)
│ ├── compaction/ # 上下文压缩
│ ├── context/ # 上下文注入插件(时间、tmux、agent-instructions…)
│ ├── core/ # ★ 核心:agent / agent-loop / session / tools /
│ │ # system-prompt / scope / agent-default-model
│ ├── credentials/ # 凭据存储
│ ├── e2b/ # 远程沙箱 POC
│ ├── examples/ # 示例组装
│ ├── llm/ # ★ LLM 层:llm(接缝)/ llm-deepseek / llm-pi-ai / llm-retry
│ ├── native/ # 原生能力(bash 终端等)
│ ├── sandbox/ # 沙箱策略与实现
│ ├── settings/ # 设置存储
│ ├── skill/ # 技能系统
│ └── … # (还有 storage、telemetry、tool-* 系列等几十个)
├── docs/ # 官方文档(architecture、subsystems、cookbook、cordis-tutorial)
├── examples/ # 顶层可运行示例(headless-agent、web-schedule、jsonrpc-agent…)
├── vendor/ # 内嵌第三方:cordis、cosmokit 等(见下)
├── website/ # 官方网站(VitePress 工程)
└── python/ # Python SDK几个一眼就能看出的设计特征:
packages/下几乎每个目录都是一个可独立发布的插件包(@deepseek-ai/dsh-*),包名就是目录名加dsh-前缀;apps/只有两个薄壳——所有逻辑在 packages 里,产品入口只是"选一个 profile 启动";vendor/内嵌了 Cordis:dsh 把插件框架源码 vendored 进仓库(同步自 cordiverse/cordis),保证与上游解耦的同时可以深度定制。
1.2 核心包的依赖方向
先看"谁 import 谁"。核心层的依赖图(箭头 = "import 了对方"):
读这张图的两个要点:
agent-loop是最大的消费方:它 import 其他五个核心包,自己是"最后被满足依赖"的那批插件。所以你在源码里看到 agent-loop 的inject声明最长。llm/llm被依赖得最狠但自身几乎不依赖业务包:词汇表是所有层的共同语言,必须保持最底层。dsh-tools的ToolDefinition里模型可见的 JSON Schema 类型也定义在 llm 层——官方注释专门解释了这一点("Declared here (not in dsh-tools) because it is part of GenerateOptions")。
1.3 三层代码的分工
读源码时先分清三种文件,避免迷路:
| 层 | 位置 | 特征 | 读法 |
|---|---|---|---|
| 类型与接口 | src/*.ts 里的 interface / type 导出 | 纯类型,编译后消失 | 先读,建立契约 |
| 服务实现 | Service 子类 | constructor(ctx, name),注册到上下文 | 看它 provide 什么、消费什么事件 |
| 插件胶水 | 包的入口导出 name / inject / apply | 把服务/注册动作包装成插件 | 看它 inject 了哪些服务 |
一个包通常同时包含三者。以 dsh-llm 为例:
text
packages/llm/llm/src/
├── types.ts # 词汇表类型:Message、StreamChunk、GenerateOptions……
├── index.ts # LlmRuntime 服务 + LlmAdapter 抽象类 + registerAdapter
├── message.ts # 消息构造与投影
├── assembler.ts # BlockAssembler:chunk 增量 → 组装块
├── call-config.ts # LlmCallConfig 与冻结/比较工具
├── retry-policy.ts # 重试策略
├── error.ts # LlmError(带 code 的类型化错误)
└── invariant.ts # 运行时断言1.4 官方文档怎么组织(用它当索引)
官方 docs/ 目录本身就是最好的源码索引,与本教程的对应关系:
| 官方文档 | 内容 | 本教程对应 |
|---|---|---|
docs/architecture.md | 架构总纲(必读) | 原理篇 3 |
docs/cordis-primer.md | Cordis 五概念速查 | 原理篇 2 |
docs/agent-lifecycle.md | turn/step 时序图 | 原理篇 5 |
docs/tool-execution-pipeline.md | 工具管线 | 原理篇 6 |
docs/subsystems/*.md | 每个子系统一页:session / core / tools / llm-streaming / scope…… | 本篇后续章节 |
docs/cookbook/*.md | 扩展实操:加工具 / 加适配器 / 加包 | Demo 4–8 |
docs/cordis-tutorial/ | Cordis 官方教程 01–07 | Demo 1–3 |
docs/cordis-api/ | 生成的 Cordis API 参考(ctx 能干什么) | 查 API 用 |
docs/config-catalog.md | 生成的配置目录(每个插件能配什么) | 查配置用 |
docs/capability-seams.md | 接缝图谱 | 原理篇 3.6 |
docs/event-producer-consumer.md | 每个事件的产生者与消费者 | 排查事件用 |
两个"生成"的文档
cordis-api/ 与 config-catalog.md 都是脚本生成的(scripts/gen-cordis-catalog.ts、scripts/gen-config-catalog.ts),并且有 CI 校验保证与源码同步。所以它们可以放心当"活文档"查:写插件时查 cordis-api(ctx 上有什么方法),写配置时查 config-catalog(每行能配哪些字段)。
1.5 阅读顺序建议
第一次读 dsh 源码,推荐按依赖的逆序(从地基到房顶):
vendor/cordis的context.ts/service.ts/events.ts(先建立"上下文 + 事件"直觉)→ 下一篇;packages/llm/llm/src/types.ts(统一词汇表,最容易读的纯类型文件)→ 第 5 篇;packages/core/session(追加式日志,结构清晰)→ 第 3 篇;packages/core/agent+agent-loop(最复杂,留到最后)→ 第 4 篇;packages/core/tools(注册表 + 管线)→ 第 6 篇。
每篇都会给出"读哪个文件、看哪几行、为什么"。
1.6 本章小结
仓库地图记住三件事:packages 是主体且每个目录基本是一个插件包;apps 是薄壳;vendor 内嵌 Cordis。 读源码的顺序遵循依赖逆序,用官方 docs/ 当索引。下一篇开始拆 Cordis 内核。
小练习:找入口
clone 仓库后(git clone --depth 1 https://github.com/deepseek-ai/deepseek-harness.git),用编辑器全局搜索 'agent/turn-stopping'。你会同时找到:事件声明处(agent 包的 runtime-types.ts)、派发处(agent-loop 包)、监听处(若干消费插件)。这就是"事件把三者解耦"的实物证据——顺着任意一个事件名,你都能画出一条完整的产生者→消费者链路。