OpenClaw 升级助手
> 平台要求: 仅限 macOS(依赖 launchd)。OpenClaw 9.x 及以上版本。同时支持 Apple Silicon(/opt/homebrew/...)和 Intel Mac(/usr/local/...)— 详见 Environment 一节。
完整的升级工作流,全闭环(无需人工干预):版本检查 → 备份 → 升级 → doctor 自检 → 重写 plist + 加载到 launchd(两步缺一不可) → gateway 重启 → 验证 launchd 已接管 → KeepAlive 破坏性测试 → 最终汇报。
> 重写 LaunchAgent plist 时有两个强制步骤: openclaw gateway install --force 只会写 plist 文件 —— 它 不会 把 plist 注册到 launchd。你必须额外显式跑 launchctl load -w,否则 gateway 会卡在"LaunchAgent installed but not loaded"状态。用户通常会去终端手动启动 gateway,这会创建一个孤儿进程(PPID = zsh 而非 1),阻塞 launchd 接管。本 skill 把 install --force + launchctl load -w 当作两个强制、串行的步骤。
环境配置(Environment)
推荐使用一条能解析当前 OpenClaw + npm 工具链的 PATH,避免锁死过时的 Node 主版本号。Homebrew 前缀因 Mac 架构而异:
# Apple Silicon (M1 / M2 / M3 / M4)
BREW_BIN="/opt/homebrew/bin"
# Intel Mac — uncomment the next line if you have an Intel Mac
# BREW_BIN="/usr/local/bin"
PATH="$BREW_BIN:/usr/bin:/bin"
如果 openclaw、npm 或 node 仍然找不到,先检查实际安装路径,不要假设某个特定的 Node formula。
Step 0:升级前健康基线(Pre-upgrade Health Baseline,跑在 Step 2 之前)
> ⚠️ 为什么需要这一步: 2026.7.1 收紧了 models.json 的 schema、并内置了 memory-core 插件,这会 追溯性地把旧配置里的脏数据暴露成问题。如果你在已有烂配置的基础上直接升级,升级后的 doctor 会报一堆看似"新增"的错误,但其实它们是早就存在的——然后你会去修一个错误的方向。所以升级前先抓一份基线。
# 0a. Save full doctor output to a timestamped file
PATH="..." openclaw doctor 2>&1 | tee /tmp/openclaw-doctor-before-$(date +%Y%m%d-%H%M).txt | tail -40
# 0b. Snapshot the things doctor checks that may need post-upgrade repair
{
echo "=== openclaw.json plugins.allow + entries ==="
python3 -c "import json; d=json.load(open('~/.openclaw/openclaw.json')); p=d.get('plugins',{}); print('allow:', p.get('allow',[])); print('entries keys:', list(p.get('entries',{}).keys()))"
echo ""
echo "=== agents/*/agent/models.json with non-canonical input arrays ==="
for f in ~/.openclaw/agents/*/agent/models.json; do
python3 -c "
import json, sys
d = json.load(open('$f'))
for pname, p in d.get('providers',{}).items():
for m in p.get('models',[]):
inp = m.get('input', [])
if inp and len(inp) > 2: print(' $f', '$pname', m.get('id'), inp)
"
done
} | tee /tmp/openclaw-baseline-$(date +%Y%m%d-%H%M).txt
解析基线输出并 向用户汇报:
| 已存在的异常 | 处理动作 |
|---|---|
| 无异常 | 静默继续 |
model input 有 3 项及以上(含 audio/video) |
警告:升级后会失败,Step 4.5 会修复 |
Provider <X>: apiKey required |
警告:Step 4.5 会修复 |
allow 中的插件配置损坏(如 mem9 等) |
警告:Step 4.6 会清理 |
| 上表未列出的真实预存错误(如插件加载失败) | 先问用户 是否要带病升级 |
Step 1:检查当前版本与最新版本
PATH="..." openclaw --version
PATH="..." npm show openclaw version
对比两个版本号。如果一致,告诉用户并停止——无需升级。
如果有新版本可用,汇报当前 → 最新版本,然后继续。
Step 2:备份
PATH="..." openclaw backup create --output ~/backups/pre-upgrade/
如果备份失败,停止并报告。没有备份就不升级。
⚠️ 必须用 --output(2026-07-23 立条,fleet-wide 永久铁律):
- 默认行为是 "current directory" → 会把 ~/<ISO时间戳>-openclaw-backup.tar.gz 落到 home 根目录
- 历史上 4 次升级都漏了这一项,每次都在 home 根目录多一个 400MB–5GB 的备份
- 必须显式 --output ~/backups/pre-upgrade/(保留 8 个最近,按 mtime 自动清理)
Step 3:升级
PATH="..." npm install -g openclaw@latest
等待最多 120 秒。然后验证已安装版本:
PATH="..." openclaw --version
Step 4:doctor 检查与自动修复
PATH="..." openclaw doctor --fix
自动跑 --fix(无需向用户确认)。解析输出并汇报:
- 升级前不存在的 error / warning
- 状态完整性问题
- 频道 / skills / plugins 状态
- 自动修复了哪些
注意: doctor 在中途可能触发 gateway 重启。如果出现 Cannot find module 'task-registry.maintenance-*.js' 这种错误,那是 npm 升级分块模块的暂时不一致,doctor 会通过重启自愈。不要中断。
Step 4.5:schema 校验清理(2026.7.1+)
> ⚠️ 为什么需要这一步: 基础的 openclaw doctor --fix 不会清理 models.json 的 schema 违规。你会看到 [agents/model-registry] model catalog load issue: Invalid models.json schema,每个受影响的 agent 都报一行。Step 4.5 修复这个问题。
# 4.5a. Save post-upgrade doctor output
PATH="..." openclaw doctor 2>&1 | tee /tmp/openclaw-doctor-after-$(date +%Y%m%d-%H%M).txt > /dev/null
# 4.5b. Parse and fix models.json schema errors
python3 << 'PYEOF'
import json, re, subprocess
from pathlib import Path
# Find every schema error in doctor output
doctor_out = subprocess.run(
["$BREW_BIN/openclaw", "doctor"],
capture_output=True, text=True, env={"PATH": "$BREW_BIN:/usr/bin:/bin"}
).stdout + subprocess.run(
["$BREW_BIN/openclaw", "doctor"],
capture_output=True, text=True, env={"PATH": "$BREW_BIN:/usr/bin:/bin"}
).stderr
errors = re.findall(r"File: (\S+)", doctor_out)
# For each affected file, fix input arrays
for f in set(errors):
p = Path(f)
if not p.exists(): continue
d = json.loads(p.read_text())
if 'providers' not in d:
# Old-schema file with no providers (e.g. {"version":1,"lastGood":{},"usageStats":{}})
# Reset to minimal valid form
p.write_text(json.dumps({"providers": {}}, indent=2))
print(f" RESET {f} (old schema → minimal valid)")
continue
changed = False
for pname, pdata in d.get('providers', {}).items():
for i, m in enumerate(pdata.get('models', []) or []):
inp = m.get('input', [])
if not inp or len(inp) <= 2:
continue
new_inp = [x for x in inp if x in ('text', 'image')]
if not new_inp: new_inp = ['text']
m['input'] = new_inp
changed = True
print(f" {f} providers.{pname}.models.{i}.input {inp} → {new_inp}")
if changed:
p.write_text(json.dumps(d, indent=2, ensure_ascii=False) + '\n')
PYEOF
# 4.5c. Apply the same fix via openclaw config set (so it survives gateway restart!)
# CRITICAL: Gateway restarts write agent/models.json from in-memory state.
# Your step 4.5b file edits WILL BE REVERTED. The real fix is `config set`.
for f in ~/.openclaw/agents/*/agent/models.json; do
python3 -c "
import json, subprocess
d = json.load(open('$f'))
for pname, pdata in d.get('providers', {}).items():
for i, m in enumerate(pdata.get('models', []) or []):
inp = m.get('input', [])
if not inp or len(inp) <= 2: continue
new_inp = [x for x in inp if x in ('text', 'image')]
if not new_inp: new_inp = ['text']
# CRITICAL: set global config, not file edit
path = f'agents.{pname}-agent.providers.{pname}.models.{i}.input' # placeholder
subprocess.run(['$BREW_BIN/openclaw', 'config', 'set',
f'agents.{pname}.models.providers.{pname}.models.{i}.input',
json.dumps(new_inp), '--strict-json'],
capture_output=True)
"
done
> 🚨 关键教训(2026-07-14): 直接编辑 agent/models.json 文件 会被 gateway 重启回滚 —— gateway 每次重启都会把它的 in-memory 模型注册表写回磁盘,覆盖你的修改。永远用 openclaw config set 改全局配置路径(如 models.providers.<provider>.models.<i>.input)。文件编辑只能用来 识别 哪些需要修,真正修要用 config set。
Step 4.6:插件工具名冲突清理(2026.7.1+)
# 4.6a. Look for plugin tool name conflicts in doctor output
grep -iE "plugin tool name conflict|disabled.*but config is present|memory.*public surface" /tmp/openclaw-doctor-after-*.txt
# 4.6b. If mem9 (or any other 'memory' plugin) is enabled but exporting tools that
# now conflict with bundled memory-core: disable it and uninstall it
# Disable the conflicting plugin
PATH="..." openclaw plugins disable <id>
# (e.g. mem9)
# Remove from openclaw.json plugins.allow AND plugins.entries to clear "config present" warning
python3 << 'PYEOF'
import json
f = '~/.openclaw/openclaw.json'
d = json.load(open(f))
plugins = d.get('plugins', {})
for sect in ('allow', 'entries'):
s = plugins.get(sect, {})
if sect == 'allow' and isinstance(s, list):
plugins[sect] = [x for x in s if x != '<id>']
elif sect == 'entries' and isinstance(s, dict):
s.pop('<id>', None)
json.dump(d, open(f, 'w'), indent=2, ensure_ascii=False)
PYEOF
# Fully uninstall (removes npm install record + plugin dir)
PATH="..." openclaw plugins uninstall <id> --force
# Trash any leftover empty npm project dir (uninstall leaves it)
trash ~/.openclaw/npm/projects/<id>-* 2>/dev/null
echo "trashed leftover npm dir"
# 4.6c. Enable the bundled replacement (e.g. memory-core)
python3 << 'PYEOF'
import json
f = '~/.openclaw/openclaw.json'
d = json.load(open(f))
plugins = d.setdefault('plugins', {}).setdefault('allow', [])
if '<replacement_id>' not in plugins:
plugins.append('<replacement_id>')
d.setdefault('plugins', {}).setdefault('entries', {})['<replacement_id>'] = {'enabled': True}
json.dump(d, open(f, 'w'), indent=2, ensure_ascii=False)
PYEOF
# 4.6d. Restart gateway to pick up the new plugin set
PATH="..." openclaw gateway restart
sleep 12
# 4.6e. Verify no more conflicts
PATH="..." openclaw doctor 2>&1 | grep -iE "plugin tool name conflict|disabled.*but config|memory.*public"
# (Empty output = clean)
Step 5:重写 plist + 加载到 launchd(两个强制步骤)
> ⚠️ 关键: openclaw gateway install --force 只写新的 plist 文件,不会把 plist 注册到 launchd。你必须额外显式跑 launchctl load -w,否则 gateway 会卡在"LaunchAgent installed but not loaded"状态,用户只能在终端手动启动(产生孤儿进程)。
# 5a. Write fresh plist (vN+1 metadata, correct ProgramArguments, RunAtLoad, KeepAlive)
PATH="..." openclaw gateway install --force
# 5b. Force-launchd to load the plist (THIS IS THE STEP THAT WAS MISSING)
launchctl load -w ~/Library/LaunchAgents/ai.openclaw.gateway.plist
# -w: writes "disabled=false" into the plist, so launchd auto-loads on next reboot too
# If you see "service already loaded", that's fine — `load -w` is idempotent
# If you see "Input/output error", run `launchctl bootout gui/$UID/ai.openclaw.gateway` first, then retry
# 5c. Verify launchd has the task
launchctl list | grep openclaw
# Expected output: <pid-or-status> 0 ai.openclaw.gateway
# The middle column "0" = last exit code. Don't worry if it's not "0" right now; the
# important thing is the line exists, proving launchd has registered the job.
如果 load -w 之后 launchctl list 仍然为空,参见下方的错误处理 § B:launchd load 失败。
Step 6:等待 KeepAlive 拉起进程,然后验证
sleep 8 # plist has ThrottleInterval=10; give it a bit
PATH="..." openclaw gateway status
确认以下各项:
- Runtime: running
- Service: LaunchAgent (loaded)
- Connectivity probe: ok
- Listening: *:18789
- 不要出现 Service config issue: Gateway service was installed by OpenClaw X; current CLI is Y 警告
然后 显式验证 launchd 是父进程(这是用户可以安全关闭终端的证据):
GW_PID=$(pgrep -f "openclaw/dist/index.js.*gateway" | head -1)
if [ -n "$GW_PID" ]; then
GW_PPID=$(ps -o ppid= -p $GW_PID | tr -d ' ')
if [ "$GW_PPID" = "1" ]; then
echo "[OK] gateway pid=$GW_PPID PPID=1 (launchd-managed)"
else
echo "[FAIL] gateway pid=$GW_PID PPID=$GW_PPID — not launchd-managed, see [§ A: Orphan process]"
fi
fi
Step 7:KeepAlive 自愈验证(破坏性测试)
这一步专门抓“plist 看着对但 KeepAlive 实际坏了”——这种状态在 gateway status 里看不出来,要等用户真去 kill 进程或关终端时才暴露。
# 7a. Note current gateway PID
OLD_PID=$(pgrep -f "openclaw/dist/index.js.*gateway" | head -1)
echo "Killing gateway pid=$OLD_PID to test KeepAlive..."
# 7b. Kill it (simulates crash / manual kill)
[ -n "$OLD_PID" ] && kill -9 $OLD_PID
# 7c. Wait for launchd to respawn (ThrottleInterval=10s, give it 15s)
sleep 15
# 7d. Verify launchd spawned a new one with PPID=1
NEW_PID=$(pgrep -f "openclaw/dist/index.js.*gateway" | head -1)
if [ -n "$NEW_PID" ] && [ "$NEW_PID" != "$OLD_PID" ]; then
NEW_PPID=$(ps -o ppid= -p $NEW_PID | tr -d ' ')
if [ "$NEW_PPID" = "1" ]; then
echo "[OK] KeepAlive works: new pid=$NEW_PID, PPID=1 (launchd)"
else
echo "[FAIL] new pid=$NEW_PID, PPID=$NEW_PPID — not launchd"
fi
else
echo "[FAIL] launchd did not respawn gateway"
fi
如果 KeepAlive 失败,用户离 gateway 死掉只差一次崩溃或一次关终端。不要宣布升级完成——参见错误处理 § C:KeepAlive 不工作。
Step 8:清理残留的孤儿 CLI 进程
当用户在终端手动跑 openclaw gateway 时,那个 CLI 父进程 会挂在 zsh 里等它启动的 gateway 子进程退出。升级完成后,那个 CLI 父进程还活着(在旧的终端里)——它不阻塞任何东西(已经不再占端口),但出于整洁应该 kill 掉。不要 kill zsh 父进程 —— 只 kill openclaw CLI 子进程。
# Find any openclaw CLI parents still hung in terminals
ps -ef | awk '/openclaw/ && !/node/ && $3 != 1 {print}'
# Example: "<uid> <pid> <ppid> ... openclaw" (PID=<pid>, PPID=<ppid>=zsh)
# Kill ONLY the openclaw CLI child, not its zsh parent
HUNG_CLI=$(ps -ef | awk '/openclaw/ && !/node/ && $3 != 1 {print $2}' | head -1)
[ -n "$HUNG_CLI" ] && kill -9 $HUNG_CLI
Step 9:向用户汇报
OpenClaw upgrade complete:
Previous: <old-version>
Current: <new-version> (commit-hash)
Backup: <path-to-backup-tarball>
Doctor: clean (or list of fixed issues)
Gateway: pid=<X>, PPID=1, launchd-managed, KeepAlive verified
Plugin drift: <list, or "none">
然后告诉用户最关键的一句保证:"现在可以安全关闭终端了 —— KeepAlive 已验证,launchd 会保持 gateway 一直在跑。"
常见症状与修复
| 症状 | 根因 | 修复 |
|---|---|---|
报 LaunchAgent installed but not loaded |
plist 已写但未注册到 launchd | 跑 launchctl load -w ~/Library/LaunchAgents/ai.openclaw.gateway.plist |
报 Service config issue: Gateway service was installed by OpenClaw X; current CLI is Y |
plist 元数据版本比当前 CLI 旧 | 跑 openclaw gateway install --force + launchctl load -w |
报 Runtime: unknown (Could not find service "ai.openclaw.gateway") |
launchd 完全没有 plist 记录 | 跑 launchctl load -w ...(新版 macOS 用 launchctl bootstrap gui/$UID ...) |
报 Cannot find module 'task-registry.maintenance-*.js' |
npm 升级分块模块暂时不一致 | doctor 会通过重启自愈;持续出现就 openclaw gateway restart |
报 Plugin version drift: feishu X → expected Y |
插件 npm 包落后于 core | 仅是 warning,不是 error —— 跑 openclaw plugins update <id> 会装上 npm 上最新的版本,但仍然可能比预期旧 |
报 State dir migration skipped: target already exists |
历史遗留状态,无害 | 忽略 |
重启后报 Connectivity probe: failed |
gateway 还在启动或端口冲突 | sleep 5 && openclaw gateway status;仍失败就 lsof -nP -iTCP:18789(PATH 里没 lsof 就用 sudo lsof) |
报 Invalid models.json schema: providers.X.models.0.input.2: must be equal to constant |
2026.7.1+ 收紧了 input array schema;旧配置含 audio/video | Step 4.5 —— 跑 openclaw config set 'models.providers.<X>.models.<i>.input',不要编辑文件 |
报 Provider X: "apiKey" is required when defining custom models |
2026.7.1+ 即使 authHeader:true 也必须有 apiKey |
Step 4.5c —— 跑 openclaw config set 'models.providers.<X>.apiKey' '...' |
报 plugin tool name conflict (mem9): memory_search |
旧插件导出了内置工具名 | Step 4.6 —— openclaw plugins disable mem9、清理 allow+entries、plugins uninstall --force、启用 memory-core |
报 plugin disabled (disabled in config) but config is present |
已禁用的插件配置没清干净 | Step 4.6 —— 还要从 openclaw.json 的 plugins.allow 和 plugins.entries 里删掉 |
报 Memory artifact repair could not be completed: Bundled plugin public surface access blocked for "memory-core" |
mem9 冲突阻塞了 memory-core 的运行时 | Step 4.6 —— 同上;mem9 一删自动恢复 |
错误处理(Error Handling)
Z. 关键 exec 编排铁律:杀/启 gateway 的 exec 里绝对不要调 openclaw gateway status(避免 probe hang 整个 exec)
症状: exec 输出 missing tool result in session history; inserted synthetic error result for transcript repair。表面看像"工具调用失败",实际上是 exec 里某条命令 hang 死导致整个 exec 超时。
根因: openclaw gateway status 内部会 WS probe ws://127.0.0.1:18789。当 18789 端口正在被重启(被 kill 后 launchd 还没拉起新的)时,那个 probe 会 hang 死,整个 exec 卡住直到超时 → OpenClaw 标成 missing tool result → 看起来失败。
铁律:
-
kill -9 <孤儿pid>+launchctl bootout+launchctl load -w这三步单独一个 exec,秒级返回,不加 sleep,不调 status:
bash kill -9 <orphan_pid> 2>/dev/null launchctl bootout gui/$UID/ai.openclaw.gateway 2>/dev/null launchctl load -w ~/Library/LaunchAgents/ai.openclaw.gateway.plist echo "kill+load done" -
verify 用
ps/pgrep/launchctl list,不用openclaw gateway status。看 PPID 是否等于 1。 -
openclaw gateway status单独放在下一个 exec,此时 gateway 已稳定运行,probe 不会 hang。 -
每个 exec 控制在 10s 以内(短命令)或单独跑
sleep 12然后 verify(长命令)。绝不在同一个 exec 里先 kill 进程再 sleep 再 verify status。 -
zsh 里
PPID是只读变量,赋值PPID=$(...)会报read-only variable: PPID。要用别的变量名,比如PARENT_PID。
反模式(不要再写):
# ❌ 千万别这么写
kill -9 <orphan_pid>
sleep 2
openclaw gateway status # ← probe hang 整个 exec
launchctl load -w ...
sleep 12
openclaw gateway status # ← 又 hang
正确写法:
# exec 1(<2s):纯操作,不 verify
kill -9 <orphan_pid> 2>/dev/null
launchctl bootout gui/$UID/ai.openclaw.gateway 2>/dev/null
launchctl load -w ~/Library/LaunchAgents/ai.openclaw.gateway.plist
echo "done"
# exec 2(~12s):sleep + 用 ps 验 PPID=1(不调 status probe)
sleep 12
GW_PID=$(pgrep -f 'openclaw/dist/index.js.*gateway' | head -1)
PARENT_PID=$(ps -o ppid= -p "$GW_PID" | tr -d ' ')
[ "$PARENT_PID" = "1" ] && echo OK
# exec 3(<2s):这时才调 status,gateway 已稳定
openclaw gateway status
A. 孤儿进程(PPID ≠ 1,"service not loaded" 但 gateway 在跑)
根因: 用户在终端跑了 openclaw gateway,CLI 启动了一个绑定 18789 的 node 子进程;但 launchd 的 plist 从来没注册过,launchd 无法重新拉起 —— 用户以为自己必须手动启动。
症状:
- openclaw gateway status 显示 LaunchAgent installed but not loaded,但同时显示 Listening: *:18789(那个手动起的进程)
- ps -ef | grep openclaw | grep gateway 显示 PPID 是 tty/zsh 的 pid,不是 1
- launchctl list | grep openclaw 为空
修复(不要动 zsh 父进程):
# 1. 找出占端口的 node 子进程
PIDS=$(lsof -nP -iTCP:18789 -sTCP:LISTEN -t 2>/dev/null)
# (PATH 里没 lsof 就用:/usr/sbin/lsof ...)
# 2. 只 kill 这些子进程(不要 kill zsh 父进程!)
[ -n "$PIDS" ] && kill -9 $PIDS
sleep 2
# 3. 验证端口已释放
lsof -nP -iTCP:18789 -sTCP:LISTEN 2>&1 | head -3
# (空输出 = 端口已释放)
# 4. 注册 plist 到 launchd
launchctl load -w ~/Library/LaunchAgents/ai.openclaw.gateway.plist
sleep 8
# 5. 验证是 launchd 拉起的
GW_PID=$(pgrep -f "openclaw/dist/index.js.*gateway" | head -1)
[ -n "$GW_PID" ] && ps -o ppid= -p $GW_PID | grep -q 1 && echo "[OK]"
# 6. 跑 KeepAlive 测试(Step 7)
> ⚠️ 不要 kill zsh 父进程。 2026-06-10 那次事故文档说要两个都 kill,但那是个特例(launchd 重试时序问题)。正常情况下只 kill 子进程才对,不会让用户终端也跟着断。旧建议过于一刀切。
B. launchctl load 失败
# 报错:"service already loaded" → 忽略,这是 no-op
# 报错:"Input/output error" → 先 bootout 再重试
launchctl bootout gui/$UID/ai.openclaw.gateway
launchctl load -w ~/Library/LaunchAgents/ai.openclaw.gateway.plist
# 报错:"Operation not permitted" → 当前用户没法跟 launchd 通信。检查
# Console.app → "launchd" 子系统。通常
# 是权限/sandbox 问题。
C. KeepAlive 不工作
如果 Step 7 失败(kill 后 15 秒内 launchd 没拉起 gateway):
- 检查 plist 本身 ——
KeepAlive真的是true吗?ProgramArguments对吗?
bash /usr/libexec/PlistBuddy -c "Print :KeepAlive" ~/Library/LaunchAgents/ai.openclaw.gateway.plist /usr/libexec/PlistBuddy -c "Print :RunAtLoad" ~/Library/LaunchAgents/ai.openclaw.gateway.plist /usr/libexec/PlistBuddy -c "Print :ProgramArguments" ~/Library/LaunchAgents/ai.openclaw.gateway.plist - 手动测试 ProgramArguments —— 把它们复制到 shell 里跑:
bash /path/to/openclaw/service-env/ai.openclaw.gateway-env-wrapper.sh \ /path/to/openclaw/service-env/ai.openclaw.gateway.env \ /opt/homebrew/opt/node/bin/node \ /opt/homebrew/lib/node_modules/openclaw/dist/index.js \ gateway --port 18789
如果这里报错(比如 node 模块路径不对),launchd 的 KeepAlive 会静默失败。 - 看 service 日志:
bash tail -50 ~/Library/Logs/openclaw/gateway.log - 重装并重新加载:
bash openclaw gateway install --force launchctl bootout gui/$UID/ai.openclaw.gateway 2>/dev/null launchctl load -w ~/Library/LaunchAgents/ai.openclaw.gateway.plist
D. 备份失败
立即停止。不要继续升级。把错误告诉用户。
E. npm install 失败
报告错误。Gateway 还跑在旧版本上,不要继续后续步骤。
F. doctor 报新的 critical error(非 transient module-not-found)
立即告诉用户。不要盲目自动修。
G. Node / 路径问题
不要假设是 node@22 或某个特定 formula。先检查:
which node
which openclaw
node --version
ls -la $(which openclaw)
如果 openclaw 是 Homebrew 软链接,它实际依赖的 node 二进制在 $(dirname $(readlink -f $(which openclaw)))/../... —— 但 macOS 上 readlink -f 不存在,用:
node -e "console.log(require.resolve.paths(''))" # shows node module search paths
which -a node # all node binaries in PATH
H. gateway 重启后 agent/models.json schema 错误被回滚
直接编辑 ~/.openclaw/agents/<id>/agent/models.json 完全无效,因为 gateway 在每次 restart 时把 in-memory model registry 写回磁盘。你的修改被覆盖。
症状:
- Step 4.5 文件编辑后,doctor 不报错了
- 但下次 openclaw gateway restart 之后又报同样错
- 文件 mtime 跟 restart 时间对齐
根因: Gateway 启动时从全局 openclaw.json 的 models.providers.* 读取模型定义,运行中维护自己的 in-memory registry,写盘时按 in-memory 状态落盘。
修复:
# ❌ 反模式:编辑文件(会回滚)
python3 -c "...; json.dump(d, open(..., 'w'))"
# ✅ 正确:用 openclaw config set 改全局(不会回滚)
openclaw config set 'models.providers.<provider>.models.<i>.input' '["text","image"]' --strict-json
openclaw config set 'models.providers.<provider>.apiKey' '<key>' --strict-json
这会写到 ~/.openclaw/openclaw.json,gateway 每次启动都从这读,不会覆盖。
I. 升级后插件工具名冲突(mem9 vs memory-core)
症状:
- doctor 日志里有 20+ 行 [plugins] plugin tool name conflict (mem9): memory_search
- doctor 同时报:Memory artifact repair could not be completed: Bundled plugin public surface access blocked for "memory-core"
- memory_search / memory_get 完全不能用(mem9 抢注,memory-core 被 block)
根因: 2026.7.1 内置了 memory-core plugin 导出 memory_search 和 memory_get。原来装的 mem9 (@mem9/mem9 第三方) 也导出同名工具,导致工具冲突。
修复(按顺序):
# 1. 关掉冲突的第三方插件
PATH="..." openclaw plugins disable mem9
# 2. 干净清理 openclaw.json(disable 不会自动删除配置,留着会一直 warning)
python3 << 'PYEOF'
import json
f = '~/.openclaw/openclaw.json'
d = json.load(open(f))
plugins = d.setdefault('plugins', {})
if isinstance(plugins.get('allow'), list):
plugins['allow'] = [x for x in plugins['allow'] if x != 'mem9']
if isinstance(plugins.get('entries'), dict):
plugins['entries'].pop('mem9', None)
json.dump(d, open(f, 'w'), indent=2, ensure_ascii=False)
PYEOF
# 3. 干净卸载(卸 install record + npm dir)
PATH="..." openclaw plugins uninstall mem9 --force
# 4. 清掉残留空目录(uninstall 不删外层 dir)
trash ~/.openclaw/npm/projects/mem9-* 2>/dev/null
# 5. 启用内置 memory-core
python3 << 'PYEOF'
import json
f = '~/.openclaw/openclaw.json'
d = json.load(open(f))
plugins = d.setdefault('plugins', {})
allow = plugins.setdefault('allow', [])
if 'memory-core' not in allow:
allow.append('memory-core')
entries = plugins.setdefault('entries', {})
entries['memory-core'] = {'enabled': True}
json.dump(d, open(f, 'w'), indent=2, ensure_ascii=False)
PYEOF
# 6. 重启 gateway
PATH="..." openclaw gateway restart
sleep 12
# 7. 验
PATH="..." openclaw doctor 2>&1 | grep -iE "plugin tool name|memory.*plugin|memory search"
# 空输出 = 干净
为什么"关掉终端 —— KeepAlive 会保持 gateway 运行"这句话是可靠的
这是面向用户的承诺,必须是真的。要做到这点:
- plist 必须已注册到 launchd(Step 5b)。
- plist 必须有
KeepAlive = true(Step 5a 默认写入正确的 plist)。 - ProgramArguments 必须真的能跑(Step 7 的破坏性测试已经验证)。
- 用户必须明白 旧 CLI 父进程(挂在 zsh 下的那个独立
openclaw进程)并不是保持 gateway 运行的家伙 —— 真正干这事的是 launchd。
四个条件都满足时,用户关掉终端、旧 zsh 死、旧 CLI 父进程死、端口空出约 10 秒,然后 launchd 重新拉起一个 PPID=1 的新 gateway。在维护者的机器上已经端到端验证过 —— 用户关终端后,gateway pid <X> 死,launchd 在 10 秒内重新拉起 pid <Y>(PPID=1),服务不中断。