Spaces:
Running
Running
Merge pull request #11 from anurag162008/main
Browse filesfeat: add multi-provider API key rotation and update docs
- .env.example +86 -1
- .gitignore +44 -0
- Dockerfile +2 -0
- iframe-fix.cjs +4 -0
- multi-provider-key-rotator.cjs +318 -0
- openclaw-sync.py +44 -9
- start.sh +89 -4
.env.example
CHANGED
|
@@ -5,7 +5,7 @@
|
|
| 5 |
# For local development: cp .env.example .env && nano .env
|
| 6 |
|
| 7 |
# ββ REQUIRED: Core Configuration ββ
|
| 8 |
-
# [REQUIRED] LLM provider API key
|
| 9 |
# - Anthropic: sk-ant-v0-...
|
| 10 |
# - OpenAI: sk-...
|
| 11 |
# - Google: AIzaSy...
|
|
@@ -124,6 +124,91 @@ LLM_API_KEY=your_api_key_here
|
|
| 124 |
# Or any other OpenClaw-supported provider (format: provider/model-name)
|
| 125 |
LLM_MODEL=anthropic/claude-sonnet-4-5
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
# Optional: custom OpenAI-compatible provider
|
| 128 |
# Only use this if you want to register your own endpoint at startup.
|
| 129 |
# Leave all of these empty unless you need a custom provider.
|
|
|
|
| 5 |
# For local development: cp .env.example .env && nano .env
|
| 6 |
|
| 7 |
# ββ REQUIRED: Core Configuration ββ
|
| 8 |
+
# [REQUIRED] Primary LLM provider API key (for the provider in LLM_MODEL)
|
| 9 |
# - Anthropic: sk-ant-v0-...
|
| 10 |
# - OpenAI: sk-...
|
| 11 |
# - Google: AIzaSy...
|
|
|
|
| 124 |
# Or any other OpenClaw-supported provider (format: provider/model-name)
|
| 125 |
LLM_MODEL=anthropic/claude-sonnet-4-5
|
| 126 |
|
| 127 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 128 |
+
# π API KEY ROTATION (per-provider key pools)
|
| 129 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 130 |
+
# Every provider supports a comma-separated key pool.
|
| 131 |
+
# Keys are rotated round-robin on every request β useful when you
|
| 132 |
+
# have multiple accounts or want to spread rate-limit quota.
|
| 133 |
+
#
|
| 134 |
+
# Pattern: <PROVIDER>_API_KEYS=key1,key2,key3
|
| 135 |
+
# Fallback order: plural pool β singular key β LLM_API_KEY
|
| 136 |
+
#
|
| 137 |
+
# Uncomment and fill in only the providers you use:
|
| 138 |
+
#
|
| 139 |
+
# ANTHROPIC_API_KEYS=sk-ant-key1,sk-ant-key2,sk-ant-key3
|
| 140 |
+
# OPENAI_API_KEYS=sk-key1,sk-key2,sk-key3
|
| 141 |
+
# GEMINI_API_KEYS=AIzaSy-key1,AIzaSy-key2
|
| 142 |
+
# DEEPSEEK_API_KEYS=key1,key2
|
| 143 |
+
# OPENROUTER_API_KEYS=sk-or-key1,sk-or-key2
|
| 144 |
+
# KILOCODE_API_KEYS=key1,key2
|
| 145 |
+
# OPENCODE_API_KEYS=key1,key2
|
| 146 |
+
# ZAI_API_KEYS=key1,key2
|
| 147 |
+
# MOONSHOT_API_KEYS=key1,key2
|
| 148 |
+
# MINIMAX_API_KEYS=key1,key2
|
| 149 |
+
# XIAOMI_API_KEYS=key1,key2
|
| 150 |
+
# VOLCANO_ENGINE_API_KEYS=key1,key2
|
| 151 |
+
# BYTEPLUS_API_KEYS=key1,key2
|
| 152 |
+
# MISTRAL_API_KEYS=key1,key2
|
| 153 |
+
# XAI_API_KEYS=key1,key2
|
| 154 |
+
# NVIDIA_API_KEYS=key1,key2,key3
|
| 155 |
+
# GROQ_API_KEYS=gsk-key1,gsk-key2,gsk-key3
|
| 156 |
+
# COHERE_API_KEYS=key1,key2
|
| 157 |
+
# TOGETHER_API_KEYS=key1,key2
|
| 158 |
+
# CEREBRAS_API_KEYS=key1,key2
|
| 159 |
+
# HUGGINGFACE_HUB_TOKENS=hf_key1,hf_key2
|
| 160 |
+
|
| 161 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 162 |
+
# π MULTI-PROVIDER SETUP (use multiple models simultaneously)
|
| 163 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 164 |
+
# LLM_MODEL sets your DEFAULT model. But you can activate multiple
|
| 165 |
+
# providers at once β OpenClaw auto-discovers any provider whose
|
| 166 |
+
# API key is present in the environment.
|
| 167 |
+
#
|
| 168 |
+
# HOW IT WORKS:
|
| 169 |
+
# 1. LLM_MODEL + LLM_API_KEY β sets your default model & exports
|
| 170 |
+
# the matching provider key automatically (e.g. ANTHROPIC_API_KEY)
|
| 171 |
+
# 2. Any additional provider key you set directly (e.g. OPENAI_API_KEY)
|
| 172 |
+
# is also picked up by OpenClaw β that provider's models become
|
| 173 |
+
# available in the Control UI for manual selection.
|
| 174 |
+
# 3. Rotation pools (*_API_KEYS) work for every active provider
|
| 175 |
+
# independently and in parallel.
|
| 176 |
+
#
|
| 177 |
+
# EXAMPLE β default Anthropic, also use OpenAI and Groq:
|
| 178 |
+
#
|
| 179 |
+
# LLM_MODEL=anthropic/claude-sonnet-4-6 # default
|
| 180 |
+
# LLM_API_KEY=sk-ant-xxx # β auto-sets ANTHROPIC_API_KEY
|
| 181 |
+
#
|
| 182 |
+
# OPENAI_API_KEY=sk-oai-xxx # activates OpenAI models
|
| 183 |
+
# OPENAI_API_KEYS=sk-oai-k1,sk-oai-k2 # optional: rotation pool
|
| 184 |
+
#
|
| 185 |
+
# GROQ_API_KEY=gsk-xxx # activates Groq models
|
| 186 |
+
# GROQ_API_KEYS=gsk-k1,gsk-k2,gsk-k3 # optional: rotation pool
|
| 187 |
+
#
|
| 188 |
+
# EXAMPLE β OpenRouter only (easiest: one key, 300+ models):
|
| 189 |
+
#
|
| 190 |
+
# LLM_MODEL=openrouter/anthropic/claude-sonnet-4-6
|
| 191 |
+
# LLM_API_KEY=sk-or-v1-xxx
|
| 192 |
+
# # Switch between any of 300+ models in Control UI β no extra keys needed
|
| 193 |
+
#
|
| 194 |
+
# Set any of these directly as HF Space Secrets (not via LLM_API_KEY):
|
| 195 |
+
# ANTHROPIC_API_KEY=sk-ant-xxx
|
| 196 |
+
# OPENAI_API_KEY=sk-oai-xxx
|
| 197 |
+
# GEMINI_API_KEY=AIzaSy-xxx
|
| 198 |
+
# DEEPSEEK_API_KEY=xxx
|
| 199 |
+
# GROQ_API_KEY=gsk-xxx
|
| 200 |
+
# MISTRAL_API_KEY=xxx
|
| 201 |
+
# XAI_API_KEY=xxx
|
| 202 |
+
# NVIDIA_API_KEY=nvapi-xxx
|
| 203 |
+
# COHERE_API_KEY=xxx
|
| 204 |
+
# TOGETHER_API_KEY=xxx
|
| 205 |
+
# CEREBRAS_API_KEY=xxx
|
| 206 |
+
# MOONSHOT_API_KEY=xxx
|
| 207 |
+
# OPENROUTER_API_KEY=sk-or-v1-xxx
|
| 208 |
+
# HUGGINGFACE_HUB_TOKEN=hf_xxx
|
| 209 |
+
|
| 210 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 211 |
+
|
| 212 |
# Optional: custom OpenAI-compatible provider
|
| 213 |
# Only use this if you want to register your own endpoint at startup.
|
| 214 |
# Leave all of these empty unless you need a custom provider.
|
.gitignore
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# personal workflows
|
| 2 |
+
.github/workflows/
|
| 3 |
+
.git*
|
| 4 |
+
# env/secrets
|
| 5 |
+
.env
|
| 6 |
+
.env.*
|
| 7 |
+
*.pem
|
| 8 |
+
*.key
|
| 9 |
+
|
| 10 |
+
# node
|
| 11 |
+
node_modules/
|
| 12 |
+
npm-debug.log*
|
| 13 |
+
yarn-debug.log*
|
| 14 |
+
pnpm-debug.log*
|
| 15 |
+
|
| 16 |
+
# python
|
| 17 |
+
__pycache__/
|
| 18 |
+
*.pyc
|
| 19 |
+
.venv/
|
| 20 |
+
venv/
|
| 21 |
+
|
| 22 |
+
# logs
|
| 23 |
+
logs/
|
| 24 |
+
*.log
|
| 25 |
+
|
| 26 |
+
# local configs
|
| 27 |
+
.vscode/
|
| 28 |
+
.idea/
|
| 29 |
+
|
| 30 |
+
# build/cache
|
| 31 |
+
dist/
|
| 32 |
+
build/
|
| 33 |
+
.cache/
|
| 34 |
+
|
| 35 |
+
# temp
|
| 36 |
+
tmp/
|
| 37 |
+
temp/
|
| 38 |
+
|
| 39 |
+
# os junk
|
| 40 |
+
.DS_Store
|
| 41 |
+
Thumbs.db
|
| 42 |
+
|
| 43 |
+
# gitignore itself
|
| 44 |
+
.gitignore
|
Dockerfile
CHANGED
|
@@ -72,6 +72,8 @@ COPY --chown=1000:1000 wa-guardian.js /home/node/app/wa-guardian.js
|
|
| 72 |
COPY --chown=1000:1000 cloudflare-keepalive-setup.py /home/node/app/cloudflare-keepalive-setup.py
|
| 73 |
COPY --chown=1000:1000 openclaw-sync.py /home/node/app/openclaw-sync.py
|
| 74 |
RUN chmod +x /home/node/app/start.sh /home/node/app/cloudflare-proxy-setup.py /home/node/app/cloudflare-keepalive-setup.py /home/node/app/openclaw-sync.py
|
|
|
|
|
|
|
| 75 |
|
| 76 |
USER node
|
| 77 |
|
|
|
|
| 72 |
COPY --chown=1000:1000 cloudflare-keepalive-setup.py /home/node/app/cloudflare-keepalive-setup.py
|
| 73 |
COPY --chown=1000:1000 openclaw-sync.py /home/node/app/openclaw-sync.py
|
| 74 |
RUN chmod +x /home/node/app/start.sh /home/node/app/cloudflare-proxy-setup.py /home/node/app/cloudflare-keepalive-setup.py /home/node/app/openclaw-sync.py
|
| 75 |
+
COPY --chown=1000:1000 multi-provider-key-rotator.cjs /home/node/app/multi-provider-key-rotator.cjs
|
| 76 |
+
RUN chmod +x /home/node/app/start.sh /home/node/app/cloudflare-proxy-setup.py /home/node/app/cloudflare-keepalive-setup.py /home/node/app/openclaw-sync.py /home/node/app/multi-provider-key-rotator.cjs
|
| 77 |
|
| 78 |
USER node
|
| 79 |
|
iframe-fix.cjs
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
/**
|
| 2 |
* iframe-fix.cjs β Node.js preload script
|
| 3 |
*
|
|
|
|
| 1 |
+
process.on('uncaughtException', function(err) {
|
| 2 |
+
if (err.code === 'EPIPE') return;
|
| 3 |
+
throw err;
|
| 4 |
+
});
|
| 5 |
/**
|
| 6 |
* iframe-fix.cjs β Node.js preload script
|
| 7 |
*
|
multi-provider-key-rotator.cjs
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use strict';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Multi-provider API key rotator for OpenClaw/HuggingClaw
|
| 5 |
+
* --------------------------------------------------------
|
| 6 |
+
* Works exactly like nvidia-key-rotator.cjs but covers every
|
| 7 |
+
* provider that HuggingClaw supports.
|
| 8 |
+
*
|
| 9 |
+
* For each provider you can supply a comma-separated pool:
|
| 10 |
+
* ANTHROPIC_API_KEYS=key1,key2,key3
|
| 11 |
+
* Falls back to the singular env var, then to LLM_API_KEY.
|
| 12 |
+
*
|
| 13 |
+
* Keys are rotated round-robin per provider independently.
|
| 14 |
+
*
|
| 15 |
+
* Patches globalThis.fetch + node:http + node:https so that
|
| 16 |
+
* virtually every caller is covered without code changes.
|
| 17 |
+
*/
|
| 18 |
+
|
| 19 |
+
const http = require('node:http');
|
| 20 |
+
const https = require('node:https');
|
| 21 |
+
|
| 22 |
+
// βββ Provider definitions ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
+
//
|
| 24 |
+
// hostname β regex tested against the request hostname (case-insensitive)
|
| 25 |
+
// envPlural β env var that holds a comma-separated key pool (preferred)
|
| 26 |
+
// envSingular β env var that holds a single key (fallback)
|
| 27 |
+
//
|
| 28 |
+
// LLM_API_KEY is the final fallback for every provider.
|
| 29 |
+
//
|
| 30 |
+
const PROVIDERS = [
|
| 31 |
+
{
|
| 32 |
+
name: 'anthropic',
|
| 33 |
+
hostname: /(?:^|\.)api\.anthropic\.com$/i,
|
| 34 |
+
envPlural: 'ANTHROPIC_API_KEYS',
|
| 35 |
+
envSingular:'ANTHROPIC_API_KEY',
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
name: 'openai',
|
| 39 |
+
hostname: /(?:^|\.)api\.openai\.com$/i,
|
| 40 |
+
envPlural: 'OPENAI_API_KEYS',
|
| 41 |
+
envSingular:'OPENAI_API_KEY',
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
name: 'gemini',
|
| 45 |
+
// Gemini uses generativelanguage API; also covers aiplatform
|
| 46 |
+
hostname: /(?:^|\.)(?:generativelanguage\.googleapis\.com|aiplatform\.googleapis\.com)$/i,
|
| 47 |
+
envPlural: 'GEMINI_API_KEYS',
|
| 48 |
+
envSingular:'GEMINI_API_KEY',
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
name: 'deepseek',
|
| 52 |
+
hostname: /(?:^|\.)api\.deepseek\.com$/i,
|
| 53 |
+
envPlural: 'DEEPSEEK_API_KEYS',
|
| 54 |
+
envSingular:'DEEPSEEK_API_KEY',
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
name: 'openrouter',
|
| 58 |
+
hostname: /(?:^|\.)openrouter\.ai$/i,
|
| 59 |
+
envPlural: 'OPENROUTER_API_KEYS',
|
| 60 |
+
envSingular:'OPENROUTER_API_KEY',
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
name: 'kilocode',
|
| 64 |
+
hostname: /(?:^|\.)kilocode\.ai$/i,
|
| 65 |
+
envPlural: 'KILOCODE_API_KEYS',
|
| 66 |
+
envSingular:'KILOCODE_API_KEY',
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
name: 'opencode',
|
| 70 |
+
hostname: /(?:^|\.)opencode\.ai$/i,
|
| 71 |
+
envPlural: 'OPENCODE_API_KEYS',
|
| 72 |
+
envSingular:'OPENCODE_API_KEY',
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
name: 'zai',
|
| 76 |
+
// Z.ai / GLM β both domains normalised to "zai" in OpenClaw
|
| 77 |
+
hostname: /(?:^|\.)(?:z\.ai|open\.bigmodel\.cn)$/i,
|
| 78 |
+
envPlural: 'ZAI_API_KEYS',
|
| 79 |
+
envSingular:'ZAI_API_KEY',
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
name: 'moonshot',
|
| 83 |
+
hostname: /(?:^|\.)api\.moonshot\.cn$/i,
|
| 84 |
+
envPlural: 'MOONSHOT_API_KEYS',
|
| 85 |
+
envSingular:'MOONSHOT_API_KEY',
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
name: 'minimax',
|
| 89 |
+
hostname: /(?:^|\.)api\.minimax\.chat$/i,
|
| 90 |
+
envPlural: 'MINIMAX_API_KEYS',
|
| 91 |
+
envSingular:'MINIMAX_API_KEY',
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
name: 'xiaomi',
|
| 95 |
+
// MiMo β update hostname if Xiaomi publishes an official domain
|
| 96 |
+
hostname: /(?:^|\.)api\.xiaomi\.com$/i,
|
| 97 |
+
envPlural: 'XIAOMI_API_KEYS',
|
| 98 |
+
envSingular:'XIAOMI_API_KEY',
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
name: 'volcengine',
|
| 102 |
+
// Volcengine / Doubao
|
| 103 |
+
hostname: /(?:^|\.)(?:ark\.cn-beijing\.volces\.com|volcengineapi\.com)$/i,
|
| 104 |
+
envPlural: 'VOLCANO_ENGINE_API_KEYS',
|
| 105 |
+
envSingular:'VOLCANO_ENGINE_API_KEY',
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
name: 'byteplus',
|
| 109 |
+
hostname: /(?:^|\.)maas-api\.ml-platform-cn-beijing\.byteplus\.com$/i,
|
| 110 |
+
envPlural: 'BYTEPLUS_API_KEYS',
|
| 111 |
+
envSingular:'BYTEPLUS_API_KEY',
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
name: 'mistral',
|
| 115 |
+
hostname: /(?:^|\.)api\.mistral\.ai$/i,
|
| 116 |
+
envPlural: 'MISTRAL_API_KEYS',
|
| 117 |
+
envSingular:'MISTRAL_API_KEY',
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
name: 'xai',
|
| 121 |
+
hostname: /(?:^|\.)api\.x\.ai$/i,
|
| 122 |
+
envPlural: 'XAI_API_KEYS',
|
| 123 |
+
envSingular:'XAI_API_KEY',
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
name: 'nvidia',
|
| 127 |
+
hostname: /(?:^|\.)(?:integrate\.api\.nvidia\.com|api\.nvidia\.com)$/i,
|
| 128 |
+
envPlural: 'NVIDIA_API_KEYS',
|
| 129 |
+
envSingular:'NVIDIA_API_KEY',
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
name: 'groq',
|
| 133 |
+
hostname: /(?:^|\.)api\.groq\.com$/i,
|
| 134 |
+
envPlural: 'GROQ_API_KEYS',
|
| 135 |
+
envSingular:'GROQ_API_KEY',
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
name: 'cohere',
|
| 139 |
+
hostname: /(?:^|\.)api\.cohere\.(?:ai|com)$/i,
|
| 140 |
+
envPlural: 'COHERE_API_KEYS',
|
| 141 |
+
envSingular:'COHERE_API_KEY',
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
name: 'together',
|
| 145 |
+
hostname: /(?:^|\.)api\.together\.(?:xyz|ai)$/i,
|
| 146 |
+
envPlural: 'TOGETHER_API_KEYS',
|
| 147 |
+
envSingular:'TOGETHER_API_KEY',
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
name: 'cerebras',
|
| 151 |
+
hostname: /(?:^|\.)api\.cerebras\.ai$/i,
|
| 152 |
+
envPlural: 'CEREBRAS_API_KEYS',
|
| 153 |
+
envSingular:'CEREBRAS_API_KEY',
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
name: 'huggingface',
|
| 157 |
+
hostname: /(?:^|\.)(?:api-inference\.huggingface\.co|router\.huggingface\.co|huggingface\.co)$/i,
|
| 158 |
+
envPlural: 'HUGGINGFACE_HUB_TOKENS', // plural variant
|
| 159 |
+
envSingular:'HUGGINGFACE_HUB_TOKEN',
|
| 160 |
+
},
|
| 161 |
+
];
|
| 162 |
+
|
| 163 |
+
// βββ Key loading βββββββοΏ½οΏ½βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 164 |
+
|
| 165 |
+
function normalizeKeys(...inputs) {
|
| 166 |
+
const seen = new Set();
|
| 167 |
+
const out = [];
|
| 168 |
+
for (const input of inputs) {
|
| 169 |
+
for (const k of String(input || '').split(',').map(s => s.trim()).filter(Boolean)) {
|
| 170 |
+
if (!seen.has(k)) { seen.add(k); out.push(k); }
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
return out;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
// Build per-provider key pools + rotation indices
|
| 177 |
+
const providerState = PROVIDERS.map(p => {
|
| 178 |
+
const keys = normalizeKeys(
|
| 179 |
+
process.env[p.envPlural] || '',
|
| 180 |
+
process.env[p.envSingular] || '',
|
| 181 |
+
process.env.LLM_API_KEY || '',
|
| 182 |
+
);
|
| 183 |
+
if (!keys.length) {
|
| 184 |
+
console.warn(`[key-rotator] No keys for provider "${p.name}"`);
|
| 185 |
+
} else {
|
| 186 |
+
console.log(`[key-rotator] ${p.name}: ${keys.length} key${keys.length === 1 ? '' : 's'}`);
|
| 187 |
+
}
|
| 188 |
+
return { ...p, keys, idx: 0 };
|
| 189 |
+
});
|
| 190 |
+
|
| 191 |
+
// βββ Runtime helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 192 |
+
|
| 193 |
+
function resolveHostname(urlLike) {
|
| 194 |
+
try {
|
| 195 |
+
const u =
|
| 196 |
+
typeof urlLike === 'string' ? new URL(urlLike)
|
| 197 |
+
: urlLike instanceof URL ? urlLike
|
| 198 |
+
: urlLike && typeof urlLike.url === 'string' ? new URL(urlLike.url)
|
| 199 |
+
: urlLike && typeof urlLike.href === 'string' ? new URL(urlLike.href)
|
| 200 |
+
: urlLike && typeof urlLike.hostname === 'string' ? urlLike
|
| 201 |
+
: null;
|
| 202 |
+
return u ? u.hostname : null;
|
| 203 |
+
} catch {
|
| 204 |
+
return null;
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
function matchProvider(hostname) {
|
| 209 |
+
if (!hostname) return null;
|
| 210 |
+
return providerState.find(p => p.hostname.test(hostname)) || null;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
function nextKey(provider) {
|
| 214 |
+
if (!provider || !provider.keys.length) return null;
|
| 215 |
+
const key = provider.keys[provider.idx % provider.keys.length];
|
| 216 |
+
provider.idx = (provider.idx + 1) % provider.keys.length;
|
| 217 |
+
return key;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
function setAuthHeader(headers, key) {
|
| 221 |
+
if (!key) return headers;
|
| 222 |
+
const authValue = `Bearer ${key}`;
|
| 223 |
+
|
| 224 |
+
if (typeof Headers !== 'undefined' && headers instanceof Headers) {
|
| 225 |
+
headers.set('authorization', authValue);
|
| 226 |
+
return headers;
|
| 227 |
+
}
|
| 228 |
+
if (Array.isArray(headers)) {
|
| 229 |
+
const out = headers.filter(([k]) => String(k).toLowerCase() !== 'authorization');
|
| 230 |
+
out.push(['authorization', authValue]);
|
| 231 |
+
return out;
|
| 232 |
+
}
|
| 233 |
+
if (headers && typeof headers === 'object') {
|
| 234 |
+
return { ...headers, authorization: authValue };
|
| 235 |
+
}
|
| 236 |
+
return { authorization: authValue };
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
// βββ Patch globalThis.fetch βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 240 |
+
|
| 241 |
+
function patchFetch() {
|
| 242 |
+
if (typeof globalThis.fetch !== 'function') return;
|
| 243 |
+
|
| 244 |
+
const originalFetch = globalThis.fetch.bind(globalThis);
|
| 245 |
+
|
| 246 |
+
globalThis.fetch = async function patchedFetch(input, init = {}) {
|
| 247 |
+
try {
|
| 248 |
+
const urlLike =
|
| 249 |
+
typeof input === 'string' || input instanceof URL
|
| 250 |
+
? input
|
| 251 |
+
: input && typeof input.url === 'string' ? input.url : null;
|
| 252 |
+
|
| 253 |
+
const hostname = resolveHostname(urlLike);
|
| 254 |
+
const provider = matchProvider(hostname);
|
| 255 |
+
|
| 256 |
+
if (provider) {
|
| 257 |
+
const key = nextKey(provider);
|
| 258 |
+
if (key) {
|
| 259 |
+
const headers = init.headers || (input && input.headers) || undefined;
|
| 260 |
+
const patchedHeaders = setAuthHeader(headers, key);
|
| 261 |
+
init = { ...init, headers: patchedHeaders };
|
| 262 |
+
|
| 263 |
+
if (input && typeof input === 'object' && !(input instanceof URL) && input.headers) {
|
| 264 |
+
try { input = new Request(input, { headers: patchedHeaders }); } catch { /* noop */ }
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
} catch (err) {
|
| 269 |
+
console.warn('[key-rotator] fetch patch error:', err?.message || err);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
return originalFetch(input, init);
|
| 273 |
+
};
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
// βββ Patch node:http / node:https ββββββββββββββββββββββββββββββββββββββββββββ
|
| 277 |
+
|
| 278 |
+
function patchHttpModule(mod) {
|
| 279 |
+
const originalRequest = mod.request;
|
| 280 |
+
|
| 281 |
+
mod.request = function patchedRequest(...args) {
|
| 282 |
+
try {
|
| 283 |
+
const options = args[0];
|
| 284 |
+
const hostname = resolveHostname(options);
|
| 285 |
+
const provider = matchProvider(hostname);
|
| 286 |
+
|
| 287 |
+
if (provider) {
|
| 288 |
+
const key = nextKey(provider);
|
| 289 |
+
if (key) {
|
| 290 |
+
if (typeof options === 'string' || options instanceof URL) {
|
| 291 |
+
const u = new URL(String(options));
|
| 292 |
+
args[0] = {
|
| 293 |
+
protocol: u.protocol,
|
| 294 |
+
hostname: u.hostname,
|
| 295 |
+
port: u.port,
|
| 296 |
+
path: `${u.pathname}${u.search}`,
|
| 297 |
+
headers: { authorization: `Bearer ${key}` },
|
| 298 |
+
};
|
| 299 |
+
} else if (options && typeof options === 'object') {
|
| 300 |
+
args[0] = { ...options, headers: setAuthHeader(options.headers, key) };
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
} catch (err) {
|
| 305 |
+
console.warn('[key-rotator] http patch error:', err?.message || err);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
return originalRequest.apply(mod, args);
|
| 309 |
+
};
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
// βββ Apply patches ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 313 |
+
|
| 314 |
+
patchFetch();
|
| 315 |
+
patchHttpModule(http);
|
| 316 |
+
patchHttpModule(https);
|
| 317 |
+
|
| 318 |
+
console.log('[key-rotator] loaded β all providers active');
|
openclaw-sync.py
CHANGED
|
@@ -51,7 +51,7 @@ EXCLUDED_SYNC_DIRS = {
|
|
| 51 |
}
|
| 52 |
MAX_FILE_SIZE_BYTES = int(os.environ.get("SYNC_MAX_FILE_BYTES", str(50 * 1024 * 1024)))
|
| 53 |
|
| 54 |
-
STATE_DIR = WORKSPACE / "
|
| 55 |
OPENCLAW_STATE_BACKUP_DIR = STATE_DIR / "openclaw"
|
| 56 |
EXCLUDED_STATE_NAMES = {
|
| 57 |
"workspace",
|
|
@@ -87,20 +87,33 @@ def count_files(path: Path) -> int:
|
|
| 87 |
def snapshot_state_into_workspace() -> None:
|
| 88 |
try:
|
| 89 |
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
for source_path in OPENCLAW_HOME.iterdir():
|
| 95 |
if source_path.name in EXCLUDED_STATE_NAMES:
|
| 96 |
continue
|
| 97 |
|
| 98 |
-
backup_path =
|
| 99 |
if source_path.is_dir():
|
| 100 |
shutil.copytree(source_path, backup_path)
|
| 101 |
elif source_path.is_file():
|
| 102 |
shutil.copy2(source_path, backup_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
except Exception as exc:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
print(f"Warning: could not snapshot OpenClaw state: {exc}")
|
| 105 |
|
| 106 |
try:
|
|
@@ -135,6 +148,23 @@ def snapshot_state_into_workspace() -> None:
|
|
| 135 |
|
| 136 |
def restore_embedded_state() -> None:
|
| 137 |
state_backup_root = STATE_DIR / "openclaw"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
if state_backup_root.is_dir():
|
| 139 |
for source_path in state_backup_root.iterdir():
|
| 140 |
name = source_path.name
|
|
@@ -398,12 +428,18 @@ def loop() -> int:
|
|
| 398 |
print(f"Workspace sync error: {exc}")
|
| 399 |
return 1
|
| 400 |
|
| 401 |
-
last_fingerprint = fingerprint_dir(WORKSPACE)
|
| 402 |
-
last_marker = metadata_marker(WORKSPACE)
|
| 403 |
-
|
| 404 |
time.sleep(INITIAL_DELAY)
|
| 405 |
print(f"Workspace sync started: every {INTERVAL}s -> {repo_id}")
|
| 406 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
while not STOP_EVENT.is_set():
|
| 408 |
try:
|
| 409 |
last_fingerprint, last_marker = sync_once(last_fingerprint, last_marker)
|
|
@@ -416,7 +452,6 @@ def loop() -> int:
|
|
| 416 |
|
| 417 |
return 0
|
| 418 |
|
| 419 |
-
|
| 420 |
def main() -> int:
|
| 421 |
WORKSPACE.mkdir(parents=True, exist_ok=True)
|
| 422 |
|
|
|
|
| 51 |
}
|
| 52 |
MAX_FILE_SIZE_BYTES = int(os.environ.get("SYNC_MAX_FILE_BYTES", str(50 * 1024 * 1024)))
|
| 53 |
|
| 54 |
+
STATE_DIR = WORKSPACE / "huggingclaw-state"
|
| 55 |
OPENCLAW_STATE_BACKUP_DIR = STATE_DIR / "openclaw"
|
| 56 |
EXCLUDED_STATE_NAMES = {
|
| 57 |
"workspace",
|
|
|
|
| 87 |
def snapshot_state_into_workspace() -> None:
|
| 88 |
try:
|
| 89 |
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
| 90 |
+
# Atomic snapshot: copy to a staging dir first, then rename.
|
| 91 |
+
# This prevents a half-written (or empty) backup if we crash mid-copy,
|
| 92 |
+
# which would otherwise be uploaded and overwrite the real HF backup.
|
| 93 |
+
staging_dir = STATE_DIR / ".openclaw-staging"
|
| 94 |
+
if staging_dir.exists():
|
| 95 |
+
shutil.rmtree(staging_dir, ignore_errors=True)
|
| 96 |
+
staging_dir.mkdir(parents=True, exist_ok=True)
|
| 97 |
|
| 98 |
for source_path in OPENCLAW_HOME.iterdir():
|
| 99 |
if source_path.name in EXCLUDED_STATE_NAMES:
|
| 100 |
continue
|
| 101 |
|
| 102 |
+
backup_path = staging_dir / source_path.name
|
| 103 |
if source_path.is_dir():
|
| 104 |
shutil.copytree(source_path, backup_path)
|
| 105 |
elif source_path.is_file():
|
| 106 |
shutil.copy2(source_path, backup_path)
|
| 107 |
+
|
| 108 |
+
# Atomically swap staging β real backup dir
|
| 109 |
+
if OPENCLAW_STATE_BACKUP_DIR.exists():
|
| 110 |
+
shutil.rmtree(OPENCLAW_STATE_BACKUP_DIR, ignore_errors=True)
|
| 111 |
+
staging_dir.rename(OPENCLAW_STATE_BACKUP_DIR)
|
| 112 |
except Exception as exc:
|
| 113 |
+
# Clean up staging on failure so it doesn't interfere next time
|
| 114 |
+
staging_dir = STATE_DIR / ".openclaw-staging"
|
| 115 |
+
if staging_dir.exists():
|
| 116 |
+
shutil.rmtree(staging_dir, ignore_errors=True)
|
| 117 |
print(f"Warning: could not snapshot OpenClaw state: {exc}")
|
| 118 |
|
| 119 |
try:
|
|
|
|
| 148 |
|
| 149 |
def restore_embedded_state() -> None:
|
| 150 |
state_backup_root = STATE_DIR / "openclaw"
|
| 151 |
+
|
| 152 |
+
# Migration fix: old backups stored state in ".huggingclaw-state/openclaw"
|
| 153 |
+
# (hidden dir). If new path doesn't exist but old hidden path does, use it
|
| 154 |
+
# and migrate it to the new path so future syncs write to the right place.
|
| 155 |
+
if not state_backup_root.is_dir():
|
| 156 |
+
legacy_state = WORKSPACE / ".huggingclaw-state" / "openclaw"
|
| 157 |
+
if legacy_state.is_dir():
|
| 158 |
+
print("Found legacy state backup at .huggingclaw-state/; migrating to huggingclaw-state/...")
|
| 159 |
+
try:
|
| 160 |
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
| 161 |
+
shutil.copytree(legacy_state, state_backup_root)
|
| 162 |
+
legacy_root = WORKSPACE / ".huggingclaw-state"
|
| 163 |
+
shutil.rmtree(legacy_root, ignore_errors=True)
|
| 164 |
+
print("Legacy state migrated and .huggingclaw-state/ removed.")
|
| 165 |
+
except Exception as exc:
|
| 166 |
+
print(f"Warning: could not migrate legacy state: {exc}")
|
| 167 |
+
|
| 168 |
if state_backup_root.is_dir():
|
| 169 |
for source_path in state_backup_root.iterdir():
|
| 170 |
name = source_path.name
|
|
|
|
| 428 |
print(f"Workspace sync error: {exc}")
|
| 429 |
return 1
|
| 430 |
|
|
|
|
|
|
|
|
|
|
| 431 |
time.sleep(INITIAL_DELAY)
|
| 432 |
print(f"Workspace sync started: every {INTERVAL}s -> {repo_id}")
|
| 433 |
|
| 434 |
+
# Take a fingerprint of the workspace AS RESTORED (after snapshotting state)
|
| 435 |
+
# so the first loop iteration only uploads if something genuinely changed.
|
| 436 |
+
# Previously this was None, which forced an unconditional upload every restart
|
| 437 |
+
# β even when restore had failed silently and the workspace was empty.
|
| 438 |
+
snapshot_state_into_workspace()
|
| 439 |
+
last_fingerprint = fingerprint_dir(WORKSPACE)
|
| 440 |
+
last_marker = metadata_marker(WORKSPACE)
|
| 441 |
+
print("Initial workspace fingerprint captured.")
|
| 442 |
+
|
| 443 |
while not STOP_EVENT.is_set():
|
| 444 |
try:
|
| 445 |
last_fingerprint, last_marker = sync_once(last_fingerprint, last_marker)
|
|
|
|
| 452 |
|
| 453 |
return 0
|
| 454 |
|
|
|
|
| 455 |
def main() -> int:
|
| 456 |
WORKSPACE.mkdir(parents=True, exist_ok=True)
|
| 457 |
|
start.sh
CHANGED
|
@@ -107,7 +107,7 @@ case "$LLM_PROVIDER" in
|
|
| 107 |
qianfan) export QIANFAN_API_KEY="$LLM_API_KEY" ;;
|
| 108 |
# ββ Western Providers ββ
|
| 109 |
mistral|mistralai) export MISTRAL_API_KEY="$LLM_API_KEY" ;;
|
| 110 |
-
xai|x-ai)
|
| 111 |
nvidia) export NVIDIA_API_KEY="$LLM_API_KEY" ;;
|
| 112 |
cohere) export COHERE_API_KEY="$LLM_API_KEY" ;;
|
| 113 |
groq) export GROQ_API_KEY="$LLM_API_KEY" ;;
|
|
@@ -420,12 +420,39 @@ if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
|
|
| 420 |
fi
|
| 421 |
|
| 422 |
# Write config
|
| 423 |
-
|
| 424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
|
| 426 |
# ββ Enable Gateway Preload Fixes ββ
|
| 427 |
# This preload script keeps iframe embedding working on HF Spaces.
|
| 428 |
-
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/app/iframe-fix.cjs"
|
| 429 |
|
| 430 |
# ββ Startup Summary ββ
|
| 431 |
echo ""
|
|
@@ -497,6 +524,7 @@ warmup_browser() {
|
|
| 497 |
) &
|
| 498 |
}
|
| 499 |
|
|
|
|
| 500 |
# ββ Start background services ββ
|
| 501 |
export LLM_MODEL="$LLM_MODEL"
|
| 502 |
# 10. Start Health Server & Dashboard
|
|
@@ -508,6 +536,63 @@ if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then
|
|
| 508 |
python3 /home/node/app/cloudflare-keepalive-setup.py || true
|
| 509 |
fi
|
| 510 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
# ββ Launch gateway ββ
|
| 512 |
echo "Launching OpenClaw gateway on port 7860..."
|
| 513 |
|
|
|
|
| 107 |
qianfan) export QIANFAN_API_KEY="$LLM_API_KEY" ;;
|
| 108 |
# ββ Western Providers ββ
|
| 109 |
mistral|mistralai) export MISTRAL_API_KEY="$LLM_API_KEY" ;;
|
| 110 |
+
xai|x-ai) export XAI_API_KEY="$LLM_API_KEY" ;;
|
| 111 |
nvidia) export NVIDIA_API_KEY="$LLM_API_KEY" ;;
|
| 112 |
cohere) export COHERE_API_KEY="$LLM_API_KEY" ;;
|
| 113 |
groq) export GROQ_API_KEY="$LLM_API_KEY" ;;
|
|
|
|
| 420 |
fi
|
| 421 |
|
| 422 |
# Write config
|
| 423 |
+
EXISTING_CONFIG="/home/node/.openclaw/openclaw.json"
|
| 424 |
+
if [ -f "$EXISTING_CONFIG" ]; then
|
| 425 |
+
echo "Restored config found β patching required fields only..."
|
| 426 |
+
PATCHED=$(jq \
|
| 427 |
+
--arg token "$GATEWAY_TOKEN" \
|
| 428 |
+
--arg model "$LLM_MODEL" \
|
| 429 |
+
--arg fileLevel "$OPENCLAW_FILE_LOG_LEVEL" \
|
| 430 |
+
--arg consoleLevel "$OPENCLAW_CONSOLE_LOG_LEVEL" \
|
| 431 |
+
--arg consoleStyle "$OPENCLAW_CONSOLE_LOG_STYLE" \
|
| 432 |
+
'.gateway.auth.token = $token
|
| 433 |
+
| .agents.defaults.model = $model
|
| 434 |
+
| .logging.level = $fileLevel
|
| 435 |
+
| .logging.consoleLevel = $consoleLevel
|
| 436 |
+
| .logging.consoleStyle = $consoleStyle' \
|
| 437 |
+
"$EXISTING_CONFIG" 2>/dev/null)
|
| 438 |
+
|
| 439 |
+
if [ -n "$PATCHED" ]; then
|
| 440 |
+
echo "$PATCHED" > "$EXISTING_CONFIG.tmp" \
|
| 441 |
+
&& mv "$EXISTING_CONFIG.tmp" "$EXISTING_CONFIG"
|
| 442 |
+
echo "Config patched successfully."
|
| 443 |
+
else
|
| 444 |
+
echo "Patch failed β writing fresh config."
|
| 445 |
+
echo "$CONFIG_JSON" > "$EXISTING_CONFIG"
|
| 446 |
+
fi
|
| 447 |
+
else
|
| 448 |
+
echo "No restored config β writing fresh config..."
|
| 449 |
+
echo "$CONFIG_JSON" > "$EXISTING_CONFIG"
|
| 450 |
+
fi
|
| 451 |
+
chmod 600 "$EXISTING_CONFIG"
|
| 452 |
|
| 453 |
# ββ Enable Gateway Preload Fixes ββ
|
| 454 |
# This preload script keeps iframe embedding working on HF Spaces.
|
| 455 |
+
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/app/iframe-fix.cjs --require /home/node/app/multi-provider-key-rotator.cjs"
|
| 456 |
|
| 457 |
# ββ Startup Summary ββ
|
| 458 |
echo ""
|
|
|
|
| 524 |
) &
|
| 525 |
}
|
| 526 |
|
| 527 |
+
|
| 528 |
# ββ Start background services ββ
|
| 529 |
export LLM_MODEL="$LLM_MODEL"
|
| 530 |
# 10. Start Health Server & Dashboard
|
|
|
|
| 536 |
python3 /home/node/app/cloudflare-keepalive-setup.py || true
|
| 537 |
fi
|
| 538 |
|
| 539 |
+
# ββ Write shell capture wrappers to .bashrc ββ
|
| 540 |
+
STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
|
| 541 |
+
cat > /home/node/.bashrc << 'BASHRC'
|
| 542 |
+
STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
|
| 543 |
+
_hc_append() {
|
| 544 |
+
local line="$*"
|
| 545 |
+
grep -qxF "$line" "$STARTUP_FILE" 2>/dev/null || echo "$line" >> "$STARTUP_FILE"
|
| 546 |
+
}
|
| 547 |
+
apt-get() {
|
| 548 |
+
command apt-get "$@"
|
| 549 |
+
[[ "$1" == "install" ]] && _hc_append "apt-get install -y ${@:2}"
|
| 550 |
+
}
|
| 551 |
+
apt() {
|
| 552 |
+
command apt "$@"
|
| 553 |
+
[[ "$1" == "install" ]] && _hc_append "apt-get install -y ${@:2}"
|
| 554 |
+
}
|
| 555 |
+
pip() { command pip "$@"; [[ "$1" == "install" ]] && _hc_append "pip install ${@:2}"; }
|
| 556 |
+
pip3() { command pip3 "$@"; [[ "$1" == "install" ]] && _hc_append "pip3 install ${@:2}"; }
|
| 557 |
+
npm() { command npm "$@"; [[ "$1" == "install" && "$2" == "-g" ]] && _hc_append "npm install -g ${@:3}"; }
|
| 558 |
+
openclaw() { command openclaw "$@"; [[ "$1" == "plugins" && "$2" == "install" ]] && _hc_append "openclaw plugins install ${@:3}"; }
|
| 559 |
+
BASHRC
|
| 560 |
+
echo "Shell capture wrappers ready."
|
| 561 |
+
|
| 562 |
+
# ββ Re-install previously installed plugins ββ
|
| 563 |
+
EXISTING_CONFIG="/home/node/.openclaw/openclaw.json"
|
| 564 |
+
if [ -f "$EXISTING_CONFIG" ]; then
|
| 565 |
+
INSTALLS=$(jq -r '.plugins.installs // {} | keys[]' "$EXISTING_CONFIG" 2>/dev/null || echo "")
|
| 566 |
+
if [ -n "$INSTALLS" ]; then
|
| 567 |
+
echo "Re-installing plugins from config..."
|
| 568 |
+
while IFS= read -r pkg; do
|
| 569 |
+
[ -z "$pkg" ] && continue
|
| 570 |
+
# Try short name first, then @openclaw/ prefix
|
| 571 |
+
if openclaw plugins install "$pkg" 2>/dev/null; then
|
| 572 |
+
echo " Installed: $pkg"
|
| 573 |
+
elif openclaw plugins install "@openclaw/$pkg" 2>/dev/null; then
|
| 574 |
+
echo " Installed: @openclaw/$pkg"
|
| 575 |
+
else
|
| 576 |
+
echo " Warning: could not install $pkg"
|
| 577 |
+
fi
|
| 578 |
+
done <<< "$INSTALLS"
|
| 579 |
+
echo "Plugins done."
|
| 580 |
+
fi
|
| 581 |
+
fi
|
| 582 |
+
|
| 583 |
+
# ββ Run workspace startup script ββ
|
| 584 |
+
STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
|
| 585 |
+
if [ ! -f "$STARTUP_FILE" ]; then
|
| 586 |
+
touch "$STARTUP_FILE"
|
| 587 |
+
chmod +x "$STARTUP_FILE"
|
| 588 |
+
echo "Created workspace/startup.sh"
|
| 589 |
+
fi
|
| 590 |
+
if [ -s "$STARTUP_FILE" ]; then
|
| 591 |
+
echo "Running workspace/startup.sh..."
|
| 592 |
+
bash "$STARTUP_FILE" || echo "Warning: startup.sh had errors, continuing..."
|
| 593 |
+
echo "Startup script complete."
|
| 594 |
+
fi
|
| 595 |
+
|
| 596 |
# ββ Launch gateway ββ
|
| 597 |
echo "Launching OpenClaw gateway on port 7860..."
|
| 598 |
|