背景:本文记录一次基于 1Panel、Docker 部署的 OpenClaw 升级故障。
内容包含:升级后先后遇到飞书授权状态迁移失败、配置 Schema 不兼容、
openclaw-lark插件无法加载,以及 SQLite ownership 丢失。最终通过状态归属修正、配置字段迁移、插件兼容补丁、本地重新打包和状态锁恢复完成修复。
一、环境与最终结果
1. 环境
- 部署方式:1Panel + Docker
- OpenClaw:
2026.9.9 - 飞书插件原版本:
@larksuite/openclaw-lark@2026.7.9 - 飞书插件升级目标:
@larksuite/openclaw-lark@2026.7.16(明显这个版本是旧版本了) - 飞书接入方式:WebSocket 长连接
- 飞书账号:
main-bot、media-bot - 其他频道:Telegram
2. 最终状态
修复后的插件检查结果:
|
|
容器状态:
|
|
最终使用本地修改后的补丁包:
|
|
二、故障并不是单点问题
这次升级同时暴露了四类兼容问题:
- 旧飞书授权状态无法确定所属账号;
- 旧飞书配置字段不符合新版严格 Schema;
openclaw-lark插件与新版 Plugin SDK、模块格式和 Runtime API 不兼容;- 修复期间 one-off 容器跨 PID namespace 访问 SQLite,导致 Gateway 丢失 ownership。
其中,第三项是工作量最大的问题;
第四项则是修复过程中额外触发的 OpenClaw 2026.9.x 状态锁问题。
三、第一阶段:旧飞书授权状态迁移失败
1. 故障日志
升级后 Doctor 首先报错:
|
|
后续迁移全部被阻塞:
|
|
2. 根因
旧授权文件名为:
|
|
这表示旧状态属于飞书账号 main。
但新版配置中的实际账号是:
|
|
配置中不存在 main。
新版 OpenClaw 会把旧 JSON 授权名单迁移到 SQLite,并要求每条记录都能明确归属到:
|
|
由于迁移程序无法判断旧 main 应该对应 main-bot 还是其他账号,为避免把授权记录迁移到错误账号,它主动拒绝继续。
3. 修复方式
首先明确默认账号:
|
|
然后将旧授权文件关联到实际账号:
|
|
同时清理没有 appId/appSecret 的遗留 accounts.default,避免它被继续识别成一个真实飞书账号。
修复后,Doctor 能继续执行 Provider catalog、Session SQLite、Agent DB 等后续迁移。
四、第二阶段:飞书配置 Schema 不兼容
1. 故障日志
插件安装或配置读取时报错:
|
|
2. 根因
旧配置包含:
|
|
新版飞书配置 Schema 使用严格校验:
|
|
只要配置中出现 Schema 未声明的字段,整个 openclaw.json 就会被拒绝。
3. 新旧字段映射
| 旧配置 | 新配置 |
|---|---|
replyMode: "auto" |
删除,由 streaming 等配置控制 |
threadSession: true |
groupSessionScope: "group_topic" |
| 线程内回复 | replyInThread: "enabled" |
4. 修复后的飞书配置
以下示例已对凭据和群 ID 做变量化处理:
|
|
其中:
groupAllowFrom中使用oc_...格式的群聊 ID;groupSessionScope: "group_topic"表示不同飞书话题使用独立会话;replyInThread: "enabled"表示机器人在话题线程中回复;requireMention: false表示白名单群中无需@机器人也可以触发。
五、第三阶段:openclaw-lark 与 OpenClaw 2026.9.9 不兼容
这是本次故障的核心部分。
1. 原始错误
|
|
插件原版本:
|
|
升级到当时的稳定版本:
|
|
但 2026.7.16 仍然没有适配 OpenClaw 2026.9.9。
2. Plugin SDK 路径已经变化
插件运行时代码仍然引用:
|
|
检查 OpenClaw 2026.9.9 的 exports 后确认:
|
|
同时验证插件使用的函数:
|
|
对应的新模块均能正确导出这些函数。
修复
在插件入口中:
|
|
改为:
|
|
在回复分发器中:
|
|
改为:
|
|
实际修改文件:
|
|
.d.ts中的import type只是 TypeScript 类型声明,不参与运行时加载,无需修改。
3. npm 包存在 CJS/ESM 发布格式错误
修复 SDK 路径后,又出现:
|
|
根因
插件实际发布的 index.js 是 CommonJS:
|
|
但 package.json 却通过 exports.import 指向不存在的 ESM 文件:
|
|
而 npm 包中真正存在的入口是:
|
|
OpenClaw Loader 因 exports.import 将 CommonJS 入口当成 ESM 执行,于是 CommonJS 的 exports 全局变量不存在。
修复
将插件明确声明为 CommonJS,并让所有入口指向实际文件:
|
|
4. CommonJS 文件中残留 import.meta
插件的 CommonJS 编译产物中还存在 ESM 专用语法:
|
|
涉及:
|
|
修复
version.js 改用 CommonJS 自带的:
|
|
token-store.js 改用:
|
|
完成后直接用 Node 验证入口:
|
|
这说明插件已经能够作为 CommonJS 被正确加载。
5. Runtime API 已从 loadConfig 迁移为 current
插件中还存在旧调用:
|
|
OpenClaw 2026.9.9 对应的新 API 是:
|
|
涉及两个文件:
|
|
修复
|
|
改为:
|
|
以及:
|
|
改为:
|
|
六、第四阶段:重新打包插件并恢复依赖与来源完整性
1. 为什么不能直接修改 managed package
直接修改 OpenClaw 管理的 npm 插件目录后,插件虽然能够加载,但检查结果会显示:
|
|
原因是:
- OpenClaw 安装时记录了 npm 包来源和完整性;
- 手工修改 JS 文件后,实际内容与登记内容不一致;
- 插件因此被标记为来源完整性异常。
2. 为什么本地目录安装还不够
第一次从本地目录安装后,又出现:
|
|
直接复制本地源码目录没有完整安装 npm dependencies。
3. 最终方案:重新打成 tgz
将修补后的插件版本标记为:
|
|
然后执行:
|
|
得到:
|
|
通过 OpenClaw 的 npm-pack: 来源覆盖安装:
|
|
这样 OpenClaw 会:
- 解压补丁包;
- 安装生产依赖;
- 处理 OpenClaw peer dependency;
- 运行插件加载检查;
- 重新登记插件来源。
最终检查结果:
|
|
origin-path 是本地补丁来源标记,不再是 provenance-invalid。
七、第五阶段:SQLite ownership 丢失
插件完成修复后,Gateway 又出现:
|
|
同时 Telegram、Device Pair 和 Delivery Recovery 持续报错:
|
|
1. 根因
修复过程中执行了多个 one-off 容器:
|
|
这些容器与正式 Gateway:
- 挂载同一个 OpenClaw SQLite;
- 但处于不同的 Linux PID namespace。
OpenClaw 2026.9.x 的 ownership 判断可能把另一个 namespace 中不可见的 PID 误判为已经死亡,从而回收或覆盖 ownership。
正式 Gateway 随后发现自己不再拥有 SQLite,于是拒绝继续读写。
2. 修复方式
完全停止正式容器:
|
|
确认没有其他共享状态目录的 OpenClaw 容器,然后等待 ownership heartbeat 保护窗口过期:
|
|
最后只启动正式容器:
|
|
恢复结果:
|
|
3. 操作规范
在 OpenClaw 2026.9.9 上:
Gateway 运行期间
不要执行:
|
|
应使用同一个容器 namespace:
|
|
Gateway 停止期间
可以使用 one-off 容器,但 one-off 命令退出后,应确认没有残留进程,并给 ownership lease 足够的释放时间,再启动正式 Gateway。
不要手工删除:
|
|
八、Session SQLite 迁移结果
升级期间还完成了大量会话迁移:
|
|
剩余 3 条历史异常:
- 两个 JSONL 的 transcript header 与原文件名不一致;
- 一个 Session 索引存在,但对应 JSONL 已丢失。
数量关系正好是:
|
|
这些问题没有阻止核心迁移和 Gateway 启动,因此当时没有直接执行 destructive recovery 或 cleanup。
在确认历史会话完整并完成备份前,不应直接运行:
|
|
九、最终修复清单
| 项目 | 修复结果 |
|---|---|
feishu-main-allowFrom.json 无法确定账号 |
映射到 main-bot |
缺少 defaultAccount |
设置为 main-bot |
无凭据的 accounts.default |
清理 |
replyMode |
删除 |
threadSession |
迁移为 groupSessionScope 和 replyInThread |
openclaw/plugin-sdk |
改为 openclaw/plugin-sdk/core |
channel-runtime |
改为 channel-outbound |
| CommonJS/ESM 冲突 | 修正 package.json |
CommonJS 中的 import.meta |
改用 __dirname / __filename |
runtime.config.loadConfig() |
改为 runtime.config.current() |
| 插件生产依赖缺失 | 使用 npm tgz 安装 |
provenance-invalid |
重新打包并登记本地来源 |
| SQLite ownership 丢失 | 停止、等待 lease 过期、单独启动 Gateway |
| 最终插件状态 | loaded |
| 飞书工具数量 | 39 |
| 容器状态 | running,重启次数 0 |
十、经验总结
1. 不要只看 peer dependency
插件声明:
|
|
并不代表它真实兼容所有更高版本。Plugin SDK、Runtime API 和插件加载器都可能发生 breaking change。
2. 应测试发布后的 npm tarball
源码仓库能构建,不代表 npm 发布包正确。本次发布包同时存在:
- CommonJS 编译产物;
- ESM
exports; - 指向不存在的
dist; - CommonJS 文件残留
import.meta; - 旧 Plugin SDK 路径。
真正需要测试的是:
|
|
生成的最终 tarball,而不是只测试源码目录。
3. 数据迁移失败时不要直接删除旧文件
allowFrom、Session JSONL 和 SQLite 都属于用户状态。迁移失败时应先确认归属关系并备份,而不是直接删除阻塞文件。
4. Docker PID namespace 会影响状态锁判断
共享同一 SQLite 的容器不等于共享 PID namespace。涉及数据库 ownership 的 CLI 应优先在正式容器中执行,或在 Gateway 完全停止后执行。
5. 补丁必须重新打包
直接修改 managed package 只适合验证,不适合作为最终状态。正式修复应:
- 保存补丁源;
- 设置可辨识版本;
- 重新打 tgz;
- 通过插件管理器安装;
- 再次验证来源、依赖和 Runtime 状态。
十一、后续建议
-
保留本地补丁包:
1local-plugins/dist/larksuite-openclaw-lark-2026.7.16-oc9.9.1.tgz -
暂时不要直接执行:
1openclaw plugins update openclaw-lark否则可能覆盖本地兼容补丁。
-
后续升级前确认新版插件已经修复:
- Plugin SDK 子路径;
- CJS/ESM 发布格式;
- npm tarball 中的真实入口;
runtime.config.current();- OpenClaw
2026.9+Runtime API; - Linux Docker 加载测试。
-
后续升级 OpenClaw 时,先备份:
1 2 3 4 5openclaw.json state/ agents/ credentials/ local-plugins/ -
继续处理非阻塞告警:
- 3 条历史 Session 异常;
- 明文模型凭据迁移到 SecretRefs;
- Browser Chromium 路径;
- Browser Relay legacy auth;
- 连续失败的 Cron 自动化;
- OpenClaw 原生备份计划。
十二、结论
这次升级的本质不是一个配置写错,而是一次跨多个版本边界的兼容性故障:
|
|
最终修复路径是:
|
|
最终 OpenClaw Gateway 正常启动,飞书插件成功加载,39 个飞书工具完成注册,容器保持稳定运行。