xia / setup-hf-config.mjs
echo8900's picture
Update setup-hf-config.mjs
41ecfd2 verified
import fs from "node:fs";
import path from "node:path";
import https from "node:https";
// ============================================================
// OpenClaw HF Spaces - Production Config Writer
//
// Storage: HF Storage Bucket at /data (100 GB persistent).
// All runtime data lives in OPENCLAW_HOME/.openclaw/.
//
// Config priority:
// 1. Existing openclaw.json in bucket -> PATCH env vars only
// 2. /app/openclaw-template.json -> use as base + patch
// 3. Neither -> minimal fresh + patch
//
// Always patched from env vars (never lost on restart):
// gateway.auth, gateway.controlUi, gateway.trustedProxies
// agents.defaults.workspace, agents.defaults.model
// env.vars, models.providers.*.apiKey, channels.telegram
// ============================================================
var HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";
var STATE_DIR = path.join(HOME, ".openclaw");
var CONFIG_PATH = path.join(STATE_DIR, "openclaw.json");
var WORKSPACE = path.join(STATE_DIR, "workspace");
var SPACE_HOST = (process.env.SPACE_HOST || "").trim();
var TEMPLATE = "/app/openclaw-template.json";
function log(msg) { console.log("[setup] " + msg); }
function err(msg) { console.error("[setup] ERROR: " + msg); }
log("Starting... HOME=" + HOME);
// ----------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------
function envStr(key) { return (process.env[key] || "").trim(); }
function parseList(val) {
if (!val || !val.trim()) return [];
return val.split(",").map(function(s){ return s.trim(); }).filter(Boolean);
}
// ----------------------------------------------------------------
// Auth (required)
// ----------------------------------------------------------------
var gatewayToken = envStr("OPENCLAW_GATEWAY_TOKEN");
var gatewayPassword = envStr("OPENCLAW_GATEWAY_PASSWORD");
if (!gatewayToken && !gatewayPassword) {
err("FATAL: set OPENCLAW_GATEWAY_TOKEN in Secrets");
process.exit(0);
}
// ----------------------------------------------------------------
// Default model
// ----------------------------------------------------------------
var defaultModel = envStr("OPENCLAW_HF_DEFAULT_MODEL") || "google/gemini-2.0-flash";
// ----------------------------------------------------------------
// Provider API key detection
// ----------------------------------------------------------------
var EXCLUDE_PREFIXES = [
"OPENCLAW_", "SPACE_", "SYSTEM_", "HF_",
"NODE_", "PATH", "HOME", "USER", "PWD", "LANG", "LC_",
"npm_", "HOSTNAME", "SHELL", "TERM", "SHLVL"
];
var INCLUDE_SUFFIXES = [
"_API_KEY", "_SECRET_KEY", "_ACCESS_TOKEN",
"_BOT_TOKEN", "_AUTH_TOKEN", "_APP_KEY"
];
function isProviderKey(k) {
var i;
for (i = 0; i < EXCLUDE_PREFIXES.length; i++) {
if (k.indexOf(EXCLUDE_PREFIXES[i]) === 0) return false;
}
for (i = 0; i < INCLUDE_SUFFIXES.length; i++) {
var s = INCLUDE_SUFFIXES[i];
if (k.length > s.length && k.indexOf(s) === k.length - s.length) return true;
}
return false;
}
var providerKeys = Object.keys(process.env).filter(function(k) {
return isProviderKey(k) && envStr(k);
}).sort();
log("Provider keys (" + providerKeys.length + "): " + providerKeys.join(", "));
// ----------------------------------------------------------------
// Trusted proxies (HF internal + standard RFC1918)
// ----------------------------------------------------------------
var envProxies = parseList(envStr("OPENCLAW_GATEWAY_TRUSTED_PROXIES"));
var trustedProxies = envProxies.length > 0 ? envProxies : [
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"10.16.0.0/12", "10.20.0.0/12",
"10.16.4.123", "10.16.7.92", "10.16.18.232",
"10.16.34.155", "10.16.43.133", "10.16.1.206",
"10.16.37.110", "10.16.43.246",
"10.20.1.9", "10.20.1.222", "10.20.26.157", "10.20.31.87",
"10.20.0.1", "172.17.0.1", "127.0.0.1"
];
// ----------------------------------------------------------------
// Provider -> env var mapping
// ----------------------------------------------------------------
var PROVIDER_KEY_MAP = {
"openrouter": "OPENROUTER_API_KEY",
"siliconflow": "SILICONFLOW_API_KEY",
"deepseek": "DEEPSEEK_API_KEY",
"groq": "GROQ_API_KEY",
"google": "GOOGLE_API_KEY",
"gemini": "GEMINI_API_KEY",
"minimax": "MINIMAX_API_KEY",
"zai": "ZAI_API_KEY",
"github": "GITHUB_MODELS_API_KEY",
"github-copilot": "GITHUB_MODELS_API_KEY",
"openai": "OPENAI_API_KEY",
"anthropic": "ANTHROPIC_API_KEY"
};
function patchProviderKeys(config) {
if (!config.models || !config.models.providers) return;
var providers = config.models.providers;
Object.keys(providers).forEach(function(name) {
var lower = name.toLowerCase();
var envKey = PROVIDER_KEY_MAP[lower];
var val = envKey ? envStr(envKey) : "";
if (!val) {
// fuzzy match: find any provider key whose name contains the provider name
var found = providerKeys.find(function(k) {
return k.toLowerCase().indexOf(lower) >= 0;
});
if (found) val = envStr(found);
}
if (val) {
providers[name].apiKey = val;
log("Provider key set: " + name + " <- " + (envKey || "fuzzy"));
} else if (providers[name].apiKey === "WILL_BE_SET_BY_ENV") {
delete providers[name].apiKey;
log("Provider key missing (skipped): " + name);
}
});
}
// ----------------------------------------------------------------
// Telegram webhook helpers
// ----------------------------------------------------------------
function tgRequest(token, method, body) {
return new Promise(function(resolve) {
var data = JSON.stringify(body || {});
var req = https.request({
hostname: "api.telegram.org",
path: "/bot" + token + "/" + method,
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(data)
}
}, function(res) {
var buf = "";
res.on("data", function(c) { buf += c; });
res.on("end", function() {
try { resolve(JSON.parse(buf)); } catch(e) { resolve(null); }
});
});
req.on("error", function() { resolve(null); });
req.setTimeout(10000, function() { req.destroy(); resolve(null); });
req.write(data);
req.end();
});
}
async function setupWebhook(token) {
if (!SPACE_HOST) {
log("Telegram: no SPACE_HOST - webhook skipped");
return;
}
var targetUrl = "https://" + SPACE_HOST + "/tg-webhook";
// Check if webhook already points to our URL - skip if already set
var info = await tgRequest(token, "getWebhookInfo", {});
if (info && info.ok && info.result && info.result.url === targetUrl) {
log("Telegram: webhook already set -> " + targetUrl);
return;
}
var r = await tgRequest(token, "setWebhook", {
url: targetUrl, drop_pending_updates: true, max_connections: 10
});
if (r && r.ok) {
log("Telegram: webhook registered -> " + targetUrl);
} else {
var desc = r && r.description ? r.description : "unknown error";
log("Telegram: webhook failed (" + desc + ")");
log("Telegram: set manually at:");
log(" https://api.telegram.org/bot" + token + "/setWebhook?url=" + targetUrl + "&drop_pending_updates=true");
}
}
// ----------------------------------------------------------------
// Seed workspace (first boot only - bucket persists after)
// ----------------------------------------------------------------
function seedWorkspace() {
var soul = path.join(WORKSPACE, "SOUL.md");
if (!fs.existsSync(soul)) {
fs.writeFileSync(soul, [
"# Soul",
"",
"You are a helpful, warm, concise AI assistant.",
"",
"## Language",
"",
"Default language: Simplified Chinese.",
"Always reply in Chinese unless the user writes in another language first.",
"",
"## Tone",
"",
"- Natural and friendly, not overly formal",
"- Concise and to the point"
].join("\n") + "\n", "utf-8");
log("Seeded SOUL.md");
}
var mem = path.join(WORKSPACE, "MEMORY.md");
if (!fs.existsSync(mem)) {
fs.writeFileSync(mem, [
"# Long-term Memory",
"",
"<!-- OpenClaw writes important facts here. -->"
].join("\n") + "\n", "utf-8");
log("Seeded MEMORY.md");
}
}
// ----------------------------------------------------------------
// Apply all env-derived patches to a config object
// ----------------------------------------------------------------
function applyEnvPatches(config, tgToken) {
// Auth
config.gateway = config.gateway || {};
config.gateway.auth = gatewayToken
? { mode: "token", token: gatewayToken }
: { mode: "password", password: gatewayPassword };
// Control UI flags required for HF Spaces iframe embedding
config.gateway.controlUi = config.gateway.controlUi || {};
config.gateway.controlUi.allowInsecureAuth = true;
config.gateway.controlUi.allowedOrigins = ["*"];
config.gateway.controlUi.dangerouslyDisableDeviceAuth = true;
config.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback = true;
// Always refresh trusted proxies (HF pod IPs can change)
config.gateway.trustedProxies = trustedProxies;
// Agents
config.agents = config.agents || {};
config.agents.defaults = config.agents.defaults || {};
config.agents.defaults.workspace = WORKSPACE;
if (!config.agents.defaults.model ||
config.agents.defaults.model === "WILL_BE_SET_BY_ENV") {
config.agents.defaults.model = defaultModel;
}
// Env vars (all provider keys available inside agent tools)
config.env = config.env || {};
config.env.vars = {};
providerKeys.forEach(function(k) { config.env.vars[k] = envStr(k); });
// Model provider API keys
patchProviderKeys(config);
// Telegram channel
if (tgToken) {
config.channels = config.channels || {};
config.channels.telegram = config.channels.telegram || {};
config.channels.telegram.enabled = true;
config.channels.telegram.accounts = config.channels.telegram.accounts || {};
config.channels.telegram.accounts.main = {
botToken: tgToken,
apiRoot: "https://api.telegram.org"
};
}
return config;
}
// ----------------------------------------------------------------
// Main
// ----------------------------------------------------------------
(async function() {
// Ensure directories exist
fs.mkdirSync(STATE_DIR, { recursive: true });
fs.mkdirSync(WORKSPACE, { recursive: true });
fs.mkdirSync(path.join(WORKSPACE, "memory"), { recursive: true });
// Seed workspace on first boot
seedWorkspace();
// Telegram webhook
var tgToken = envStr("TELEGRAM_BOT_TOKEN");
if (tgToken) {
await setupWebhook(tgToken);
} else {
log("Telegram: disabled (no TELEGRAM_BOT_TOKEN)");
}
// Load base config
// Standard OpenClaw pattern: the bucket config is the single source
// of truth. It contains everything: models, channels, telegram bindings,
// devices, credentials. It persists across all restarts unchanged.
// The template (uploaded openclaw.json) is only used on first boot
// when no bucket config exists yet.
//
// To update models/providers after first boot: use the Control UI.
// Changes are saved directly to the bucket config automatically.
var config = null;
var mode = "";
// Primary: bucket config (preserves ALL user settings across restarts)
if (fs.existsSync(CONFIG_PATH)) {
try {
config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8").trim());
mode = "bucket (full config preserved)";
} catch(e) {
log("Bucket config unreadable (" + e.message + ") - falling back to template");
}
}
// First boot only: use template to seed the bucket
if (!config && fs.existsSync(TEMPLATE)) {
try {
config = JSON.parse(fs.readFileSync(TEMPLATE, "utf-8").trim());
mode = "template (first boot - seeding bucket)";
} catch(e) {
log("Template unreadable (" + e.message + ")");
}
}
if (!config) {
config = { gateway: {}, agents: { defaults: {} }, env: { vars: {} } };
mode = "fresh (no template found)";
}
// Apply env patches
config = applyEnvPatches(config, tgToken);
// Atomic write: backup then overwrite
if (fs.existsSync(CONFIG_PATH)) {
fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + ".bak");
}
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
// Summary
var provCount = config.models && config.models.providers
? Object.keys(config.models.providers).length : 0;
var modelCount = config.models && config.models.providers
? Object.values(config.models.providers).reduce(function(n, p) {
return n + (p.models ? p.models.length : 0);
}, 0) : 0;
log("Done. Mode: " + mode);
log(" auth = " + (gatewayToken ? "token" : "password"));
log(" model = " + (config.agents.defaults.model || defaultModel));
log(" workspace = " + WORKSPACE);
log(" proxies = " + trustedProxies.length);
log(" env.vars = " + providerKeys.length);
log(" providers = " + provCount + " (" + modelCount + " models)");
log(" storage = bucket at /data (100 GB persistent)");
})().catch(function(e) {
err("Fatal: " + e.message);
process.exit(0);
});