import fs from "node:fs"; import path from "node:path"; // ============================================================ // 生成运行配置 (实时状态在本地磁盘 /home/user) // 基础配置优先级: // 1. /data/.openclaw/openclaw.json (bucket 里的旧配置, 只读, 已被 2026.9.7 doctor 验证过) // 2. /app/openclaw-template.json // 3. 最小空配置 // 每次启动都会用 Secrets 覆盖: 网关令牌 / 模型 API Key / 可选 Telegram 代理 // ============================================================ var HOME = process.env.OPENCLAW_HOME || "/home/user"; var STATE_DIR = path.join(HOME, ".openclaw"); var CONFIG_PATH = path.join(STATE_DIR, "openclaw.json"); var TEMPLATE = "/app/openclaw-template.json"; var LEGACY = process.env.LEGACY_CONFIG || "/data/.openclaw/openclaw.json"; var LEGACY_WS = "/data/.openclaw/workspace"; function log(m) { console.log("[setup] " + m); } function err(m) { console.error("[setup] ERROR: " + m); } function envStr(k) { return (process.env[k] || "").trim(); } function loadJson(p) { try { return JSON.parse(fs.readFileSync(p, "utf-8").trim()); } catch (e) { return null; } } function writableDir(d) { try { fs.mkdirSync(d, { recursive: true }); fs.accessSync(d, fs.constants.W_OK); return true; } catch (e) { return false; } } function stripKey(o, k) { if (Array.isArray(o)) { o.forEach(function (x) { stripKey(x, k); }); } else if (o && typeof o === "object") { delete o[k]; Object.keys(o).forEach(function (kk) { stripKey(o[kk], k); }); } } function mapStrings(o, from, to) { if (Array.isArray(o)) { o.forEach(function (x, i) { if (typeof x === "string") { if (x.indexOf(from) === 0) o[i] = to + x.slice(from.length); } else mapStrings(x, from, to); }); } else if (o && typeof o === "object") { Object.keys(o).forEach(function (k) { var v = o[k]; if (typeof v === "string") { if (v.indexOf(from) === 0) o[k] = to + v.slice(from.length); } else mapStrings(v, from, to); }); } } function collectStrings(o, prefix, out) { if (Array.isArray(o)) o.forEach(function (x) { collectStrings(x, prefix, out); }); else if (o && typeof o === "object") Object.keys(o).forEach(function (k) { collectStrings(o[k], prefix, out); }); else if (typeof o === "string" && o.indexOf(prefix) === 0) out.push(o); } log("Starting... HOME=" + HOME); var gatewayToken = envStr("OPENCLAW_GATEWAY_TOKEN"); var config = null, mode = ""; var legacy = loadJson(LEGACY); if (legacy) { config = legacy; mode = "legacy bucket config (read-only import)"; } else { var tpl = loadJson(TEMPLATE); if (tpl) { config = tpl; mode = "template"; } } if (!config) { config = { gateway: {}, agents: { defaults: {} }, env: { vars: {} } }; mode = "fresh"; } // ── gateway ── config.gateway = config.gateway || {}; config.gateway.mode = "local"; config.gateway.port = 7860; config.gateway.bind = "lan"; if (gatewayToken) { config.gateway.auth = { mode: "token", token: gatewayToken }; if (!/^[A-Za-z0-9]{32,}$/.test(gatewayToken)) log("WARN: OPENCLAW_GATEWAY_TOKEN should be 32+ letters/digits"); } else if (config.gateway.auth && (config.gateway.auth.token || config.gateway.auth.password) && config.gateway.auth.token !== "WILL_BE_SET_BY_ENV") { log("WARN: OPENCLAW_GATEWAY_TOKEN not set - keeping the token from the imported config (rotate it!)"); } else { err("FATAL: set OPENCLAW_GATEWAY_TOKEN in Space Secrets"); process.exit(0); } config.gateway.controlUi = config.gateway.controlUi || {}; config.gateway.controlUi.allowInsecureAuth = true; config.gateway.controlUi.allowedOrigins = ["*"]; config.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback = true; delete config.gateway.controlUi.dangerouslyDisableDeviceAuth; // 新版已废弃 var host = envStr("SPACE_HOST"); if (host && !config.gateway.publicOrigin) config.gateway.publicOrigin = "https://" + host; config.gateway.trustedProxies = [ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.1" ]; // ── agents: 去掉 agentDir, 工作区(纯文件)可留在 bucket, 不可写则落本地 ── config.agents = config.agents || {}; config.agents.defaults = config.agents.defaults || {}; stripKey(config.agents, "agentDir"); var wsOnBucket = envStr("WORKSPACE_ON_BUCKET") !== "0" && writableDir(LEGACY_WS); var localWs = path.join(STATE_DIR, "workspace"); if (!wsOnBucket) mapStrings(config.agents, LEGACY_WS, localWs); if (!config.agents.defaults.workspace || config.agents.defaults.workspace === "WILL_BE_SET_BY_ENV") { config.agents.defaults.workspace = wsOnBucket ? LEGACY_WS : localWs; } var wsList = []; collectStrings(config.agents, wsOnBucket ? LEGACY_WS : localWs, wsList); wsList.forEach(function (d) { try { fs.mkdirSync(d, { recursive: true }); } catch (e) { /* ignore */ } }); if (!config.agents.defaults.model || config.agents.defaults.model === "WILL_BE_SET_BY_ENV") { config.agents.defaults.model = envStr("OPENCLAW_HF_DEFAULT_MODEL") || "groq/llama-3.3-70b-versatile"; } log("workspace on " + (wsOnBucket ? "bucket (/data)" : "local disk") + ", dirs: " + wsList.length); // seed SOUL/MEMORY if missing var mainEntry = config.agents.entries && config.agents.entries.main; var mainWs = (mainEntry && mainEntry.workspace) || config.agents.defaults.workspace; try { fs.mkdirSync(path.join(mainWs, "memory"), { recursive: true }); var soul = path.join(mainWs, "SOUL.md"); if (!fs.existsSync(soul)) { fs.writeFileSync(soul, "# Soul\n\nYou are a helpful, warm, concise AI assistant.\nDefault language: Simplified Chinese.\n", "utf-8"); } var mem = path.join(mainWs, "MEMORY.md"); if (!fs.existsSync(mem)) fs.writeFileSync(mem, "# Long-term Memory\n", "utf-8"); } catch (e) { log("seed skipped: " + e.message); } // ── 模型 Key: 只在 Secrets 里有值时才覆盖 ── var 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-copilot": "GITHUB_MODELS_API_KEY", github: "GITHUB_MODELS_API_KEY", openai: "OPENAI_API_KEY", anthropic: "ANTHROPIC_API_KEY" }; var providers = (config.models && config.models.providers) || {}; Object.keys(providers).forEach(function (name) { var ek = MAP[name.toLowerCase()]; var v = ek ? envStr(ek) : ""; if (v) { providers[name].apiKey = v; log("Provider key from Secrets: " + name); } else if (providers[name].apiKey === "WILL_BE_SET_BY_ENV") { delete providers[name].apiKey; log("Provider key missing: " + name); } }); config.env = config.env || {}; config.env.vars = config.env.vars || {}; Object.keys(process.env).forEach(function (k) { if (/^(OPENCLAW_|SPACE_|SYSTEM_|HF_|NODE_|npm_)/.test(k)) return; if (/(_API_KEY|_SECRET_KEY|_ACCESS_TOKEN|_AUTH_TOKEN|_APP_KEY)$/.test(k) && envStr(k)) config.env.vars[k] = envStr(k); }); // ── 没有对应插件的自定义提供商(如 siliconflow)必须声明 api, 否则 models.json 加载失败 ── var KNOWN_API = ["openrouter", "deepseek", "groq", "minimax", "zai", "anthropic", "openai", "google", "gemini", "github", "github-copilot"]; Object.keys(providers).forEach(function (name) { var p = providers[name]; if (KNOWN_API.indexOf(name.toLowerCase()) >= 0 || p.api) return; var ms = p.models || []; if (ms.length && ms.every(function (m) { return m && m.api; })) return; p.api = "openai-completions"; log("api=openai-completions set for provider: " + name); }); // ── Telegram ── var tg = config.channels && config.channels.telegram; var tgToken = envStr("TELEGRAM_BOT_TOKEN"); if (!tg && tgToken) { config.channels = config.channels || {}; tg = config.channels.telegram = { enabled: true, accounts: { main: { botToken: tgToken, apiRoot: "https://api.telegram.org" } } }; } var accNames = tg && tg.accounts ? Object.keys(tg.accounts) : []; var proxy = envStr("TELEGRAM_PROXY"); if (tg && proxy) { accNames.forEach(function (n) { tg.accounts[n].proxy = proxy; }); log("Telegram proxy set on " + accNames.length + " account(s)"); } var apiRoot = envStr("TELEGRAM_API_ROOT"); if (tg && apiRoot) accNames.forEach(function (n) { tg.accounts[n].apiRoot = apiRoot; }); if (tg && accNames.length && !tg.defaultAccount) tg.defaultAccount = accNames.indexOf("custom-1") >= 0 ? "custom-1" : accNames[0]; log("Telegram accounts: " + (accNames.join(", ") || "none")); // ── 写出 ── fs.mkdirSync(STATE_DIR, { recursive: true }); fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 0o600 }); log("Done. base = " + mode + " | auth = token(" + (gatewayToken ? "env" : "imported") + ") | providers = " + Object.keys(providers).length);