From 61d99cf6e57675303ba4686daf2beba7357ca242 Mon Sep 17 00:00:00 2001 From: XiaoHuo888-hue Date: Sat, 22 Aug 2026 16:13:48 +0000 Subject: [PATCH] feat(config): add OrcaRouter gateway template as alternative LLM provider Add config-templates/openclaw-orcarouter.json mirroring the existing openclaw-awk.json structure: an OpenAI-compatible provider pointing at https://api.orcarouter.ai/v1 with models orcarouter/auto, fusion, fusion-mini, fusion-flash, free, open-code and code-review, using the openai-completions API adapter and ORCAROUTER_API_KEY env var. Also: - docker-entrypoint.sh: make API-key rendering provider-aware so each configured provider gets its own key (AWK_API_KEY for bailian-token-plan/awk, ORCAROUTER_API_KEY for orcarouter), and validate the required key based on which provider the active openclaw.json actually uses. - docker-compose.yml / docker/.env.example: pass through ORCAROUTER_API_KEY. - README / config README / env templates: document the OrcaRouter alternative template. Co-Authored-By: Claude Signed-off-by: XiaoHuo888-hue --- README.md | 5 +- config-templates/openclaw-orcarouter.json | 449 ++++++++++++++++++++++ config/.env.template | 4 + config/README.md | 1 + docker/.env.example | 4 + docker/README.md | 7 + docker/docker-compose.yml | 2 + docker/docker-entrypoint.sh | 60 ++- 8 files changed, 515 insertions(+), 17 deletions(-) create mode 100644 config-templates/openclaw-orcarouter.json diff --git a/README.md b/README.md index 084c16c9..a14b3d9b 100755 --- a/README.md +++ b/README.md @@ -172,6 +172,8 @@ irm https://raw.atomgit.com/wiseflow/xiaobei/raw/master/scripts/install-atomgit. > - **主力模型(强烈推荐)**:[阿里云百炼「Token Plan」套餐](https://www.aliyun.com/benefit/ai/aistar?clubBiz=subTask..12766005..10274..) — 一个套餐覆盖 DeepSeek-V4-Flash、GLM-5.2、Qwen3.6-Flash 等主流模型,**无月限额、不限购**。前期熟悉安装可选 Lite 版 39 元/月,正常使用建议 Standard 版 139 元/月。开通后获得 `AWK_API_KEY`,xiaobei 默认主力模型 DeepSeek-V4-Flash 即走此通道。 > > - **仍想用火山方舟 Coding Plan 的用户**:在默认配置模板基础上参考 [openclaw-awk.json](config-templates/openclaw-awk.json),手动替换 `provider` 和 `agents.default` 字段即可。 +> +> - **想用 [OrcaRouter](https://www.orcarouter.ai) 的用户**:OrcaRouter 是 OpenAI 兼容的模型网关,一份 key 即可路由 DeepSeek、GLM、Qwen、Claude、Gemini 等主流模型。它还在同一端点运行网关级、零信任的 AI Agent 安全防护——以默认拒绝(default-deny)的方式审查每条 prompt/response 并约束每个工具调用,无需改动应用代码。开通后获得 `ORCAROUTER_API_KEY`(`sk-orca-...`),参考 [openclaw-orcarouter.json](config-templates/openclaw-orcarouter.json) 替换 `provider` 和 `agents.default` 字段即可(或用它替换默认模板)。 > **🎬 视频生成模型配置** > @@ -303,7 +305,8 @@ wiseflow/ │ └── overrides.sh # pnpm 依赖覆盖(如替换 playwright → patchright) ├── config-templates/ # 配置模板(开箱即用的最佳实践) │ ├── openclaw.json # 默认配置模板(阿里云百炼主力 + fts-only 记忆 + dream 关) -│ └── openclaw-awk.json # 火山引擎方舟 Coding Plan 备选模板(主力 GLM-5.2) +│ ├── openclaw-awk.json # 火山引擎方舟 Coding Plan 备选模板(主力 GLM-5.2) +│ └── openclaw-orcarouter.json # OrcaRouter 网关备选模板(主力 orcarouter/auto) ├── scripts/ # 工具脚本(详见 scripts/README.md) │ ├── lib/ # 脚本共享工具(agent-skills.sh 等) │ ├── install.sh # 一键安装 + 升级(预构建 tarball 路线,macOS + Linux,GitHub 线路;重跑即升级,保留 ~/.openclaw) diff --git a/config-templates/openclaw-orcarouter.json b/config-templates/openclaw-orcarouter.json new file mode 100644 index 00000000..4325d3ed --- /dev/null +++ b/config-templates/openclaw-orcarouter.json @@ -0,0 +1,449 @@ +{ + "browser": { + "enabled": true, + "headless": true, + "ssrfPolicy": { + "dangerouslyAllowPrivateNetwork": true + } + }, + "models": { + "mode": "merge", + "providers": { + "orcarouter": { + "baseUrl": "https://api.orcarouter.ai/v1", + "api": "openai-completions", + "apiKey": "${ORCAROUTER_API_KEY}", + "models": [ + { + "id": "orcarouter/auto", + "name": "OrcaRouter Auto", + "reasoning": true, + "input": [ + "text" + ], + "contextWindow": 1000000, + "maxTokens": 65536, + "compat": { + "supportsReasoningEffort": true, + "supportsUsageInStreaming": true, + "maxTokensField": "max_tokens" + }, + "api": "openai-completions" + }, + { + "id": "orcarouter/fusion", + "name": "OrcaRouter Fusion", + "reasoning": true, + "input": [ + "text" + ], + "contextWindow": 1000000, + "maxTokens": 65536, + "compat": { + "supportsReasoningEffort": true, + "supportsUsageInStreaming": true, + "maxTokensField": "max_tokens" + }, + "api": "openai-completions" + }, + { + "id": "orcarouter/fusion-mini", + "name": "OrcaRouter Fusion Mini", + "reasoning": true, + "input": [ + "text" + ], + "contextWindow": 1000000, + "maxTokens": 65536, + "compat": { + "supportsReasoningEffort": true, + "supportsUsageInStreaming": true, + "maxTokensField": "max_tokens" + }, + "api": "openai-completions" + }, + { + "id": "orcarouter/fusion-flash", + "name": "OrcaRouter Fusion Flash", + "reasoning": true, + "input": [ + "text" + ], + "contextWindow": 200000, + "maxTokens": 65536, + "compat": { + "supportsReasoningEffort": true, + "supportsUsageInStreaming": true, + "maxTokensField": "max_tokens" + }, + "api": "openai-completions" + }, + { + "id": "orcarouter/free", + "name": "OrcaRouter Free", + "reasoning": false, + "input": [ + "text" + ], + "contextWindow": 200000, + "maxTokens": 16384, + "api": "openai-completions" + }, + { + "id": "orcarouter/open-code", + "name": "OrcaRouter Open Code", + "reasoning": true, + "input": [ + "text" + ], + "contextWindow": 200000, + "maxTokens": 65536, + "compat": { + "supportsReasoningEffort": true, + "supportsUsageInStreaming": true, + "maxTokensField": "max_tokens" + }, + "api": "openai-completions" + }, + { + "id": "orcarouter/code-review", + "name": "OrcaRouter Code Review", + "reasoning": true, + "input": [ + "text" + ], + "contextWindow": 200000, + "maxTokens": 65536, + "compat": { + "supportsReasoningEffort": true, + "supportsUsageInStreaming": true, + "maxTokensField": "max_tokens" + }, + "api": "openai-completions" + } + ] + } + } + }, + "agents": { + "defaults": { + "model": { + "primary": "orcarouter/orcarouter/auto", + "fallbacks": [ + "orcarouter/orcarouter/fusion", + "orcarouter/orcarouter/fusion-mini" + ] + }, + "imageModel": { + "primary": "orcarouter/orcarouter/fusion-flash" + }, + "memorySearch": { + "provider": "none" + }, + "models": { + "orcarouter/orcarouter/auto": { + "alias": "orcarouter/auto" + }, + "orcarouter/orcarouter/fusion": { + "alias": "orcarouter/fusion" + }, + "orcarouter/orcarouter/fusion-mini": { + "alias": "orcarouter/fusion-mini" + }, + "orcarouter/orcarouter/fusion-flash": { + "alias": "orcarouter/fusion-flash" + }, + "orcarouter/orcarouter/free": { + "alias": "orcarouter/free" + }, + "orcarouter/orcarouter/open-code": { + "alias": "orcarouter/open-code" + }, + "orcarouter/orcarouter/code-review": { + "alias": "orcarouter/code-review" + } + }, + "compaction": { + "mode": "safeguard" + }, + "thinkingDefault": "medium", + "maxConcurrent": 3, + "subagents": { + "maxConcurrent": 4, + "announceTimeoutMs": 3600000, + "maxSpawnDepth": 2 + } + }, + "list": [ + { + "id": "main", + "default": true, + "name": "小贝", + "workspace": "~/.openclaw/workspace-main", + "skills": [ + "smart-search", + "browser-guide", + "email-ops", + "council", + "session-logs", + "tmux" + ], + "subagents": { + "allowAgents": [ + "it-engineer", + "content-producer" + ] + }, + "tools": { + "exec": { + "host": "gateway", + "security": "full", + "ask": "off" + } + }, + "heartbeat": { + "every": "1d", + "target": "none", + "isolatedSession": true + }, + "reasoningDefault": "off" + }, + { + "id": "it-engineer", + "name": "IT Engineer", + "workspace": "~/.openclaw/workspace-it-engineer", + "skills": [ + "healthcheck", + "nano-pdf", + "skill-creator", + "session-logs", + "tmux", + "weather", + "summarize", + "gifgrep", + "node-connect", + "github", + "gh-issues", + "coding-agent" + ], + "tools": { + "exec": { + "host": "gateway", + "security": "full", + "ask": "off" + } + }, + "thinkingDefault": "high", + "reasoningDefault": "off" + }, + { + "id": "content-producer", + "name": "producer", + "workspace": "~/.openclaw/workspace-content-producer", + "skills": [ + "smart-search", + "browser-guide", + "session-logs", + "tmux" + ], + "subagents": { + "allowAgents": [ + "it-engineer" + ] + }, + "tools": { + "exec": { + "host": "gateway", + "security": "allowlist", + "ask": "off" + } + }, + "reasoningDefault": "off" + } + ] + }, + "bindings": [ + { + "agentId": "main", + "comment": "openclaw-weixin -> Main Agent onboarding entry", + "match": { + "channel": "openclaw-weixin" + } + } + ], + "messages": { + "ackReactionScope": "group-mentions", + "inbound": { + "debounceMs": 1500 + } + }, + "session": { + "dmScope": "per-channel-peer", + "reset": { + "mode": "idle", + "idleMinutes": 2880 + } + }, + "commands": { + "native": "auto", + "nativeSkills": "auto", + "restart": true, + "ownerDisplay": "raw" + }, + "hooks": { + "internal": { + "enabled": true, + "entries": { + "boot-md": { + "enabled": false + }, + "command-logger": { + "enabled": true + }, + "session-memory": { + "enabled": true + } + } + } + }, + "channels": { + "openclaw-weixin": { + "enabled": true + } + }, + "gateway": { + "port": 18789, + "mode": "local", + "bind": "loopback", + "auth": { + "mode": "token", + "token": "" + }, + "tailscale": { + "mode": "off", + "resetOnExit": false + }, + "nodes": { + "denyCommands": [ + "camera.snap", + "camera.clip", + "screen.record", + "calendar.add", + "contacts.add", + "reminders.add" + ] + } + }, + "skills": { + "entries": { + "notion": { + "enabled": false + }, + "obsidian": { + "enabled": false + }, + "trello": { + "enabled": false + }, + "github": { + "enabled": true + }, + "gh-issues": { + "enabled": true + }, + "coding-agent": { + "enabled": true + }, + "slack": { + "enabled": false + }, + "wacli": { + "enabled": false + }, + "gemini": { + "enabled": false + }, + "openai-whisper": { + "enabled": false + }, + "openai-whisper-api": { + "enabled": false + }, + "voice-call": { + "enabled": false + }, + "sherpa-onnx-tts": { + "enabled": false + }, + "spotify-player": { + "enabled": false + }, + "mcporter": { + "enabled": false + }, + "xurl": { + "enabled": false + }, + "clawhub": { + "enabled": false + }, + "discord": { + "enabled": false + }, + "canvas": { + "enabled": false + }, + "taskflow-inbox-triage": { + "enabled": false + }, + "ordercli": { + "enabled": false + } + } + }, + "plugins": { + "load": { + "paths": ["${XIAOBEI_HOME}/awada"] + }, + "entries": { + "awada": { + "enabled": false + }, + "xai": { + "enabled": false + }, + "browser": { + "enabled": true + }, + "phone-control": { + "enabled": false + }, + "codex": { + "enabled": false + }, + "memory-core": { + "enabled": true, + "config": { + "dreaming": { + "enabled": false + } + } + }, + "openclaw-weixin": { + "enabled": true + } + } + }, + "tools": { + "exec": { + "host": "gateway" + }, + "web": { + "fetch": { + "ssrfPolicy": { + "allowRfc2544BenchmarkRange": true + } + } + } + } +} diff --git a/config/.env.template b/config/.env.template index 3c03e713..9a43ac77 100644 --- a/config/.env.template +++ b/config/.env.template @@ -16,6 +16,10 @@ AWK_API_KEY=__FILL_AWK_API_KEY__ # OFB_KEY:产品方发放的中转服务凭证(X-OFB-Key header),用到 relay 中转的技能需要 # OFB_KEY= +# ORCAROUTER_API_KEY:使用 openclaw-orcarouter.json 模板时的 OrcaRouter 网关 key +# (sk-orca-...,https://api.orcarouter.ai/v1)。仅在使用该模板时需要。 +# ORCAROUTER_API_KEY= + # 企业微信凭据(朋友圈 + 微盘共用,实例级一套) # 获取方式见 crews/main/skills/wxwork-moments/REFERENCE.md # WXWORK_CORP_ID= diff --git a/config/README.md b/config/README.md index 31a99abb..5c8dafb8 100644 --- a/config/README.md +++ b/config/README.md @@ -15,6 +15,7 @@ build 期由 Dockerfile 阶段 3(wiseflow-layer)把 `config-templates/opencl |------|------|------| | `daemon.env.template` | daemon 环境变量模板,entrypoint 渲染 | ✅ 占位就位(AWK_API_KEY / OFB_KEY / RELAY_BASE_URL / SMTP_*) | | `workspace-skeleton/` | 通用 workspace 骨架 | ✅ 结构就位,运行期内容不进镜像 | +| `config-templates/openclaw-orcarouter.json` | OrcaRouter 网关备选模板(OpenAI 兼容,`api=openai-completions`) | ✅ 新增(镜像 openclaw-awk.json 结构) | ## openclaw.json 目标态(Phase 7) diff --git a/docker/.env.example b/docker/.env.example index 31f9ef73..2c94c7a6 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -16,6 +16,10 @@ AWK_API_KEY=__FILL_AWK_API_KEY__ # Optional: 火山方舟图像生成专用 key(siliconflow-img-gen 技能用,与 LLM 的 AWK_API_KEY 分离) # AWK_GEN_KEY= +# Optional: OrcaRouter 网关 key(sk-orca-...)。使用 openclaw-orcarouter.json 模板时配置, +# 否则可忽略。 +# ORCAROUTER_API_KEY= + # Optional: override the image tag (defaults to the published image). For local # validation after running ./scripts/build-image.sh: # IMAGE=xiaobei:local diff --git a/docker/README.md b/docker/README.md index ead179af..507b6c75 100644 --- a/docker/README.md +++ b/docker/README.md @@ -3,6 +3,12 @@ > **开箱即用**:镜像内已装好 openclaw 引擎 + 全部 skills/crews + camoufox-cli + Firefox + > openclaw-weixin 插件 + Xvfb/fluxbox/x11vnc/websockify/noVNC 显示栈。 > 用户本地 build 镜像后只需填 `AWK_API_KEY`,`docker compose up -d` 即可启动。 +> +> **备选 LLM 网关**:如需使用 [OrcaRouter](https://www.orcarouter.ai)(OpenAI 兼容, +> 一份 key 路由 DeepSeek/GLM/Qwen/Claude/Gemini 等模型,并在同一端点提供网关级零信任 +> AI Agent 安全防护——默认拒绝审查每条 prompt/response 与工具调用),把 +> `config-templates/openclaw-orcarouter.json` 替换为镜像内的 openclaw.json,并在 +> `.env` 配 `ORCAROUTER_API_KEY`(`sk-orca-...`)即可。 ## 快速开始 @@ -103,6 +109,7 @@ compose override 引入。`docker-compose.yml` 已为这种场景预留接入点 ## 安全边界 - `AWK_API_KEY` 仅从运行环境读取,**不写入镜像层**也不写 `openclaw.json` 的明文。 +- 同理 `ORCAROUTER_API_KEY`(若用 OrcaRouter 模板)也只走环境变量注入。 - 首启会为 gateway 生成随机 `OPENCLAW_GATEWAY_TOKEN`,以 `0600` 写入持久化 `.env`。 - Gateway 和 noVNC 在 Compose 中只映射到 `127.0.0.1`。**不要直接把 6080 暴露到公网**。 - 当前 Camoufox sandbox 需要 `SYS_ADMIN` capability;只运行受信任的官方镜像,并保持 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e07edb0f..e4f8492b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -24,6 +24,8 @@ services: # ${AWK_API_KEY:-__FILL_AWK_API_KEY__} 让 `compose config|ps|down` 在无 .env 时也能 # 成功;entrypoint 在运行时拒绝 placeholder 值。 AWK_API_KEY: ${AWK_API_KEY:-__FILL_AWK_API_KEY__} + # OrcaRouter 网关 key(用 openclaw-orcarouter.json 模板时配置) + ORCAROUTER_API_KEY: ${ORCAROUTER_API_KEY:-__FILL_ORCAROUTER_API_KEY__} # wx-mp-hunter posts-list 需 docker exec 进微信客户端容器读 SQLCipher 消息库。 # WX_BIZ_CONTAINER = 微信客户端容器名(由有权限用户自行 override,不在本公开仓暴露) # WX_BIZ_USER_DIR = 容器内微信用户数据根目录 diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 838df67f..94acd4a8 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -4,7 +4,8 @@ # 职责: # 1. 首启从镜像内 runtime-seed 初始化空的 /root/.openclaw 和 /root/.camoufox-cli 卷 # (已有卷绝不覆盖——升级镜像时登录态和用户配置会保留) -# 2. 加载持久化 .env / daemon.env,把 AWK_API_KEY 渲染进 openclaw.json 的 apiKey 字段 +# 2. 加载持久化 .env / daemon.env,把各 LLM 网关 key 渲染进 openclaw.json 的 apiKey 字段 +# (AWK_API_KEY → bailian-token-plan/awk,ORCAROUTER_API_KEY → orcarouter) # 3. 首启生成随机 OPENCLAW_GATEWAY_TOKEN 写 ~/.openclaw/.env(不进镜像层) # 4. 启动显示栈:Xvfb(虚拟显示,camoufox 有头模式跑这里)+ fluxbox + x11vnc + websockify + noVNC # 用户浏览器开 http://localhost:6080 操作 VNC 桌面,里面能看到 camoufox 浏览器窗口 @@ -103,29 +104,45 @@ start_display_stack() { echo "[xiaobei] noVNC web client: http://localhost:6080/vnc.html" } -# ─── 5. 渲染 AWK_API_KEY 进 openclaw.json ─────────────────────────── -# 每次启动都用当前 AWK_API_KEY 环境变量覆盖 openclaw.json 里的 apiKey 字段。 +# ─── 5. 渲染 LLM 网关 key 进 openclaw.json ───────────────────────── +# 每次启动都用当前环境变量覆盖 openclaw.json 里各 provider 的 apiKey 字段。 # # 历史问题:旧版用 raw.replace(/\$\{AWK_API_KEY\}/g, key) 替换 placeholder, # 但第一次渲染后 placeholder 就没了,第二次启动找不到 placeholder 就跳过写入—— # openclaw.json 里永远是第一次的值,后面改环境变量没用。 # -# 修复:改成 JSON 解析,直接定位 models.providers["bailian-token-plan"].apiKey, -# 每次启动用当前 AWK_API_KEY 覆盖。无论 openclaw.json 里是 ${AWK_API_KEY} -# placeholder 还是已渲染的真值,都能正确更新。 -render_awk_api_key() { +# 修复:改成 JSON 解析,直接定位 models.providers[""].apiKey, +# 每次启动用当前环境变量覆盖。无论 openclaw.json 里是 ${VAR} placeholder 还是 +# 已渲染的真值,都能正确更新。 +# +# 每个 provider 用各自的 key 环境变量: +# bailian-token-plan / awk → AWK_API_KEY +# orcarouter → ORCAROUTER_API_KEY +render_provider_api_keys() { node -e ' const fs = require("fs"); const p = process.argv[1]; - const key = process.env.AWK_API_KEY; - if (!key) { console.error("[xiaobei] AWK_API_KEY missing — cannot render openclaw.json"); process.exit(1); } - const config = JSON.parse(fs.readFileSync(p, "utf8")); const providers = config?.models?.providers || {}; - let updated = false; + // provider 名(归一化小写)→ 该 provider 的 key 环境变量 + const keyEnvByProvider = { + "bailian-token-plan": "AWK_API_KEY", + "awk": "AWK_API_KEY", + "orcarouter": "ORCAROUTER_API_KEY", + }; + + let updated = false; for (const [name, provider] of Object.entries(providers)) { - if (provider && typeof provider.apiKey === "string" && provider.apiKey !== key) { + if (!provider || typeof provider.apiKey !== "string") continue; + const envVar = keyEnvByProvider[name.toLowerCase()]; + if (!envVar) continue; // 未知 provider,不碰 + const key = process.env[envVar]; + if (!key || key.includes("__FILL")) { + console.error(`[xiaobei] ${envVar} missing — skipping provider "${name}"`); + continue; + } + if (provider.apiKey !== key) { provider.apiKey = key; updated = true; } @@ -133,7 +150,7 @@ render_awk_api_key() { if (updated) { fs.writeFileSync(p, JSON.stringify(config, null, 2) + "\n"); - console.log("[xiaobei] AWK_API_KEY rendered into openclaw.json"); + console.log("[xiaobei] provider API keys rendered into openclaw.json"); } ' "$OPENCLAW_HOME/openclaw.json" } @@ -198,12 +215,23 @@ bind_weixin_channel() { bootstrap_runtime_state load_runtime_environment -if [ -z "${AWK_API_KEY:-}" ] || [[ "$AWK_API_KEY" == __FILL_*__ ]]; then - fail "AWK_API_KEY is required; run: AWK_API_KEY= docker compose up -d" +# 根据 openclaw.json 里实际存在的 provider 校验对应 key(用户换了模板则校验对应 key) +missing_provider_keys="" +for provider_env in "bailian-token-plan:AWK_API_KEY" "awk:AWK_API_KEY" "orcarouter:ORCAROUTER_API_KEY"; do + provider="${provider_env%%:*}" + env_var="${provider_env##*:}" + if node -e 'const fs=require("fs");const c=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));const p=c?.models?.providers||{};process.exit(p[process.argv[2]]?0:1)' "$OPENCLAW_HOME/openclaw.json" "$provider"; then + if [ -z "${!env_var:-}" ] || [[ "${!env_var}" == __FILL_*__ ]]; then + missing_provider_keys="${missing_provider_keys} ${env_var}" + fi + fi +done +if [ -n "$missing_provider_keys" ]; then + fail "missing required provider key(s):${missing_provider_keys}; run with the matching env var, e.g. AWK_API_KEY= or ORCAROUTER_API_KEY= docker compose up -d" fi ensure_gateway_token -render_awk_api_key +render_provider_api_keys start_display_stack enable_weixin_channel bind_weixin_channel