setup-config.js
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
// setup-config.js — Generates all config files for OpenVoiceUI Pinokio install.
// Reads API keys from PINOKIO_* environment variables (set by install.js env: block).
// This avoids the broken json.set / pinokio-input.json flow entirely.
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function getKey(envName) {
// install.js passes keys as PINOKIO_<KEY_NAME> via env
const v = process.env["PINOKIO_" + envName] || "";
if (!v || v === "undefined" || v === "null" || v.startsWith("{{")) return "";
return v.trim();
}
const PORT = getKey("PORT") || "5001";
const token = crypto.randomBytes(24).toString("hex");
const secret = crypto.randomBytes(32).toString("hex");
// ---------------------------------------------------------------------------
// 1. Write openclaw.json (nested gateway/agents format for v2026.3.2+)
// ---------------------------------------------------------------------------
const openclawConfig = {
gateway: {
mode: "local",
port: 18791,
bind: "lan",
auth: { mode: "token", token: token },
trustedProxies: ["127.0.0.1", "172.16.0.0/12", "10.0.0.0/8"],
controlUi: {
allowInsecureAuth: true,
dangerouslyDisableDeviceAuth: true,
},
},
agents: {
defaults: {
// No model forced here — OpenClaw auto-selects based on available API keys
// in auth-profiles.json. User's provided keys drive model selection.
thinkingDefault: "off",
blockStreamingDefault: "on",
blockStreamingBreak: "text_end",
timeoutSeconds: 120,
memorySearch: { enabled: true },
compaction: {
mode: "default",
reserveTokens: 80000,
keepRecentTokens: 8000,
reserveTokensFloor: 80000,
memoryFlush: { enabled: true, softThresholdTokens: 6000 },
},
contextPruning: {
mode: "cache-ttl",
ttl: "30m",
keepLastAssistants: 3,
softTrimRatio: 0.3,
hardClearRatio: 0.5,
},
},
list: [{ id: "main", default: true, workspace: "/root/.openclaw/workspace" }],
},
// Plugins configured by individual plugin installers
};
fs.mkdirSync("openclaw-data/workspace", { recursive: true });
// Mountpoint for the bundled agent skills (docker-compose mounts ./skills here read-only).
// Created now so Docker does not create openclaw-data/skills root-owned on Linux, which would
// stop you adding your own skills next to it. See docs/SKILLS.md.
fs.mkdirSync("openclaw-data/skills/openvoiceui", { recursive: true });
fs.writeFileSync(
"openclaw-data/openclaw.json",
JSON.stringify(openclawConfig, null, 2) + "\n"
);
console.log(" Wrote openclaw-data/openclaw.json");
// ---------------------------------------------------------------------------
// 2. Write .env
// ---------------------------------------------------------------------------
const envLines = [
"# OpenVoiceUI — generated by Pinokio installer",
`PORT=${PORT}`,
"DOMAIN=localhost",
`SECRET_KEY=${secret}`,
"",
"# OpenClaw Gateway",
"CLAWDBOT_GATEWAY_URL=ws://127.0.0.1:18791",
`CLAWDBOT_AUTH_TOKEN=${token}`,
"GATEWAY_SESSION_KEY=voice-main-1",
"",
"# AI Provider Keys",
];
const envKeyList = [
"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY",
"OPENROUTER_API_KEY", "MISTRAL_API_KEY", "XAI_API_KEY", "ZAI_API_KEY",
"CEREBRAS_API_KEY", "TOGETHER_API_KEY", "HF_TOKEN",
"MOONSHOT_API_KEY", "KIMI_API_KEY", "MINIMAX_API_KEY", "QIANFAN_API_KEY",
"MODELSTUDIO_API_KEY", "XIAOMI_API_KEY", "VOLCANO_ENGINE_API_KEY",
"BYTEPLUS_API_KEY", "SYNTHETIC_API_KEY", "VENICE_API_KEY",
"OPENCODE_ZEN_API_KEY", "KILOCODE_API_KEY", "AI_GATEWAY_API_KEY",
"CLOUDFLARE_AI_GATEWAY_API_KEY", "LITELLM_API_KEY", "SUNO_API_KEY",
"ELEVENLABS_API_KEY", "ELEVENLABS_VOICE_ID",
"RESEMBLE_API_KEY", "RESEMBLE_VOICE_UUID",
];
for (const k of envKeyList) envLines.push(`${k}=${getKey(k)}`);
envLines.push(
"",
"# TTS",
`GROQ_API_KEY=${getKey("GROQ_API_KEY")}`,
"USE_GROQ=true",
"USE_GROQ_TTS=true",
"",
"# STT",
`DEEPGRAM_API_KEY=${getKey("DEEPGRAM_API_KEY")}`,
"",
"# Supertonic TTS",
"SUPERTONIC_API_URL=http://supertonic:8765"
);
fs.writeFileSync(".env", envLines.join("\n") + "\n");
console.log(" Wrote .env");
// ---------------------------------------------------------------------------
// 3. Write auth-profiles.json (so OpenClaw has working providers on first start)
// ---------------------------------------------------------------------------
const providerMap = {
ANTHROPIC_API_KEY: "anthropic",
OPENAI_API_KEY: "openai",
GEMINI_API_KEY: "google",
OPENROUTER_API_KEY: "openrouter",
MISTRAL_API_KEY: "mistral",
XAI_API_KEY: "xai",
ZAI_API_KEY: "zai",
CEREBRAS_API_KEY: "cerebras",
TOGETHER_API_KEY: "together",
HF_TOKEN: "huggingface",
GROQ_API_KEY: "groq",
DEEPGRAM_API_KEY: "deepgram",
MOONSHOT_API_KEY: "moonshot",
KIMI_API_KEY: "kimi-coding",
MINIMAX_API_KEY: "minimax",
QIANFAN_API_KEY: "qianfan",
MODELSTUDIO_API_KEY: "modelstudio",
XIAOMI_API_KEY: "xiaomi",
VOLCANO_ENGINE_API_KEY: "volcano-engine",
BYTEPLUS_API_KEY: "byteplus",
SYNTHETIC_API_KEY: "synthetic",
VENICE_API_KEY: "venice",
OPENCODE_ZEN_API_KEY: "opencode",
KILOCODE_API_KEY: "kilocode",
AI_GATEWAY_API_KEY: "ai-gateway",
CLOUDFLARE_AI_GATEWAY_API_KEY: "cloudflare-ai-gateway",
LITELLM_API_KEY: "litellm",
SUNO_API_KEY: "suno",
ELEVENLABS_API_KEY: "elevenlabs",
};
const authProfiles = {};
let keyCount = 0;
for (const [envVar, providerId] of Object.entries(providerMap)) {
const key = getKey(envVar);
if (key) {
authProfiles[providerId] = [
{
id: "default",
type: "api_key",
key,
lastUsed: null,
disabled: false,
cooldown: null,
},
];
keyCount++;
}
}
const agentDir = "openclaw-data/agents/main/agent";
fs.mkdirSync(agentDir, { recursive: true });
fs.writeFileSync(
path.join(agentDir, "auth-profiles.json"),
JSON.stringify(authProfiles, null, 2) + "\n"
);
console.log(` Wrote auth-profiles.json (${keyCount} provider(s))`);
// ---------------------------------------------------------------------------
// 4. Pre-generate device identity and pre-pair it
// ---------------------------------------------------------------------------
// OpenClaw requires device pairing for WebSocket connections. Even with
// dangerouslyDisableDeviceAuth:true, the gateway still requires pairing for
// the WS protocol (that flag only affects the control UI).
//
// Fix: generate an Ed25519 keypair at install time, register the public key
// in OpenClaw's devices/paired.json, and save the full identity so start.js
// can inject it into the OpenVoiceUI container via docker exec.
// Generate Ed25519 keypair
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
// Raw 32-byte public key — extract via JWK.x (most reliable cross-platform)
const devJwk = publicKey.export({ format: "jwk" });
const rawPub = Buffer.from(devJwk.x, "base64url");
// deviceId = SHA256(raw public key) — matches Python's hashlib.sha256(raw_pub).hexdigest()
const deviceId = crypto.createHash("sha256").update(rawPub).digest("hex");
// PEM formats — OpenVoiceUI Python client uses PEM for signing
const pubPem = publicKey.export({ type: "spki", format: "pem" });
const privPem = privateKey.export({ type: "pkcs8", format: "pem" });
// base64url of raw Ed25519 bytes — this is what the gateway compares during WS handshake
const pubB64url = rawPub.toString("base64url");
// Save full identity for injection into OpenVoiceUI container at start time
// (Python client reads PEM format for Ed25519 signing)
const deviceIdentity = {
deviceId: deviceId,
publicKeyPem: pubPem,
privateKeyPem: privPem,
};
fs.writeFileSync(
"openclaw-data/pre-paired-device.json",
JSON.stringify(deviceIdentity, null, 2) + "\n"
);
console.log(` Generated device identity: ${deviceId.slice(0, 16)}...`);
// Pre-register in OpenClaw's devices/paired.json so it accepts this device.
// Format must match what approveDevicePairing() writes:
// - publicKey: base64url of raw Ed25519 bytes (NOT PEM — gateway compares literally)
// - role/roles/scopes/approvedScopes: authorization metadata
// - tokens: per-role auth tokens for verifyDeviceToken() calls
fs.mkdirSync("openclaw-data/devices", { recursive: true });
const nowMs = Date.now();
const pairingToken = crypto.randomBytes(32).toString("hex");
const pairedDevices = {};
pairedDevices[deviceId] = {
deviceId: deviceId,
publicKey: pubB64url,
displayName: "pinokio-openvoiceui",
platform: "linux",
clientId: "cli",
clientMode: "cli",
role: "operator",
roles: ["operator"],
scopes: ["operator.admin", "operator.read", "operator.write"],
approvedScopes: ["operator.admin", "operator.read", "operator.write"],
tokens: {
operator: {
token: pairingToken,
role: "operator",
scopes: ["operator.admin", "operator.read", "operator.write"],
createdAtMs: nowMs,
},
},
createdAtMs: nowMs,
approvedAtMs: nowMs,
};
fs.writeFileSync(
"openclaw-data/devices/paired.json",
JSON.stringify(pairedDevices, null, 2) + "\n"
);
// Clear pending.json — stale entries with silent:false permanently block auto-approval
fs.writeFileSync("openclaw-data/devices/pending.json", "{}\n");
console.log(" Wrote devices/paired.json (pre-paired)");
// ---------------------------------------------------------------------------
// Done
// ---------------------------------------------------------------------------
console.log("\n Configuration complete!\n");