DeepSeek Harness: 依赖驱动加载与自动重载

最后更新:2026-08-31

从"手动控制加载顺序"到"声明依赖,框架自动安排"——依赖驱动加载让开发者只需声明关系。加上热重载(HMR)机制,修改代码后无需重启,插件自动重载,开发体验质的飞跃。

💡 提示:依赖驱动 + HMR = "改了就生效"。你只需声明依赖关系,框架负责加载顺序;你只需保存代码,框架负责热重载。开发效率提升的核心在于减少"等待重启"的时间。

📋 前置知识:已完成 14-inject.md,理解 inject 依赖声明

1. 你将学到


2. 依赖图与拓扑排序

(1) 依赖图的构建

框架启动时,遍历所有插件的 inject 声明,构建有向无环图(DAG):

TYPESCRIPT
function buildDependencyGraph(plugins: Plugin[]) {
  const graph = new DAG()
  for (const plugin of plugins) {
    graph.addNode(plugin.name)
    for (const dep of plugin.inject) {
      graph.addEdge(dep, plugin.name)
    }
  }
  return graph
}

(2) 拓扑排序

拓扑排序决定加载顺序:

TEXT 📖 仅展示
插件声明:
  core:    inject = []
  tools:   inject = ['core']
  llm:     inject = ['core']
  my-tool: inject = ['tools', 'llm']

依赖图:
  core → tools → my-tool
  core → llm   → my-tool

拓扑排序结果: [core, tools, llm, my-tool]

(3) 并行加载

没有依赖关系的插件并行加载:

100%
graph TB
    subgraph Phase1[Phase 1]
        CORE[core]
    end
    subgraph Phase2[Phase 2 - 并行]
        TOOLS[tools]
        LLM[llm]
    end
    subgraph Phase3[Phase 3]
        MYTOOL[my-tool]
    end
    CORE --> TOOLS
    CORE --> LLM
    TOOLS --> MYTOOL
    LLM --> MYTOOL

(4) ▶ 示例 4

100%
graph TB
    CORE[core] --> TOOLS[tools]
    CORE --> SESSIONS[sessions]
    CORE --> LOGGER[logger]
    TOOLS --> MY_TOOL[my-tool]
    SESSIONS --> MY_TOOL
    LOGGER --> TRAJECTORY[trajectory]
    SESSIONS --> TRAJECTORY

3. 依赖就绪后自动加载

(1) 动态依赖满足

插件不需要在启动时就满足所有依赖。依赖的服务后来注册时,pending 的插件自动激活:

TYPESCRIPT
// 插件 A: inject = ['tools'] — tools 尚未注册
// → Fiber state: pending

// 稍后,tools 插件加载并注册服务
// → 插件 A 的 Fiber 自动转为 active,调用 apply

(2) ▶ 示例 2

TYPESCRIPT
export function apply(ctx: Context) {
  if (someCondition) {
    ctx.provide('optional-service', impl)
    // 此时依赖 optional-service 的 pending 插件会自动激活
  }
}

(3) ▶ 示例 3

100%
sequenceDiagram
    participant F as Framework
    participant A as Plugin A (inject: tools)
    participant T as Tools Plugin
    
    F->>A: 注册 → pending (tools 未就绪)
    F->>T: 注册 → active
    T->>F: 注册 tools 服务
    F->>A: 依赖就绪 → active
    A->>F: apply() 执行

(4) 永远无法满足的依赖

如果声明的必需依赖永远无法满足:

TEXT 📖 仅展示
[warn] plugin my-plugin has unsatisfied dependency: nonexistent-service
[warn] my-plugin will remain in pending state

插件不会报错,只是永远停留在 pending。可选依赖(带 ?)不满足时不会有警告。


4. 热重载(HMR)机制

依赖驱动加载与重载

(1) HMR 概念

热重载(Hot Module Replacement)允许在运行时替换插件代码,无需重启整个 DSH:

100%
graph LR
    CHANGE[代码修改] --> DETECT[文件变更检测]
    DETECT --> DISPOSE[旧 Fiber disposing]
    DISPOSE --> LOAD[新代码加载]
    LOAD --> ACTIVE[新 Fiber active]

(2) 启用 HMR

BASH
pnpm dsh web --patch --watch

--watch 参数启用文件监视,当监听到插件源码变更时自动触发重载。

(3) HMR 的完整流程

  1. 文件系统监视器检测到 src/index.ts 变更
  2. 旧插件的 Fiber 进入 disposing 状态
  3. 执行所有清理函数(ctx.effect、自动清理)
  4. 新代码编译并加载
  5. 新 Fiber 创建,进入 pending/active
  6. 依赖插件按需重新加载

(4) HMR 的限制

场景 HMR 支持 说明
修改 execute 函数 工具逻辑热更新
修改 Config 配置重新校验
修改 inject ⚠️ 可能触发级联重载
修改 name 需要手动重启
修改依赖版本 需要手动重启

(5) 级联重载

当被依赖的插件重载时,依赖它的插件也会重载:

TEXT 📖 仅展示
tools 插件 HMR 重载 → 依赖 tools 的 my-tool 也重载

这确保了依赖关系始终一致,但可能产生"重载风暴":

TEXT 📖 仅展示
core 重载 → tools 重载 → my-tool 重载 → ... (整条依赖链重载)

5. 嵌套上下文(Nested Context)

(1) 上下文层级

Cordis 支持嵌套上下文——子上下文继承父上下文的服务,但可以覆盖:

TYPESCRIPT
export function apply(ctx: Context) {
  const childCtx = ctx.extend({
    // 覆盖或新增服务
  })
  
  childCtx.plugin({
    name: 'child-plugin',
    apply(innerCtx) {
      // innerCtx 继承 ctx 的服务
    }
  })
}

(2) 上下文继承规则

TEXT 📖 仅展示
父 ctx: { tools, llm, sessions }
子 ctx: { tools(覆盖), cache(新增) }

子 ctx 可见: { tools(覆盖版), llm, sessions, cache }

(3) 嵌套上下文的使用场景

场景 说明
会话隔离 每个会话有独立的 ctx
请求作用域 每个请求创建临时 ctx
测试 创建隔离的测试上下文
多 Agent 每个 Agent 有独立的工具集

(4) 嵌套深度

理论上无深度限制,但过深的嵌套会影响性能:

TYPESCRIPT
// ❌ 过深嵌套
ctx.extend().extend().extend().extend()

// ✅ 适度嵌套
const sessionCtx = ctx.extend({ session })

6. 配置变更触发的重载

(1) 自动重载

当用户在 Web UI 修改插件配置时,框架自动触发重载:

100%
graph LR
    UI[Web UI 修改配置] --> VALID[Schema 校验]
    VALID --> OLD[旧 Fiber disposing]
    OLD --> NEW[新 Config + 新 Fiber]
    NEW --> ACTIVE[Fiber active]

(2) 部分配置热更新

某些配置变更不需要完整重载:

TYPESCRIPT
export function apply(ctx: Context) {
  ctx.on('config/updated', (newConfig) => {
    if (newConfig.debug !== ctx.config.debug) {
      ctx.logger.level = newConfig.debug ? 'debug' : 'info'
    }
  })
}

(3) 需要重载的配置

以下配置变更需要完整重载:

(4) 重载与持久化

配置变更在重载后持久化到 cordis.yml:

YAML
plugins:
  my-plugin:
    config:
      debug: true  # 用户通过 Web UI 修改,自动持久化

7. 开发时 HMR 最佳实践

(1) 保持 apply 幂等

apply 函数应该是幂等的——多次调用结果一致:

TYPESCRIPT
// ✅ 幂等:每次 apply 都注册同样的工具
export function apply(ctx: Context) {
  ctx.tools.register(fileCountTool)
}

// ❌ 非幂等:apply 会累积副作用
let counter = 0
export function apply(ctx: Context) {
  counter++  // 重载后 counter 会递增
}

(2) 避免全局状态

TYPESCRIPT
// ❌ 全局状态:HMR 重载后旧状态残留
const globalCache = new Map()

// ✅ 闭包状态:每次 apply 创建新状态
export function apply(ctx: Context) {
  const cache = new Map()
  ctx.effect(() => () => cache.clear())
}

(3) 清理函数要完整

HMR 重载时,旧 Fiber 的清理函数必须完整清理所有资源:

TYPESCRIPT
export function apply(ctx: Context) {
  const ws = new WebSocket('ws://localhost:8080')
  
  // ✅ 注册清理
  ctx.effect(() => () => ws.close())
  
  // ❌ 忘记清理 → 重载后旧连接泄漏
}

(4) 开发工作流

Alice 的推荐 HMR 开发循环:

BASH
# 1. 启动带 HMR 的开发模式
pnpm dsh web --patch --watch

# 2. 正常编写代码,保存后自动重载
# 终端输出:
# [hmr] file changed: src/index.ts
# [hmr] disposing my-plugin (old)
# [hmr] loading my-plugin (new)
# [my-plugin] plugin reloaded

# 3. 检查重载日志,确认无清理错误

(5) HMR 调试技巧

TYPESCRIPT
// 在 apply 开头加调试日志
export function apply(ctx: Context) {
  ctx.logger.info('apply called at', new Date().toISOString())
  // ...
}
// 如果看到 apply 被意外调用多次,说明 HMR 级联重载

❓ 常见问题

Q HMR 和手动重启有什么区别?
A HMR 只重载变更的插件及其依赖链,其他插件不受影响。手动重启所有插件重新加载,耗时更长。
Q HMR 重载时会话会丢失吗?
A 不会。会话数据由 sessions 服务管理,它不受 HMR 影响。只有被重载的插件的状态会丢失。
Q 如何判断重载是否成功?
A 查看终端日志。成功的重载会输出 [hmr] loading xxx (new)[xxx] plugin reloaded。失败会输出错误信息。
Q 级联重载太频繁怎么办?
A 检查是否被底层插件(如 core)不必要地依赖。如果工具插件只依赖 tools,就不需要因为 core 重载而级联。
Q 嵌套上下文中的 HMR 行为?
A 子上下文中的插件重载时,只影响该子上下文内的插件,父上下文不受影响。
Q 生产环境应该用 HMR 吗?
A 不应该。HMR 是开发工具,生产环境应使用稳定加载。--watch 参数仅用于开发。 ---

📖 小节


📝 作业

1. ⭐ 基础题:启动 pnpm dsh web --patch --watch,修改一个已加载插件的 apply 函数(添加一行日志),保存后观察终端输出的 HMR 重载信息。

2. ⭐⭐ 进阶题:创建两个有依赖关系的插件(A inject B),启动 HMR 后修改 B 的代码,观察 A 是否被级联重载。然后只修改 A 的代码,确认 B 不受影响。

3. ⭐⭐⭐ 挑战题:编写一个使用全局变量的插件(非幂等),在 HMR 重载后观察变量值的变化。然后重构为闭包状态(幂等),验证重载后行为一致。记录重构前后的终端输出对比。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏