1 line
38 KiB
JSON
1 line
38 KiB
JSON
{"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"}}}} |