坑是陷人的地方,账是未来要承担的后果。 读不完的话(估计很少有人会读完的):第 2 节讲它真正做了什么(其实只有一个栈),第 4 节讲它哪里长错了,第 8 节讲你该不该用。 0. 两个主角 被评估的:Cordis Koishi 聊天机器人框架的内核,后来 DeepSeek 的 DSH ( DeepSeek Harness ,Agent 运行时)拿它当底座。 类型上是 插件元框架 :本身不含任何业务能力,只管插件怎么装进一个长期运行的进程、怎么互相依赖、怎么卸载。Koishi 社区插件 4000+,DSH 第三方索引收录 6000+。 仓库: https://github.com/cordiverse/cordis 官方文档: https://deepseek-harness.github.io/deepseek-harness/en/reference/cordis-primer 上手教程: https://deepseek-harness.github.io/deepseek-harness/en/develop/cordis-tutorial 用来做参照的:tool-func 系列 isdk 的一组小库,从下往上是一条链: tool-func :函数注册表,让函数能自描述、被别人发现 tool-rpc :把注册的函数暴露到网络上,本地/远程对调用方透明 tool-event :把实时事件也当作"一个工具",复用 RPC 的生态 第 4 节会拿它当尺子: 同一个需求,另一种长法是什么样子。 1. 它解决什么问题 一个 bot 进程要 7×24 运行,插件要能不重启进程地装卸和升级。 这句话听起来平平无奇,把它变成画面: 你的机器人已经连续跑了半年,群里几千人在用。现在你要给"天气查询"插件修一个 bug 。 重启意味着全体下线半分钟。 你要的是:改完代码、保存、这个插件自己卸掉旧版本、装上新版本,群里的对话一次都没断。 要做到这件事,必须解决四个问题: 需求 需要什么 插件能装进进程 一个注册表,按名字找到插件 插件能卸载且不留垃圾 卸载时清理它注册过的所有东西:定时器、监听器、临时文件 清理不能乱序 后创建的先释放 插件之间有依赖 依赖失效时下游跟着拆,依赖回来时跟着装回去 这四条是主干。 主干之外 Cordis 还提供了:五种事件分发方法、配置系统、声明合并式的服务查找。那些是否必要、代价是什么,第 4 节和第 5 节逐项说明。 时间线上:2020 年 Koishi 先跑起来,Cordis 是后来从它内核剥离出的独立包,论文出现在 DSH 采用之后——工程在前,理论在后。 2. 核心机制:一个栈 第 1 节里第 2 、3 条需求(能清理、不乱序),实现出来就是一个 函数栈 。 2.1 不用框架时会怎么写 插件启动做了三件事: const pool = openPool() // 1. 开一个数据库连接池 const timer = setInterval(() => pool.query(), 1000) // 2. 每秒用连接池查一次 ctx.on('message', handler) // 3. 挂一个消息监听器 卸载时你得手动写三行,而且顺序不能错: ctx.off('message', handler) // 先摘监听器 clearInterval(timer) // 再停定时器 pool.close() // 最后关连接池 为什么必须是这个顺序? 假设你先关连接池、后停定时器——在"池已关、定时器还在"的那几毫秒里,定时器可能又触发一次 pool.query() ,拿一个已关闭的连接池查数据,必然报错。反过来,先停定时器再关池,怎么都不会错。 2.2 Cordis 的方案 它做的全部事情,是把这个顺序交给一个栈: // 注册时:acquire 立即执行,返回值和清理函数成对入栈 function effect (acquire: () => T, release: (value: T) => void): T { const value = acquire() stack.push([value, release]) // 资源和"怎么拆它"存在一起 return value } // 卸载时:从栈顶往下弹 while (stack.length) { const [value, release] = stack.pop() release(value) } 用 ctx.effect() 写刚才那三件事: const pool = ctx.effect(() => openPool(), p => p.close()) const timer = ctx.effect(() => setInterval(...), t => clearInterval(t)) ctx.effect(() => ctx.on('message', handler), () => ctx.off('message', handler)) 卸载时一行 dispose() ,栈自己从顶往下弹。 你不再需要记住顺序。 2.3 为什么"反着来"总是对的 规律是: 后创建的东西往往依赖先创建的东西。 这个"往往"为什么可靠到不需要分析依赖?因为 变量作用域 已经替你保证了:定时器回调里引用了 pool 这个变量,那么 openPool() 那一行就必然写在 setInterval 前面——"先定义后使用"和"依赖在前、依赖者在后"是同一个顺序。 LIFO (后进先出)把注册顺序一反转,恰好反转了依赖顺序。 它不是猜对了依赖,它是复用了语言几十年的"先定义后使用"规则。 准确的说法是: LIFO 不分析依赖,它只是沿用注册顺序的逆序。 刻意违反时不成立: let pool: Pool ctx.effect(() => setInterval(() => pool.query(), 1000), t => clearInterval(t)) // 先注册 pool = ctx.effect(() => openPool(), p => p.close()) // 后才拿到值 // 卸载时 LIFO:先关池、后停定时器 —— 顺序错了,且没有任何报错 这段代码能通过编译( let 的声明被提升了)。机制的全部实现是一个数组加一个 pop() 。 这个 effect 业界早有现成的名字 :Haskell 的 bracket 、Effect-TS 的 acquireRelease ,中文就是"成对的资源获取与释放"。C++ 的 RAII 也算近亲,但它绑在词法作用域退出(离开大括号就析构),而 Cordis 这个不绑作用域、绑运行时——所以更准确的名字是 运行时可组合的 acquireRelease 。 所以 Cordis 在这里的真实增量只有一步 :把"谁后获取谁先释放"从写在代码里的语法结构( try/finally ),变成运行时可组合的数据结构(栈)。互不相识的插件各自往栈里压一对"资源 + 清理函数",卸载时统一弹出。这一步有价值,没有更多了。 2.4 插件与插件之间,也是这个顺序 A 插件提供数据


  • 情报分类:技术学习与提效
  • 分类依据:内容涉及技术、AI、软件工具或工程实践
  • 信息来源:服务器 / V2EX
  • 发布时间:2026/10/7 22:27:42