技术破壁专栏 · 第9期
记录周期:2026年6月至9月(3个月6次升级踩坑)
核心挑战:每次OpenClaw升级都踩出不同类型的问题——从路径硬编码到SQLite残留,从插件breaking change到AsyncLocalStorage传播bug
关键词:systemd ExecStart、pnpm归属、plugin-sdk、GatewayDrainingError、AsyncLocalStorage、SQLite handoff
2026年9月18日下午3点,一位客户在企业微信客服通道发消息咨询青蟹。蟹蟹没有回复。
日志里的错误信息冷冰冰的:
GatewayDrainingError: Gateway is draining; new tasks are not accepted
这不是第一次了。从6月到9月,每次OpenClaw升级都像开盲盒——你永远不知道会踩到什么。systemd服务文件指向旧路径、包管理器归属信息丢失、插件API breaking change、SQLite残留记录导致draining死循环……
老林说了一句话:
"升级前它是好的,升级后它就不好了。"
本文记录这条升级链路上的6个真实坑案,以及最终建立的双层防护体系。
坑案一:systemd硬编码版本路径,升级后加载旧代码
坑案二:openclaw命令指向root用户目录
坑案三:pnpm归属丢失,升级被拒绝 + 坑案四:插件SDK breaking change
坑案五:升级后数据索引格式变更,chatreview同步中断
坑案六:GatewayDrainingError死循环(双重根因叠加)
老林在微信上执行升级,CLI显示成功,但Gateway启动后仍然加载旧版本代码。新功能不可用,旧bug还在。
检查systemd服务文件:
[Service]
ExecStart=/usr/bin/node .../openclaw/2026.6.5/.../dist/index.js gateway --port 11589
路径写死了2026.6.5。pnpm升级后新版本装在2026.6.8路径下,但service文件没更新——Gateway进程加载的还是老代码。
OpenClaw通过pnpm global安装,每次升级创建新的store路径(带版本号hash)。openclaw gateway install生成的systemd service文件把当时的完整路径硬编码进ExecStart。升级后如果不重新install,service文件指向的还是旧路径。
手动更新service文件中的3处版本信息(Description、ExecStart、OPENCLAW_SERVICE_VERSION),reload + restart。
执行openclaw命令时报权限错误,无法正常运行。
$ which openclaw
/usr/bin/openclaw
$ ls -la /usr/bin/openclaw
lrwxrwxrwx 1 root root ... /home/ubuntu/.nvm/.../openclaw
软链接指向root用户目录下的nvm路径,ubuntu用户无权访问。
某次安装过程中使用了sudo,导致软链接创建到root的nvm路径下,而非ubuntu用户的pnpm路径。
重新创建软链接,指向正确的ubuntu用户pnpm路径。
执行openclaw update直接报错:
Error: package manager owner is unknown
系统未做任何更改,升级被拒绝。
OpenClaw的更新机制需要检测当前安装是由哪个包管理器(npm/pnpm/Bun)装的。这台机器最初通过pnpm global安装,但检测不到pnpm的所有权信息了。
排查发现:pnpm版本升级后home store发生了迁移,导致包的归属元数据丢失。OpenClaw无法确认"这个包是谁装的",拒绝更新。
9月17日诊断确认根因后,9月18日通过pnpm直接安装指定版本绕过检测:
pnpm add -g openclaw@2026.9.4
然后强制重装service:
openclaw gateway install --force
package manager owner is unknown,直接用pnpm add -g openclaw@<版本>绕过。升级到2026.9.2后,wecom-kf插件(v2026.7.1)报错:
ERR_PACKAGE_PATH_NOT_EXPORTED
龙虾教官的客服通道完全不通。
OpenClaw 2026.9.2移除了./plugin-sdk裸导出,改为./plugin-sdk/core子路径导出。wecom-kf插件还在用旧的import方式:
// 旧(9.2之前可用)
import { ... } from "openclaw/plugin-sdk";
// 新(9.2要求)
import { ... } from "openclaw/plugin-sdk/core";
手动给wecom-kf插件打补丁,修改import路径:
# 找到插件文件
grep -rl "openclaw/plugin-sdk" ~/.openclaw/plugins/wecom-kf/
# 替换为子路径
sed -i 's|"openclaw/plugin-sdk"|"openclaw/plugin-sdk/core"|g' <plugin-file>
openclaw doctor会报告插件兼容性状态。另外,这个补丁在openclaw update后会被覆盖(新版本路径换了),需要重新打。后来在2026.9.4升级时,我们写了一个专用技能文件openclaw-plugin-compat-repair来标准化这个修复流程。
wecom-kf插件修好了,但chatreview同步任务从9月9日起连续失败。客户的聊天记录无法同步到本地分析系统。
chatreview同步脚本依赖sessions.json索引文件来定位会话数据。OpenClaw 2026.9.2升级后,不再生成这个文件——改为SQLite数据库存储。
脚本在文件系统里找不到索引文件,直接报错退出。
将同步脚本从读取sessions.json改为直接扫描.jsonl文件。这是一个两步修复:
sessions.json检查逻辑修复后手动运行成功,同步了20个客户255条消息。
这是最严重的一个坑——升级后客户消息全部被拒,而且反复重启都修不好。
升级到2026.9.4后,Gateway启动正常,4个渠道全部OK。但客户消息进来时全部报错:
[wecom-kf] [ERROR] dispatchTranscriptTurn failed:
GatewayDrainingError: Gateway is draining; new tasks are not accepted
重启Gateway——还是一样。再重启——还是一样。
这是两个独立根因叠加的结果。
OpenClaw在重启前会向SQLite写入一条gateway_restart_handoff记录,包含当前PID和60秒TTL。新进程启动时读取并消费这条记录。
问题出在:9月8日升级失败时,进程在draining过程中被强杀(kill -9 / systemd超时),handoff记录没被消费。10天后重启,新进程读到这条旧记录——PID不匹配,TTL早过期——但代码仍然认为自己还在draining。
created: 2026-09-08T15:41:07 (10天前)
expires: 2026-09-08T15:42:07 (10天前就过期了)
PID: 2866622 (早就没了)
当前PID: 1995342 (完全不匹配)
于是新进程永远卡在draining状态,拒绝所有消息,直到手动清理这条记录。
清理handoff记录并重启后,wecom-kf消息仍然被拒。这次根因不同——是Node.js的AsyncLocalStorage在Promise链中传播已释放的admission context。
机制如下:
root work admission,通过AsyncLocalStorage.run()传播processKfEvent被延迟执行admission.release()标记释放AsyncLocalStorage.getStore()返回的是已释放的admission(released: true)isGatewaySubordinateWorkAdmissionClosed()检查到current.released === true,返回true——误判为draining源码对比(9.2和9.4的upstream代码完全一样,都有这个bug):
// upstream(有bug)
function isGatewaySubordinateWorkAdmissionClosed() {
if (...) return true;
const current = currentRootWork.getStore();
if (current) return current.released; // released=true → 误判为draining
...
}
根因A:清理SQLite中的stale记录 + 删除JSON handoff文件 + 重启。
根因B:修改isGatewaySubordinateWorkAdmissionClosed函数,让已释放的admission回退到全局suspendPhase检查而非直接返回true:
// 补丁后
function isGatewaySubordinateWorkAdmissionClosed() {
if (...) return true;
const current = currentRootWork.getStore();
if (current && !current.released) return false; // 未释放 → 放行
if (current && current.released) // 已释放 → 看全局状态
return suspendPhase !== "accepting";
return suspendPhase !== "accepting";
}
setImmediate在Node.js 22+上不足以切断AsyncLocalStorage传播,这个问题在官方文档里没有提及。经历这6个坑之后,我们建立了两层防护,确保升级后的常见问题自动修复,不再需要人工介入。
在Gateway服务文件中添加启动前清理:
[Service]
# 启动前自动清理stale handoff记录
ExecStartPre=/usr/bin/node -e "const {DatabaseSync}=require('node:sqlite');try{const db=new DatabaseSync('.../openclaw.sqlite');db.prepare('DELETE FROM gateway_restart_handoff').run();db.prepare('DELETE FROM gateway_restart_intent').run();db.close();console.log('[ExecStartPre] stale handoff cleaned')}catch(e){console.log('[ExecStartPre] no handoff to clean')}"
ExecStartPre=/bin/rm -f .../gateway-supervisor-restart-handoff.json
ExecStart=/usr/bin/node ... openclaw ... gateway --port 11589
效果:无论上一个进程怎么死的(kill -9、断电、崩溃),新进程启动前handoff记录已被清掉。从源头杜绝draining死循环。
在OpenClaw的gateway-work-admission-*.mjs文件中打补丁。补丁内容见坑案六。
查找文件(路径随版本变化):
grep -l "isGatewaySubordinateWorkAdmissionClosed" \
~/.local/share/pnpm/store/v11/links/@/openclaw/<version>/*/node_modules/openclaw/dist/gateway-work-admission-*.mjs
效果:deferred的wecom-kf任务即使继承了已释放的admission context,也不会被误判为draining。
两层防护都会在升级时丢失,需要重新部署:
| 步骤 | 命令 | 说明 |
|---|---|---|
| 1. 升级前 | openclaw doctor | 检查插件兼容性,记录当前状态 |
| 2. 升级 | pnpm add -g openclaw@<版本> | 绕过package manager owner检测 |
| 3. 重装service | openclaw gateway install --force | 更新systemd文件中的路径 |
| 4. 重新添加ExecStartPre | 编辑service文件 | openclaw gateway install --force会覆盖 |
| 5. 重新打admission补丁 | sed或手动编辑 | 新版本=新文件 |
| 6. 检查wecom-kf补丁 | grep "plugin-sdk/core" | 确认import路径补丁还在 |
| 7. daemon-reload + restart | systemctl --user daemon-reload && systemctl --user restart openclaw-gateway | 应用所有变更 |
| 8. 验证 | openclaw status + 检查渠道 | 确认4个渠道OK、无draining错误 |
openclaw update只换CLI包。systemd service文件、插件补丁、admission补丁——这些都需要手动跟进。把升级Checklist固化下来,每次照着走。
清理了stale handoff记录后draining还在?不要慌。可能是AsyncLocalStorage传播bug在deferred任务上独立触发。诊断时看日志里的错误来源:如果admission closed: restart drain来自全局状态,是根因A;如果来自current.released,是根因B。
不要指望进程总是优雅退出。断电、OOM、systemd超时强制杀死——这些都会发生。ExecStartPre是systemd原生的启动前钩子,比写额外的监控脚本简单100倍。每次启动自动清理,零成本兜底。
从6月17日第一次踩systemd硬编码路径,到9月18日建立双层防护体系,3个月6个坑。每个坑的根因不同——有的是OpenClaw的设计缺陷(handoff记录不随进程死亡自动清理),有的是Node.js运行时的特性(AsyncLocalStorage传播),有的是包管理器的副作用(pnpm home store迁移),有的是Breaking Change(plugin-sdk路径变更)。
但最终,所有踩过的坑都沉淀为了可复用的防护措施和检查清单。这不是"踩坑记录",这是用真金白银换来的运维知识资产。
老林说:
"先把业务想清楚再让AI学业务。"
升级也一样——先把坑想清楚,再点那个update。
本文记录的所有问题均来自塘口拾鲜(台州)科技有限公司的真实生产环境。敏感信息已脱敏。