suing / setup-hf-config.mjs
Subsequently's picture
Update setup-hf-config.mjs
30aed71 verified
Raw History Blame Contribute Delete
8.73 kB
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);