详情

首页手游攻略 深度拆解DeepSeek Harness插件热更新实现方式原理实用指南

深度拆解DeepSeek Harness插件热更新实现方式原理实用指南

佚名 2026-08-23 09:20:01

平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“深度拆解DeepSeek Harness插件热更新实现方法原理”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。

目录
  • 0. 这一篇解决什么
  • 1. 一切贡献都要走ctx.effect()或ctx.on()
  • 2.ctx.effect的两种签名
    • 2.1 函数得到一个 disposer
    • 2.2 Generator:yield出 disposer
  • 3. 教科书样例:LlmRuntime.registerAdapter
    • 3.1 setup / teardown 写在一个函数里
    • 3.2 提供三种撤销路径(都指向同一份 teardown)
    • 3.3handle.replace原子替换 route
  • 4. 事件器:同样是 effect
    • 5. Service 注册:也是 effect
      • 6. Fiber 是一个"事务边界"
        • 7. 完整的热更循环:一个例子
          • 8. 手写副作用:一个典型的错误
            • 9. 代码位置速查
              • 全系列小结

                0. 这一篇解决什么

                在这个场景下,到这里为止四篇内容合起来是一句话:插件借助 Service / 函数插件两种形态摆到 Context 上,靠 inject 声明依赖,借助五种事件模式相互通信。

                但整个体系有一个必须成立的前提:所有这些注册操作必须是可逆的。否则:

                • 卸载一个插件这件事就是幻想 —— 服务、adapter、tool、listener 全留在 map / 数组里
                • HMR / 热更新不可能干净 —— 老 adapter 和新 adapter 抢路由
                • isolation scope里的临时服务无法安全撤销 —— 主作用域可能拿到子作用域残留的实例

                这一篇讲清楚:dsh 是靠什么把"注册 = 可逆副作用"这条不变量做出来的

                1. 一切贡献都要走ctx.effect()或ctx.on()

                CLAUDE.md 里那条硬约束:

                Registrations are effects: every contribution goes through ctx.effect() / ctx.on(); a registry’s register() returns the disposer.

                意思是:

                • 结合项目来看,你不能把 this.adapters.set(...) 直接写在 apply(ctx) 里,就完事
                • 你必须把它包在 ctx.effect(() => { setup; return teardown }) 里
                • 或者把它藏在 ctx.on(...) 的 listener 里 —— ctx.on 内部本身就调 ctx.effect

                这样做的直接结果:落到代码里,每个副作用都自带撤销路径,每个插件 fiber 卸载时框架自动把它们逆序跑掉

                2.ctx.effect的两种签名

                vendor/cordis/src/fiber.ts:415

                effect(execute: () => SyncEffect,  label?: string): Disposable<Promise<void>>
                effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
                effect(execute: () => Effect, label = 'anonymous'): any {
                  this.assertActive()
                  if (this.state === FiberState.UNLOADING) {
                    throw new CordisError('INACTIVE_EFFECT')
                  }
                  // …
                }

                参数是一个函数execute)。执行它得到 setup 结果 + 一份"怎么清理"的 disposer 表达。有两种表达方式:

                2.1 函数得到一个 disposer

                ctx.effect(() => {
                  const timer = setInterval(tick, 1000) // setup
                  return () => clearInterval(timer) // teardown
                }, 'my-timer')

                短平快,适合"只登记一个东西"的场景。

                2.2 Generator:yield出 disposer

                ctx.effect(function* () {
                  const timer = setInterval(tick, 1000)
                  const port = openPort(3000)
                  yield () => clearInterval(timer) // teardown #1
                  yield () => port.close() // teardown #2
                }, 'my-multiple-effects')

                理解这一步时,yield 出来的东西会被 fiber 收集起来,逆序执行。Generator 语义完美贴合"多步 setup + 反向 teardown":

                • 想加一步 setup?往前 yield 之前塞一行
                • 想加对应的 teardown?把它 yield 出来
                • teardown 会自动逆序跑(先关 port,再关 timer)

                vendor/cordis/src/fiber.ts:424:

                const disposables: Disposable[] = []
                // …
                runner.collect = (dispose) => {
                  disposables.push(dispose)
                  // …
                }

                结合项目来看,所以你 yield 一次 = 往 disposables 数组塞一个函数;fiber 卸载时(vendor/cordis/src/fiber.ts:431):

                for (const disposable of disposables.splice(0).reverse()) {   // ← reverse!
                  // 逐个 await 跑掉
                }

                逆序是关键:符合"资源栈"的直觉——先建的最后拆,后建的先拆。

                3. 教科书样例:LlmRuntime.registerAdapter

                packages/llm/llm/src/index.ts:338-367 是 dsh 里"registry 的 register() 得到 disposer" 这条规则最完整的示范:

                registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle {
                  const owned = new Set<string>()
                  let released = false
                  const dispose = this.ctx.effect(function* (this: LlmRuntime) {
                    if (providers.length === 0) {
                      throw new LlmError('an adapter must register at least one provider', 'INVALID_ADAPTER')
                    }
                    // ── setup ─────────────────────
                    this.commitRoutes(owned, this.prepareRoutes(providers, adapter, owned))
                    // ── yield 出 teardown ────────
                    yield () => {
                      released = true
                      for (const provider of owned) this.adapters.delete(provider)
                      owned.clear()
                      this.emitAdaptersUpdated()
                    }
                  }.bind(this), 'llm.registerAdapter()')
                  const handle = (() => void dispose()) as AdapterRegistrationHandle
                  handle.replace = (next: string[]): void => {
                    if (released) {
                      throw new LlmError('a disposed adapter registration cannot replace its routes', 'REGISTRATION_DISPOSED')
                    }
                    this.commitRoutes(owned, this.prepareRoutes(next, adapter, owned))
                  }
                  return handle
                }

                三个漂亮的地方:

                3.1 setup / teardown 写在一个函数里

                实际处理时,老式的写法是"注册得到 disposer",靠命名约定;新写法用 generator,setup 和 teardown 之间只隔一个 yield,视觉上就能对齐"我登记了 X,卸载时就撤销 X"。

                一眼看得出来的对称关系:

                this.commitRoutes(owned, prepareRoutes(providers, adapter, owned))   ← 建
                yield () => {
                  this.adapters.delete(provider); owned.clear(); emitAdaptersUpdated() ← 拆
                }

                漏写 teardown 会立刻在 code review 里被看出来。

                3.2 提供三种撤销路径(都指向同一份 teardown)

                1. 插件 fiber 卸载 → fiber 自动跑 disposables.reverse() → teardown 执行
                2. 落到代码里,调 dispose()(即 handle 本身) → 立即触发 teardown,随后从 fiber 的 disposables 里摘掉
                3. 实际处理时,调 handle.replace([...]) → 不撤销这次 registration,而是原子替换里面的 route

                第 3 点是精髓,见下节。

                3.3handle.replace原子替换 route

                DeepSeek 的 provider 兼容热更 retryPolicy(packages/llm/llm-deepseek/src/index.ts:258 附近):

                const ensureRegistrationFacts = (): void => {
                  const policy = options().retryPolicy
                  if (deepEqualJson(policy, registeredPolicy)) return
                  registration.replace([PROVIDER]) // ★ 原子替换
                  registeredPolicy = policy
                }
                installSettingsSection(ctx, NS, Config, config, {
                  setSource: (source) => { current = source },
                  onChange: ensureRegistrationFacts, // 用户在 Web 改设置 → 自动重注册
                })

                replace 内部做的(packages/llm/llm/src/index.ts:405-413):

                private commitRoutes(owned: Set<string>, registrations: readonly AdapterRegistration[]): void {
                  for (const provider of owned) this.adapters.delete(provider) // 删旧
                  owned.clear()
                  for (const registration of registrations) {
                    this.adapters.set(registration.provider.id, registration) // 加新
                    owned.add(registration.provider.id)
                  }
                  this.emitAdaptersUpdated()
                }

                落到代码里,注意这是同步的 for 循环:删旧 + 加新在一个 tick 内完成,没有异步等待。中间不会有任何观察者(比如 agent-loop 里正在跑的 stream() 调用)拿到"provider 消失了"的中间态。

                这就叫原子替换,是 dsh 热更能力的核心 primitive。

                4. 事件器:同样是 effect

                回顾 04 · 6 里的代码(vendor/cordis/src/events.ts:254):

                register(label: string, hooks: Hook[], callback: any, options: EventOptions): () => void {
                  const method = options.prepend ? 'unshift' : 'push'
                  return this.ctx.fiber.effect(() => {
                    hooks[method]({ ctx: this.ctx, callback, ...options }) // setup: 塞进 hooks 数组
                    return () => this.unregister(hooks, callback) // teardown: 从数组里删掉
                  }, label)
                }

                实际处理时,任何一个 ctx.on('llm/stream', ...) 都是一次 ctx.effect 调用。插件卸载 → fiber 卸载 → effect 逆序跑 → listener 被 splice 掉。这是"零手工清理"的根本。

                5. Service 注册:也是 effect

                vendor/cordis/src/reflect.ts:277

                provide(name: string, value?: any, check?: () => boolean) {
                  return this.ctx.fiber.effect(() => {
                    // …
                    const key = this.ctx[symbols.isolate][name]
                    const impl: Impl = { name, value, fiber: this.ctx.fiber, check }
                    if (this.store[key]) {
                      throw new Error(`service "${name}" has been registered at <${this.store[key].fiber.name}>`)
                    }
                    this.store[key] = impl
                    this.ctx.fiber.store![name] = impl
                    if (this.ctx.fiber.state === FiberState.ACTIVE) {
                      this.notify([name])
                    }
                    return async () => { // ← teardown
                      delete this.store[key]
                      const fibers = this.notify([name])
                      await Promise.allSettled(fibers.map(fiber => fiber.await()))
                      delete this.ctx.fiber.store![name]
                    }
                  }, `ctx.provide(${JSON.stringify(name)})`)
                }

                super(ctx, 'llm')本质就是 ctx.fiber.effectService 也是 effect——一切副作用都遵循同一条规则。

                6. Fiber 是一个"事务边界"

                实际处理时,在 dsh 里"一个插件"和"一个 fiber"是一一对应的(除非有 subagent / isolation scope 引入的子 fiber)。fiber 内部维护一个 _disposables 列表:

                plugin fiber (state = ACTIVE)
                  _disposables: ← disposer 栈(按注册顺序)
                   [0] service register (ctx.llm) ← super(ctx, 'llm')
                   [1] event listener 'llm/stream' ← ctx.on
                   [2] adapter registration ← ctx.llm.registerAdapter
                   [3] settings section install ← installSettingsSection
                   [4] tools register 'bash' ← ctx.tools.register
                   …

                结合项目来看,fiber 从 ACTIVE 转 DISPOSED 时,_disposables 逆序全部跑掉。这个"逆序清理"是 vendor/cordis/src/fiber.ts:431 里的 disposables.splice(0).reverse()。

                分享时最直观的类比:fiber ≈ 数据库事务。整个 fiber 是一次"要么全部生效,要么全部回滚"的事务:

                • 事务开始:fiber 从 PENDING → LOADING → ACTIVE
                • 每次注册 = 事务里的一步 write
                • 事务结束(卸载):所有 write 逆序 undo

                这条心智模型解释了 dsh 的很多设计决策:

                • 从实现思路看,为什么禁止 apply 里搞裸的 setInterval? 因为它不受 fiber 管理,卸载时不会被回收。要么写成 ctx.effect(() => { const t = setInterval(...); return () => clearInterval(t) }),要么用 ctx.setTimeout(Cordis 提供的 fiber-aware 版本)。
                • 从实现思路看,为什么服务注册用 ctx.reflect.provide 而不是 Object.assign(ctx, { llm })? 因为后者不受 fiber 管理,同名冲突和卸载语义都没有。
                • 在这个场景下,为什么 handle.replace 要设计成同步原子操作? 因为 fiber 是事务,事务内部不允许中间态泄漏。

                7. 完整的热更循环:一个例子

                实际处理时,把前面 4 篇 + 这一篇的知识串起来。用户在 Web UI 上改 llm-deepseekretryPolicy,会发生什么?

                用户在 Settings 页改 retryPolicy         ← Web 事件
                  │
                  ▼
                settings service 触发 onChange
                  │
                  ▼
                ensureRegistrationFacts() (llm-deepseek 里定义的)
                  ├─ deepEqualJson 判断变化 → true
                  ├─ registration.replace([PROVIDER]) ← 原子替换
                  │ └─ commitRoutes():
                  │ ├─ this.adapters.delete('deepseek-official') ← 摘旧 route(同步)
                  │ ├─ this.adapters.set('deepseek-official', {...retryPolicy: new}) ← 塞新 route
                  │ └─ emitAdaptersUpdated() ← 广播事件
                  └─ registeredPolicy = policy
                  │
                  ▼
                agent-loop / 别的 consumer 拿到 'llm/adapters-updated' 事件
                  └─ 可以选择刷新自己的路由缓存 —— 但不会看到"provider 消失"的中间态
                  │
                  ▼
                下一次 ctx.llm.stream(options) 就用新的 retryPolicy 了

                理解这一步时,整个过程没有重启进程,没有卸载/重新加载插件,甚至连 waterfall listener 都不受影响。因为原子替换发生在 LlmRuntime.adapters 这张 map 里,从 map 外面观察到的只是"值变了"。

                实际处理时,反过来,如果整个 llm-deepseek 插件被禁用(cordis.yml 里加 disabled: true):

                Loader 判定 llm-deepseek 应该 disabled
                  │
                  ▼
                fiber 从 ACTIVE → UNLOADING → DISPOSED
                  │
                  ▼
                _disposables 逆序清理:
                  ├─ installSettingsSection 撤销 ← 设置面板消失
                  ├─ registerAdapter teardown ← this.adapters.delete('deepseek-official')
                  ├─ registerConfigurableProviders 撤销 ← Web 端选择框里 DeepSeek 消失
                  └─ apply 里注册的其它 effect …
                  │
                  ▼
                所有依赖 llm-deepseek 隐含的 route 的插件(比如某个 consumer 记住了 provider)会被通知
                (如果它们 inject 了 llm,llm fiber 还在,所以它们不会 pending;但 provider 消失是运行时事实)

                从实现思路看,注册即副作用、副作用可逆这条规律,让"禁用一个功能"从"重启服务"变成"一次事务回滚"。

                8. 手写副作用:一个典型的错误

                分享时能够现场演示"为什么不能绕过 ctx.effect"。

                // ❌ 反例
                export function apply(ctx: Context) {
                  const timer = setInterval(() => {
                    ctx.logger.info('tick')
                  }, 1000)
                  // 期望:插件卸载时清理 timer
                  // 现实:ctx.effect / ctx.on 都没走,fiber 卸载不会做任何事
                  // 结果:timer 永远在跑,卸载后还在打日志(甚至用一个已经无效的 ctx)
                }

                // ✅ 正确
                export function apply(ctx: Context) {
                  ctx.effect(() => {
                    const timer = setInterval(() => ctx.logger.info('tick'), 1000)
                    return () => clearInterval(timer)
                  }, 'tick-logger')
                }

                结合项目来看,或者用 Cordis 提供的 fiber-aware setInterval / setTimeout(它们内部就是走 ctx.effect 的)。

                9. 代码位置速查

                主题文件关键位置
                Effect / SyncEffect / Disposable 类型vendor/cordis/src/fiber.ts类型定义顶部
                ctx.effect 主实现vendor/cordis/src/fiber.tsL415-561
                _disposables 逆序清理vendor/cordis/src/fiber.tsL431 splice(0).reverse()
                getEffects 诊断入口vendor/cordis/src/fiber.tsL568-572
                ctx.on 走 fiber.effectvendor/cordis/src/events.tsL254-260
                ctx.reflect.provide 走 fiber.effectvendor/cordis/src/reflect.tsL277-305
                registerAdapter 教科书样例packages/llm/llm/src/index.tsL338-367
                commitRoutes 原子替换packages/llm/llm/src/index.tsL405-413
                ensureRegistrationFacts 热更packages/llm/llm-deepseek/src/index.tsinstallSettingsSection 附近
                硬约束"Registrations are effects"CLAUDE.mdConventions 段
                一切副作用可逆的语义讨论docs/defensive-patterns.mdteardown 相关章节

                全系列小结

                “如何把一次 LLM 调用改造成可插拔、可热更、可解耦的工程系统?”

                • 把每个能力做成插件(Service Definition / Provider / Consumer)
                • 从实现思路看,服务放到 Context 上,用类型化的 ctx.<key> 而不是 import
                • 依赖用 inject 声明,由 Loader 拓扑推导装配顺序
                • 插件之间用五种事件模式通信,waterfall 是环绕拦截的枢纽
                • 一切注册都是 ctx.effect,插件是一个事务,卸载时逆序回滚

                结合项目来看,到此这篇关于深度拆解DeepSeek Harness插件热更新实现原理的文章就介绍到这了,更多相关DeepSeek Harness插件热更新内容请搜索脚本之家以前的文章或继续浏览下面的相关文章,希望大家以后多多兼容脚本之家!

                相关资讯
                点击查看更多
                游戏推荐
                推荐专题
                热门阅读
                推荐下载