/** * voko-server 连接与基础设施配置的唯一解析点。业务运行参数由所属模块直接读取。 */ import { devMysqlUrl } from '@drama/db' /** * @typedef {{ * nodeEnv: string, * port: number, * databaseUrl: string, * logLevel: 'debug'|'info'|'warn'|'error', * authTokenSecret: string, * webhookSecretKey: string, * publicUrl: string, * email: { host: string, port: number, secure: boolean, user: string, pass: string, from: string }, * oauth: { googleClientId: string|null, googleClientSecret: string|null }, * llm: { provider: string, baseUrl: string, model: string, apiKey: string }, * }} VokoEnv */ /** * @param {Record} rawEnv * @returns {VokoEnv} */ export function readEnv(rawEnv) { const nodeEnv = rawEnv.NODE_ENV || 'development' const databaseUrl = rawEnv.DATABASE_URL || devMysqlUrl('voko') const scheme = new URL(databaseUrl).protocol if (scheme !== 'mysql:' && scheme !== 'mysql2:') throw new Error(`voko-server: DATABASE_URL 必须是 MySQL,收到 ${scheme}`) if (nodeEnv === 'production' && !rawEnv.AUTH_TOKEN_SECRET) throw new Error('voko-server: 生产环境必须设置 AUTH_TOKEN_SECRET') if (nodeEnv === 'production' && !rawEnv.WEBHOOK_SECRET_KEY) throw new Error('voko-server: 生产环境必须设置 WEBHOOK_SECRET_KEY') const email = { host: rawEnv.SMTP_HOST, port: Number(rawEnv.SMTP_PORT || 465), secure: rawEnv.SMTP_SECURE !== 'false', user: rawEnv.SMTP_USER, pass: rawEnv.SMTP_PASS, from: rawEnv.EMAIL_FROM, } const missing = [['SMTP_HOST', email.host], ['SMTP_USER', email.user], ['SMTP_PASS', email.pass], ['EMAIL_FROM', email.from]] .filter(([, value]) => !value) .map(([name]) => name) if (missing.length > 0) throw new Error(`voko-server: 邮件缺少配置,拒绝启动(无 fake 降级):${missing.join(', ')}`) const llm = { provider: rawEnv.LLM_PROVIDER, baseUrl: rawEnv.LLM_BASE_URL, model: rawEnv.LLM_MODEL, apiKey: rawEnv.LLM_API_KEY } const missingLlm = [['LLM_PROVIDER', llm.provider], ['LLM_BASE_URL', llm.baseUrl], ['LLM_MODEL', llm.model], ['LLM_API_KEY', llm.apiKey]].filter(([, value]) => !value).map(([name]) => name) if (missingLlm.length > 0) throw new Error(`voko-server: 大模型缺少配置,拒绝启动(无 fake 降级):${missingLlm.join(', ')}`) return { nodeEnv, port: parsePort(rawEnv.PORT, 9201), databaseUrl, logLevel: parseLogLevel(rawEnv.LOG_LEVEL), authTokenSecret: rawEnv.AUTH_TOKEN_SECRET || 'voko-dev-token-secret', webhookSecretKey: rawEnv.WEBHOOK_SECRET_KEY || 'voko-dev-webhook-secret-key', publicUrl: (rawEnv.PUBLIC_URL || 'http://localhost:5201').replace(/\/$/, ''), email: /** @type {VokoEnv['email']} */ (email), llm: /** @type {VokoEnv['llm']} */ (llm), oauth: { googleClientId: rawEnv.GOOGLE_OAUTH_CLIENT_ID || null, googleClientSecret: rawEnv.GOOGLE_OAUTH_CLIENT_SECRET || null, }, } } /** * @param {string|undefined} raw * @param {number} fallback */ function parsePort(raw, fallback) { if (raw === undefined || raw === '') return fallback const port = Number(raw) if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new Error(`voko-server: PORT 非法:${raw}`) return port } /** * @param {string|undefined} raw * @returns {'debug'|'info'|'warn'|'error'} */ function parseLogLevel(raw) { const level = raw || 'info' switch (level) { case 'debug': case 'info': case 'warn': case 'error': return level default: throw new Error(`voko-server: LOG_LEVEL 非法:${level}`) } }