1实现原理 · 为什么它能做到
只有一个 240 行的脚本,工作方式就是「读 openclaw.json → 改 JSON → 覆写回原文件」,没有守护进程、没有 API 封装。
def save_config(path: Path, config: dict): with open(path, "w", encoding="utf-8") as f: json.dump(config, f, indent=2, ensure_ascii=False) f.write("\n")
配置发现是「显式 --config 短路,否则把三个候选里所有存在的都列为编辑目标」,并把它们当成必须一起改的镜像。
CONFIG_CANDIDATES = [ Path.home() / ".openclaw" / "openclaw.json", Path.home() / ".kimi" / "kimi-claw" / "openclaw.json", Path.home() / ".kimi_openclaw" / "openclaw.json", ]
「同步镜像」的实现是对每份配置文件各跑一遍同样的改动逻辑,而不是把主配置复制到其余文件。
print(f"Found {len(configs)} config files; will edit and sync all:")
每份被改文件在写之前都会备份到 <配置目录>/config-backups/openclaw-<本地时间戳>.json,且没有任何保留上限或裁剪逻辑。
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") backup_path = backup_dir / f"openclaw-{timestamp}.json" shutil.copy2(path, backup_path)
模型定义缺失时会自动补一条内置定义:KNOWN_MODELS 里硬编码 k3 / k2p6 / kimi-k2.7-code 的 id、contextWindow、maxTokens、input 模态,并把 provider 级 headers 继承进去。
"k3": { "id": "k3", "name": "k3", "reasoning": True, "input": ["text", "image"], "contextWindow": 1048576, "maxTokens": 32768, },
默认模型引用写成 agents.defaults.model.primary(直接下标写,不做形态判断),同时改写 agents.defaults.models 别名映射。
config["agents"]["defaults"]["model"]["primary"] = f"{provider_name}/{model_id}"
改默认模型时会顺手清掉别名映射里所有以「当前 provider/」开头的键,然后只写回新模型的空对象。
stale_keys = [k for k in models_map if k.startswith(f"{provider_name}/")] for k in stale_keys: del models_map[k] models_map[f"{provider_name}/{model_id}"] = {}
改写后还要维护元数据时间戳:meta.lastTouchedAt 写 UTC ISO 时间,meta.lastTouchedVersion 存在则保留、不存在填 "unknown"。
config.setdefault("meta", {})["lastTouchedAt"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
重启只有一条路径:`openclaw gateway restart`,失败只打警告并让用户手动重启,没有 systemd 回退。
["openclaw", "gateway", "restart"],
脚本之外,真正的「技能深度」在文档层的诊断协议:先用 curl 对着 <baseUrl>/v1/messages 做一次真实完成探针,改完再用 openclaw agent 跑一轮验证。
curl -sS -o /tmp/probe.json -w "HTTP %{http_code}\n" \ -X POST "<baseUrl>/v1/messages" \ -H "Authorization: Bearer <apiKey>" \ -H "Content-Type: application/json" \ -H "anthropic-version: 2023-06-01" \
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| cli | openclaw CLI(gateway restart;脚本实际调用) |
| cli | openclaw gateway status(读 Config (service) 判断网关真正读哪份配置) |
| cli | openclaw agent --local --json(Step 4 端到端验证,看 result/fallbackUsed) |
| cli | curl(Step 2 端点探针;由 agent 按文档执行,非脚本调用) |
| cli | python3 -m json.tool(手工编辑后的 JSON 校验建议) |
| api | Anthropic Messages 兼容接口(探针目标:POST <baseUrl>/v1/messages,Bearer + anthropic-version 头) |
| network | Kimi 官方端点(references 里给出的 provider baseUrl 示例) |
| network | 本地 echo 抓包服务器(trap 目录附录的 wire-capture 配方,读真实 x-api-key) |
| package | Python 3 标准库 only(json/os/shutil/subprocess/argparse/pathlib/datetime;无第三方依赖) |
4风险提醒 风险提醒:黄色 · 留意使用
- 改默认模型会连带删除同一 provider 下其它模型的别名 — stale_keys = [k for k in models_map if k.startswith(f"{provider_name}/")] 后全部 del,再只写回新模型一条;用户在 agents.defaults.models 里给其它模型起的中文显示名(如「DeepSeek V4 Pro」式)会被静默清除。可按 config-backups/ 备份回滚,但脚本不会提示发生了删除。
- 没有 dry-run、没有结构门禁,写错即生效 — 对比同仓 openclaw 的 --dry-run + 写前 audit 中止,本脚本直接 load→改→save;默认模型用 config["agents"]["defaults"]["model"]["primary"] 硬下标,配置里 model 若为字符串或 agents.defaults 缺失会抛 TypeError/KeyError——此时若已改过前面的镜像文件,则留下半改状态。
- 多镜像模式无原子性与回滚 — provider 缺失时循环内 sys.exit(1),可能第一个文件已写入而第二个未写;文档未要求改完后逐一核对所有镜像,用户容易以为「一次命令全部生效」。
- 凭证暴露面在文档层 — Step 2 让 key 进 curl 命令行(会留在 shell history/进程列表);附录抓包配方把真实 x-api-key 写进 /tmp/captured.txt,且文档没说改完要删该文件或收紧权限(默认 umask 022 下为 644)。脚本本身不读 env、不外发。
- 文档层的 shell 插值注入面 — curl 命令把配置里的 <baseUrl>/<apiKey>/<model-id> 插进双引号;双引号内 $() 与反引号仍会被 shell 展开。若配置文件被他人污染或模型名来自不可信来源,agent 按文档拼命令时可能执行被注入的内容(脚本自身 argv 固定,无此问题)。
- 备份目录无限增长且时间戳为本地时区 — backup_config 无裁剪(openclaw 的同名函数保留 20 份),每改一次多一个 openclaw-<ts>.json;文件名用 datetime.now()(本地时间),跨时区机器排查「哪份最新」时不如 UTC 直观。
5第二遍独立确认
- [discrepancy] 文档称脚本 'syncs mirror files'(SKILL.md Step 3)是否等于内容同步 — 代码只对每个发现的文件各自施加同一处修改(循环里 load→backup→改 provider/默认模型→save),从不把主配置内容复制到其它文件;因此若两份 openclaw.json 原本就有差异,改完仍然有差异,只是「默认模型」这一处一致了。SKILL.md 的 'syncs mirror files' 与脚本实际的 'edit all of them identically' 不是一回事,属措辞夸大(脚本自己的打印也是 'will edit and sync all')。
- [discrepancy] README 声称 openclaw-model-switch 有 'Model validation against provider's model list' — 脚本里没有校验门禁:模型不在 provider 的 models 里时只做 model_exists → 若内置 KNOWN_MODELS 也没有该 id,仅打印 Warning('Model X is not defined in config and not in built-in known models.')然后照常写入并返回 0。SKILL.md 自身并未这样宣称(它靠 Step 2 探针 + Step 4 验证兜底),所以是 README 层面的描述与代码能力不符。
- [ok] 「Step 2 端点探针 / Step 4 端到端验证」是否由 skill 代码实现 — 复核确认这不是代码能力而是文档纪律:grep 全脚本无任何 HTTP 客户端与 curl 调用,Step 2/Step 4 都是给 agent 的操作指令(curl 与 openclaw agent 两条命令)。本报告已在 how_it_works 末条、capabilities 与 external_deps 中统一标注为「文档指示、非脚本执行」,避免把文档承诺记成代码能力。
- [ok] 脚本是否发网络请求 / 读 env 里的 API key(用户指定的两个重点) — 反向扫描 scripts/ 零命中:无 urllib/requests/socket/httpx/curl/wget;无 os.environ/getenv/apiKey 读取。附带发现 import os 在该文件中完全未被使用(无 os.* 调用),说明连标准库层面的环境访问都没接上——凭证交接 100% 发生在文档层(curl 头)而非代码层。
- [ok] 备份是否真的先于写入、且每份被改文件都有备份 — 循环体内顺序为 load_config → 解析 provider → backup_config(打印 '[path] backed up to: …')→ add_model_definition → update_default_model → 写 meta → save_config,备份严格前置于任何 save_config;显式 --config 时不走 discover_configs 的多文件分支,只备份那一个文件,与文档一致。
- [ok] 别名清理的破坏范围(第一遍记录「删除 stale keys」,第二遍量化影响) — 确认删除条件是 k.startswith(f"{provider_name}/"),粒度是整个 provider 前缀而非「旧模型」——同一 provider 下所有其它模型的别名映射条目都会被 del,随后只补回新模型的一条空对象(models_map[f"{provider_name}/{model_id}"] = {});若属性顺序上是先删后写,用户其它模型的显示名别名确实会永久丢失(备份可回滚)。已升级为 verdict.risks 首位。
- [ok] 多文件模式下的失败原子性 — provider 缺失时在循环体内直接 sys.exit(1),若第一个候选文件已成功写入而第二个文件里没有该 provider,进程会在中途退出:出现「有的文件改了、有的没改」的半改状态,且没有回滚逻辑(文档也未提示需人工核对全部镜像)。
- [ok] SKILL.md 的故障知识是否能在 references 里落地(声明 vs 资产) — SKILL.md 表格里的 5 类症状与 references/troubleshooting-model-config.md 的 5 条 trap 一一对应(含 env KIMI_API_KEY 劫持、插件 binary thinking、canonicalModelId 解等级、relay 分组 503、改错文件),kimi-models.md 也确实给出 k3/k2p6/kimi-k2.7-code 的规格与配置片段;文档承诺的知识资产存在且自洽,无空头引用。
6结论
c53908d204b24a25…d5c4678cb5