目处于开发者预览阶段,命令和 API 可能发生破坏性变化;发生冲突时,以链接的源码、包 README 和生成目录为准。
1. 一页速览
DeepSeek Harness(命令名 dsh)不是一个写死流程的聊天应用,而是一个由 Cordis 驱动的可组合智能体框架。LLM 适配器、Agent Loop、工具、会话日志、持久化、沙箱、Web UI 都是插件;Profile 选择一组 Bundle,Bundle 提供补丁层,用户仍可通过 cordis.patch.yml 或 --patch 替换任意配置行。

最快体验:已发布版本使用 npx @deepseek-ai/dsh web;源码版本在仓库根目录依次运行 pnpm install、pnpm run build、pnpm dsh web,然后访问 http://127.0.0.1:3080。
2. 本机验证结论
| 项目 | 本机结果 |
|---|---|
| Linux / Node.js | Linux x64;Node v24.14.1,满足 `^22.19.0 |
| pnpm | 通过 Corepack 启用仓库固定的 11.7.0 |
| Python / Git | Python 3.12.8;Git 2.34.1 |
| 依赖安装 | 成功;共 238 个 workspace project、923 个依赖包 |
| 原生依赖 | node-pty 使用 Node 安装目录内的 24.14.1 头文件完成本机编译 |
| 构建 | pnpm run build 成功,Host、Client、Web 前端均生成产物 |
| 类型检查 | pnpm run typecheck 成功 |
| 实战类型检查 | greet-plugin/tsconfig.json 仅消费构建后的公共声明,检查成功 |
| 针对性测试 | CLI 参数及 base/web-app/headless Bundle:4 个文件、25 项测试全部通过 |
| 自定义模型 | /v1/models 与 /v1/chat/completions 均 HTTP 200;默认 deepseek-v4-flash |
| 浏览器 | 自定义 Provider 和 4 个 Demo 模块已启用;真实模型依次调用两个 Tool 成功 |
| 私有配置 | 仓库根 .env 已填写 Key 且被 .gitignore 忽略;验证过程未输出 Key |
安装时如果 node-pty 因无法下载 Node 头文件失败,而本机使用 NVM,可将 npm_config_nodedir 指向当前 Node 安装目录后重试。不要用版本不匹配的系统头文件。
3. 安装、运行与凭据
3.1 前置条件
- •Node.js 22.19+,或 24+;CI 覆盖 22.19、24、26。
- •Corepack 管理的 pnpm 11.7.0。
- •Git 2.26+。
- •Python SDK 路线需要 Python 3.10+;支持 Linux x64/arm64、macOS 14+ arm64。
- •真实模型调用需要 DeepSeek 兼容端点和 API Key;单纯启动 Web UI、浏览设置与运行 keyless 测试不需要 Key。
3.2 源码路线
- •在仓库根目录运行
corepack enable,再确认pnpm --version为 11.7.0。 - •运行
pnpm install。安装脚本会设置 worktree-local Lefthook 和翻译合并驱动。 - •运行
pnpm run build。源码 CLI 虽通过 TSX 启动,但 Typert Host 产物和 Web/Client bundle 仍须预先构建。 - •运行
pnpm run typecheck验证环境。 - •运行
pnpm dsh web,访问终端打印的 URL,默认是http://127.0.0.1:3080。
3.3 API 地址与 Key 填在哪里
本实验室已在仓库根创建被 Git 忽略的 .env:
DEEPSEEK_API_KEY=
使用公共 DeepSeek API 时只填 Key。接入自定义地址有两种不同机制:官方 dsh-llm-deepseek route 的端点覆盖使用启动 shell 中的 DEEPSEEK_BASE_URL;它是 bootstrap-only 变量,产品启动器会拒绝它出现在 .env。本教材第 7 章使用更通用的 dsh-llm-pi-ai 手工 route,地址直接写在不含机密的 overlay baseURL 中,因此不需要 DEEPSEEK_BASE_URL。不要把真实 Key 发到聊天、日志或提交中。Web 用户也可在"设置 → 模型"添加自定义 Provider:明文密钥写入 $DSH_HOME/.credentials.yaml(默认 ~/.dsh/.credentials.yaml,权限 0600),settings.yaml 只保存引用,浏览器只收到脱敏描述符。
API Key 等普通凭据的优先级从高到低为:启动进程继承的环境变量 → $DSH_HOME/.credentials.yaml → 启动目录 .env → $DSH_HOME/.env。调用目录 .env 只在启动时读取一次,不会跟随后来在 Web 中选择的 workspace。官方 Adapter 的 DEEPSEEK_BASE_URL、代理地址等 bootstrap-only 变量只能来自启动环境,不参与文件层;pi-ai route 的非机密 baseURL 是插件配置,不受该限制。
$DSH_HOME/.credentials.yaml 是扁平 YAML mapping,例如 DEEPSEEK_API_KEY: sk-...,不是嵌套的 deepseek.apiKey。启动环境变量始终压过 UI 管理的文件值;若想让 UI 写入值生效,不要在启动 shell 中留下同名旧变量。
3.4 CLI 入口
| 命令形态 | 用途 |
|---|---|
pnpm dsh web |
启动 Web Profile;默认只绑定 127.0.0.1:3080 |
pnpm dsh web --port 8080 |
Web 应用参数必须位于 launcher 参数之后 |
pnpm dsh --profile headless "任务" |
创建一个新会话,完成一次任务,打印最终回答并退出 |
pnpm dsh --profile web --dump-default-config |
仅打印 Bundle 层,不启动插件 |
pnpm dsh web --patch ./extra.yml --dump-config |
打印包含用户层、Home 层和 overlay 的最终树 |
pnpm dsh plugin --profile web add <包> |
在 Profile 目录中通过 pnpm 安装外部插件/Bundle |
pnpm dsh plugin --profile web remove <包> |
从 Profile 移除外部插件/Bundle(详见第 11 章) |
pnpm run demo:acp |
启动 ACP stdio 自动化服务 |
pnpm run demo:cordis |
运行可检查、修改自身插件树的演示 |
Launcher 参数在前;第一个不属于 Launcher 的参数开始都交给应用。Web 支持 --host、--port、可重复的 --trusted-host。当前故意拒绝 --host 0.0.0.0。Headless 只接收一个非空任务文本,不启动 HTTP、Host 或浏览器。
3.5 Python SDK
创建虚拟环境并安装 deepseek-harness-sdk。已发布 wheel 自带匹配的运行时,目标机器无需 Node.js。仓库的完整可运行入口是 examples/jsonrpc-agent/minimal.py,组合文件是 minimal.cordis.yml。
运行时必须明确传入隔离 workspace、session root 和 session id。Minimal 组合使用 danger-full-access,持久 Bash 与编辑器可访问运行时用户能访问的任意路径,只应对一次性 checkout 或容器使用。复用同一 session id 会延续对话和持久 PTY 状态;独立任务使用不同 id。
4. 顶层目录与根文件地图
| 路径 | 作用 |
|---|---|
apps/ |
产品入口:cli/ 是 Profile Launcher,web/ 是 Vite 浏览器前端 |
assets/ |
根 README 使用的社区二维码等静态资源 |
docs/ |
架构、子系统、教程、用户指南、生成目录、中英配对文档 |
examples/ |
可运行组合:ACP、Headless、JSON-RPC、MCP、Web overlay |
native/ |
Linux Landlock 自限制启动器及平台 npm 包 |
packages/ |
49 个能力组、219 个左右的工作区包;产品主体 |
patches/ |
pnpm dependency patch,目前包含 node-pty@1.1.0.patch |
python/ |
sdk/ Python API 与 sdk-runtime/ 内置运行时打包 |
scripts/ |
构建门禁、生成器、文档校验、发布与仓库维护脚本 |
vendor/ |
仓库内维护的 Cordis 等 vendored 依赖;改动前读 vendor/README.md |
website/ |
VitePress 文档站;docs.ts 决定仓库文档到站点的投影 |
.agents/ |
Agent Notes、技能和 agent 工作约定;决策理由的权威位置 |
.github/ |
CI、Issue 管理和 GitHub 自动化 |
package.json |
Monorepo 脚本、Node/pnpm 版本、工作区入口 |
pnpm-workspace.yaml / pnpm-lock.yaml |
工作区范围、依赖补丁与可复现锁文件 |
tsconfig.base*.json |
Host/Client 共享编译面与 paths;根 tsconfig.json 是 solution |
tsconfig.host.json / tsconfig.client.json |
互相隔离的 Host 与浏览器 aggregate program |
tsdown.config.ts |
两阶段包构建,Host 阶段运行 Typert,Client 阶段构建浏览器面 |
vitest*.config.ts |
单元、e2e、快照、Web、性能、压力等测试分层 |
AGENTS.md |
全仓开发约定;修改代码前必读 |
README*.md |
产品介绍和最短启动路径 |
CONTRIBUTING*.md |
贡献政策与流程 |
BENCHMARK.md |
基准测试入口,指向 Python SDK + JSON-RPC minimal 组合 |
LICENSE / THIRD_PARTY_NOTICES.md |
MIT 许可证和第三方声明 |
lib/、apps/web/dist/、node_modules/、.sessions/、.storages/、.artifacts/ 都是生成或本机状态,不应提交。
5. packages/:49 个能力组与全部子包
每个组的 README.md 是该组"包 → 职责 → ctx key"映射的权威来源。以下列表用于导航,具体 API 以各包 README、JSDoc、docs/config-catalog.md 和 docs/tool-catalog.md 为准。
- •
core/:产品 API 脊柱。scope、session、system-prompt、tools、agent、agent-default-model、agent-tool-presentation、agent-loop。 - •
api/:Remote BFF 与 Typert RPC。remotes、gateway。 - •
attachment/:耐久附件身份与本地内容存储。attachment、attachment-local。 - •
boot/:共享启动胶水。app-boot、cmdline。 - •
bundle/:可安装 Profile 补丁层。base、headless、web-app。 - •
client/:浏览器运行时和 UI 插件。web、modules、web-react、connection、runtime、hmr、locale、schema-form、ui-slots、ui-theme、ui-primitives、ui-attachment、ui-layout、ui-sidebar、ui-workspace、ui-conversation、ui-tool、ui-workflow-run、ui-goal、ui-trajectory、ui-commands、ui-input-trigger、ui-skill、ui-subagent、ui-jobs、ui-model-selection、ui-permission-presets、ui-plan、ui-settings、ui-settings-general、ui-settings-models、ui-settings-plugin-inventory、ui-settings-plugins、ui-user-questions、ui-agent-preset、ui-message-feedback、ui-deliverables、ui-directory-picker-browse、ui-directory-picker-native。 - •
code-runtime/:代码执行服务与 Worker Thread Provider。code-runtime、code-runtime-worker-thread。 - •
compaction/:上下文压缩。compaction、compaction-basic、command-compact、compaction-tool-result-pruner。 - •
context/:模型可见请求上下文。agent-instructions、session-reference、time-context、tmux-context。 - •
credentials/:凭据引用与本地分层存储。credentials、credentials-local。 - •
e2b/:E2B 沙箱 POC。e2b、fs-e2b、subprocess-e2b。 - •
examples/:示例所复用的应用脊柱和 bin。acp-demo、agent-spine-demo、jsonrpc-demo。 - •
extensions/:运行时自检与自修改。cordis-client-runner、cordis-host-runner、tool-cordis、ui-cordis。 - •
feedback/:人类反馈。command-feedback、message-feedback。 - •
fs/:文件系统 seam、Provider 与工具。fs、fs-local、fs-sandbox、fs-observation-policy、tool-fs、tool-fs-search、tool-str-replace-editor。 - •
goal/:同会话目标状态。goal、goal-round-driver、command-goal、tool-goal。 - •
guard/:Loop 卫生与超时。repeat-tool-reminder、timeout-policy。 - •
hooks/:Claude Code/Codex Hook Bridge。hook-protocol、hooks-claude-code、hooks-codex。 - •
host/:Web Host 半边。webserver、apiproxy、directory-picker、directory-picker-auto、directory-picker-browse、directory-picker-native、frontend-static、plugin-inventory。 - •
identity/:共享匿名身份。anonymous-user-id。 - •
interaction/:命令、审批、权限与提问。commands、permission-presets、tool-ask-user、user-approval、user-questions。 - •
jobs/:后台作业。jobs、jobs-local、tool-jobs。 - •
llm/:LLM seam 与适配器。llm、token-meter、llm-retry、llm-deepseek、llm-pi-ai。 - •
lsp/:语言服务器能力。lsp、lsp-stdio、tool-lsp。 - •
mcp/:通用 MCP 客户端。mcp-client。 - •
plan/:可审阅退出的计划协作状态。plan-mode。 - •
preset/:每会话 Agent 组合。agent-presets、persona。 - •
runtime-diagnostics/:开发时运行不变量。invariants。 - •
sandbox/:进程限制 seam。sandbox、sandbox-local、sandbox-policy、sandbox-windows-acl。 - •
schedule/:会话内定时跟进。schedule。 - •
sdk/:进程外 JSON-RPC SDK。client、protocol、server。 - •
session/:持久化、投影、标题、统计、遥测。session-persistence、session-checkpoint-policy、session-persistence-jsonl、session-persistence-sqlite、session-projection、session-projection-cache、session-stats、session-title、session-title-llm、session-title-first-prompt-llm、session-title-all-prompts-llm、session-telemetry、session-telemetry-otel。 - •
session-query/:会话读取、谱系、全文检索和导出。session-query、session-query-sqlite、session-log-export、tool-session-query。 - •
settings/:用户设置 seam。settings、settings-file。 - •
shell/:Shell seam、本地/沙箱 Provider 和模型工具。shell、bash-local、bash-sandbox、pwsh-local、pwsh-sandbox、shell-env、tool-bash、tool-pwsh、tool-bash-persistent。 - •
skill/:技能注册、文件发现和模型工具。skill、skill-badge、skill-filesystem、tool-skill。 - •
spill/:超大工具结果溢出。spill、spill-local、spill-policy。 - •
storage/:非会话通用存储。storage、storage-domain、storage-json、storage-sqlite。 - •
subagent/:子 Agent seam、Provider 和控制工具。subagent、subagent-acp、subagent-claude-code、subagent-codex、subagent-dsh-sdk、subagent-fork-in-process、subagent-spawn-in-process、subagent-in-process-driver、tool-subagent、tool-subagent-control、tool-subagent-report。 - •
subprocess/:托管子进程树。subprocess、subprocess-local。 - •
terminal/:持久 PTY。terminal、terminal-bash、tool-terminal。 - •
test-support/:测试基础设施。acp-snapshot、agent-loop-testkit、client-runtime、loader-smoke、llm-mock-server、llm-replay。 - •
todo/:模型任务清单。tool-todo。 - •
typert/:类型图生成、装载、协议与注册。generator、loader、protocol、registry。 - •
util/:零 Harness 依赖工具。atomic-write、brand、home-paths、launch-environment、native-command、output-retention、timeout。 - •
web/:搜索/抓取 seam 与 Provider。web、web-fetch-http、web-search-deepseek、web-search-exa、web-search-perplexity、tool-web。 - •
workflow/:动态工作流 seam。workflow、workflow-worker-thread、tool-workflow、tool-ralph。 - •
workspace/:工作区实体。workspace。 - •
acp/:自动化专用 Agent Client Protocol 服务。acp。
依赖方向的核心纪律:扩展依赖 Service Definition,不依赖具体 Provider;组合 Bundle 可以依赖完整脊柱。完整依赖图由 docs/module-graph.md 生成,不应手抄维护。
6. 从零理解运行时:插件、服务、事件和生命周期
这一章不是阅读清单,而是后续开发必须掌握的概念正文。先理解"谁拥有状态、谁负责清理、调用怎样穿过系统",再写代码会省掉大量调试时间。
6.1 Cordis Context:不是全局变量,而是当前插件可见的能力集合
每个插件的入口都会收到 ctx: Context。ctx.tools、ctx.llm、ctx.sessions 等属性是其他插件提供的服务;ctx.on()、ctx.effect()、ctx.plugin() 则用于贡献行为。Context 还携带当前作用域,所以同一个服务名在不同隔离 realm 中可以解析到不同实例。
最小函数插件只有一个 apply:
import type { Context } from '@deepseek-ai/cordis'export const name = 'hello-plugin'export function apply(ctx: Context): void { console.log('plugin loaded')}
对象插件把 name、inject、apply 放在一个默认导出对象中;类插件通常继承 Service,用于向 ctx 提供命名 API。简单行为优先使用函数插件,只有确实拥有服务状态时才使用类。
6.2 Fiber:每个插件实例的生命周期所有者
Cordis 为每个插件实例创建一个 Fiber。状态机是:
PENDING → LOADING → ACTIVE ↘ FAILEDACTIVE → UNLOADING → DISPOSED
- •
PENDING:配置行存在,但inject的依赖尚未全部出现。 - •
LOADING:依赖就绪,正在运行apply或构造类插件。 - •
ACTIVE:插件已提供服务、监听器或工具。 - •
FAILED:加载过程抛错;错误应直接修复,不要静默跳过。 - •
UNLOADING:依赖消失、配置热替换、父插件卸载或主动 dispose,正在清理。 - •
DISPOSED:该实例的贡献和子 Fiber 已全部撤销。
const fiber = ctx.plugin(plugin, config) 会创建子 Fiber;await fiber.dispose() 会卸载它、递归卸载子插件并等待异步清理完成。
6.3 Effect:所有注册都必须可逆
Harness 的核心纪律是"注册即 Effect"。ctx.on()、服务注册、ctx.tools.register() 等 API 自己返回或内部登记 disposer;自有资源用 ctx.effect():
export function apply(ctx: Context): void { ctx.effect(() => { const timer = setInterval(() => console.log('tick'), 5000) return () => clearInterval(timer) })}
卸载时 disposer 以注册顺序的逆序开始调用,但多个异步 disposer 可能并发完成。如果关闭 B 必须发生在关闭 A 之后,就把两步放进同一个 disposer 并显式 await,不要依赖两个 Effect 的完成顺序。
为什么重要:HMR、Profile 修改、Provider 替换、Agent Preset 卸载都会触发生命周期变化。绕过 Effect 注册的 timer、socket、event listener 会变成幽灵资源。
6.4 inject:依赖关系,不是建议
export const inject = ['tools', 'textTransform']export function apply(ctx: Context): void { ctx.tools.register(/* ... */) void ctx.textTransform}
Loader 不依赖 YAML 行顺序决定加载;它等待 tools 和 textTransform 都可见后才执行 apply。必需服务消失时,消费插件自动卸载;服务恢复时重新加载。可选依赖不要写进 inject,而是在使用点通过 ctx.get('serviceName') 查询并处理 undefined。
6.5 Service 与 TypeScript 声明合并
服务是插件向其他插件公开的命名能力。运行时注册与编译时类型是两件事,必须同时完成:
import { Service, type Context } from '@deepseek-ai/cordis'declare module '@deepseek-ai/cordis' { interface Context { counter: CounterService }}export class CounterService extends Service { private value = 0 constructor(ctx: Context) { super(ctx, 'counter') // 运行时提供 ctx.counter } increment(): number { return ++this.value }}
declare module 是 TypeScript module augmentation:让编译器知道 ctx.counter 的类型;super(ctx, 'counter') 才是真正的运行时注册。只写前者会编译通过但运行时不存在,只写后者会运行但消费方没有类型。
6.6 类型事件与四种分发方式
事件用于松耦合通信。先扩展 Events:
declare module '@deepseek-ai/cordis' { interface Events { 'demo/ready': (id: string) => void 'demo/check': (input: string) => string | undefined 'demo/transform': ( input: string, next: () => Promise<string>, ) => Promise<string> }}
四种常用分发语义:
- •
ctx.emit(name, ...args):同步广播给所有监听器,忽略返回值,适合"事实已经发生"。 - •
ctx.bail(name, ...args):同步顺序查询,第一个不是null、false、undefined的值结束分发,适合轻量匹配。 - •
await ctx.serial(name, ...args):异步顺序查询,第一个有效值结束后续处理,适合按优先级寻找处理者。 - •
await ctx.waterfall(name, input, terminal):中间件链,监听器通过next()委托下游,并可包装返回值。
Waterfall 最容易出错:
ctx.on('demo/transform', async (_input, next) => { const downstream = await next() // 观察或包装时必须调用 return downstream.trim()})
不调用 next() 表示故意短路,不是"监听完自动继续"。Harness 的 agent/pre-step、agent/request、llm/stream、tools/pre-execute、tools/execute、tools/post-execute 都是 waterfall。
不要混淆 Cordis 实时事件与 Session Event:ctx.emit() 是进程内扩展点;turn/start、assistant/message、tool/result 等是写入会话日志的耐久事实。观察后者应监听 session/event 并检查 event.type。
6.7 Config Schema:类型、默认值和加载期校验
部署可变值不得硬编码。插件导出同名的 Config interface 与 Schemastery schema:
import Schema from '@deepseek-ai/schemastery'export interface Config { mode: 'upper' | 'lower' timeoutMs: number}export const Config: Schema<Config> = Schema.object({ mode: Schema.union(['upper', 'lower']).default('upper'), timeoutMs: Schema.number().default(30000),})export function apply(ctx: Context, config: Config): void { if (!Number.isFinite(config.timeoutMs) || config.timeoutMs <= 0) { throw new Error('timeoutMs must be a positive finite number') }}
Schema 处理字段类型、枚举、必填项和默认值;正数、跨字段关系等 DSL 未表达的约束在最早可判断处显式校验。配置错误应阻止加载并给出修复方向,不能"使用一个猜测值继续"。
6.8 Profile、Bundle、Patch 与 Overlay
Profile 是 $DSH_HOME/profiles/<name> 下的用户组合,Bundle 是可分发的补丁层。最终树的顺序是:Bundle 列表 → Profile 自有 patch → $DSH_HOME/cordis.patch.yml → 命令行中的每个 --patch。

后层按 row id 胜出。最关键的规则:patch 替换目标 row 的完整 config,不做深合并。覆盖 agent-default-model 时必须同时重述 provider 和 model。插入本地 TS 文件时使用绝对路径,因为 Loader 从 Profile 目录解析模块,不从 patch 文件目录解析。
先用 --dump-config 验证配置命中,再启动,是最省时的工作流。
6.9 Agent、Session、Turn 与 Step
- •Agent:活跃驱动对象,有 inbox、状态、取消、注入和当前作用域 Context。
- •Session:追加式事件日志,是持久事实源;Agent 可以释放,Session 仍可恢复。
- •Turn:从一次输入被接纳开始,到没有后续工作时结束;可能没有模型请求,也可能包含多个 Step。
- •Step:一次模型请求,加该响应触发的一组工具调用。

- LLMSession LogAgent LoopUser/UIToolsLLMSession LogAgent LoopUser/UIlooptool callinbox messageturn/startagent/pre-stepstep/start + user/messagederiveMessages()agent/request → llm/streamassistant/chunk* → assistant/messagepre-execute → execute → post-executetool/resultstep/endturn
"模型可见即已记录"是硬性不变量。模型下一次请求看到的历史由 Session Log 的 deriveMessages() 投影得到;如果插件添加了模型可见上下文,却没有对应的日志事实,恢复、Fork、回放和遥测会与原请求不一致。
6.10 System Prompt、Tool Schema 与 LLM 请求
ctx.systemPrompt 聚合 persona、插件贡献的 section、动态 context 和工具 schema。Tool 注册变化会改变模型请求中的工具目录。LLM Adapter 接收提供方无关的 GenerateOptions,把系统提示、历史、工具、模型和取消信号转换为某个 HTTP/SDK 协议,再产生统一 StreamChunk。
完整流至少遵循:每个 block-start 对应 block-end;文本使用 text-delta;工具参数使用原始 JSON 增量;usage 位于结束前;finish 是最后分片。写新 Adapter 时,还要传递 options.signal、应用归因 headers,并用稳定 code 的 LlmError 表达传输与协议失败。
多数 OpenAI-compatible 网关无需自己写 Adapter:配置 dsh-llm-pi-ai 的 api: openai-completions 即可。只有协议字段或流语义无法表达时,才实现新的 LlmAdapter。
6.11 Tool 的五层契约
一个生产级 Tool 同时有五层:
- •
name/description:模型用来决定是否调用。 - •
parameters:模型参数 Schema;defineTool在执行前校验并推导 TypeScript 类型。 - •
execute(args, exec):领域行为;args已校验且按只读输入处理,长操作必须遵守exec.signal。 - •
output.schema:程序化规范值;Code Mode 得到的是这个值,不是人类文本。 - •
output.render与presentCall/presentResult:前者生成模型可见 ContentBlock,后者生成 UI 卡片意图。
对象输出必须显式声明 additionalProperties: true | false。领域内的"不存在"可以是成功规范值;基础设施故障、取消和无法满足契约则抛异常。展示函数会在实时和日志回放时重复执行,所以必须是纯函数:不能做 I/O、读当前时间或随机数。
UI 卡片类型包括 generic、terminal、diff、search、web。工具不导入 React 或 Client 类型,只返回中性的 render intent。
6.12 Capability Seam:Definition、Provider、Consumer
当能力需要替换后端时,拆成三个角色:
Service Definition ← Provider ↑ Consumer/Tool
- •Definition 拥有接口、Request/Result 类型和
ctxkey。 - •Provider 只依赖 Definition,实现本地、沙箱或远端行为。
- •Consumer 也只依赖 Definition,把能力提供给模型、CLI 或其他服务。
Provider 与 Consumer 互不依赖,替换 Provider 不改 Tool。简单工具不必预防性拆包;只有角色确实独立演进时才拆分。
6.13 Host、Client、Typert 与 Web
Node Host 拥有 Agent、文件系统、模型和持久化;Browser Client 只通过 API 操作 Host 并渲染 Session Event。两边使用独立 TypeScript aggregate,因为它们会对相同 Context key 做不同声明合并。Host 构建中的 Typert 扫描 @Remote/@RemoteScope,生成运行时反射和 Client Remote 投影;浏览器通过 ctx.remote 调用,而不是直接 import Host 实现。
apps/web 只是 Vite 入口;真正 UI 功能来自 packages/client/ui-* 插件。想增加业务消息展示,优先注册 Conversation Node;想增加后端能力,写 Host 插件和 Remote;不要把业务逻辑塞进 React 组件。
6.14 权限、持久化、凭据和遥测
默认新会话使用 workspace-write + ask:写操作被限制并可能审批,但读取、网络、同 UID 文件和进程可见性不是完整机密隔离。danger-full-access 不审批,只能用于可丢弃环境。
Session 默认持久化到 $DSH_HOME/sessions 的 JSONL。Credential 文档默认是 $DSH_HOME/.credentials.yaml,权限 0600;同 UID 的 Agent 工具理论上仍可读取它,因此文件权限不是抵御自身 Agent 的安全边界。
遥测默认关闭。显式开启 FULL 或 FEEDBACK_ONLY 可能上传消息、工具参数/结果和路径;任何非空 DSH_TELEMETRY_DISABLED 都会强制关闭。
7. 本机自定义模型:地址、模型切换与启动实例
7.1 已实测的网关事实
- •用户提供地址:
http://x.x.x.x:3000/。 - •根路径
/models返回 HTML 管理页面,不是模型 JSON。 - •OpenAI-compatible API 前缀:
http://x.x.x.x:3000/v1。 - •
GET /v1/models携带 Bearer Key 返回模型目录。 - •
POST /v1/chat/completions使用deepseek-v4-flash返回 HTTP 200;服务实际报告模型deepseek-v4-flash-202605。 - •最小验证回复为
DSH_ENDPOINT_OK。
7.2 完整自定义 Provider overlay
文件:.artifacts/deepseek-harness-learning-lab/custom-model/cordis.yml。完整内容如下;Key 不写入 YAML,只引用 DEEPSEEK_API_KEY:
# 自定义 OpenAI-compatible 网关。切换地址或模型时,只改 baseURL 与 models[0].id,# 并同步修改 agent-default-model.model。API Key 只从 DEEPSEEK_API_KEY 解析。- id: llm-pi-ai config: providers: learning-gateway: displayName: '学习用自定义网关' apiKeyEnv: DEEPSEEK_API_KEY api: openai-completions baseURL: 'http://x.x.x.x:3000/v1' defaultContextWindow: 131072 defaultMaxTokens: 8192 models: - id: deepseek-v4-flash name: DeepSeek V4 Flash(自定义网关) contextWindow: 131072 maxTokens: 8192- id: agent-default-model config: provider: learning-gateway model: deepseek-v4-flash
contextWindow 与 maxTokens 是本学习实例的部署声明,不是从 /models 自动获得的权威容量;网关限制不同就按实际值修改。models 是该 route 的允许目录,列多个模型即可在 Web 模型选择器切换,例如:
models: - id: deepseek-v4-flash name: DeepSeek V4 Flash - id: deepseek-v4-pro name: DeepSeek V4 Pro - id: deepseek-chat name: DeepSeek Chat - id: deepseek-reasoner name: DeepSeek Reasoner
切换默认模型要同步修改 agent-default-model.model。Web 中临时切换可点击输入框右下角模型按钮;已开始请求的 Session 会保留日志中的模型身份,新默认只影响之后创建或明确切换的请求。
7.3 一条命令启动完整学习实例
仓库根 .env 已含你的私有 DEEPSEEK_API_KEY。启动命令是:
pnpm dsh web \ --patch ./.artifacts/deepseek-harness-learning-lab/custom-model/cordis.yml \ --patch ./.artifacts/deepseek-harness-learning-lab/greet-plugin/cordis.yml \ --patch ./.artifacts/deepseek-harness-learning-lab/text-transform-plugin/cordis.yml
当前实例已运行在 http://127.0.0.1:3080。验证顺序:
- •打开地址;设置 → 模型应出现"学习用自定义网关",并显示 Key 已配置。
- •选择工作区
/root/deepseek-ai/deepseek-harness。 - •输入框右下角应显示"DeepSeek V4 Flash(自定义网关)"。
- •设置 → 插件 → 插件列表应看到 greet,以及 text-transform 的 provider/tool/audit,均为已启用。
- •发送:
请务必依次调用 greet 工具问候 Ada,再调用 transform_text 工具转换文本 DeepSeek Harness。不要使用其他工具,最后只总结两个工具的结果。 - •预期:
你好,Ada!与RESULT: DEEPSEEK HARNESS。
模型页中的自定义 Provider(绿色圆点表示凭据已配置,页面不会回显明文 Key):

插件列表筛选 learning-lab 后,可看到简单 Demo 与复杂 Demo 的四个模块均已启用:

本机真实结果:1 个 Turn、2 个 Step,两个 Tool Call 均成功,最终回答在约 5 秒内完成。终端的 audit 插件打印 [text-transform] mode=upper input="DeepSeek Harness"。

7.4 换地址或协议
- •同类 OpenAI Chat Completions 网关:修改
baseURL,保留api: openai-completions。 - •地址必须包含服务实际 API 前缀;本例必须有
/v1。 - •Key 变量名可改为
MY_GATEWAY_API_KEY,但必须同步改apiKeyEnv并通过环境、Credentials 页或 0600 credential 文件提供。 - •
DEEPSEEK_BASE_URL只用于官方dsh-llm-deepseekroute,且是 bootstrap-only 变量;本例使用 pi-ai 手工 route,不需要它。 - •网关不支持
GET /models时可手写models,不影响 Chat Completions。
8. 简单 Demo:单文件可配置 greet Tool
8.1 目录
greet-plugin/├── cordis.yml├── tsconfig.json└── src/index.ts
8.2 完整 TypeScript 源码
import type { Context } from '@deepseek-ai/cordis'import Schema from '@deepseek-ai/schemastery'import { defineTool } from '@deepseek-ai/dsh-tools'export const name = 'learning-greet-tool'export const inject = ['tools']export interface Config { greeting: string punctuation: string}export const Config: Schema<Config> = Schema.object({ greeting: Schema.string().default('你好'), punctuation: Schema.string().default('!'),})export function apply(ctx: Context, config: Config): void { ctx.tools.register(defineTool({ name: 'greet', description: '使用配置好的问候语向指定的人问好。', parameters: { name: { type: 'string', required: true, description: '要问候的人名' }, }, output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }], }, async execute(args) { return `${config.greeting},${args.name}${config.punctuation}` }, }))}
8.3 Overlay 完整代码
- insert: - id: learning-greet-tool name: '/root/deepseek-ai/deepseek-harness/.artifacts/deepseek-harness-learning-lab/greet-plugin/src/index.ts' config: greeting: '你好' punctuation: '!'
8.4 逐段解释
- •
name是插件诊断名;Tool 自己的模型可见名字是greet,两者不是同一命名空间。 - •
inject = ['tools']使 Fiber 在ctx.tools可用后才加载。 - •
Configinterface 给 TypeScript 使用,同名 Schema 给 Loader 运行时校验并填默认值。 - •
parameters生成模型看到的 JSON Schema;required: true让args.name成为必填字符串。 - •
execute返回规范值,本例规范值就是字符串。 - •
output.schema验证返回值;返回数字会被转成 Tool 错误,而不是悄悄 stringify。 - •
render将规范值转成模型可见的 ContentBlock。UI 若没有专用卡片,会使用 generic fallback。 - •注册由当前 Fiber 所有,修改 overlay 触发热替换时旧 Tool 自动注销。
8.5 练习与验收
- •把
greeting改成欢迎,重启或让配置热替换,调用greet。 - •增加可选参数
title;未提供时只用姓名,提供时输出"你好,Dr. Ada!"。 - •把输出改成对象
{ message, target },必须增加对象 schema 和additionalProperties: false。 - •故意返回错误类型,观察 Tool Result 的
isError,再修复。
验收标准:--dump-config 能找到 row;设置插件列表显示已启用;模型明确调用后返回配置文本;改配置不会出现重复 Tool 注册。
9. 复杂 Demo:Definition → Provider → Consumer → Event
9.0 text-transform-plugin 到底是做什么的
先说结论:text-transform-plugin 是本教材编写的教学插件,不是 DeepSeek Harness 内置的产品功能。它本身的能力很小------把一个字符串按配置转成大写或小写、可加前缀、带一个 30 秒自动清空的缓存------但它的价值不在于"转换文本",而在于用四段代码把一个完整**能力 seam(Definition → Provider → Consumer → Event)**的协作关系演出来,作为第 6.12 节那套概念的可运行样板。
- •它是给学习者看的:真实项目里"稳定接口"和"可替换实现"怎么拆、运行时注册和编译时类型怎么配对、事件怎么跨模块松耦合。
- •它不是给产品用的:没有任何一条产品需求需要"把文本转大写"这个工具,所以它不会也不应该被合并进仓库、发布到 npm。
- •它当前没有安装进 Profile:只是通过第 7.3 节启动命令末尾的
--patch text-transform-plugin/cordis.yml临时加载。重启时去掉那一行--patch,它就消失了,无需也不应执行pnpm uninstall。
四段代码分别扮演四个角色,一次运行就能看到整条链:
模型(browser) → transform_text Tool(tool.ts, Consumer) → ctx.textTransform(provider.ts, Provider) → 本地实现 + 缓存 + 清理 → emit 'learning-text/transformed' → audit.ts 监听并打印终端日志
也就是说,浏览器里点一次"转换文本",终端会同时打印 [text-transform] mode=upper input="DeepSeek Harness"。这条链路覆盖了本教材第 6 章的 inject、Service、声明合并、ctx.plugin 子 Fiber、ctx.effect 清理、类型事件广播,是后续替换成 HTTP Provider(第 9.6 节)的起点。
text-transform-plugin/├── cordis.yml├── tsconfig.json└── src/ ├── service.ts # Service Definition + Request/Result + typed event ├── provider.ts # Local Provider + Config + cache + cleanup ├── tool.ts # Consumer Tool + canonical output + UI intent └── audit.ts # Event observer
9.1 service.ts:稳定能力接口
import { Service, type Context } from '@deepseek-ai/cordis'export interface TransformRequest { text: string}export interface TransformResult { input: string output: string mode: 'upper' | 'lower'}declare module '@deepseek-ai/cordis' { interface Context { textTransform: TextTransformService } interface Events { 'learning-text/transformed': (result: TransformResult) => void }}/** Service Definition:定义调用者依赖的稳定能力,不决定能力如何实现。 */export abstract class TextTransformService extends Service { constructor(ctx: Context) { super(ctx, 'textTransform') } abstract transform(request: TransformRequest): Promise<TransformResult>}
Definition 拥有 TransformRequest/TransformResult,因此 Provider 与 Consumer 对同一契约编译。抽象类注册 ctx.textTransform 的运行时名字;module augmentation 提供编译时类型。事件表示"转换已发生",所以使用 emit 广播,不返回决策。
9.2 provider.ts:本地实现、配置、缓存与清理
import type { Context } from '@deepseek-ai/cordis'import Schema from '@deepseek-ai/schemastery'import { TextTransformService, type TransformRequest, type TransformResult,} from './service.ts'export interface Config { mode: 'upper' | 'lower' prefix: string cleanupIntervalMs: number}export const Config: Schema<Config> = Schema.object({ mode: Schema.union(['upper', 'lower']).default('upper'), prefix: Schema.string().default(''), cleanupIntervalMs: Schema.number().default(30000),})class LocalTextTransformService extends TextTransformService { private readonly cache = new Map<string, TransformResult>() private readonly mode: Config['mode'] private readonly prefix: string constructor(ctx: Context, config: Config) { super(ctx) this.mode = config.mode this.prefix = config.prefix const timer = setInterval(() => this.cache.clear(), config.cleanupIntervalMs) this.ctx.effect(() => () => clearInterval(timer)) } async transform(request: TransformRequest): Promise<TransformResult> { const cached = this.cache.get(request.text) if (cached !== undefined) return cached const transformed = this.mode === 'upper' ? request.text.toUpperCase() : request.text.toLowerCase() const result: TransformResult = { input: request.text, output: `${this.prefix}${transformed}`, mode: this.mode, } this.cache.set(request.text, result) this.ctx.emit('learning-text/transformed', result) return result }}export const name = 'learning-text-transform-provider'export function apply(ctx: Context, config: Config): void { if (!Number.isFinite(config.cleanupIntervalMs) || config.cleanupIntervalMs <= 0) { throw new Error('cleanupIntervalMs must be a positive finite number') } ctx.plugin(LocalTextTransformService, config)}
外层函数插件负责配置 Schema,内层 Service 类负责状态。ctx.plugin(LocalTextTransformService, config) 创建子 Fiber;父插件卸载会递归卸载 Service。Timer 是自有资源,因此放入 Effect。缓存命中不再次 emit,事件语义是"完成了一次实际转换",不是"每次 API 调用"。
9.3 tool.ts:模型 Consumer 与结构化输出
import type { Context } from '@deepseek-ai/cordis'import { defineTool } from '@deepseek-ai/dsh-tools'import './service.ts'export const name = 'learning-text-transform-tool'export const inject = ['tools', 'textTransform']export function apply(ctx: Context): void { ctx.tools.register(defineTool({ name: 'transform_text', description: 'Transform text with the configured text transformation provider.', parameters: { text: { type: 'string', required: true, description: 'Text to transform' }, }, output: { schema: { type: 'object', additionalProperties: false, properties: { input: { type: 'string', required: true }, output: { type: 'string', required: true }, mode: { type: 'string', required: true }, }, }, render: (_args, value) => [{ type: 'text', text: `Mode: ${value.mode}\nInput: ${value.input}\nOutput: ${value.output}`, }], }, presentCall: args => ({ card: 'generic', title: 'Transform text', rawInput: args.text, }), async execute(args, exec) { if (exec.signal.aborted) throw exec.signal.reason if (args.text.trim() === '') throw new Error('text must not be blank') return ctx.textTransform.transform({ text: args.text }) }, }))}
Consumer 只依赖 Service Definition,不知道本地缓存如何实现。Schema DSL 能保证字符串类型,但不能表达"trim 后非空",所以在 execute 最早检查。exec.signal 已中止时立即失败;真实网络 Provider 还应把该 signal 传给 fetch。规范对象适合 Code Mode,render 文本适合模型,presentCall 只决定 UI pending 卡片。
9.4 audit.ts:类型事件观察者
import type { Context } from '@deepseek-ai/cordis'import './service.ts'export const name = 'learning-text-transform-audit'export function apply(ctx: Context): void { ctx.on('learning-text/transformed', (result) => { console.log(`[text-transform] mode=${result.mode} input=${JSON.stringify(result.input)}`) })}
ctx.on() 的参数由 Events 声明自动推导。监听器随 audit Fiber 自动移除,不需要 off()。这个事件只写终端日志,不是 Session Event,也不会进入模型上下文。
9.5 cordis.yml:组合三个角色
- insert: - id: learning-text-transform-provider name: '/root/deepseek-ai/deepseek-harness/.artifacts/deepseek-harness-learning-lab/text-transform-plugin/src/provider.ts' config: mode: upper prefix: 'RESULT: ' cleanupIntervalMs: 30000 - id: learning-text-transform-tool name: '/root/deepseek-ai/deepseek-harness/.artifacts/deepseek-harness-learning-lab/text-transform-plugin/src/tool.ts' - id: learning-text-transform-audit name: '/root/deepseek-ai/deepseek-harness/.artifacts/deepseek-harness-learning-lab/text-transform-plugin/src/audit.ts'
YAML 顺序不是依赖关系:Tool 的 inject 才保证 Provider 已提供 textTransform。把 mode 改成 lower,Provider Fiber 会被替换,Tool 因必需服务短暂消失而自动重载。
9.6 如何把 Provider 换成 HTTP
保留 service.ts 与 tool.ts,只新增另一个 Provider:
class HttpTextTransformService extends TextTransformService { constructor(ctx: Context, private readonly endpoint: string) { super(ctx) } async transform(request: TransformRequest): Promise<TransformResult> { const response = await fetch(this.endpoint, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(request), }) if (!response.ok) throw new Error(`transform endpoint returned ${response.status}`) return response.json() as Promise<TransformResult> }}
生产版本应继续加入超时/调用方 signal、响应边界校验、凭据引用和稳定错误分类。关键收益是 Tool 完全不改,模型契约也不改。
9.7 复杂 Demo 的练习与验收
- •将
mode改成lower,预期RESULT: deepseek harness。 - •连续两次转换同一文本,观察 audit 只在首次实际计算时打印。
- •删除 Provider row,Tool 应因缺少
textTransform进入 PENDING/卸载,而不是调用 undefined。 - •写
ReverseTextTransformService并替换 Provider,不改 Tool。 - •新增
learning-text/before-transformwaterfall,允许策略拒绝超长输入;有意拒绝时不调用next(),普通观察时必须调用。
验收标准:TypeScript 检查通过;--dump-config 有三行;插件列表全启用;真实模型能调用;配置替换后没有残留 timer;替换 Provider 后 Tool Schema 不变。
10. Tool 开发方法全集
10.1 参数 Schema
标量支持 string、number、integer、boolean、null;复杂参数可使用 array、object、联合和字面量约束。隐式 parameters 根对象由 Tool DSL 管理;显式 object 节点必须声明 additionalProperties。Schema 负责 JSON 结构,业务规则仍在 execute 校验。
10.2 规范值与模型文本分离
错误做法是 execute 返回"job started: abc-1",然后另一个 Tool 从文本解析 id。正确做法是返回 { kind: 'background', jobId: 'abc-1' },render 再生成人类文本。这样 Code Mode、UI 和测试都使用稳定字段。
10.3 取消与长任务
前台任务把 exec.signal 传到 fetch、文件 API 或子进程。后台任务使用 ctx.jobs.start() 后由 Job 自己的取消信号拥有生命周期;外层 Tool Call 结束不应杀死已经发布的后台 Job。
10.4 策略与观察
- •
tools/pre-execute:允许、拒绝、审批;ctx.tools.guard()可设置后续无法撤销的最终拒绝。 - •
tools/execute:around-dispatch,可加超时、重试和指标。 - •
tools/post-execute:修改模型展示、阻止结果或附加上下文。 - •
tools/result:只观察最终不可变结果。
不要把所有部署策略写死在每个 Tool 内;策略插件可以统一覆盖整条流水线。
10.5 UI Render Intent
- •读取或普通动作:
generic,可附locations。 - •Shell 命令:
terminal。 - •文件变更:
diff,回放需要的旧/新文本应来自持久 meta。 - •Glob/Grep:
search。 - •搜索/抓取:
web。
presentCall/presentResult 必须是纯函数。实时执行与历史回放都调用它们,任何 I/O 都会让同一 Session 显示不一致。
10.6 Code Mode
已注册工具自动以 await tools.<name>(args) 暴露给 Code Mode,无需再写适配器。成功值是 output.schema 对应的规范 JSON;失败抛 ToolCallError。因此 Schema 也是程序化 API,字段命名要稳定明确。
11. 插件发布:从本地 Overlay 到可安装 Bundle
本地实验使用绝对路径;交给别人应创建 npm Bundle:
dsh-text-transform-bundle/├── package.json├── cordis.patch.yml└── lib/ ├── service.js ├── provider.js ├── tool.js └── audit.js
package.json:
{ "name": "dsh-text-transform-bundle", "version": "0.1.0", "type": "module", "files": ["lib", "cordis.patch.yml"], "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }}
Bundle 中的 row 使用包 exports,不使用开发机绝对路径。安装:
dsh plugin --profile web add dsh-text-transform-bundledsh --profile web --dump-configdsh web
从 Git 安装源码包时,作者必须提供自包含 prepare 构建;pnpm 10+ 要求用户在 Profile 的 pnpm-workspace.yaml 明确 allowBuilds。这等同授权依赖在 Agent 沙箱外执行安装脚本,只对可信且锁定 commit 的仓库授权。npm 预构建包或 pnpm pack tarball 不需要安装期构建权限。
11.1 DeepSeek Harness 插件怎么安装
"插件"在这里有三条完全不同的安装路径,先分清各自适用场景,避免用错命令:
| 路径 | 命令 | 何时用 | 卸载方式 |
|---|---|---|---|
| 临时 Overlay | dsh web --patch ./xxx/cordis.yml |
本地开发、学习、调试,每次启动临时加载 | 重启时去掉该 --patch,什么都不用删 |
| 正式 npm Bundle | dsh plugin --profile web add <包名> |
分发可复用的第三方插件/Bundle | dsh plugin --profile web remove <包名> |
| 源码目录 / Git | dsh plugin --profile web add ./本地目录 或 <git url> |
尚未发布到 npm 的源码 checkout | dsh plugin --profile web remove <包名> |
路径一:临时 Overlay(本教材两个 Demo 用的就是这条)。 --patch 只是把一份 cordis.yml 作为最末一层覆盖到启动树,不写入任何 Profile 文件,不创建 node_modules 依赖。greet-plugin 和 text-transform-plugin 都是用绝对路径 name 直接指向 .ts 源码,由源码 CLI 的 TSX 现场执行,所以根本谈不上"卸载":重启时不带这一行就消失。
路径二/三:正式 Bundle(dsh plugin)。 这是真正的"安装"。Bundle 是带 dsh.bundle.patch 清单(见上文 package.json)的 npm 包,add 之后它成为 Profile 目录里 pnpm-workspace.yaml 声明的依赖,随 dsh --profile web 每次启动稳定加载:
# 从 npm 安装已发布包dsh plugin --profile web add dsh-text-transform-bundle# 从本地目录安装(相对路径以调用目录为锚点)dsh plugin --profile web add ./dsh-text-transform-bundle# 从 Git 仓库安装(需作者提供 prepare 构建,见上文 allowBuilds)dsh plugin --profile web add https://github.com/you/dsh-text-transform-bundle.git
11.2 查看已装插件与状态
安装前后都先"看"再"动",三处互相印证:
# 1. 看最终组合树里有哪些 row、是否命中你的覆盖dsh --profile web --dump-config# 2. 看 Profile 目录里实际声明了哪些插件依赖dsh plugin --profile web list # 若当前版本提供;否则看 $DSH_HOME/profiles/web/pnpm-workspace.yaml# 3. 运行时看每个 Fiber 是 ACTIVE / PENDING / FAILED# 浏览器打开 Web UI → 设置 → 插件 → 插件列表
浏览器插件列表是最直观的一处:本教材四个 Demo 模块(learning-greet-tool、learning-text-transform-provider/tool/audit)都显示"已启用";若某个模块停在 PENDING,通常是它的 inject 依赖(如 textTransform)没有加载。
11.3 禁用、升级与彻底卸载
临时 Overlay:
- •禁用某一行:在该
cordis.yml的 row 上写disabled: true(保留 row 便于恢复),或直接删掉该 row。 - •彻底卸载:重启时从命令中去掉对应的
--patch。示例中的text-transform-plugin从未写进 Profile,因此"卸载"就是重启时不带它,不要执行pnpm uninstall或dsh plugin remove。
正式 Bundle:
# 升级到新版本dsh plugin --profile web add dsh-text-transform-bundle@latest# 彻底卸载:移除依赖并在 Profile 中清除该 patch 层dsh plugin --profile web remove dsh-text-transform-bundle# 卸载后确认 row 已消失dsh --profile web --dump-config
remove 会从 Profile 的依赖清单中移除该包并撤销其补丁层;它操作的是 Profile 的插件清单,不是仓库根 package.json,所以和 pnpm remove 是两回事。卸载后如仍能在 --dump-config 看到该 row,说明它还来自 Profile 自有 patch、$DSH_HOME/cordis.patch.yml 或某个 --patch,逐层排查而不是重复执行 remove。
12. 具体学习路线:每一步学什么、做什么、如何验收
第 1 阶段:产品使用与数据流(约 1 小时)
学习内容:Provider 是连接路由,Model 是路由内身份,Workspace 是工具默认文件根,Permission 决定写操作与审批,Session 是耐久日志,Agent 是当前驱动。
动手:启动第 7.3 节实例;查看设置的模型与插件;选择 workspace;发送普通问答;打开 Session log;找到 turn/start、request/header、assistant/message、turn/end。
验收:能解释为什么关闭浏览器不会删除 Session,为什么切换默认模型不一定修改已有 Session,为什么未选择 workspace 时输入框禁用。
第 2 阶段:组合系统(约 1 小时)
学习内容:Profile 是用户组合,Bundle 是发布层,Patch 按 row id 覆盖,Overlay 是临时最后层,配置整行替换。
动手:分别 dump 默认 Web、加入 custom-model、再加入两个 Demo;搜索 agent-default-model、llm-pi-ai、learning-greet-tool。
验收:能画出层顺序;能预测交换两个 --patch 的结果;能说明为什么覆盖 row 时不能只写一个 config 字段。
第 3 阶段:第一个 Tool(约 1 小时)
学习内容:Plugin/Fiber/inject/Config/Tool parameters/execute/output/render。
动手:逐字重建第 8 章代码;增加 title 可选参数;把输出从 string 改为 object;故意制造 Schema 不匹配再观察错误。
验收:插件列表已启用;模型能调用;Code Mode 可获得结构化值;热替换不产生重复注册。
第 4 阶段:生命周期与事件(约 1.5 小时)
学习内容:Effect 自动清理、子 Fiber、服务消失导致消费方卸载、emit/bail/serial/waterfall。
动手:为 greet 增加计数 Service;每次执行 emit;增加 observer;增加 timer 并通过 Effect 清理;增加 waterfall 将名字 trim。
验收:卸载插件后 timer 停止;普通 waterfall listener 调用 next;故意短路时下游不运行;事件参数有类型推导。
第 5 阶段:完整能力 Seam(约 2 小时)
学习内容:Definition 拥有类型,Provider 和 Consumer 互不依赖,运行时 Service 与编译时 module augmentation 缺一不可。
动手:完整重建第 9 章;写 reverse Provider;再写 HTTP Provider;只改 overlay 切换实现。
验收:Tool 源码完全不变;Provider 切换后结果改变;缺 Provider 时 Tool 不加载;审计事件只记录实际转换。
第 6 阶段:Agent Loop 与持久化(约 2 小时)
学习内容:Turn/Step、三段 Tool Pipeline、流式 Chunk、Session Event 投影、"模型可见即记录"。
动手:发送同时调用两个 Tool 的提示;在轨迹页确认 1 Turn/2 Steps;导出 Session;从日志手工定位两个 tool/call 与 tool/result。
验收:能从日志解释下一次模型请求的历史;能说明 Cordis audit event 为什么不会进入 Session;能指出新增模型上下文时为何要新增耐久事件。
第 7 阶段:模型与 Web 扩展(半天)
学习内容:pi-ai route、手写 LLM Adapter 的适用边界、Host/Client、Typert Remote、Conversation Node、UI Tool intent。
动手:在 custom route 增加第二个模型并从 UI 切换;观察 request/header;给 Tool 增加 generic locations;阅读一条生成 Remote 的 Host 方法和 Client 调用。
验收:能判断一个需求应放 Adapter、Provider、Tool、Remote 还是 Client UI;能说明为什么业务后端不应写进 React。
13. 测试、验证与调试
13.1 本教材已执行的验证
- •
pnpm install成功;原生node-pty编译成功。 - •
pnpm run build成功。 - •
pnpm run typecheck成功。 - •两个 Demo 独立 TypeScript 检查成功。
- •CLI 与 base/web-app/headless Bundle:4 个文件、25 项测试通过。
- •自定义网关
/v1/models和/v1/chat/completions均 HTTP 200。 - •Web 模型页确认
learning-gateway与 Key 已配置。 - •四个 Demo 模块均在插件列表显示已启用。
- •真实模型依次调用两个 Tool,结果符合预期。
13.2 开发时最短检查链
- •
pnpm dsh web --patch ... --dump-config:配置是否命中。 - •
pnpm exec tsc -p <demo>/tsconfig.json:类型是否满足公开声明。 - •启动 Web,设置 → 插件:Fiber 是否挂载。
- •使用明确提示强制调用:区分"模型选择不调用"和"Tool 未注册"。
- •查看 Session log/轨迹:参数、结果、Step 是否正确。
- •修改配置,验证旧注册、timer、socket 是否清理。
13.3 仓库命令
| 命令 | 用途 |
|---|---|
pnpm run build |
Host lib → Client lib → Web 前端 |
pnpm run typecheck |
严格 TypeScript 与 Typert 生成契约 |
pnpm run lint |
Oxlint |
pnpm run test |
默认 Vitest |
pnpm run test:coverage |
CI 的逐文件覆盖率门禁 |
pnpm run test:e2e |
真实 API;无 Key 的相关项自跳过 |
pnpm run test:snapshot |
Keyless 模型响应回放 |
pnpm run test:web |
构建后的浏览器测试 |
pnpm run doc-sync |
文档生成、新鲜度、链接与配对 |
pnpm run hygiene |
knip、publint、约束与运行闭包 |
13.4 常见故障
- •Web 可开但不能输入:先添加并选择 workspace,再确认 Model。
- •
MISSING_CREDENTIAL:Key 未被 route 的apiKeyEnv解析;重启后检查高优先级启动环境和 Credentials 页面。 - •根
/models是 HTML:尝试端点实际 API 前缀;本例是/v1/models。 - •
UNKNOWN_MODEL:模型不在 pi-ai route 的models中,或模型 id 拼错。 - •401/403:Key 无效或未传;不要打印 Key 调试,只看来源描述符和 HTTP 状态。
- •404 Chat Completions:
baseURL缺/v1,或网关不是openai-completions。 - •Overlay 改一个字段导致其他字段消失:目标
config是整体替换。 - •插件 PENDING:
inject的服务不存在;检查服务运行时名字与 module augmentation 是否一致。 - •Tool 注册但不调用:明确要求调用,检查模型是否支持 Tool Calling,再看请求 Tool Schema。
- •
.env修改无效:启动快照已冻结,需要重启;bootstrap-only 变量根本不允许在文件层。 - •Credential 权限错误:POSIX 上执行
chmod 600 ~/.dsh/.credentials.yaml。 - •源码 CLI 缺 Typert/Client bundle:先运行
pnpm run build。 - •
node-pty下载头文件失败:NVM 环境可把npm_config_nodedir指向当前精确 Node 版本安装目录后重试。
14. 初学者最容易犯的设计错误
- •靠 YAML 顺序代替
inject:加载在热替换或隔离场景下会随机失败。 - •在 waterfall 观察者中忘记
next():整个请求链被短路。 - •注册原生 listener/timer 却不用 Effect:HMR 后重复执行并泄漏。
- •在 Service constructor 启动不受管理的异步任务:服务已可见但初始化未完成。
- •只有 module augmentation,没有
super(ctx, key):编译有属性,运行时没有服务。 - •只有运行时服务,没有声明合并:消费方失去类型安全。
- •Tool 返回自然语言句柄:Code Mode 和其他 Tool 只能脆弱解析文本。
- •对象 output 不写
additionalProperties:契约不明确或定义被拒。 - •忽略
exec.signal:取消 Agent 后网络或进程仍在运行。 - •在
presentCall做 I/O:历史回放与实时 UI 不一致。 - •把模型可见信息仅塞进内存:恢复后请求无法重建。
- •把 Provider 和 Consumer 相互 import:无法替换后端,依赖图形成环。
- •把 Key 写进 YAML 或提交:凭据进入配置、日志和 Git 历史。
- •把网关主页当 API Base URL:
/models得到 HTML,Chat 请求 404。 - •把模型容量示例当权威:上下文和输出上限应按部署事实配置。
15. 其他运行形态
Headless
pnpm dsh --profile headless "summarize this workspace"
它创建一个新持久 Session,运行到空闲,打印最后非空 Assistant 文本后退出;没有 HTTP 或浏览器。自定义模型可同样通过 --patch custom-model/cordis.yml 覆盖。
Python SDK
from pathlib import Pathfrom deepseek_harness import DeepSeekHarnesswith DeepSeekHarness( provider="deepseek-official", model="deepseek-v4-flash", cwd=str(Path("/tmp/disposable-workspace").resolve()), session_root=str(Path("/tmp/dsh-sessions").resolve()), cordis=str(Path("examples/jsonrpc-agent/minimal.cordis.yml").resolve()),) as harness: result = harness.run("Inspect this workspace", session_id="study-001")print(result.final_response)
Minimal 示例使用 danger-full-access 和持久 PTY,只在可丢弃 checkout 或容器运行。独立任务使用不同 Session ID;复用 ID 会延续会话和 Shell 状态。
ACP 与 JSON-RPC
pnpm run demo:acp 通过 stdio 提供 Agent Client Protocol;packages/sdk/{protocol,server,client} 提供通用 JSON-RPC。自动化程序通过协议创建 Session、调用 Turn、接收通知和取消,而不是解析 CLI 文本。
MCP、Schedule、Workflow 与 Subagent
- •MCP:
dsh-mcp-client连接外部工具服务器;server 命令是部署可信代码,不在 Agent 沙箱内。 - •Schedule:
examples/web-schedule增加 Session 内持久提醒。 - •Workflow:模型编写的 Worker Thread 工作流,通过
workflow/ralphTool 执行。 - •Subagent:Provider 可以是进程内 spawn/fork、ACP、Claude Code、Codex 或 DSH SDK;模型 Consumer 仍使用统一 subagent Tool。
16. 单文件之外的权威来源
这份文件已经内联快速掌握所需知识;遇到版本变化时按以下所有权查证:架构看 docs/architecture.zh.md,类型与服务方法看 docs/subsystems/,全部 Tool/Config 看生成的 docs/tool-catalog.md 与 docs/config-catalog.md,单包语义看对应 README,设计理由看 .agents/notes/implemented/,机器实际组合看 dsh --dump-config。不要用旧教程替代当前源码和生成目录。