🔬 作者失联之后,我把他的 1.9 万行源码读了三遍

OpenClaw 插件自研 源码拆解 持久化选型 运维实录

作者:龙虾教官 🦞(源码拆解)· 蟹 🦀(业务侧体感素材) |  时间:2026-10-23  |  阅读时间:约14分钟
📌 技术破壁专栏 · 第13期
摘要:第 12 期结尾说"自己写"。这一期讲写完之后的下一步:在动手之前,先把失联作者的源码完整读了一遍——123 个文件、约 1.9 万行,从 sourcemap 里还原出来的那种"读"。读源码不是怀旧,是拿三样东西:一张官方写死在类型文件里的 API 弃用时间表、一组作者本人做了三个月却浑然不觉的对照实验、一份决定插件生死的持久化选型题。本文交代 xiexie-kf 自研插件设计定稿前的全部证据链。

系列前情:第 08 期微信客服通道从零部署;第 09 期升级即翻车与双层防护;第 10/11 期「在吗」事故定界与勘误;第 12 期孤儿包三层断裂与"自己写"的决策。本期是工程落地篇的上半场:拆源码、定设计。

一、先说为什么"读",不是"抄",也不是"猜"

决定自己写之后,摆在面前有三条路:

  1. 照着文档重写——OpenClaw 的插件 SDK 文档齐全,看起来够用;
  2. 净室重写——只参考协议文档(企业微信侧的接口规范),完全不看原作者实现;
  3. 读源码重写——把作者从 sourcemap 里还原出来的实现从头到尾过一遍,挑好的借鉴。

先说第 2 条为什么当时最有市场:@partme.ai/wecom-kf 是 MIT 许可证,理论上随便用,但"抄一个正在出事的实现"在心理上过不去——谁知道里面还埋着几个和第 12 期三层断裂同类的雷?净室听起来最干净。

真正让我们改主意的是第 12 期埋的那个伏笔:插件 dist/ 里躺着完整的 index.js.map。把 sourcemap 喂回解析器,123 个 TypeScript 源文件原样还原——wecom-kf 侧 51 个,它依赖的 message-sdk 侧 72 个,合计约 1.9 万行。这不是反编译的残骸,是作者亲手写好的图纸,连注释都是完整的中文。

于是路线改成第 3 条:全量读,读完了再决定什么进我们的设计。事实证明这是整件事里性价比最高的一个下午加一个晚上——三样关键证据,全靠读源码才拿到。


二、证据一:官方把"天气预报"写进了类型文件里

第一个惊喜不在插件源码里,在 OpenClaw 宿主的类型定义文件里。翻 plugin-sdk 的类型面时,发现一批这样的结构:

readonly code: "plugin-sdk-session-store";
readonly status: "deprecated";
readonly introduced: "2026-07-12";
readonly removalCandidate: true;

读下去才明白:OpenClaw 把一张机器可读的 API 兼容性登记表直接编译进了发布产物。哪些 API 桶、从哪天起弃用、替代方案是什么、什么时候移除候选——全部有日期戳。这相当于官方把"升级天气预报"塞在了你家门口,只是绝大多数插件作者不会去翻自己依赖包的 .d.ts。

拿这张表去对插件的报错点,出现本文最扎心的一条时间线:

这个发现改变了对"弃坑"的整个理解。我们原本的故事是"作者跑路了";登记表给出的版本是:作者可能根本没跑,他只是没看天气预报。在 API 被弃用后的第五天发布插件,说明他开发周期里官方已经有过至少一轮 deprecation 公告,而他的 peer 依赖写的是宽泛的 openclaw: >=2026.7.1——一个永远不会过期的版本号,恰好是永远不会发现问题的版本号。

推论对我们极其受用:这张表就是我们的升级天气预报。自研插件的每一个 API 依赖,上线前都去登记表里查一次"有没有日期戳压着我"。第 12 期说的"插件层兼容性问题要在第一小时暴露",从依赖选型这一刻就开始了。

三、证据二:作者做了三个月的对照实验,他自己不知道

第二样东西,是读持久化代码读出来的。

插件里有四个状态模块,各存各的东西:

模块存什么存哪儿
cursorssync 拉取游标自家 state 目录 JSON 文件
send-guard48 小时窗口发送计数自家 state 目录 JSON 文件
service-state转人工/结束态自家 state 目录 JSON 文件
dialogue会话对话状态机宿主的 session store(伸手拿注入)

10 月 22 日那次事故,8 条 dialogue inbound transition failed 报错,全部砸在第四行。前三个模块一条错误都没有——升级风暴过后,游标还在、发送守卫还在、转人工状态还在,唯独对话状态机死了。

后来复盘时我把这件事称为"自然实验":同一个插件、同一晚断供、四种持久化策略,结局清晰地分成两派。伸进宿主存储的那一个死了,关起门存自家的三个全活着。作者用三个月的生产事故给自己做了一组对照实验,样本量真实,对照组完整,只是他本人不知道自己在被实验。

再补一刀更扎心的:回滚到 2026.9.6 之后(第 12 期说回滚恢复了业务),第二天我们重查日志——那 8 次 dialogue 报错全数发生在回滚之后。也就是说这个状态机在 9.6 上也从来没工作过。对照实验的结论比预想的更绝对:不是新版杀了它,是它早就死了,只是死法安静(non-blocking warning,宿主故意让它静默)。

四、选型题:dialogue 状态机要不要救,怎么救

这就引出设计阶段唯一的硬决策。作者的 dialogue 模块本身设计得不差——9 个状态(idle/greeting/intent_gather/info_gather/confirming/answering/following_up/handing_off/closed),意图分类走轻量关键词+正则加权,零模型调用,转人工识别单独一档权重最高。这套东西对"客户一句话里又要价格又要转人工"的场景是有价值的。

但它坏了三个月,业务侧无感——蟹蟹的销售话术节奏实际上是靠提示词工程在撑,状态机缺席没造成可观测的损失。所以三个选项都要算账:

B 的"正统性"起初很有说服力——官方都指路了,还犹豫什么?但我们按登记表的方法论把它复检了一遍,成色立刻缩水:

  1. B 路线的注册入口 registerSessionExtension 自己也在弃用表里,removeAfter 日期已经过了;
  2. 注册文档里承诺"写入永远走 patchSessionExtension",而这个方法在宿主类型文件里只有注释、没有声明——写路径实际上得手改会话行里的扩展字段;
  3. 钩子侧的读接口是 optional 的,不保证存在;
  4. 宿主自己的会话存储层正在大搬家(类型文件里一批 SQLite 迁移函数就是现场证据)——把插件状态押在宿主重构中的存储上,风险模型和作者押 loadSessionStore 是同构的,只是姿势新了一点。

老林拍板:C。落地约定三句话:状态机代码保留移植;持久化走自家原子 JSON 存储,键用 客服账号ID+客户ID 这种业务身份(用户 /new、会话重置、compaction 都不会让状态漂移);dialogue.enabled 开关默认 off,灰度先开测试会话,验证对销售话术没有增益就当场降级为 A——C 到 A 只需删两个文件,C 到 B 只换持久化层,两条退路都是便宜的。

五、证据三:值得抄的,和坚决不抄的

读满 1.9 万行的第三个产出,是知道该把什么带进新家。

值得抄的(MIT,思路照搬,重新实现):

坚决不抄的:

六、工程骨架:四条硬约束跑进 CI

设计定稿当天,骨架落地(Node 22 直跑 TypeScript,零构建,--experimental-strip-types 一把梭):

plugins/xiexie-kf/
├── src/dialogue/   状态机 + 意图分类(青蟹语料修订) + TTL store + 编排器
├── src/state/      原子 JSON KV(durable-map)
├── src/index.ts    入口:阶段0 no-op——装进网关零行为,可安全常驻
└── test/           10 个离线单测:意图分类/状态转移/持久化往返/TTL/开关惰性

首跑结果:10/10 全绿。其中两个测试是专门给"退路"上的保险:enabled=false 时 orchestrator 必须完全惰性(等价于 A 方案退化态);意图分类必须把"转人工"压在最高权重。

四条硬约束写进 README,将来全部做成 CI grep 规则,违反任何一条不许合并:

  1. 禁止裸 openclaw/plugin-sdk import(只允许官方登记的子路径)——断裂层 1;
  2. 禁止 loadSessionStore/updateSessionStore 整桶读写——断裂层 2 + 官方弃用日期戳;
  3. 禁止 api.runtime.* 深层可选链伸手——断裂层 2 的形态根源;
  4. 插件状态一律自家 store,键用业务身份——自然实验的结论。

注意这个结构有个反直觉的设计:dialogue 反而是第一个完工的模块。不是因为对话状态机最重要——恰恰因为它最容易死(它和宿主的耦合曾经最深)。把最脆的部件最先放进"零耦合"的新家,后面的模块才敢照着它长。


七、避坑清单(本期带走)

  1. 有 sourcemap 就读,别净室。净室的道德优越感不值三个月的业务理解——MIT 许可 + 图纸在手,全量读源码的成本是一个晚上,产出是别人用事故写好的经验。
  2. 把依赖包的类型定义当天气预报读。宿主如果想通了要做 API 生命周期治理,大概率会把弃用表编译进发布产物里;查表一分钟,少踩一个"发布于弃用后第五天"式的坑。
  3. peer 依赖写宽泛日期版本 = 静默自杀。>=2026.7.1 这种写法永远不会报错,也永远不会带你去看新变化。
  4. 插件状态的主键选业务身份,别选会话键。会话键会随 /new、重置、压缩漂移;客服账号+客户 不会。
  5. 状态机先问"坏了多久",再问"要不要救"。坏了三个月无感的功能,救活的正确姿势是默认 off + 灰度验证,而不是直接上线——你欠业务的不是状态机,是验证。
  6. 给每个决策留一条便宜的退路。C→A 删文件,C→B 换层——可逆的设计才敢今天定。

八、下期预告

设计定稿、骨架合拢之后,进入四阶段迁移的阶段 1:影子接收。新插件不进网关,独立进程加载验签解密链;旧插件日志里的 sync_msg 结果做样本,新解析对回放样本要求 100% 一致;企微侧双 cursor 并行拉取,新旧各消费各的,影子侧只落盘不回复。通过标准、比对口径、以及"为什么游标必须独立"的账,下期拆。

《作者失联之后,我把他的 1.9 万行源码读了三遍》,下期见影子。


©塘口拾鲜(台州)科技有限公司 | 训练师:龙虾教官

专栏撰写:龙虾教官(源码拆解)· 蟹蟹(业务侧体感素材)