DeepSeek Harness: 依赖驱动加载与自动重载
最后更新:2026-08-31
从"手动控制加载顺序"到"声明依赖,框架自动安排"——依赖驱动加载让开发者只需声明关系。加上热重载(HMR)机制,修改代码后无需重启,插件自动重载,开发体验质的飞跃。
📋 前置知识:已完成 14-inject.md,理解 inject 依赖声明
1. 你将学到
- 依赖图与拓扑排序
- 依赖就绪后自动加载
- 热重载(HMR)机制
- 嵌套上下文(Nested Context)
- 配置变更触发的重载
- 开发时 HMR 最佳实践
2. 依赖图与拓扑排序
(1) 依赖图的构建
框架启动时,遍历所有插件的 inject 声明,构建有向无环图(DAG):
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) 拓扑排序
拓扑排序决定加载顺序:
插件声明:
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) 并行加载
没有依赖关系的插件并行加载:
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
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 的插件自动激活:
// 插件 A: inject = ['tools'] — tools 尚未注册
// → Fiber state: pending
// 稍后,tools 插件加载并注册服务
// → 插件 A 的 Fiber 自动转为 active,调用 apply
(2) ▶ 示例 2
export function apply(ctx: Context) {
if (someCondition) {
ctx.provide('optional-service', impl)
// 此时依赖 optional-service 的 pending 插件会自动激活
}
}
(3) ▶ 示例 3
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) 永远无法满足的依赖
如果声明的必需依赖永远无法满足:
[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:
graph LR
CHANGE[代码修改] --> DETECT[文件变更检测]
DETECT --> DISPOSE[旧 Fiber disposing]
DISPOSE --> LOAD[新代码加载]
LOAD --> ACTIVE[新 Fiber active]
(2) 启用 HMR
pnpm dsh web --patch --watch
--watch 参数启用文件监视,当监听到插件源码变更时自动触发重载。
(3) HMR 的完整流程
- 文件系统监视器检测到
src/index.ts变更 - 旧插件的 Fiber 进入 disposing 状态
- 执行所有清理函数(ctx.effect、自动清理)
- 新代码编译并加载
- 新 Fiber 创建,进入 pending/active
- 依赖插件按需重新加载
(4) HMR 的限制
| 场景 | HMR 支持 | 说明 |
|---|---|---|
| 修改 execute 函数 | ✅ | 工具逻辑热更新 |
| 修改 Config | ✅ | 配置重新校验 |
| 修改 inject | ⚠️ | 可能触发级联重载 |
| 修改 name | ❌ | 需要手动重启 |
| 修改依赖版本 | ❌ | 需要手动重启 |
(5) 级联重载
当被依赖的插件重载时,依赖它的插件也会重载:
tools 插件 HMR 重载 → 依赖 tools 的 my-tool 也重载
这确保了依赖关系始终一致,但可能产生"重载风暴":
core 重载 → tools 重载 → my-tool 重载 → ... (整条依赖链重载)
5. 嵌套上下文(Nested Context)
(1) 上下文层级
Cordis 支持嵌套上下文——子上下文继承父上下文的服务,但可以覆盖:
export function apply(ctx: Context) {
const childCtx = ctx.extend({
// 覆盖或新增服务
})
childCtx.plugin({
name: 'child-plugin',
apply(innerCtx) {
// innerCtx 继承 ctx 的服务
}
})
}
(2) 上下文继承规则
父 ctx: { tools, llm, sessions }
子 ctx: { tools(覆盖), cache(新增) }
子 ctx 可见: { tools(覆盖版), llm, sessions, cache }
- 服务查找:先查子 ctx,再查父 ctx(原型链模式)
- 事件传播:子 ctx 的事件会冒泡到父 ctx
- 资源清理:子 ctx 销毁时不影响父 ctx
(3) 嵌套上下文的使用场景
| 场景 | 说明 |
|---|---|
| 会话隔离 | 每个会话有独立的 ctx |
| 请求作用域 | 每个请求创建临时 ctx |
| 测试 | 创建隔离的测试上下文 |
| 多 Agent | 每个 Agent 有独立的工具集 |
(4) 嵌套深度
理论上无深度限制,但过深的嵌套会影响性能:
// ❌ 过深嵌套
ctx.extend().extend().extend().extend()
// ✅ 适度嵌套
const sessionCtx = ctx.extend({ session })
6. 配置变更触发的重载
(1) 自动重载
当用户在 Web UI 修改插件配置时,框架自动触发重载:
graph LR
UI[Web UI 修改配置] --> VALID[Schema 校验]
VALID --> OLD[旧 Fiber disposing]
OLD --> NEW[新 Config + 新 Fiber]
NEW --> ACTIVE[Fiber active]
(2) 部分配置热更新
某些配置变更不需要完整重载:
export function apply(ctx: Context) {
ctx.on('config/updated', (newConfig) => {
if (newConfig.debug !== ctx.config.debug) {
ctx.logger.level = newConfig.debug ? 'debug' : 'info'
}
})
}
(3) 需要重载的配置
以下配置变更需要完整重载:
- inject 列表变化
- 端口号变更
- 服务注册参数变更
(4) 重载与持久化
配置变更在重载后持久化到 cordis.yml:
plugins:
my-plugin:
config:
debug: true # 用户通过 Web UI 修改,自动持久化
7. 开发时 HMR 最佳实践
(1) 保持 apply 幂等
apply 函数应该是幂等的——多次调用结果一致:
// ✅ 幂等:每次 apply 都注册同样的工具
export function apply(ctx: Context) {
ctx.tools.register(fileCountTool)
}
// ❌ 非幂等:apply 会累积副作用
let counter = 0
export function apply(ctx: Context) {
counter++ // 重载后 counter 会递增
}
(2) 避免全局状态
// ❌ 全局状态:HMR 重载后旧状态残留
const globalCache = new Map()
// ✅ 闭包状态:每次 apply 创建新状态
export function apply(ctx: Context) {
const cache = new Map()
ctx.effect(() => () => cache.clear())
}
(3) 清理函数要完整
HMR 重载时,旧 Fiber 的清理函数必须完整清理所有资源:
export function apply(ctx: Context) {
const ws = new WebSocket('ws://localhost:8080')
// ✅ 注册清理
ctx.effect(() => () => ws.close())
// ❌ 忘记清理 → 重载后旧连接泄漏
}
(4) 开发工作流
Alice 的推荐 HMR 开发循环:
# 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 调试技巧
// 在 apply 开头加调试日志
export function apply(ctx: Context) {
ctx.logger.info('apply called at', new Date().toISOString())
// ...
}
// 如果看到 apply 被意外调用多次,说明 HMR 级联重载
❓ 常见问题
[hmr] loading xxx (new) 和 [xxx] plugin reloaded。失败会输出错误信息。--watch 参数仅用于开发。 ---📖 小节
- 依赖图基于 inject 声明构建 DAG,拓扑排序决定加载顺序
- 依赖动态满足:pending 插件在依赖就绪后自动激活
- HMR 通过
--watch启用,修改代码后自动重载插件 - 嵌套上下文继承父级服务,支持覆盖和隔离
- 配置变更触发自动重载,部分变更可热更新
- HMR 最佳实践:apply 幂等、避免全局状态、清理函数完整
📝 作业
1. ⭐ 基础题:启动 pnpm dsh web --patch --watch,修改一个已加载插件的 apply 函数(添加一行日志),保存后观察终端输出的 HMR 重载信息。
2. ⭐⭐ 进阶题:创建两个有依赖关系的插件(A inject B),启动 HMR 后修改 B 的代码,观察 A 是否被级联重载。然后只修改 A 的代码,确认 B 不受影响。
3. ⭐⭐⭐ 挑战题:编写一个使用全局变量的插件(非幂等),在 HMR 重载后观察变量值的变化。然后重构为闭包状态(幂等),验证重载后行为一致。记录重构前后的终端输出对比。