Files

1 line
38 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "chengjiaxi/bailongma", "files": {"config.json": {"48282ce15c02724f0b60bdd28e45f31d87437fb3": {"t": 1785746454.6562283, "n": 699, "h": "336bb32929fe94b5", "c": "{\n \"clawbot\": {},\n \"voice\": {\n \"whisperModel\": \"small\",\n \"voiceProvider\": \"volcengine\",\n \"volcAsrApiKey\": \"0f9a6c2b-8d91-4f2b-92b0-531c357b24da\",\n \"volcAsrResourceId\": \"volc.bigasr.sauc.duration\"\n },\n \"tts\": {\n \"ttsProvider\": \"doubao\",\n \"ttsVoiceId\": \"zh_female_xiaohe_uranus_bigtts\",\n \"doubaoKey\": \"0f9a6c2b-8d91-4f2b-92b0-531c357b24da\"\n },\n \"embedding\": {},\n \"provider\": \"deepseek\",\n \"apiKey\": \"sk-cb56ef2c8e62444d9d03e357e1f1688b\",\n \"model\": \"deepseek-reasoner\",\n \"activatedAt\": \"2026-06-06T00:52:58.300Z\",\n \"social\": {},\n \"security\": {\n \"fileSandbox\": false,\n \"execSandbox\": false,\n \"blockedTools\": [],\n \"updatedAt\": \"2026-06-06T12:00:07+08:00\"\n }\n}"}}, "src/config.js": {"48282ce15c02724f0b60bdd28e45f31d87437fb3": {"t": 1785746679.8649602, "n": 35590, "h": "37ef21610c50e9b9", "c": "import fs from 'fs'\nimport { paths } from './paths.js'\nimport { nowTimestamp } from './time.js'\n\nexport const DEEPSEEK_PROVIDER = 'deepseek'\nexport const MINIMAX_PROVIDER = 'minimax'\nexport const OPENAI_PROVIDER = 'openai'\nexport const QWEN_PROVIDER = 'qwen'\nexport const MOONSHOT_PROVIDER = 'moonshot'\nexport const ZHIPU_PROVIDER = 'zhipu'\nexport const MIMO_PROVIDER = 'mimo'\n\nexport const DEFAULT_DEEPSEEK_MODEL = 'deepseek-v4-pro'\nexport const DEFAULT_MINIMAX_MODEL = 'MiniMax-M2.7'\nexport const DEFAULT_OPENAI_MODEL = 'gpt-4o-mini'\nexport const DEFAULT_QWEN_MODEL = 'qwen-turbo'\nexport const DEFAULT_MOONSHOT_MODEL = 'moonshot-v1-8k'\nexport const DEFAULT_ZHIPU_MODEL = 'glm-4-flash'\nexport const DEFAULT_MIMO_MODEL = 'mimo-v2.5'\n\nexport const DEEPSEEK_MODELS = [\n {\n id: 'deepseek-v4-flash',\n label: 'deepseek-v4-flash',\n deprecated: false,\n },\n {\n id: 'deepseek-v4-pro',\n label: 'deepseek-v4-pro',\n deprecated: false,\n },\n {\n id: 'deepseek-chat',\n label: 'deepseek-chat (deprecated 2026/07/24)',\n deprecated: true,\n },\n {\n id: 'deepseek-reasoner',\n label: 'deepseek-reasoner (deprecated 2026/07/24)',\n deprecated: true,\n },\n]\n\nexport const MINIMAX_MODELS = [\n {\n id: 'MiniMax-M2.7',\n label: 'MiniMax-M2.7',\n deprecated: false,\n },\n {\n id: 'MiniMax-M1',\n label: 'MiniMax-M1',\n deprecated: false,\n },\n]\n\nexport const OPENAI_MODELS = [\n {\n id: 'gpt-4o-mini',\n label: 'gpt-4o-mini',\n deprecated: false,\n },\n {\n id: 'gpt-4o',\n label: 'gpt-4o',\n deprecated: false,\n },\n]\n\nexport const QWEN_MODELS = [\n {\n id: 'qwen-turbo',\n label: 'qwen-turbo',\n deprecated: false,\n },\n {\n id: 'qwen-plus',\n label: 'qwen-plus',\n deprecated: false,\n },\n]\n\nexport const MOONSHOT_MODELS = [\n {\n id: 'moonshot-v1-8k',\n label: 'moonshot-v1-8k',\n deprecated: false,\n },\n {\n id: 'moonshot-v1-32k',\n label: 'moonshot-v1-32k',\n deprecated: false,\n },\n]\n\nexport const ZHIPU_MODELS = [\n {\n id: 'glm-4-flash',\n label: 'glm-4-flash',\n deprecated: false,\n },\n {\n id: 'glm-4-plus',\n label: 'glm-4-plus',\n deprecated: false,\n },\n]\n\nexport const MIMO_MODELS = [\n {\n id: 'mimo-v2.5',\n label: 'MiMo-V2.5',\n deprecated: false,\n },\n {\n id: 'mimo-v2.5-pro',\n label: 'MiMo-V2.5-Pro',\n deprecated: false,\n },\n {\n id: 'mimo-v2-pro',\n label: 'MiMo-V2-Pro',\n deprecated: false,\n },\n {\n id: 'mimo-v2-flash',\n label: 'MiMo-V2-Flash',\n deprecated: false,\n },\n]\n\nconst PROVIDER_CONFIG = {\n [DEEPSEEK_PROVIDER]: {\n label: 'DeepSeek',\n baseURL: 'https://api.deepseek.com',\n envVar: 'DEEPSEEK_API_KEY',\n models: DEEPSEEK_MODELS,\n defaultModel: DEFAULT_DEEPSEEK_MODEL,\n },\n [MINIMAX_PROVIDER]: {\n label: 'MiniMax',\n baseURL: 'https://api.minimax.chat/v1',\n envVar: 'MINIMAX_API_KEY',\n models: MINIMAX_MODELS,\n defaultModel: DEFAULT_MINIMAX_MODEL,\n },\n [OPENAI_PROVIDER]: {\n label: 'OpenAI',\n baseURL: 'https://api.openai.com/v1',\n envVar: 'OPENAI_API_KEY',\n models: OPENAI_MODELS,\n defaultModel: DEFAULT_OPENAI_MODEL,\n },\n [QWEN_PROVIDER]: {\n label: 'Qwen',\n baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n envVar: 'DASHSCOPE_API_KEY',\n models: QWEN_MODELS,\n defaultModel: DEFAULT_QWEN_MODEL,\n },\n [MOONSHOT_PROVIDER]: {\n label: 'Moonshot',\n baseURL: 'https://api.moonshot.cn/v1',\n envVar: 'MOONSHOT_API_KEY',\n models: MOONSHOT_MODELS,\n defaultModel: DEFAULT_MOONSHOT_MODEL,\n },\n [ZHIPU_PROVIDER]: {\n label: 'Zhipu',\n baseURL: 'https://open.bigmodel.cn/api/paas/v4',\n envVar: 'ZHIPU_API_KEY',\n models: ZHIPU_MODELS,\n defaultModel: DEFAULT_ZHIPU_MODEL,\n },\n [MIMO_PROVIDER]: {\n label: '小米 MiMo',\n baseURL: 'https://api.xiaomimimo.com/v1',\n envVar: 'MIMO_API_KEY',\n models: MIMO_MODELS,\n defaultModel: DEFAULT_MIMO_MODEL,\n },\n}\n\nconst AUTO_PROVIDER = 'auto'\nconst PROBE_TIMEOUT_MS = 12000\n\nfunction normalizeModel(model, provider = DEEPSEEK_PROVIDER) {\n const pConfig = PROVIDER_CONFIG[provider] || PROVIDER_CONFIG[DEEPSEEK_PROVIDER]\n const value = String(model || '').trim()\n const validIds = new Set(pConfig.models.map(m => m.id))\n if (validIds.has(value)) return value\n return pConfig.defaultModel\n}\n\nfunction isThinkingEnabledForModel(model) {\n return normalizeModel(model) !== 'deepseek-chat'\n}\n\nfunction getProvidersForAutoDetect() {\n return Object.entries(PROVIDER_CONFIG)\n}\n\nfunction getProviderErrorMessage(err) {\n const status = err?.status ?? err?.response?.status\n const message = err?.message || String(err)\n return status ? `${status} ${message}` : message\n}\n\nfunction withTimeout(promise, ms, label) {\n let timer\n const timeout = new Promise((_, reject) => {\n timer = setTimeout(() => reject(new Error(`${label} timeout after ${ms}ms`)), ms)\n })\n return Promise.race([promise, timeout]).finally(() => clearTimeout(timer))\n}\n\nfunction buildPingParams(provider, model) {\n const pingParams = {\n model,\n messages: [{ role: 'user', content: 'Reply with exactly: hello' }],\n max_tokens: 8,\n temperature: 0,\n stream: false,\n }\n if (provider === DEEPSEEK_PROVIDER) {\n pingParams.reasoning_effort = 'high'\n pingParams.thinking = { type: isThinkingEnabledForModel(model) ? 'enabled' : 'disabled' }\n }\n return pingParams\n}\n\nasync function probeProvider(OpenAI, provider, apiKey, requestedModel) {\n const pConfig = PROVIDER_CONFIG[provider]\n const model = normalizeModel(requestedModel, provider)\n const client = new OpenAI({\n apiKey,\n baseURL: pConfig.baseURL,\n timeout: PROBE_TIMEOUT_MS,\n })\n await withTimeout(\n client.chat.completions.create(buildPingParams(provider, model)),\n PROBE_TIMEOUT_MS,\n provider,\n )\n return { provider, model, pConfig }\n}\n\nasync function detectProvider(OpenAI, apiKey, requestedModel) {\n const providers = getProvidersForAutoDetect()\n const errors = []\n\n return await new Promise((resolve, reject) => {\n let pending = providers.length\n for (const [provider] of providers) {\n probeProvider(OpenAI, provider, apiKey, requestedModel)\n .then(resolve)\n .catch((err) => {\n errors.push(`${provider}: ${getProviderErrorMessage(err)}`)\n pending -= 1\n if (pending === 0) {\n reject(new Error(`Could not identify the provider for this API key. Tried: ${providers.map(([name]) => name).join(', ')}. Last errors: ${errors.slice(-3).join(' | ')}`))\n }\n })\n }\n })\n}\n\nfunction readStoredConfig() {\n try {\n if (!fs.existsSync(paths.configFile)) return null\n const raw = fs.readFileSync(paths.configFile, 'utf-8')\n const parsed = JSON.parse(raw)\n if (!parsed || typeof parsed !== 'object') return null\n if (!parsed.provider) return null\n if (parsed.provider === 'custom') {\n if (!parsed.baseURL || typeof parsed.baseURL !== 'string') return null\n if (!parsed.model || typeof parsed.model !== 'string') return null\n return parsed\n }\n if (!PROVIDER_CONFIG[parsed.provider]) return null\n if (!parsed.apiKey || typeof parsed.apiKey !== 'string') return null\n return parsed\n } catch {\n return null\n }\n}\n\nfunction writeStoredConfig(obj) {\n const tmp = paths.configFile + '.tmp'\n fs.writeFileSync(tmp, JSON.stringify(obj, null, 2), 'utf-8')\n fs.renameSync(tmp, paths.configFile)\n}\n\n// 读出 config.json 现有内容(失败返回空对象)。\n// activate() 等写操作必须基于它合并,否则会抹掉 voice/tts/security 等其它字段。\nfunction readExistingStoredConfig() {\n try { return JSON.parse(fs.readFileSync(paths.configFile, 'utf-8')) || {} }\n catch { return {} }\n}\n\nfunction shouldAllowEnvFallback() {\n return !process.versions?.electron\n}\n\nfunction loadFromEnv() {\n const deepseekKey = process.env['DEEPSEEK_API_KEY']\n if (deepseekKey) {\n return {\n provider: DEEPSEEK_PROVIDER,\n apiKey: deepseekKey,\n model: normalizeModel(process.env.DEEPSEEK_MODEL, DEEPSEEK_PROVIDER),\n }\n }\n const minimaxKey = process.env['MINIMAX_API_KEY']\n if (minimaxKey) {\n return {\n provider: MINIMAX_PROVIDER,\n apiKey: minimaxKey,\n model: normalizeModel(process.env.MINIMAX_MODEL, MINIMAX_PROVIDER),\n }\n }\n for (const [provider, pConfig] of Object.entries(PROVIDER_CONFIG)) {\n if (provider === DEEPSEEK_PROVIDER || provider === MINIMAX_PROVIDER) continue\n const key = process.env[pConfig.envVar]\n if (key) {\n return {\n provider,\n apiKey: key,\n model: normalizeModel(process.env[`${pConfig.envVar.replace(/_API_KEY$/, '')}_MODEL`], provider),\n }\n }\n }\n return null\n}\n\nfunction applyConfig(provider, apiKey, model, customBaseURL) {\n if (provider === 'custom') {\n config.provider = 'custom'\n config.model = String(model || '').trim()\n config.apiKey = apiKey || 'none'\n config.baseURL = String(customBaseURL || '').trim()\n config.needsActivation = false\n return\n }\n const pConfig = PROVIDER_CONFIG[provider]\n config.provider = provider\n config.model = normalizeModel(model, provider)\n config.apiKey = apiKey\n config.baseURL = pConfig.baseURL\n config.needsActivation = false\n}\n\nexport const config = {\n tickInterval: 20 * 60 * 1000,\n provider: null,\n model: null,\n apiKey: null,\n baseURL: null,\n needsActivation: true,\n temperature: 0.5,\n security: {\n fileSandbox: true,\n execSandbox: true,\n blockedTools: [],\n updatedAt: null,\n },\n}\n\nconst stored = readStoredConfig()\nif (stored) {\n applyConfig(stored.provider, stored.apiKey, stored.model, stored.baseURL)\n if (typeof stored.temperature === 'number' && stored.temperature >= 0 && stored.temperature <= 2) {\n config.temperature = stored.temperature\n }\n if (stored.security && typeof stored.security === 'object') {\n if (typeof stored.security.fileSandbox === 'boolean') config.security.fileSandbox = stored.security.fileSandbox\n if (typeof stored.security.execSandbox === 'boolean') config.security.execSandbox = stored.security.execSandbox\n if (Array.isArray(stored.security.blockedTools)) config.security.blockedTools = stored.security.blockedTools\n if (typeof stored.security.updatedAt === 'string') config.security.updatedAt = stored.security.updatedAt\n }\n} else if (shouldAllowEnvFallback()) {\n const fromEnv = loadFromEnv()\n if (fromEnv) applyConfig(fromEnv.provider, fromEnv.apiKey, fromEnv.model)\n}\n\n// At startup, copy social credentials from the config file into process.env so connectors can read them\n;(function loadSocialEnv() {\n try {\n const raw = fs.readFileSync(paths.configFile, 'utf-8')\n const social = JSON.parse(raw)?.social || {}\n for (const [key, val] of Object.entries(social)) {\n if (typeof val === 'string' && val && globalThis.process?.env) {\n globalThis.process.env[key] = val\n }\n }\n } catch {}\n})()\n\nexport async function activate({ provider = AUTO_PROVIDER, apiKey, model, baseURL }) {\n const p = String(provider || AUTO_PROVIDER).toLowerCase()\n\n if (p === 'custom') {\n const normalizedBaseURL = String(baseURL || '').trim()\n if (!normalizedBaseURL) throw new Error('Custom endpoint requires a Base URL')\n const normalizedModel = String(model || '').trim()\n if (!normalizedModel) throw new Error('Custom endpoint requires a model name')\n const normalizedKey = String(apiKey || '').trim() || 'none'\n\n const { default: OpenAI } = await import('openai')\n const client = new OpenAI({ apiKey: normalizedKey, baseURL: normalizedBaseURL, timeout: PROBE_TIMEOUT_MS })\n try {\n await withTimeout(\n client.chat.completions.create({\n model: normalizedModel,\n messages: [{ role: 'user', content: 'Reply with exactly: hello' }],\n max_tokens: 16,\n temperature: 0,\n stream: false,\n }),\n PROBE_TIMEOUT_MS,\n 'custom',\n )\n } catch (err) {\n const message = err?.message || String(err)\n throw new Error(`Custom endpoint connection failed: ${message}`)\n }\n\n applyConfig('custom', normalizedKey, normalizedModel, normalizedBaseURL)\n writeStoredConfig({\n ...readExistingStoredConfig(), // 保留 voice/tts/security 等其它字段\n provider: 'custom',\n apiKey: normalizedKey,\n model: normalizedModel,\n baseURL: normalizedBaseURL,\n activatedAt: new Date().toISOString(),\n })\n return {\n provider: 'custom',\n model: normalizedModel,\n models: [{ id: normalizedModel, label: normalizedModel, deprecated: false }],\n }\n }\n\n const pConfig = PROVIDER_CONFIG[p]\n if (p !== AUTO_PROVIDER && !pConfig) {\n throw new Error(`Unsupported provider: \"${p}\". Available: ${Object.keys(PROVIDER_CONFIG).join(', ')}`)\n }\n\n const normalizedKey = String(apiKey || '').trim()\n const normalizedModel = normalizeModel(model, p)\n if (normalizedKey.length < 8) {\n throw new Error(`${p} key is invalid`)\n }\n\n const { default: OpenAI } = await import('openai')\n if (p === AUTO_PROVIDER) {\n const detected = await detectProvider(OpenAI, normalizedKey, model)\n applyConfig(detected.provider, normalizedKey, detected.model)\n writeStoredConfig({\n ...readExistingStoredConfig(), // 保留其它字段\n provider: detected.provider,\n apiKey: normalizedKey,\n model: detected.model,\n baseURL: undefined, // 非 custom:清掉可能残留的旧 baseURL\n activatedAt: new Date().toISOString(),\n })\n return {\n provider: detected.provider,\n model: detected.model,\n models: detected.pConfig.models,\n }\n }\n\n const client = new OpenAI({ apiKey: normalizedKey, baseURL: pConfig.baseURL, timeout: PROBE_TIMEOUT_MS })\n\n try {\n await withTimeout(\n client.chat.completions.create(buildPingParams(p, normalizedModel)),\n PROBE_TIMEOUT_MS,\n p,\n )\n } catch (err) {\n const message = err?.message || String(err)\n if (/401|unauthoriz|invalid.*api.*key|authentication/i.test(message)) {\n throw new Error(`${p} key validation failed — please check that the key is correct`)\n }\n throw new Error(`${p} validation failed: ${message}`)\n }\n\n applyConfig(p, normalizedKey, normalizedModel)\n writeStoredConfig({\n ...readExistingStoredConfig(), // 保留 voice/tts/security 等其它字段\n provider: p,\n apiKey: normalizedKey,\n model: normalizedModel,\n baseURL: undefined, // 非 custom:清掉可能残留的旧 baseURL\n activatedAt: new Date().toISOString(),\n })\n\n return {\n provider: p,\n model: normalizedModel,\n models: pConfig.models,\n }\n}\n\nexport function getActivationStatus() {\n const pConfig = config.provider && config.provider !== 'custom' ? PROVIDER_CONFIG[config.provider] : null\n const customModels = config.model ? [{ id: config.model, label: config.model, deprecated: false }] : DEEPSEEK_MODELS\n return {\n activated: !config.needsActivation,\n provider: config.provider,\n model: config.model,\n baseURL: config.provider === 'custom' ? config.baseURL : undefined,\n models: pConfig ? pConfig.models : customModels,\n defaultModel: pConfig ? pConfig.defaultModel : (config.model || DEFAULT_DEEPSEEK_MODEL),\n }\n}\n\nexport function getProviderSummaries() {\n const result = Object.fromEntries(Object.entries(PROVIDER_CONFIG).map(([name, pConfig]) => [\n name,\n {\n label: pConfig.label || name,\n models: pConfig.models,\n defaultModel: pConfig.defaultModel,\n },\n ]))\n result.custom = { label: 'Custom Endpoint', models: [], defaultModel: '' }\n return result\n}\n\nexport function deactivate() {\n try {\n if (fs.existsSync(paths.configFile)) fs.unlinkSync(paths.configFile)\n } catch {}\n config.provider = null\n config.model = null\n config.apiKey = null\n config.baseURL = null\n config.needsActivation = true\n}\n\nexport function switchModel(model) {\n if (!config.apiKey) throw new Error('Not activated — cannot switch model')\n if (config.provider === 'custom') {\n const trimmed = String(model || '').trim()\n if (!trimmed) throw new Error('Model name cannot be empty')\n config.model = trimmed\n try {\n const existing = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8'))\n writeStoredConfig({ ...existing, model: trimmed })\n } catch {}\n return { provider: 'custom', model: trimmed }\n }\n const normalized = normalizeModel(model, config.provider)\n config.model = normalized\n try {\n const existing = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8'))\n writeStoredConfig({ ...existing, model: normalized })\n } catch {}\n return { provider: config.provider, model: normalized }\n}\n\nexport function setTemperature(t) {\n const v = Math.min(2, Math.max(0, Number(t) || 0.5))\n config.temperature = v\n try {\n const existing = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8'))\n writeStoredConfig({ ...existing, temperature: v })\n } catch {}\n return { temperature: v }\n}\n\nexport function getSecurity() {\n return {\n fileSandbox: config.security.fileSandbox,\n execSandbox: config.security.execSandbox,\n blockedTools: [...config.security.blockedTools],\n updatedAt: config.security.updatedAt || null,\n }\n}\n\nexport function setSecurity(updates) {\n const before = getSecurity()\n if (typeof updates.fileSandbox === 'boolean') config.security.fileSandbox = updates.fileSandbox\n if (typeof updates.execSandbox === 'boolean') config.security.execSandbox = updates.execSandbox\n if (Array.isArray(updates.blockedTools)) {\n config.security.blockedTools = updates.blockedTools.filter(t => typeof t === 'string')\n }\n const changed = before.fileSandbox !== config.security.fileSandbox\n || before.execSandbox !== config.security.execSandbox\n || JSON.stringify(before.blockedTools) !== JSON.stringify(config.security.blockedTools)\n if (changed) config.security.updatedAt = nowTimestamp()\n try {\n const existing = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8'))\n writeStoredConfig({ ...existing, security: { ...config.security } })\n } catch {}\n return getSecurity()\n}\n\nexport function getMinimaxKey() {\n try {\n const raw = fs.readFileSync(paths.configFile, 'utf-8')\n const parsed = JSON.parse(raw)\n return typeof parsed?.minimax_api_key === 'string' ? parsed.minimax_api_key : null\n } catch { return null }\n}\n\nexport function setMinimaxKey(key) {\n const trimmed = String(key || '').trim()\n let existing = {}\n try { existing = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8')) } catch {}\n if (trimmed) {\n writeStoredConfig({ ...existing, minimax_api_key: trimmed })\n } else {\n const { minimax_api_key: _removed, ...rest } = existing\n writeStoredConfig(rest)\n }\n}\n\n// ── Seedance AI 视频生成(火山方舟 Ark)配置 ──\n// 存于 config.json 的 seedance 字段:{ apiKey, model, baseURL }\n// 中国区默认走 ark.cn-beijing.volces.com;model 是 doubao-* 形态的模型 ID 或推理接入点 ep-xxx,\n// 因不同账号开通的版本号不同,做成可配置,给一个合理默认值,错了由调用错误回传引导用户改。\nconst SEEDANCE_DEFAULT_BASE_URL = 'https://ark.cn-beijing.volces.com/api/v3'\nconst SEEDANCE_DEFAULT_MODEL = 'doubao-seedance-2-0-260128'\n\n// seedance.json 读写(独立文件,只放 seedance 配置,谁都不会全量覆盖它)\nfunction readSeedanceFile() {\n try { return JSON.parse(fs.readFileSync(paths.seedanceConfigFile, 'utf-8')) || {} }\n catch { return {} }\n}\nfunction writeSeedanceFile(obj) {\n const tmp = paths.seedanceConfigFile + '.tmp'\n fs.writeFileSync(tmp, JSON.stringify(obj, null, 2), 'utf-8')\n fs.renameSync(tmp, paths.seedanceConfigFile)\n}\n\n// 一次性迁移:旧版把 seedance 存在 config.json 里。若独立文件尚无、而 config.json 里还有,\n// 就搬过去并从 config.json 删除该字段,之后只认独立文件。\nfunction migrateLegacySeedance() {\n if (fs.existsSync(paths.seedanceConfigFile)) return\n let mainCfg\n try { mainCfg = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8')) } catch { return }\n const legacy = mainCfg?.seedance\n if (!legacy || typeof legacy !== 'object') return\n try {\n writeSeedanceFile(legacy)\n const { seedance: _removed, ...rest } = mainCfg\n writeStoredConfig(rest)\n console.log('[config] 已把旧的 seedance 配置从 config.json 迁移到 seedance.json')\n } catch (e) {\n console.warn('[config] seedance 迁移失败:', e.message)\n }\n}\n\nexport function getSeedanceConfig() {\n // 环境变量优先(ARK_API_KEY),方便开发/部署注入\n const envKey = String(process.env.ARK_API_KEY || process.env.SEEDANCE_API_KEY || '').trim()\n migrateLegacySeedance()\n const stored = readSeedanceFile()\n const apiKey = envKey || String(stored.apiKey || '').trim()\n return {\n apiKey,\n model: String(stored.model || '').trim() || SEEDANCE_DEFAULT_MODEL,\n baseURL: String(stored.baseURL || '').trim() || SEEDANCE_DEFAULT_BASE_URL,\n configured: Boolean(apiKey),\n }\n}\n\nexport function isSeedanceConfigured() {\n return getSeedanceConfig().configured\n}\n\nexport function setSeedanceConfig({ apiKey, model, baseURL } = {}) {\n migrateLegacySeedance()\n const next = { ...readSeedanceFile() }\n if (apiKey !== undefined) next.apiKey = String(apiKey || '').trim()\n if (model !== undefined) next.model = String(model || '').trim()\n if (baseURL !== undefined) next.baseURL = String(baseURL || '').trim()\n // 没有 key 时删掉独立文件,保持干净\n if (!next.apiKey) {\n try { fs.rmSync(paths.seedanceConfigFile, { force: true }) } catch {}\n return getSeedanceConfig()\n }\n writeSeedanceFile(next)\n return getSeedanceConfig()\n}\n\n// ── Social media platform config ──\n\nconst SOCIAL_ENV_KEYS = [\n 'DISCORD_BOT_TOKEN',\n 'FEISHU_APP_ID', 'FEISHU_APP_SECRET', 'FEISHU_VERIFICATION_TOKEN',\n 'WECHAT_OFFICIAL_APP_ID', 'WECHAT_OFFICIAL_APP_SECRET', 'WECHAT_OFFICIAL_TOKEN',\n 'WECOM_BOT_KEY', 'WECOM_INCOMING_TOKEN',\n]\n\n// ── WeChat ClawBot credentials (written automatically after QR scan, not exposed in SOCIAL_ENV_KEYS) ──\n\nexport function getClawbotCredentials() {\n try {\n const stored = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8'))\n const c = stored?.clawbot\n return (c?.accountId && c?.botToken) ? c : null\n } catch { return null }\n}\n\nexport function setClawbotCredentials({ accountId, botToken, baseUrl }) {\n let existing = {}\n try { existing = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8')) } catch {}\n writeStoredConfig({ ...existing, clawbot: { accountId, botToken, baseUrl } })\n}\n\nexport function clearClawbotCredentials() {\n let existing = {}\n try { existing = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8')) } catch {}\n const { clawbot: _, ...rest } = existing\n writeStoredConfig(rest)\n}\n\nexport function getSocialConfig() {\n let stored = {}\n try { stored = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8'))?.social || {} } catch {}\n const result = {}\n for (const key of SOCIAL_ENV_KEYS) {\n const val = stored[key] || globalThis.process?.env?.[key] || ''\n result[key] = { configured: !!val }\n }\n return result\n}\n\nexport function setSocialConfig(updates) {\n let existing = {}\n try { existing = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8')) } catch {}\n const current = existing.social || {}\n const next = { ...current }\n for (const [key, val] of Object.entries(updates)) {\n if (!SOCIAL_ENV_KEYS.includes(key)) continue\n const trimmed = String(val || '').trim()\n if (trimmed) {\n next[key] = trimmed\n // Take effect immediately without restart\n if (globalThis.process?.env) globalThis.process.env[key] = trimmed\n } else {\n delete next[key]\n }\n }\n writeStoredConfig({ ...existing, social: next })\n}\n\nconst VOICE_CONFIG_KEYS = [\n 'voiceProvider',\n 'aliyunApiKey',\n 'tencentSecretId', 'tencentSecretKey', 'tencentAppId',\n 'xunfeiAppId', 'xunfeiApiKey', 'xunfeiApiSecret',\n 'volcAsrApiKey', 'volcAsrAppKey', 'volcAsrAccessKey', 'volcAsrResourceId',\n]\n\nfunction isValidAliyunAsrKey(value) {\n return /^sk-[A-Za-z0-9_\\-.]{20,}$/.test(String(value || '').trim())\n}\n\nconst CHAT_PROVIDERS_WITH_AMBIGUOUS_SK_KEYS = new Set([\n DEEPSEEK_PROVIDER,\n MINIMAX_PROVIDER,\n OPENAI_PROVIDER,\n MOONSHOT_PROVIDER,\n ZHIPU_PROVIDER,\n MIMO_PROVIDER,\n])\n\nexport function getVoiceConfig() {\n let stored = {}\n try { stored = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8'))?.voice || {} } catch {}\n const result = { voiceProvider: stored.voiceProvider || 'aliyun' }\n for (const key of VOICE_CONFIG_KEYS) {\n if (key === 'voiceProvider') continue\n result[key] = { configured: !!(stored[key]) }\n if (key === 'aliyunApiKey' && stored[key]) {\n result[key] = {\n configured: isValidAliyunAsrKey(stored[key]),\n invalidFormat: !isValidAliyunAsrKey(stored[key]),\n }\n }\n }\n return result\n}\n\nexport function setVoiceConfig(updates) {\n let existing = {}\n try { existing = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8')) } catch {}\n const current = existing.voice || {}\n const next = { ...current }\n for (const [key, val] of Object.entries(updates)) {\n if (!VOICE_CONFIG_KEYS.includes(key)) continue\n const trimmed = String(val || '').trim()\n if (key === 'aliyunApiKey' && trimmed && !isValidAliyunAsrKey(trimmed)) {\n console.warn('[voice-config] Ignoring invalid Aliyun ASR key format; expected DashScope sk-* API key')\n continue\n }\n if (\n key === 'aliyunApiKey' &&\n trimmed &&\n existing.apiKey &&\n trimmed === existing.apiKey &&\n CHAT_PROVIDERS_WITH_AMBIGUOUS_SK_KEYS.has(existing.provider)\n ) {\n console.warn('[voice-config] Ignoring Aliyun ASR key because it matches the active chat provider API key')\n continue\n }\n if (trimmed) next[key] = trimmed\n else delete next[key]\n }\n writeStoredConfig({ ...existing, voice: next })\n}\n\n// TTS config\nconst TTS_CONFIG_KEYS = [\n 'ttsProvider', 'ttsVoiceId',\n 'minimaxKey',\n 'doubaoKey', 'doubaoAppId', 'doubaoAccessKey', 'doubaoResourceId',\n 'openaiTtsKey', 'openaiTtsBaseURL',\n 'elevenLabsKey',\n 'volcanoAppId', 'volcanoToken',\n]\n\nexport function getTTSConfig() {\n let stored = {}\n try { stored = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8'))?.tts || {} } catch {}\n return {\n ttsProvider: stored.ttsProvider || 'doubao',\n ttsVoiceId: stored.ttsVoiceId || 'zh_female_xiaohe_uranus_bigtts',\n minimaxKey: { configured: !!(stored.minimaxKey || process.env.MINIMAX_API_KEY || getMinimaxKey()) },\n doubaoKey: { configured: !!(stored.doubaoKey) },\n doubaoAppId: { configured: !!(stored.doubaoAppId), value: stored.doubaoAppId || '' },\n doubaoAccessKey: { configured: !!(stored.doubaoAccessKey) },\n doubaoResourceId: stored.doubaoResourceId || '',\n openaiTtsBaseURL: stored.openaiTtsBaseURL || '',\n openaiTtsKey: { configured: !!(stored.openaiTtsKey) },\n elevenLabsKey: { configured: !!(stored.elevenLabsKey) },\n volcanoAppId: { configured: !!(stored.volcanoAppId), value: stored.volcanoAppId || '' },\n volcanoToken: { configured: !!(stored.volcanoToken) },\n }\n}\n\n// Read plaintext TTS credentials (backend use only — not exposed to frontend)\nexport function getTTSCredentials() {\n let stored = {}\n try { stored = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8'))?.tts || {} } catch {}\n return {\n provider: stored.ttsProvider || 'doubao',\n voiceId: stored.ttsVoiceId || 'zh_female_xiaohe_uranus_bigtts',\n doubaoKey: stored.doubaoKey || process.env.DOUBAO_TTS_API_KEY || '',\n doubaoAppId: stored.doubaoAppId || process.env.DOUBAO_TTS_APP_ID || '',\n doubaoAccessKey: stored.doubaoAccessKey || process.env.DOUBAO_TTS_ACCESS_KEY || '',\n doubaoResourceId: stored.doubaoResourceId || process.env.DOUBAO_TTS_RESOURCE_ID || '',\n minimaxKey: process.env.MINIMAX_API_KEY || stored.minimaxKey || getMinimaxKey() || (config.provider === 'minimax' ? config.apiKey : '') || '',\n openaiKey: stored.openaiTtsKey || '',\n openaiBaseURL: stored.openaiTtsBaseURL || '',\n elevenLabsKey: stored.elevenLabsKey || '',\n volcanoAppId: stored.volcanoAppId || '',\n volcanoToken: stored.volcanoToken || '',\n }\n}\n\nexport function setTTSConfig(updates) {\n let existing = {}\n try { existing = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8')) } catch {}\n const current = existing.tts || {}\n const next = { ...current }\n for (const [key, val] of Object.entries(updates)) {\n if (!TTS_CONFIG_KEYS.includes(key)) continue\n const trimmed = String(val || '').trim()\n if (trimmed) next[key] = trimmed\n else delete next[key]\n }\n writeStoredConfig({ ...existing, tts: next })\n}\n\n// ── Embedding config ──────────────────────────────────────────────────────────\n// Embedding 与 chat provider 完全独立。DeepSeek/Moonshot 没 embedding API,\n// 所以必须分开存。结构:config.json 的 \"embedding\" 块。\n//\n// 字段:\n// provider: 'openai' | 'qwen' | 'zhipu' | 'minimax' | 'custom'\n// model: 模型名(参考 EMBEDDING_PROVIDER_PRESETS)\n// apiKey: 凭证(明文存储,与现有 chat apiKey 一样)\n// baseURL: custom 时必填;其他 provider 留空走预设\n// dimensions: 可选,仅 OpenAI text-embedding-3-* 系列支持显式指定\n\nconst EMBEDDING_CONFIG_KEYS = ['provider', 'model', 'apiKey', 'baseURL', 'dimensions']\n\nexport const EMBEDDING_PROVIDER_PRESETS = {\n openai: { baseURL: 'https://api.openai.com/v1', defaultModel: 'text-embedding-3-small', defaultDims: 1536 },\n qwen: { baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', defaultModel: 'text-embedding-v2', defaultDims: 1536 },\n zhipu: { baseURL: 'https://open.bigmodel.cn/api/paas/v4', defaultModel: 'embedding-3', defaultDims: 2048 },\n minimax: { baseURL: 'https://api.minimax.chat/v1', defaultModel: 'embo-01', defaultDims: 1536 },\n custom: { baseURL: '', defaultModel: '', defaultDims: 1536 },\n}\n\nlet _embeddingBlockCache = null\nlet _embeddingBlockCacheMtime = -1\n\nfunction readEmbeddingBlock() {\n let mtime = -1\n try {\n mtime = fs.statSync(paths.configFile).mtimeMs\n } catch {\n // config 文件不存在或访问失败:直接返回 {},不缓存(让下次有机会重试)\n return {}\n }\n\n if (_embeddingBlockCache !== null && mtime === _embeddingBlockCacheMtime) {\n return _embeddingBlockCache\n }\n\n let block = {}\n try {\n const raw = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8'))\n if (raw?.embedding && typeof raw.embedding === 'object') {\n block = raw.embedding\n }\n } catch {\n block = {}\n }\n\n _embeddingBlockCache = block\n _embeddingBlockCacheMtime = mtime\n return block\n}\n\n// 前端可见视图:不暴露 apiKey 明文,只暴露 configured 布尔\nexport function getEmbeddingConfig() {\n const stored = readEmbeddingBlock()\n const provider = typeof stored.provider === 'string' ? stored.provider : ''\n const model = typeof stored.model === 'string' ? stored.model : ''\n const baseURL = typeof stored.baseURL === 'string' ? stored.baseURL : ''\n const dimensions = Number.isFinite(stored.dimensions) ? stored.dimensions : null\n const configured = !!(stored.apiKey && model)\n return { provider, model, baseURL, dimensions, configured }\n}\n\n// Backend-only:读明文 apiKey。供 src/embedding.js 内部用,不要给前端。\nexport function getEmbeddingCredentials() {\n const stored = readEmbeddingBlock()\n const provider = typeof stored.provider === 'string' ? stored.provider : ''\n let baseURL = typeof stored.baseURL === 'string' && stored.baseURL ? stored.baseURL : ''\n if (!baseURL && provider && EMBEDDING_PROVIDER_PRESETS[provider]) {\n baseURL = EMBEDDING_PROVIDER_PRESETS[provider].baseURL || ''\n }\n return {\n provider,\n model: typeof stored.model === 'string' ? stored.model : '',\n apiKey: typeof stored.apiKey === 'string' ? stored.apiKey : '',\n baseURL,\n dimensions: Number.isFinite(stored.dimensions) ? stored.dimensions : null,\n }\n}\n\nexport function setEmbeddingConfig(updates) {\n let existing = {}\n try { existing = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8')) } catch {}\n const current = existing.embedding || {}\n const next = { ...current }\n for (const [key, val] of Object.entries(updates || {})) {\n if (!EMBEDDING_CONFIG_KEYS.includes(key)) continue\n if (key === 'dimensions') {\n const n = Number(val)\n if (Number.isFinite(n) && n > 0) next.dimensions = n\n else delete next.dimensions\n continue\n }\n const trimmed = String(val || '').trim()\n if (trimmed) next[key] = trimmed\n else delete next[key]\n }\n writeStoredConfig({ ...existing, embedding: next })\n}\n\n// ── Web Search 配置 ──\n// 顶级字段(与现有 serper_api_key 兼容),不嵌套到子块\n// 字段:serper_api_key / searxng_url / jina_api_key\nconst WEB_SEARCH_KEY_MAP = {\n serperKey: 'serper_api_key',\n searxngUrl: 'searxng_url',\n jinaKey: 'jina_api_key',\n braveKey: 'brave_api_key',\n tavilyKey: 'tavily_api_key',\n}\n\nfunction readWebSearchBlock() {\n try {\n const raw = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8'))\n return {\n serperKey: typeof raw.serper_api_key === 'string' ? raw.serper_api_key : '',\n searxngUrl: typeof raw.searxng_url === 'string' ? raw.searxng_url : '',\n jinaKey: typeof raw.jina_api_key === 'string' ? raw.jina_api_key : '',\n braveKey: typeof raw.brave_api_key === 'string' ? raw.brave_api_key : '',\n tavilyKey: typeof raw.tavily_api_key === 'string' ? raw.tavily_api_key : '',\n }\n } catch {\n return { serperKey: '', searxngUrl: '', jinaKey: '', braveKey: '', tavilyKey: '' }\n }\n}\n\n// 前端可见视图:不暴露 key 明文,只暴露 configured 布尔 + searxngUrl(URL 不算敏感)\n// configured 同时考虑 env 兜底,避免\"env 里有 key 但 UI 标未配置\"的误导\n// xxxFromEnv 提示来源,让 UI 标注\"已配置(环境变量)\",并暗示清空输入框不会真正生效\nexport function getWebSearchConfig() {\n const stored = readWebSearchBlock()\n const envSerper = process.env.SERPER_API_KEY || ''\n const envJina = process.env.JINA_API_KEY || ''\n const envSearxng = process.env.SEARXNG_URL || ''\n const envBrave = process.env.BRAVE_API_KEY || ''\n const envTavily = process.env.TAVILY_API_KEY || ''\n return {\n serperConfigured: !!(stored.serperKey || envSerper),\n jinaConfigured: !!(stored.jinaKey || envJina),\n braveConfigured: !!(stored.braveKey || envBrave),\n tavilyConfigured: !!(stored.tavilyKey || envTavily),\n // 输入框只回显 stored 值,避免用户以为能编辑 env 值\n searxngUrl: stored.searxngUrl,\n // effective URL(含 env 兜底),UI 可显示在状态行\n effectiveSearxngUrl: stored.searxngUrl || envSearxng,\n serperFromEnv: !stored.serperKey && !!envSerper,\n jinaFromEnv: !stored.jinaKey && !!envJina,\n braveFromEnv: !stored.braveKey && !!envBrave,\n tavilyFromEnv: !stored.tavilyKey && !!envTavily,\n searxngFromEnv: !stored.searxngUrl && !!envSearxng,\n }\n}\n\n// Backend-only:读明文 key。供 src/capabilities/executor.js 内部用,不要给前端\nexport function getWebSearchCredentials() {\n const stored = readWebSearchBlock()\n return {\n serperKey: stored.serperKey || process.env.SERPER_API_KEY || '',\n searxngUrl: stored.searxngUrl || process.env.SEARXNG_URL || '',\n jinaKey: stored.jinaKey || process.env.JINA_API_KEY || '',\n braveKey: stored.braveKey || process.env.BRAVE_API_KEY || '',\n tavilyKey: stored.tavilyKey || process.env.TAVILY_API_KEY || '',\n }\n}\n\nexport function setWebSearchConfig(updates) {\n let existing = {}\n try { existing = JSON.parse(fs.readFileSync(paths.configFile, 'utf-8')) } catch {}\n const next = { ...existing }\n for (const [key, val] of Object.entries(updates || {})) {\n const cfgField = WEB_SEARCH_KEY_MAP[key]\n if (!cfgField) continue\n const trimmed = String(val || '').trim()\n if (key === 'searxngUrl' && trimmed && !/^https?:\\/\\//i.test(trimmed)) {\n throw new Error('searxngUrl must start with http:// or https://')\n }\n if (trimmed) next[cfgField] = trimmed\n else delete next[cfgField]\n }\n writeStoredConfig(next)\n}\n\nexport const __internals = {\n DEEPSEEK_MODELS,\n MINIMAX_MODELS,\n OPENAI_MODELS,\n QWEN_MODELS,\n MOONSHOT_MODELS,\n ZHIPU_MODELS,\n MIMO_MODELS,\n normalizeModel,\n isThinkingEnabledForModel,\n buildPingParams,\n}\n"}}}}