为什么写这篇

10 月 30 日,这台机器上 OpenClaw 的三条模型通道同时不可用:

  • dashscope/qwen-plus(当时的主模型)返回 Arrearage——账号欠费;
  • bailian-cli/qwen3.8-max 是按月买的 token 包,insufficient_quota,11-10 才重置;
  • cb/hy3-preview-agent(CodeBuddy 转换器)返回 403。

openclaw cron list 上最直观:每天 03:00 那个「每日实操文」任务 status: error (5x),last error 是

⚠️ cb/hy3-preview-agent request failed (provider internal error).
This is usually temporary — try again shortly. | 403

"provider internal error / usually temporary" 这种提示最坑人——它根本不是临时的。下面是我实际用的排查顺序,四步,每步都有可抄的命令。

第一步:先分层,别把 403 当成一个错

同一个"模型用不了",可能是四种完全不同的故障:

  1. 进程层:转换器没在跑 / 端口没监听;
  1. 鉴权层:token 过期、凭据文件读不到;
  1. 账号层:账号被限制、欠费、额度用尽;
  1. 配置层:OpenClaw 指向了错误的 provider 或模型名。

区分方式很简单——先只探第一、二层:

curl -s --max-time 5 http://127.0.0.1:8787/health   # 进程活着 + 它看到的凭据
curl -s --max-time 5 http://127.0.0.1:8787/v1/models # 上游能不能列模型

本机 /health 返回(节选):

{"status":"ok","python":"3.10.12",
 "auth_file":"/root/.local/share/CodeBuddyExtension/Data/Public/auth/Tencent-Cloud.coding-copilot.info",
 "mode":"direct-proxy (native function calling)",
 "credential":{"uid":"fda4dd80-1ede-...","nickname":"爱国 敬业 诚信 友善",
               "token_expires_at":1794381034894,"token_expired":false}}

到这一步结论已经出来了:进程活着、token 没过期、/v1/models 还能正常吐 12 个模型——那 403 只可能是第三层(账号层)。

实战踩坑提醒 1:/v1/models 能拉到列表,不代表能推理。列表接口常常不走计费/风控,真正花钱的 chat/completions 才会撞限制。别拿 models 当健康检查,要发一条最小 completion:
curl -s --max-time 50 -X POST http://127.0.0.1:8787/v1/chat/completions \
  -H "Content-Type: application/json" -H "Authorization: Bearer dummy-placeholder-key" \
  -d '{"model":"hy3-preview-agent","messages":[{"role":"user","content":"say OK"}],"max_tokens":16}'

本机这条命令返回的就是答案本身:

{"code":11140,"msg":"request illegal","retry_policy":{"retry":false},
 "displayMsg":{"zh":"账号存在异常行为,已被限制使用"},
 "actions":["EDIT_INPUT","SUBMIT_FEEDBACK"]}

retry: false + 11140 = 账号级封禁,重试没意义。OpenClaw 把它包装成 "provider internal error",是信息损失,不是模型坏了。

第二步:看进程是谁在跑

服务器今天重启过(uptime 54 分钟),但转换器由 systemd 拉起,没掉链子:

uptime
systemctl status codebuddy-converter.service --no-pager | head -12
ps -o user=,pid=,args= -p 779

本机返回:Active: active (running) since Fri 2026-10-30 16:05:52,Main PID: 779,属主 root。

这一步看着废话,但它决定了下一件事的答案:进程属主是谁,它就只认那个用户的家目录里的凭据。

第三步:让服务自己交代在读哪份凭据

转换器代码里定位 auth 目录是这样写的(converter.py):

xdg = Path(os.environ.get("XDG_DATA_HOME", home / ".local" / "share"))
return [xdg / "CodeBuddyExtension" / "Data" / "Public" / "auth"]

home 来自运行它的用户。root 跑 → 读 /root/.local/share/...。/health 里那行 auth_file 就是这个逻辑的输出,不用猜,直接看。

第四步:比对 uid,才发现"换账号登录"白做了

按建议,我打算换一个没被限制的 CodeBuddy 账号重新登录,于是用 codebuddy CLI 走了 /login → 「Log in via Chinese Site」拿设备链接扫码。扫完检查凭据:

for f in /root/.local/share/CodeBuddyExtension/Data/Public/auth/*.info \
         /home/admin/.local/share/CodeBuddyExtension/Data/Public/auth/*.info; do
  python3 -c "import json,sys;d=json.load(open(sys.argv[1]));a=d.get('auth') or {};
print(sys.argv[1]);print(' uid:',(d.get('account') or {}).get('uid'));
print(' expiresAt:',a.get('expiresAt'))" "$f"
done

本机真实输出:

/root/.local/share/.../Tencent-Cloud.coding-copilot.info
 uid: fda4dd80-1ede-4d88-88cf-800a42ba58af   expiresAt: 1794381034894
/home/admin/.local/share/.../Tencent-Cloud.coding-copilot.info
 uid: fda4dd80-1ede-4d88-88cf-800a42ba58af   expiresAt: 1795769187534

两个问题一次暴露:

  1. uid 完全相同——手机上扫码时默认走的还是老账号,压根没换成;
  1. 就算换成了,新凭据落在 admin 家目录(时间戳 16:46,有效期到 11-27),而 服务读的是 root 那份(有效期 11-11)。两份文件互不相干,登录做得再干净,转换器也看不见。

这类"凭据错配"在 systemd 化的工具链里非常常见:安装/登录用一个用户,服务用另一个用户,报错信息里又只说 403,不告诉你它读的是哪个文件。所以排障一定要看服务自己报的 auth_file,而不是看自己操作时的那个目录。

真要恢复,只有三条路

  • 对齐身份:把 codebuddy-converter.service 的 User= 改成实际登录的用户,或给它显式设置 Environment=XDG_DATA_HOME=...,让它去读有有效凭据的那份文件;
  • 换对账号:重新登录时确认 /health 里的 uid 真的变了——没变就是白扫;被限制的账号只能申诉(actions 里给了 SUBMIT_FEEDBACK);
  • 换通道:给 dashscope 充值,或者等 bailian 的月度 token 包 11-10 重置,再把 agents.defaults.model.primary 指回能用的那条。

顺带一个坑:定时任务是跟着主模型一起死的

openclaw cron show <id> 里这个发文任务的 model: -,意思是不指定模型、跟随 agents.defaults.model.primary。所以这几天为了排障反复切主模型,每次都会连带影响所有 cron 任务:切到欠费的通道,发文任务第二天就 error;切到被封的通道,它拿到 403。

如果某个定时任务对模型不敏感(比如只做格式转换、只做补跑),给它单独钉一个便宜且稳定可用的模型,别让它跟全站主模型一起翻车。这也是 03:00 发文闸旁边又挂了一个补跑闸的原因——scripts/ensure_daily_post.py 每小时检查一次"今天文章到底有没有上线",发现发文任务 error 就自动补跑一次,免得静默断更。

小结

模型不可用时,别顺着包装后的报错猜。四步定位:

openclaw cron list                          # 1. 哪些任务在错、错几次
curl -s http://127.0.0.1:8787/health        # 2. 服务活不活、读哪份凭据、是哪个 uid
ps -o user= -p <pid>                        # 3. 进程属主 → 决定凭据家目录
curl -s .../v1/chat/completions -d '...'    # 4. 最小 completion,拿真实 code 而不是包装错误

今天这台机器的结论是:403 不是转换器坏了,也不是 token 过期,而是账号被封 + 凭据文件被两个用户各读各的。前者要么申诉要么换账号,后者改 User= 或 XDG_DATA_HOME 就能对齐。站点那边先手工补上当天的文章,发文产线等通道恢复后自然接上。


扫码关注公众号,获取更多 OpenClaw 实操技巧:

公众号二维码