#!/usr/bin/env node import { spawn } from 'child_process' import { config } from 'dotenv' import path from 'path' config({ path: path.resolve(process.cwd(), '.env'), quiet: true }) const BUILD_MEMORY_MB = 6144 const DEFAULT_NODE_OPTIONS = `--max-old-space-size=${BUILD_MEMORY_MB}` const HIDDEN_VALUE = '[已隐藏]' const SUPPORTED_AGGREGATE_LOGIN_TYPES = [ 'qq', 'wx', 'alipay', 'sina', 'baidu', 'douyin', 'huawei', 'xiaomi', 'gitee', 'gitea', 'bilibili', 'kuaishou' ] const SUPPORTED_ENV_VARIABLES = [ 'NODE_ENV', 'NODE_OPTIONS', 'NITRO_PRESET', 'VERCEL', 'VERCEL_ENV', 'NETLIFY', 'PREBUILT', 'SKIP_INSTALL', 'SKIP_BUILD', 'CI', 'DATABASE_URL', 'JWT_SECRET', 'REDIS_URL', 'DEBUG_SQL', 'WEBAUTHN_RP_ID', 'WEBAUTHN_ORIGIN', 'GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET', 'CASDOOR_CLIENT_ID', 'CASDOOR_CLIENT_SECRET', 'CASDOOR_ENDPOINT', 'CASDOOR_ORGANIZATION_NAME', 'GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', 'AGGREGATE_OAUTH_APP_ID', 'AGGREGATE_OAUTH_APP_KEY', 'AGGREGATE_OAUTH_LOGIN_TYPE', 'AGGREGATE_OAUTH_ENDPOINT', 'OAUTH_REDIRECT_URI', 'OAUTH_STATE_SECRET', 'NUXT_PUBLIC_HOST', 'NUXT_PUBLIC_SITE_TITLE', 'NUXT_PUBLIC_SITE_DESCRIPTION', 'NUXT_PUBLIC_SITE_LOGO', 'NUXT_PUBLIC_SEO_CONFIG', 'SENTRY_DSN', 'SENTRY_ENVIRONMENT', 'SENTRY_RELEASE', 'SENTRY_TRACES_SAMPLE_RATE', 'NUXT_PUBLIC_SENTRY_DSN', 'NUXT_PUBLIC_SENTRY_ENVIRONMENT', 'NUXT_PUBLIC_SENTRY_RELEASE', 'NUXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE', 'NUXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE', 'NUXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE', 'VERCEL_GIT_COMMIT_SHA', 'COMMIT_REF' ] const colors = { reset: '\x1b[0m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m', dim: '\x1b[2m' } function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`) } function isProvided(value) { return typeof value === 'string' && value.trim().length > 0 } function normalizeBlankEnvironment() { for (const name of SUPPORTED_ENV_VARIABLES) { const value = process.env[name] if (typeof value === 'string' && value.length > 0 && value.trim().length === 0) { delete process.env[name] } } } function providedState(value) { return isProvided(value) ? '已配置' : '未配置' } function strictBoolean(value) { return value === 'true' } function numberInUnitInterval(value, fallback) { if (!isProvided(value)) return { value: fallback, source: '默认值' } const parsed = Number(value) if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) { return { value: fallback, source: `无效值 ${JSON.stringify(value)},已回退` } } return { value: parsed, source: `来自 ${JSON.stringify(value)}` } } function displayValue(value, fallback = '未配置') { return isProvided(value) ? JSON.stringify(value) : fallback } function hiddenValue(value) { return isProvided(value) ? HIDDEN_VALUE : '未配置' } function resolveAggregateLoginTypeState(value) { if (!isProvided(value)) return '使用 qq(默认值)' let values try { const parsed = JSON.parse(value) values = Array.isArray(parsed) ? parsed : [parsed] } catch { values = value.split(',') } const normalized = [ ...new Set( values .filter((item) => typeof item === 'string') .map((item) => item.trim().toLowerCase()) .filter((item) => SUPPORTED_AGGREGATE_LOGIN_TYPES.includes(item)) ) ] return normalized.length > 0 ? '配置有效(具体值已隐藏)' : '未识别到受支持的登录方式,导入时回退到 qq' } function printItem(name, rawState, result) { log(` ${name}: ${rawState} -> ${result}`) } function printSensitiveItem(name, value, result) { printItem(name, hiddenValue(value), result ?? providedState(value)) } function resolveNitroPreset() { if (process.env.VERCEL) return 'vercel(由 VERCEL 检测)' if (process.env.NETLIFY) return 'netlify(由 NETLIFY 检测)' return `${process.env.NITRO_PRESET || 'node-server'}${process.env.NITRO_PRESET ? '' : '(默认值)'}` } function resolveNodeOptions(value) { const options = isProvided(value) ? value.trim() : '' const hasMemoryLimit = /(?:^|\s)--max[-_]old[-_]space[-_]size(?:=|\s|$)/.test(options) if (hasMemoryLimit) return options return options ? `${options} ${DEFAULT_NODE_OPTIONS}` : DEFAULT_NODE_OPTIONS } function resolveSeoConfig() { if (!isProvided(process.env.NUXT_PUBLIC_SEO_CONFIG)) { return { state: '未配置', result: '使用分项配置或默认值' } } try { const parsed = JSON.parse(process.env.NUXT_PUBLIC_SEO_CONFIG) const fields = ['title', 'shortName', 'description', 'logo'].filter((field) => isProvided(parsed?.[field]) ) return { state: '已配置', result: fields.length > 0 ? `解析成功:${fields.join(', ')}` : '解析成功,但未提供有效字段' } } catch { return { state: '已配置', result: '解析失败,将回退到分项配置或默认值' } } } function printBuildEnvironment(rawNodeOptions) { const backendTraces = numberInUnitInterval(process.env.SENTRY_TRACES_SAMPLE_RATE, 0.1) const frontendTraces = numberInUnitInterval( process.env.NUXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE || process.env.SENTRY_TRACES_SAMPLE_RATE, 0.1 ) const sessionReplay = numberInUnitInterval( process.env.NUXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE, 0 ) const errorReplay = numberInUnitInterval( process.env.NUXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE, 0 ) const production = process.env.NODE_ENV === 'production' const seoConfig = resolveSeoConfig() log('\n🔎 构建环境变量诊断', 'cyan') log('仅列出 VoiceHub 支持的变量;敏感值始终隐藏。', 'dim') log('\n构建与部署:', 'cyan') printItem('NODE_ENV', displayValue(process.env.NODE_ENV), process.env.NODE_ENV || '由 Nuxt 决定') printItem('NODE_OPTIONS', displayValue(rawNodeOptions), process.env.NODE_OPTIONS) printItem('NITRO_PRESET', displayValue(process.env.NITRO_PRESET), resolveNitroPreset()) printItem('VERCEL', displayValue(process.env.VERCEL), process.env.VERCEL ? '已检测' : '未检测') printItem('VERCEL_ENV', displayValue(process.env.VERCEL_ENV), process.env.VERCEL_ENV || '未检测') printItem('NETLIFY', displayValue(process.env.NETLIFY), process.env.NETLIFY ? '已检测' : '未检测') printItem( 'PREBUILT', displayValue(process.env.PREBUILT), strictBoolean(process.env.PREBUILT) ? '启用' : '停用' ) printItem( 'SKIP_INSTALL', displayValue(process.env.SKIP_INSTALL), strictBoolean(process.env.SKIP_INSTALL) ? '启用' : '停用' ) printItem( 'SKIP_BUILD', displayValue(process.env.SKIP_BUILD), strictBoolean(process.env.SKIP_BUILD) ? '启用' : '停用' ) printItem('CI', displayValue(process.env.CI), process.env.CI ? 'CI 环境' : '非 CI 环境') log('\n服务能力:', 'cyan') printSensitiveItem('DATABASE_URL', process.env.DATABASE_URL) printSensitiveItem('JWT_SECRET', process.env.JWT_SECRET) printSensitiveItem( 'REDIS_URL', process.env.REDIS_URL, isProvided(process.env.REDIS_URL) ? 'Redis 启用' : 'Redis 停用' ) printItem( 'DEBUG_SQL', displayValue(process.env.DEBUG_SQL), strictBoolean(process.env.DEBUG_SQL) ? '启用' : '停用' ) printItem( 'WEBAUTHN_RP_ID', displayValue(process.env.WEBAUTHN_RP_ID), providedState(process.env.WEBAUTHN_RP_ID) ) printItem( 'WEBAUTHN_ORIGIN', displayValue(process.env.WEBAUTHN_ORIGIN), providedState(process.env.WEBAUTHN_ORIGIN) ) log('\nOAuth 环境变量(仅用于后台导入):', 'cyan') printItem( 'GitHub CLIENT_ID', hiddenValue(process.env.GITHUB_CLIENT_ID), isProvided(process.env.GITHUB_CLIENT_ID) ? 'OAuth 启用' : 'OAuth 停用' ) printSensitiveItem('GitHub CLIENT_SECRET', process.env.GITHUB_CLIENT_SECRET) printItem( 'Casdoor CLIENT_ID', hiddenValue(process.env.CASDOOR_CLIENT_ID), isProvided(process.env.CASDOOR_CLIENT_ID) ? 'OAuth 启用' : 'OAuth 停用' ) printSensitiveItem('Casdoor CLIENT_SECRET', process.env.CASDOOR_CLIENT_SECRET) printItem( 'Google CLIENT_ID', hiddenValue(process.env.GOOGLE_CLIENT_ID), isProvided(process.env.GOOGLE_CLIENT_ID) ? 'OAuth 启用' : 'OAuth 停用' ) printSensitiveItem('Google CLIENT_SECRET', process.env.GOOGLE_CLIENT_SECRET) printItem( '聚合登陆 APP_ID', hiddenValue(process.env.AGGREGATE_OAUTH_APP_ID), isProvided(process.env.AGGREGATE_OAUTH_APP_ID) ? 'OAuth 启用' : 'OAuth 停用' ) printSensitiveItem('聚合登陆 APP_KEY', process.env.AGGREGATE_OAUTH_APP_KEY) printSensitiveItem( 'AGGREGATE_OAUTH_LOGIN_TYPE', process.env.AGGREGATE_OAUTH_LOGIN_TYPE, resolveAggregateLoginTypeState(process.env.AGGREGATE_OAUTH_LOGIN_TYPE) ) printSensitiveItem( 'AGGREGATE_OAUTH_ENDPOINT', process.env.AGGREGATE_OAUTH_ENDPOINT, isProvided(process.env.AGGREGATE_OAUTH_ENDPOINT) ? '使用自定义接口地址(值已隐藏)' : '使用默认接口地址' ) printItem( 'CASDOOR_ENDPOINT', displayValue(process.env.CASDOOR_ENDPOINT), providedState(process.env.CASDOOR_ENDPOINT) ) printItem( 'CASDOOR_ORGANIZATION_NAME', displayValue(process.env.CASDOOR_ORGANIZATION_NAME), process.env.CASDOOR_ORGANIZATION_NAME || 'built-in(默认值)' ) printSensitiveItem('OAUTH_REDIRECT_URI', process.env.OAUTH_REDIRECT_URI) printSensitiveItem('OAUTH_STATE_SECRET', process.env.OAUTH_STATE_SECRET) log('\n站点公开配置:', 'cyan') printItem( 'NUXT_PUBLIC_HOST', displayValue(process.env.NUXT_PUBLIC_HOST), providedState(process.env.NUXT_PUBLIC_HOST) ) printItem( 'NUXT_PUBLIC_SITE_TITLE', displayValue(process.env.NUXT_PUBLIC_SITE_TITLE), providedState(process.env.NUXT_PUBLIC_SITE_TITLE) ) printItem( 'NUXT_PUBLIC_SITE_DESCRIPTION', displayValue(process.env.NUXT_PUBLIC_SITE_DESCRIPTION), providedState(process.env.NUXT_PUBLIC_SITE_DESCRIPTION) ) printItem( 'NUXT_PUBLIC_SITE_LOGO', displayValue(process.env.NUXT_PUBLIC_SITE_LOGO), providedState(process.env.NUXT_PUBLIC_SITE_LOGO) ) printItem('NUXT_PUBLIC_SEO_CONFIG', seoConfig.state, seoConfig.result) log('\nSentry:', 'cyan') printItem( '运行状态', displayValue(process.env.NODE_ENV), production ? '启用(仍受后台遥测开关控制)' : '停用' ) printSensitiveItem( 'SENTRY_DSN', process.env.SENTRY_DSN, isProvided(process.env.SENTRY_DSN) ? '使用自定义后端 DSN' : '使用内置后端 DSN' ) printSensitiveItem( 'NUXT_PUBLIC_SENTRY_DSN', process.env.NUXT_PUBLIC_SENTRY_DSN, isProvided(process.env.NUXT_PUBLIC_SENTRY_DSN) ? '使用自定义前端 DSN' : '使用内置前端 DSN' ) printItem('SENTRY_TRACES_SAMPLE_RATE', backendTraces.source, backendTraces.value) printItem('NUXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE', frontendTraces.source, frontendTraces.value) printItem( 'NUXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE', sessionReplay.source, sessionReplay.value ) printItem( 'NUXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE', errorReplay.source, errorReplay.value ) printItem( 'SENTRY_ENVIRONMENT', displayValue(process.env.SENTRY_ENVIRONMENT), process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV || 'development' ) printItem( 'NUXT_PUBLIC_SENTRY_ENVIRONMENT', displayValue(process.env.NUXT_PUBLIC_SENTRY_ENVIRONMENT), process.env.NUXT_PUBLIC_SENTRY_ENVIRONMENT || process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV || 'development' ) printItem( 'SENTRY_RELEASE', providedState(process.env.SENTRY_RELEASE), process.env.SENTRY_RELEASE || process.env.VERCEL_GIT_COMMIT_SHA || process.env.COMMIT_REF || '未设置' ) printItem( 'NUXT_PUBLIC_SENTRY_RELEASE', providedState(process.env.NUXT_PUBLIC_SENTRY_RELEASE), process.env.NUXT_PUBLIC_SENTRY_RELEASE || process.env.SENTRY_RELEASE || process.env.VERCEL_GIT_COMMIT_SHA || process.env.COMMIT_REF || '未设置' ) log('') } function runNuxtBuild() { return new Promise((resolve) => { let settled = false const finish = (success) => { if (settled) return settled = true resolve(success) } let child try { child = spawn(process.execPath, ['./node_modules/nuxt/bin/nuxt.mjs', 'build'], { stdio: 'inherit', env: process.env }) } catch { finish(false) return } const forwardSignal = (signal) => { if (child.exitCode === null && child.signalCode === null) child.kill(signal) } const onSigterm = () => forwardSignal('SIGTERM') const onSigint = () => forwardSignal('SIGINT') const cleanup = () => { process.off('SIGTERM', onSigterm) process.off('SIGINT', onSigint) } process.once('SIGTERM', onSigterm) process.once('SIGINT', onSigint) child.once('error', () => { cleanup() finish(false) }) child.once('exit', (code) => { cleanup() finish(code === 0) }) }) } async function build() { const rawNodeOptions = process.env.NODE_OPTIONS normalizeBlankEnvironment() process.env.NODE_OPTIONS = resolveNodeOptions(rawNodeOptions) printBuildEnvironment(rawNodeOptions) if (process.argv.includes('--diagnostics-only')) return log('🔨 开始执行 Nuxt 构建...', 'cyan') if (!(await runNuxtBuild())) throw new Error('Nuxt 构建失败') log('✅ Nuxt 构建完成', 'green') } build().catch((error) => { log(`❌ ${error.message}`, 'yellow') process.exit(1) })