返回知识库
0

第二章:环境与运行

本章目标:帮助你理解 dsh 的开发环境配置、CLI 启动机制、Profile 系统的完整工作原理,以及如何从源码运行和调试 dsh。阅读本章后,你应该能成功运行 dsh --profile headless "hello" 并理解背后发生的每一步。


2.1 开发环境配置

2.1.1 前置条件

工具版本要求用途
Node.js^22.19 或 >=24运行时,利用原生 TypeScript 支持
pnpm最新版包管理,支持 workspace 协议
Git任意现代版本版本控制

2.1.2 安装与构建

# 1. 克隆仓库
git clone https://github.com/deepseek-harness/deepseek-harness.git
cd deepseek-harness

# 2. 安装依赖
pnpm install

# 3. 构建所有包
pnpm run build

# 4. 验证安装
dsh --profile headless "hello world"

2.1.3 关键环境变量

变量必需?说明
DEEPSEEK_API_KEYDeepSeek API 密钥,用于 LLM 调用
DEEPSEEK_BASE_URL自定义 API 端点
DSH_HOMEHarness home 目录(默认 ~/.dsh
DSH_TELEMETRY_DISABLED任意非空值禁用遥测

2.1.4 .env 文件

dsh 支持根目录 .env 文件,真实 API 测试和演示从这里读取凭证。永远不要提交凭证到版本控制。


2.2 CLI 启动入口

2.2.1 启动入口文件

所有支持的 Node 应用程序都从 dsh CLI 启动。唯一的入口文件是:

apps/cli/src/profile-boot.ts

这个文件导出的核心函数是 runProfile(),它是所有 Profile 启动的统一入口。

2.2.2 runProfile 的工作流

预览
源码
插件树Cordis Runtimeapp-bootcomposeProfileHTTP Proxyprofile-boot.tsdsh CLI插件树Cordis Runtimeapp-bootcomposeProfileHTTP Proxyprofile-boot.tsdsh CLI从环境快照解析代理(不是从 process.env)runProfile({profile, patchFiles, args, environment})installProxyFromEnvironment()composeProfile(profile, patchFiles)loadProfile(NAME, name, INSTALL_ANCHOR)Profile 对象loadOptionalPatches(homePatchPath)loadOverlayPatches(patchFiles)composeEntries(bundlePatches, profile.patches, homePatches, overlays)resolveTelemetryPatch(DSH_TELEMETRY_DISABLED)ComposedProfilecreateAppReady()createProcessShutdown()注册 SIGTERM/SIGINT 处理boot(rootConfig, allPatches)依次应用各 Patch 层挂载插件、注册服务ctx readycommit() → 触发 onReady 监听{ctx, shutdown}
sequenceDiagram
    participant CLI as dsh CLI
    participant PB as profile-boot.ts
    participant Proxy as HTTP Proxy
    participant CP as composeProfile
    participant AB as app-boot
    participant Cordis as Cordis Runtime
    participant Plugins as 插件树

    CLI->>PB: runProfile({profile, patchFiles, args, environment})
    PB->>Proxy: installProxyFromEnvironment()
    Note over Proxy: 从环境快照解析代理<br/>(不是从 process.env)
    PB->>CP: composeProfile(profile, patchFiles)
    CP->>AB: loadProfile(NAME, name, INSTALL_ANCHOR)
    AB-->>CP: Profile 对象
    CP->>CP: loadOptionalPatches(homePatchPath)
    CP->>CP: loadOverlayPatches(patchFiles)
    CP->>CP: composeEntries(bundlePatches, profile.patches, homePatches, overlays)
    CP->>CP: resolveTelemetryPatch(DSH_TELEMETRY_DISABLED)
    CP-->>PB: ComposedProfile
    PB->>PB: createAppReady()
    PB->>PB: createProcessShutdown()
    PB->>PB: 注册 SIGTERM/SIGINT 处理
    PB->>Cordis: boot(rootConfig, allPatches)
    Cordis->>Plugins: 依次应用各 Patch 层
    Plugins->>Plugins: 挂载插件、注册服务
    Plugins-->>PB: ctx ready
    PB->>PB: commit() → 触发 onReady 监听
    PB-->>CLI: {ctx, shutdown}

2.2.3 runProfile 选项

// 来自 apps/cli/src/profile-boot.ts
interface RunProfileOptions {
  /** 冻结的环境快照,在任何 entry 挂载前提供 */
  environment: LaunchEnvironmentSnapshot
  /** 要启动的 profile 名称 */
  profile: string
  /** --patch 覆盖文件路径,按 argv 顺序 */
  patchFiles: readonly string[]
  /** 调用的内部参数,通过 ctx.cmdlineArgs 提供给树 */
  args: readonly string[]
}

2.2.4 信号处理

// 来自 apps/cli/src/profile-boot.ts
process.on('SIGTERM', () => { interrupt(0) })    // 监控器的正常停止
process.on('SIGINT', () => { interrupt(130) })   // 用户中断,退出码 130

SIGTERM 是监控器的普通停止请求,在每个表面上都以 0 退出;SIGINT 是用户中断,报告 130。


2.3 Profile 系统

2.3.1 Profile 的概念

Profile 是一个命名的插件组合,存储在 Harness home($DSH_HOME)中。它包含:

  1. Bundle 列表:定义堆叠顺序

  2. 用户 Patch 层cordis.patch.yml

  3. Out-of-tree 插件:用户安装的外部插件

2.3.2 预定义 Profile

Profile用途Bundle
web浏览器应用dsh-base + dsh-web-app
headless单次任务执行,无服务器dsh-base + dsh-headless
sdkJSON-RPC SDK 服务器dsh-base + dsh-sdk-app
sdk-minimal精简 SDK(不使用 dsh-base)独立 dsh-sdk-minimal
acp自动化 Agent Client Protocoldsh-base + dsh-acp-app

2.3.3 dsh-base:共享基础层

dsh-basewebheadlesssdkacp 共享的第一层,包含:

  • 模型适配器

  • 工具注册

  • 持久化

  • 沙箱和审批策略

  • 设置

  • 凭证

  • 遥测

2.3.4 Patch 层叠加顺序

预览
源码

空 Entry 列表
([])

Bundle 层
按 dsh.profile.bundles 顺序

Profile 的 cordis.patch.yml

Home 级 cordis.patch.yml
($DSH_HOME/cordis.patch.yml)

--patch 覆盖层

遥测开关

graph TB
    A["空 Entry 列表<br/>([])"] --> B["Bundle 层<br/>按 dsh.profile.bundles 顺序"]
    B --> C["Profile 的 cordis.patch.yml"]
    C --> D["Home 级 cordis.patch.yml<br/>($DSH_HOME/cordis.patch.yml)"]
    D --> E["--patch 覆盖层"]
    E --> F["遥测开关"]

重要:Home 级 Patch 覆盖 Profile 级,因为机器本地偏好应该覆盖单个 Profile 的设置。

2.3.5 Profile 的源码实现

Profile 加载的核心在 packages/boot/app-boot/src/profile.tsloadProfile() 函数:

// 概念性描述(简化自 profile-boot.ts:119)
function prepareProfile(name: string, userLayer = true): Profile {
  // 1. 加载 profile 元数据
  const profile = loadProfile(NAME, name, INSTALL_ANCHOR, undefined, { userLayer })
  // 2. 重写空根配置(因为 Loader 需要一个真实的 include root)
  writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG)
  return profile
}

2.3.6 自定义 Profile

# 在 $DSH_HOME/profiles/my-profile/package.json
{
  "dsh": {
    "profile": {
      "bundles": ["dsh-base", "dsh-headless"]
    }
  }
}
# $DSH_HOME/profiles/my-profile/cordis.patch.yml
# 覆盖或添加插件配置
- id: my-custom-plugin
  config:
    key: value

然后运行:

dsh --profile my-profile "my task"

2.4 从源码运行

2.4.1 直接运行(推荐)

# 从源码运行,使用 tsx 的 ESM hook
pnpm dsh --profile headless "run the tests"

这使用 node --import tsx/esm 来运行 TypeScript 源码,无需先构建。

2.4.2 Headless 模式

Headless 模式是一次性任务执行:

dsh --profile headless "run the tests"

其启动插件(packages/bundle/headless/src/startup.ts)的工作原理:

// 来自 packages/bundle/headless/src/startup.ts
export function apply(ctx: Context): void {
  const program = headlessCommand()
  program.action(() => {
    const task = program.args.join(' ')
    if (task.trim() === '') program.error('error: a task is required')
    ctx.provide(HEADLESS_STARTUP_SERVICE, { task })
  })
  parseCmdline(ctx, program)
}

它将任务文本作为 Cordis 服务提供,然后 runner 插件从服务中读取并执行。

2.4.3 SDK 模式

dsh --profile sdk

启动 JSON-RPC 服务器,TypeScript SDK 可以连接并发送请求。

2.4.4 PTC 模式(Python Tool Calling)

pnpm run demo:ptc -- "task"

2.5 构建系统

2.5.1 TypeScript 项目布局

dsh 使用双平面布局:

  • Source 平面src/ 中的 TypeScript 源码,通过 tsconfig paths 解析

  • Artifact 平面lib/ 中的构建输出(lib/types/ 为类型声明,lib/ 为运行时)

预览
源码

Artifact 平面

Source 平面

tsc

tsdown

src/index.ts

lib/types/index.d.ts

lib/index.mjs

graph LR
    subgraph "Source 平面"
        A["src/index.ts"]
    end
    subgraph "Artifact 平面"
        B["lib/types/index.d.ts"]
        C["lib/index.mjs"]
    end
    A -->|tsc| B
    A -->|tsdown| C

2.5.2 常用命令

pnpm install            # 安装所有 workspace 依赖
pnpm run build          # 构建所有包
pnpm run test           # 运行单元测试
pnpm run test:coverage  # CI 覆盖率门控(100% per-file)
pnpm run typecheck      # TypeScript 类型检查
pnpm run lint           # ESLint 检查
pnpm run hygiene        # publint + workspace 检查 + NodeNext 消费者检查

2.5.3 ESM 约束

所有包使用纯 ESM("type": "module")。关键约束:

  • 跨包使用包名导入

  • 本地相对导入使用 .ts 扩展名

  • dsh CLI 源码启动通过 tsx 的 ESM-only hook(node --import tsx/esm

  • 所有到达的模块必须保持 ESM(不能有 CJS-only 导出)


2.6 调试与诊断

2.6.1 查看配置

# 查看当前 Profile 的完整配置树
dsh --profile web --dump-config

打印出的每一行都可以被你的 Patch 替换。

2.6.2 查看事件映射

# 生成事件生产者-消费者映射
pnpm run doc-sync

2.6.3 查看模块依赖图

pnpm run gen-module-graph

生成的文件在 docs/module-graph.md


2.7 测试

2.7.1 测试类型

命令用途需要 API Key?
pnpm run test单元测试
pnpm run test:coverageCI 覆盖率门控
pnpm run test:e2e真实 API 测试
pnpm run test:expected进程预期测试
pnpm run test:snapshot快照回放测试
pnpm run test:snapshot:record重录快照

2.7.2 快照测试

快照测试是 dsh 中最重要的测试类型之一。它通过键控无密钥录制-会话回放来验证行为:

# 运行所有快照测试
pnpm run test:snapshot

# 过滤特定测试
pnpm run test:snapshot -t <name>

# 重新录制预期输出(需要 API key)
pnpm run test:snapshot:record

2.7.3 Host 沙箱失败

如果需要 ghpnpm、构建、测试或生成器命令因为沙箱阻断凭证、网络、IPC、监视或嵌套 sandbox-exec 而失败,应使用最窄的主机升级重试。永远不要绕过测试失败。


2.8 常见问题

Q: 为什么 dsh 不是一个独立的可执行文件?

因为 dsh 的设计理念是所有功能都是插件。CLI 只是一个启动器,它从配置文件加载插件树。你通过修改配置来改变行为,而不是通过编译不同的可执行文件。

Q: 如何添加一个新的 Profile?

  1. $DSH_HOME/profiles/ 下创建目录

  2. 添加 package.json,在 dsh.profile.bundles 中列出需要的 Bundle

  3. 添加 cordis.patch.yml 进行自定义配置

  4. 运行 dsh --profile <name>

Q: 为什么使用 Vendored Cordis?

  • 审计:框架代码和产品代码在同一个 monorepo

  • 修补:可以直接修复框架 bug

  • Scope 重命名:避免在 npm 上抢占上游包名

  • 锁定:确保所有用户使用相同版本的框架

Q: pnpm dshdsh 有什么区别?

  • pnpm dsh:从源码运行,使用 tsx 的 ESM hook

  • dsh:从构建后的 lib/ 运行,需要先 pnpm run build


2.9 小结

概念一句话解释
Profile命名的 Bundle 组合 + 用户 Patch
Bundle可分发的配置行 + 代码
Patch 层按顺序叠加的配置覆盖
runProfile()统一的启动入口
Source/Artifact 平面src/ 和 lib/ 的双平面布局
ESM全部使用纯 ESM,禁止 CJS

下一步第三章:Cordis 框架基础——深入理解 Cordis 的 Plugin/Service/Context 模型,以及如何编写你的第一个 Cordis 插件。

DeepSeek-Harness / 02-环境与运行 0 0 iliuqi
2026-09-04T04:08:17.867007424Z 2026-09-04T07:56:36.550639328Z