env.js 3.71 KB
import { fileURLToPath } from 'node:url'
/**
 * voko-server 连接与基础设施配置的唯一解析点。业务运行参数由所属模块直接读取。
 */

import { devMysqlUrl } from '@drama/db'

/**
 * @typedef {{
 *   nodeEnv: string,
 *   port: number,
 *   databaseUrl: string,
 *   logLevel: 'debug'|'info'|'warn'|'error',
 *   authTokenSecret: string,
 *   webhookSecretKey: string,
 *   filesDir: 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<string, string|undefined>} 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',
    filesDir: rawEnv.VOKO_FILES_DIR || fileURLToPath(new URL('../data/files', import.meta.url)),
    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}`)
  }
}