QClaw 自定义模型配置
核心原则
- 先备份再动手 — 改配置前必须备份
- 先测试再配置 — 不直接改配置,先用用户信息测试模型能不能通
- 用户确认门控 — 完整方案必须经用户确认后才执行
- 失败可回滚 — 任何步骤出问题,主动提供恢复备份的选项
执行流程(7 步)
第 1 步:环境检测
使用 exec 工具执行以下 Python 代码(内联脚本,无需外部文件):
import json, os, sys
from pathlib import Path
home = os.environ.get("USERPROFILE", os.path.expanduser("~"))
config_path = Path(home) / ".qclaw" / "openclaw.json"
if not config_path.exists():
print(json.dumps({"status": "error", "message": "Config file not found"}))
sys.exit(1)
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
result = {
"status": "ok",
"config_path": str(config_path),
"models_mode": config.get("models", {}).get("mode", "unknown"),
"providers": [],
"default_model": None,
"default_timeout": None,
"multimodal_providers": [],
}
agents = config.get("agents", {})
defaults = agents.get("defaults", {})
model_cfg = defaults.get("model", {})
result["default_model"] = model_cfg.get("primary")
result["default_timeout"] = defaults.get("timeoutSeconds")
result["has_fallbacks"] = "fallback" in model_cfg
models = config.get("models", {})
providers = models.get("providers", {})
for provider_id, provider in providers.items():
provider_info = {
"id": provider_id,
"base_url": provider.get("baseUrl", ""),
"api": provider.get("api", "openai-completions"),
"model_count": len(provider.get("models", [])),
"models": [],
}
for model in provider.get("models", []):
model_spec = {
"id": model.get("id", ""),
"name": model.get("name", ""),
"full_ref": f"{provider_id}/{model.get('id', '')}",
"input_types": model.get("input", ["text"]),
"supports_images": "image" in model.get("input", ["text"]),
}
provider_info["models"].append(model_spec)
if "image" in model.get("input", []):
result["multimodal_providers"].append(model_spec["full_ref"])
result["providers"].append(provider_info)
print(json.dumps(result, ensure_ascii=False, indent=2))
提取信息:
- 当前默认模型是什么
- 已配置了哪些服务商和模型
- 哪些模型支持看图(多模态)
- 当前超时设置
- 是否已有 fallback 配置
给用户的输出: 用通俗语言总结当前状态。例如:「你目前使用的是 Kimi-K2.6 模型,通过 QClaw 官方服务访问。你有 2 个自定义服务商,超时是 72000 秒。」
第 2 步:收集用户信息
用大白话询问,不要用专业术语。按以下顺序逐一询问:
2.1 基本模型信息(必问)
问法示例:
「告诉我你的模型信息就行,就这几项:
- 你的模型叫什么名字?(比如 DeepSeek-V3、GLM-4)
- 你的密钥(Key)是什么?一般是一串英文字母和数字
- 你的模型地址(网址)是什么?一般像 https://api.xxx.com 这样」
2.2 多模型路由(引导式询问)
如果用户只提供一个模型,跳到 2.3。
如果用户提到多个模型,或者你发现可能需要多个模型,这样引导:
「不同的问题可能需要不同的模型来处理。我帮你理一下:
- 日常聊天、快问快答,用哪个?
- 需要思考的复杂问题(写代码、分析),用哪个?
- 处理图片的,用哪个?
- 如果某个模型挂了,自动切到哪个?」
用表格帮用户理清:
| 场景 | 推荐模型 | 用户选择 | |------|---------|---------| | 日常聊天 | (用户模型) | | | 复杂推理 | (用户模型或更强的) | | | 看图/图片处理 | (支持多模态的模型) | | | 备用兜底 | (原默认模型) | |
2.3 超时和重试(主动建议)
检测完后,如果当前超时设置不是特别长(<120 秒),主动提示:
「提醒一下:很多模型平台在高峰期会算力紧张,导致响应慢甚至超时。建议一起调整这些设置,避免以后出问题。你看需要吗?」
建议值:
| 设置 | 建议值 | 原值 | |------|--------|------| | 请求超时 | 180 秒(3 分钟) | (当前值) | | Fallback 触发 | 超时也触发(不只是报错) | - |
只需询问用户是否需要调整,不要强推。用户说"不用"就跳过。
第 3 步:备份 & 连通测试
3.1 备份当前配置
使用 exec 执行以下 Python 代码:
import json, shutil, os, sys
from pathlib import Path
from datetime import datetime
home = os.environ.get("USERPROFILE", os.path.expanduser("~"))
config_path = Path(home) / ".qclaw" / "openclaw.json"
backup_dir = Path(home) / ".qclaw" / "config_backups"
backup_dir.mkdir(parents=True, exist_ok=True)
if not config_path.exists():
print(json.dumps({"status": "error", "message": "Config file not found"}))
sys.exit(1)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = backup_dir / f"openclaw_{timestamp}.json"
shutil.copy2(config_path, backup_path)
# Cleanup old backups, keep 3 most recent
backups = sorted([p for p in backup_dir.glob("openclaw_*.json")], key=lambda p: p.stat().st_mtime, reverse=True)
for old in backups[3:]:
old.unlink()
print(json.dumps({"status": "ok", "backup": str(backup_path), "timestamp": timestamp}, ensure_ascii=False))
确认备份成功,告知用户:「已备份当前配置,备份保存在 XXX 路径,如有问题可以恢复。」
3.2 测试每个用户提供的模型
使用 exec 执行以下 Python 代码(替换占位符为实际值):
import json, sys, urllib.request, urllib.error, ssl
base_url = "{用户提供的地址}"
api_key = "{用户提供的Key}"
model_id = "{用户提供的模型名}"
timeout = 30
url = base_url.rstrip("/")
if url.endswith("/v1"):
url = url.rstrip("/v1").rstrip("/")
if url.endswith("/chat/completions"):
full_url = url
else:
full_url = url.rstrip("/") + "/v1/chat/completions"
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
payload = json.dumps({"model": model_id, "messages": [{"role": "user", "content": "Hi. Please reply with exactly: OK"}], "max_tokens": 10, "temperature": 0}).encode("utf-8")
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
try:
req = urllib.request.Request(full_url, data=payload, headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
body = json.loads(resp.read().decode("utf-8"))
if "choices" in body and len(body["choices"]) > 0:
content = body["choices"][0].get("message", {}).get("content", "")
result = {"status": "pass", "url_tested": full_url, "model": model_id, "response": content.strip()}
else:
result = {"status": "fail", "url_tested": full_url, "model": model_id, "error": "No valid choices in response"}
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8", errors="replace")[:500] if e.fp else ""
hints = {401: "Key/API Key is wrong or expired.", 403: "Access denied.", 404: "Model name not found.", 429: "Too many requests or usage limit reached.", 500: "Server error on provider side.", 502: "Bad gateway.", 503: "Service unavailable."}
hint = hints.get(e.code, "")
if "quota" in error_body.lower(): hint += " You may have run out of credits."
result = {"status": "fail", "url_tested": full_url, "model": model_id, "error": f"HTTP {e.code}: {e.reason}", "hint": hint}
except Exception as e:
result = {"status": "fail", "url_tested": full_url, "model": model_id, "error": str(e)}
print(json.dumps(result, ensure_ascii=False, indent=2))
如果失败:
- 把错误信息翻译成大白话告诉用户
- 给出可能的原因和解决建议(Key 错了、地址错了、额度用完等)
- 问用户是否要修正信息重试
如果成功:
- 告知用户:「你的模型连通正常,响应速度约 X 秒」
如果用户提供多个模型: 逐个测试。
额外测试: 也测一下当前默认模型(作为 fallback 候选)是否还正常。
第 4 步:多模态(看图能力)检测
对每个用户提供的模型,使用 exec 执行以下 Python 代码:
import json, urllib.request, urllib.error, ssl, base64
base_url = "{地址}"
api_key = "{Key}"
model_id = "{模型名}"
timeout = 60
TINY_PNG_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
url = base_url.rstrip("/")
if url.endswith("/v1"): url = url.rstrip("/v1").rstrip("/")
if url.endswith("/chat/completions"): full_url = url
else: full_url = url.rstrip("/") + "/v1/chat/completions"
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
payload = json.dumps({"model": model_id, "messages": [{"role": "user", "content": [{"type": "text", "text": "This is a test image. Please reply with exactly: IMAGE_OK"}, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{TINY_PNG_B64}", "detail": "low"}}]}], "max_tokens": 20, "temperature": 0}).encode("utf-8")
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
try:
req = urllib.request.Request(full_url, data=payload, headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
body = json.loads(resp.read().decode("utf-8"))
if "choices" in body and len(body["choices"]) > 0:
result = {"status": "pass", "supports_images": True, "response": body["choices"][0].get("message", {}).get("content", "").strip()}
else:
result = {"status": "fail", "supports_images": False, "error": "No valid response"}
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8", errors="replace")[:500] if e.fp else ""
no_mm_markers = ["does not support images", "does not support vision", "multimodal", "image is not supported", "not_implemented", "unsupported"]
likely_no_mm = any(m in error_body.lower() for m in no_mm_markers)
result = {"status": "fail", "supports_images": False if likely_no_mm else None, "error": f"HTTP {e.code}: {e.reason}", "certain": likely_no_mm}
except Exception as e:
result = {"status": "fail", "supports_images": None, "error": str(e)}
print(json.dumps(result, ensure_ascii=False, indent=2))
三种结果及处理:
A. 模型支持看图(status=pass)
「好消息!你的模型支持看图,配置后可以直接用。」
B. 模型不支持看图(status=fail + certain=true)
「你的模型只能处理文字,不能看图。这是很多模型的正常情况。我可以这样安排:
- 方案A:日常全部用你的模型(文字),遇到图片时自动切换到能看图的模型(推荐)
- 方案B:不管你需不需要看图,只用你的模型 你选哪个?」
如果用户选方案A,询问使用哪个支持多模态的模型。从第 1 步检测结果中列出可选的多模态模型。
C. 无法确定(status=fail + certain=false 或 supports_images=None)
「我不确定你的模型能不能看图。你可以帮确认一下吗?
- 去你的模型平台管理页面,看有没有标注"支持视觉/多模态/Vision"
- 或者你有其他方式确认吗? 如果确认不支持,我们按之前的方案A/B来。」
第 5 步:方案确认(门控!必须等用户同意)
总结完整方案,用大白话呈现给用户:
═══════════════════════════════════
📋 配置方案确认
═══════════════════════════════════
🆕 新增模型:
- 名称:xxx
- 地址:xxx
- 主用:日常聊天 / 复杂推理
🖼️ 看图能力:
- 你的模型:(支持/不支持)
- 图片处理:(由 xxx 模型负责 / 不使用)
🔄 备用方案:
- 如果主力挂了:自动切到 xxx
⏱️ 超时设置:
- 从 xx 秒 → 180 秒
═══════════════════════════════════
然后说:
「以上是完整的配置方案。确认没问题我就开始配置。你只需要回『没问题』或『开始』就行。想改什么也直接说。」
⚠️ 此步是门控!用户未确认前不得进入第 6 步。
第 6 步:执行配置
用户确认后,使用 exec 执行以下 Python 代码(替换所有占位符为实际值):
import json, subprocess, os
from datetime import datetime
def gateway_config_get():
cmd = ["openclaw.cmd" if os.name == "nt" else "openclaw", "gateway", "call", "config.get", "--params", "{}", "--json"]
try:
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", timeout=30)
if result.returncode != 0: return None
lines = result.stdout.strip().split("\n")
json_start = next((i for i, l in enumerate(lines) if l.strip().startswith("{")), 0)
return json.loads("\n".join(lines[json_start:]))
except Exception: return None
def gateway_config_patch(patch_dict):
config_data = gateway_config_get()
if not config_data: return {"status": "error", "message": "Failed to get current config hash"}
base_hash = config_data.get("hash", "")
raw_json = json.dumps(patch_dict, ensure_ascii=False)
params = json.dumps({"raw": raw_json, "baseHash": base_hash})
cmd = ["openclaw.cmd" if os.name == "nt" else "openclaw", "gateway", "call", "config.patch", "--params", params, "--json"]
try:
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", timeout=30)
if result.returncode == 0:
lines = result.stdout.strip().split("\n")
json_start = next((i for i, l in enumerate(lines) if l.strip().startswith("{")), 0)
return {"status": "ok", "output": json.dumps(json.loads("\n".join(lines[json_start:])), ensure_ascii=False)[:500]}
else:
return {"status": "error", "message": result.stderr or result.stdout}
except Exception as e:
return {"status": "error", "message": str(e)}
# 构建 patch
provider_id = f"custom-{int(datetime.now().timestamp() * 1000)}"
patch = {
"models": {
"providers": {
provider_id: {
"baseUrl": "{用户提供的地址}",
"apiKey": "{用户提供的Key}",
"api": "openai-completions",
"models": [{"id": "{模型ID}", "name": "{显示名称}", "input": ["text"]}]
}
}
},
"agents": {
"defaults": {
"model": {
"primary": f"{provider_id}/{模型ID}",
"fallbacks": ["qclaw/pool-deepseek-v4-pro"]
},
"timeoutSeconds": 180
}
}
}
# 如需图片处理,添加 image 字段(注意:可能不被 schema 支持)
# patch["agents"]["defaults"]["model"]["image"] = "qclaw/pool-deepseek-v4-pro"
result = gateway_config_patch(patch)
print(json.dumps(result, ensure_ascii=False, indent=2))
参数说明:
input:["text"]— 仅文字,["text","image"]— 支持文字+图片primary: 默认主力模型(格式:provider_id/model_id)fallbacks: 备用模型列表timeoutSeconds: 请求超时
多模型场景:
- 每个模型分别调用一次 patch 添加 provider
- 最后汇总调用一次设置 primary、fallbacks 等
配置后重启 & 验证
修改配置后,告知用户需要重启 QClaw 网关使配置生效。尝试:
openclaw gateway restart
如果无法自动重启(需要用户权限),告知用户:「配置已保存,请手动重启 QClaw,然后跟我说一声,我帮你验证。」
重启后,逐项验证:
- 默认模型检查:重新运行第 1 步的 detect_env 代码,确认 primary model 已切换
- 文本对话检查:用新配置的模型发一条测试消息验证
- 多模态检查:如果配置了图片方案,发一张图验证系统能正确处理
- Fallback 检查:确认 fallback 模型仍然可连通
如果任一项失败,描述问题和可能原因,提供解决思路。让用户决定:尝试修复 or 恢复备份。
第 7 步:最终汇报
全部成功
✅ 配置完成!
你的 QClaw 现在使用:
🧠 主力模型:xxx
🖼️ 图片处理:xxx
🔄 备用模型:xxx(故障自动切换)
⏱️ 超时设置:xxx 秒
一切正常,可以开始使用了。
部分问题
描述问题,列出解决思路(最多 2-3 个选项),让用户选择。
无法解决
主动建议恢复:
「看起来这个问题暂时解决不了。要恢复之前的配置吗?恢复后我会验证一下之前的配置是否正常工作。」
用户确认后,使用 exec 执行以下 Python 代码:
import json, shutil, os
from pathlib import Path
home = os.environ.get("USERPROFILE", os.path.expanduser("~"))
config_path = Path(home) / ".qclaw" / "openclaw.json"
backup_dir = Path(home) / ".qclaw" / "config_backups"
# Find latest backup
backups = sorted([p for p in backup_dir.glob("openclaw_*.json")], key=lambda p: p.stat().st_mtime, reverse=True)
if not backups:
print(json.dumps({"status": "error", "message": "No backups found"}))
else:
backup_file = backups[0]
shutil.copy2(backup_file, config_path)
print(json.dumps({"status": "ok", "restored_from": str(backup_file)}, ensure_ascii=False))
恢复后:
- 验证配置已恢复(重新运行 detect_env)
- 测试连通性确认恢复后的模型正常
- 告知用户:「已恢复之前的配置,现在和修改前一样了。备份文件保留在 xxx,你可以随时再试。」
注意事项
- 所有内联脚本输出都是 JSON,方便程序解析
- emoji 输出需要设置
$env:PYTHONIOENCODING='utf-8'(仅在 PowerShell 中需要) - 备份目录:
~/.qclaw/config_backups/ - 配置文件:
~/.qclaw/openclaw.json - API 格式优先支持 OpenAI 兼容接口(
openai-completions) - Model ref 格式:
{provider_id}/{model_id}(如qclaw/pool-kimi-k2.6)
⚠️ 关键约束(实测发现)
1. 必须通过 Gateway API 修改配置
禁止直接编辑 openclaw.json 文件! QClaw 主应用有配置保护机制,会自动恢复被直接修改的文件。
正确方式:通过 openclaw gateway call config.patch API 修改,它会安全地写入配置并通知 QClaw 热重载。
2. config.patch 需要 baseHash
每次调用 config.patch 必须提供当前配置的 baseHash(从 config.get 获取),否则会报错:config base hash required。这是乐观锁机制,防止并发修改冲突。
3. schema 限制
agents.defaults.model 当前只支持 primary 和 fallbacks 字段。image 字段可写入但不确定是否生效。已知不支持的字段会被 Gateway 拒绝。
4. Windows 上 openclaw 命令
Windows 上命令是 openclaw.cmd,不是 openclaw。Python subprocess 必须用 openclaw.cmd。
5. PowerShell 引号问题
PowerShell 对嵌套 JSON 引号处理极差。强烈建议用 Python subprocess 调用 gateway 命令,而不是在 PowerShell 中直接传 JSON。
微信扫一扫