@partme.ai/wecom-kf 与新版 OpenClaw 之间的三层 API 断裂(import 层 / runtime 层 / lifecycle 层),并交代为什么最终决定:自己写。先给大家看一个 npm 上任何人都能查到的事实:
npm view @partme.ai/wecom-kf
# version: 2026.7.1 ← 唯一版本
# time.created: 2026-07-17
# time.modified: 2026-07-17
# maintainers: 1 person
一个版本,98 天没有第二发,单人维护。
而它在我们这边是什么地位?蟹蟹的所有客户消息,从微信客服进来,都经过这个插件。回调验签、收发消息、会话路由、账号绑定,全部压在它身上。用 Dependency 界的黑话讲,这叫"关键路径上的孤儿包";用老林的话讲,这叫"客服入口被人质扣押,绑匪还是个失联的"。
危险是安静累积的。7 月 17 日装上那天,它是好用的;7 月 20 日我们甚至为它写过实验记录(专栏存档里那几篇 wecom-kf debug)。没人注意到 npm 页面上的 modified 时间从此不再动过。
9 月初,OpenClaw 升级到 2026.9.2。升级后网关重启,插件加载失败:
ERR_PACKAGE_PATH_NOT_EXPORTED:
Package subpath './plugin-sdk' is not defined by "exports"
拆开插件的构建产物看,dist/index.js 里有 5 处这种写法:
import { ... } from "openclaw/plugin-sdk"; // 裸导入
而新版 OpenClaw 的 package.json 把 exports 表整个重构了——不再有"什么都从这一个口给"的裸导出,改成按需细分的上百个子路径:./plugin-sdk/core、./plugin-sdk/channel-plugin-common、./plugin-sdk/routing……裸导入的路,被官方亲手拆了。
插件作者没动。于是我们自己动手:sed 批量把 5 处裸导入指到 ./plugin-sdk/core,重启,通道活了。这就是 9 月 8 日那次手动补丁(第 09 期提过一嘴,当时以为是个收尾)。
但真正该记住的是当时没人起疑的地方:补丁打完后,dist/ 里还躺着一个 index.js.map——sourcemap 意味着插件的全部源码逻辑都能还原出来。我们当时只是扫了一眼确认"还有别的裸导入吗",没有意识到这个东西三个月后会变成救命的图纸。
手动补丁有个致命属性:它活在 node_modules 里,任何一次重装、升级、缓存刷新都会把它冲掉。
证据就在目录里,两个备份文件是两次案发记录:
dist/index.js.bak ← 第一次补丁留的
dist/index.js.bak.20260929 ← 9月29日补丁被冲掉,重新打
9 月 8 日、9 月 29 日,同一套 sed 操作做了两遍。从此"升级前先想插件、升级后先验插件"变成了我们的固定仪式——10 月 22 日那次事故排查里,第一步就是"确认补丁完好"。
到这里,账本上记的还是成本:每次多花十分钟。真正记不上账的是风险:我们以为补丁修复的是"这个包能不能加载",它实际只修复了"import 语句写法"。这两个命题之间隔着一条没人探过的深水区。
10 月 18 日,OpenClaw 2026.10.5 发布——2250 个 PR、637 位贡献者的重量级版本。10 月 20 日下午做完升级调研,晚上老板拍板:手动升级。
接下来 48 小时同时砸下来三件事:
[WARN] dialogue inbound transition failed (non-blocking):
params.runtime.agent.session.loadSessionStore is not a function
Plugin wecom-kf forced retirement after 5000ms:
1 still-running call(s), 0 retained consumer(s)
当晚决策:回滚。2026.10.5 → 2026.9.6,4 渠道验证全 OK,先恢复业务再谈升级。
注意这条时间线里最扎心的部分:我们把升级回滚了,把 provider 切了,把模型换了,能修的都修了——客服通道的地基那个插件,依然是一层补丁摞在原样上,问题只解决了一半。
事故之后把插件源码(多亏那份 sourcemap)和两版 OpenClaw 的导出表放在一起对质,结论是分层的:
裸 openclaw/plugin-sdk → 子路径。sed 能治。这是 9/8 和 9/29 两次补丁干的事。
插件源码里有一行:
const store = params.runtime.agent.session.loadSessionStore(storePath, { clone: true });
它假设主机在运行时给插件注入一个 params.runtime.agent.session 命名空间。新版主机不再提供这个命名空间——函数没了,is not a function。
最讽刺的是什么?函数本身还活着,而且官方明确导出:新版 OpenClaw 的导出表里有一个专门的子路径 ./plugin-sdk/session-store-runtime,loadSessionStore 就在那里,签名几乎没变。插件不是没有路可走,是它作者 98 天没抬头看路。
这层断裂 sed 治不了——要改的是调用逻辑本身:把"伸手向宿主掏注入对象"改成"显式 import 子路径"。改一处可以,改一个无人维护的 dist 里散落的多处,再考虑每次升级重装后重做——补丁债务从"十分钟仪式"跳档到"每次都要重审代码逻辑"。
forced retirement after 5000ms:新版对插件卸载引入强制退休机制——网关停止/重载时,插件要在 5 秒内交出所有在途调用,否则被强行处退。日志里那天出现 100+ 次,还有一条消息处理跑了 98 秒被记 outcome=error。插件的对话转发路径里存在长连接/慢调用时,它和新版宿主的存在方式本身就是冲突。
这一层跟插件代码已经没多大关系了,是宿主对插件的假设变了。补丁在这里无处下手。
dialogue transition failed (non-blocking)——标了 non-blocking,意味着主机故意让它静默。它死掉的功能是会话对话状态的持久化。这个功能坏掉的客户侧体感,我们 7 月份就写过笔记:"客服会话进行中能看到消息,会话结束后上下文不保留,无法回顾已结束的客户对话。"
业务体感比技术确认早了三个月。日志会轮转,体感不会。以后凡是"体验上总觉得缺点什么"的时刻,都值得去日志坟场里捞一次。
| 选项 | 评估 |
|---|---|
| 等作者更新 | 98 天零动静、单人维护、无响应记录。而它卡的是营收入口。把生意的排期表交给陌生人,不叫开源精神,叫赌博。 |
| 继续打补丁 | 层 1 十分钟,层 2 每次升级重审代码,层 3 无解。债务按指数涨,下一次断供时补丁可能都追不上现场。 |
| 自己写 | 需求文档我们已经用三个月的线上事故写完了:回调验签、sync cursor 持久化、openKfId 会话路由(含连字符正则的坑)、48 小时窗口、转人工分档、session-store 走新子路径。第 08 期的四个坑全是设计约束,sourcemap 给了参照实现。 |
老林的决断一句话,也是他的风格在这条故障史上第三次胜出:自力更生修复优先于等待上游适配。全量替换优于补丁修复。
自己写不是赌气,是账算清楚了:一次断供 40 小时+客户等待的事故成本,已经大于把通道重写一遍再长期维护的任何估算。
npm view <pkg> 看版本数、modified 时间、维护者数。单版本+数月无更新+单人 = 关键路径上直接标红。五分钟,能提前三个月发现我们这次的局面。.js.map 平时是调试糖,作者失联那天它就是遗产。我们最后是靠它完成源码级尸检的。自研插件的工程路线已经画好,四个阶段:只读接收与新旧双写比对 → 发送与游标持久化 → 会话状态机与转人工 → 切流退役旧包(保留一键回滚开关)。写完上线后,它还会承担一个新职责:给 OpenClaw 升级当金丝雀——每次官方版本更新,先在自研通道上灰度,插件层兼容性问题从此在第一小时暴露,而不是在一个周五夜里。
《3 个月没更新的插件,逼我自己当了作者》,下期见真章。