返回 Skill 列表
extension
分类: 其它需要 API Key

qclaw-custom-model

qclaw-custom-model 技能描述/用途: 帮用户在 QClaw 中快速、安全地配置自定义大模型(硅基流动、DeepSeek、OpenAI 等),支持切换默认模型、多模型路由、故障自动切换。 核心流程(7步): 环境检测 — 读取当前模型配置状态 收集信息 — 问用户模型名称、Key、地址 备份 & 测试 — 备份配置,测试模型连通性 多模态检测 — 检查模型是否支持看图 方案确认 — 完整方案给用户确认(门控,必须同意才执行) 执行配置 — 通过 Gateway API 安全修改配置 验证 & 汇报 — 重启后验证,成功/失败都有回滚方案 安全机制: 先备份再动手 先测试再配置 用户确认门控 失败可回滚 配套脚本: detect_env.py — 环境检测 backup_config.py — 配置备份 test_connectivity.py — 连通性测试 test_multimodal.py — 多模态检测 apply_config.py — 写入配置 restore_backup.py — 恢复备份 关键约束: 禁止直接编辑 openclaw.json,必须通过 Gateway API (config.patch) 需要 baseHash 乐观锁 Windows 用 openclaw.cmd 建议用 Python subprocess 调用,避免 PowerShell 引号问题

person作者: user_5c543922hubcommunity

QClaw 自定义模型配置

核心原则

  1. 先备份再动手 — 改配置前必须备份
  2. 先测试再配置 — 不直接改配置,先用用户信息测试模型能不能通
  3. 用户确认门控 — 完整方案必须经用户确认后才执行
  4. 失败可回滚 — 任何步骤出问题,主动提供恢复备份的选项

执行流程(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 基本模型信息(必问)

问法示例:

「告诉我你的模型信息就行,就这几项:

  1. 你的模型叫什么名字?(比如 DeepSeek-V3、GLM-4)
  2. 你的密钥(Key)是什么?一般是一串英文字母和数字
  3. 你的模型地址(网址)是什么?一般像 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. 默认模型检查:重新运行第 1 步的 detect_env 代码,确认 primary model 已切换
  2. 文本对话检查:用新配置的模型发一条测试消息验证
  3. 多模态检查:如果配置了图片方案,发一张图验证系统能正确处理
  4. 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 当前只支持 primaryfallbacks 字段。image 字段可写入但不确定是否生效。已知不支持的字段会被 Gateway 拒绝。

4. Windows 上 openclaw 命令

Windows 上命令是 openclaw.cmd,不是 openclaw。Python subprocess 必须用 openclaw.cmd

5. PowerShell 引号问题

PowerShell 对嵌套 JSON 引号处理极差。强烈建议用 Python subprocess 调用 gateway 命令,而不是在 PowerShell 中直接传 JSON。