require('dotenv').config(); const http = require('http'); const express = require('express'); const cors = require('cors'); const path = require('path'); const db = require('./db'); const assistantProfileRoutes = require('./routes/assistantProfile'); const voiceRoutes = require('./routes/voice'); const chatRoutes = require('./routes/chat'); const sessionRoutes = require('./routes/session'); const { setupNativeVoiceGateway } = require('./services/nativeVoiceGateway'); // ========== 环境变量校验 ========== function validateEnv() { const required = [ { key: 'VOLC_S2S_APP_ID', desc: 'S2S 端到端语音 AppID' }, { key: 'VOLC_S2S_TOKEN', desc: 'S2S 端到端语音 Token' }, { key: 'VOLC_ARK_ENDPOINT_ID', desc: '方舟 LLM 推理接入点 ID' }, ]; const missing = []; const placeholder = []; for (const { key, desc } of required) { const val = process.env[key]; if (!val) { missing.push({ key, desc }); } else if (val.startsWith('your_')) { placeholder.push({ key, desc }); } } if (missing.length > 0) { console.warn('\n⚠️ 以下必需环境变量未设置:'); missing.forEach(({ key, desc }) => console.warn(` ❌ ${key} — ${desc}`)); } if (placeholder.length > 0) { console.warn('\n⚠️ 以下环境变量仍为占位符(Mock 模式):'); placeholder.forEach(({ key, desc }) => console.warn(` 🔶 ${key} — ${desc}`)); } const ready = missing.length === 0 && placeholder.length === 0; if (ready) { console.log('✅ 所有环境变量已配置'); } else { console.warn('\n💡 请编辑 server/.env 填入真实凭证,当前将以 Mock 模式运行'); } // 可选变量提示 const optional = [ { key: 'COZE_API_TOKEN', desc: 'Coze 智能体(文字对话)' }, { key: 'COZE_BOT_ID', desc: 'Coze Bot ID' }, { key: 'VOLC_WEBSEARCH_API_KEY', desc: '联网搜索' }, { key: 'VOLC_S2S_SPEAKER_ID', desc: '自定义音色' }, { key: 'VOLC_ARK_KNOWLEDGE_BASE_IDS', desc: '方舟私域知识库(语音)' }, { key: 'ASSISTANT_PROFILE_API_URL', desc: '外接助手资料接口' }, ]; const configuredOptional = optional.filter(({ key }) => { const v = process.env[key]; return v && !v.startsWith('your_'); }); if (configuredOptional.length > 0) { console.log(`📦 可选功能已启用:${configuredOptional.map(o => o.desc).join('、')}`); } return ready; } // ========== Express 应用 ========== const app = express(); const server = http.createServer(app); const PORT = process.env.PORT || 3001; app.use(cors()); app.use(express.json({ limit: '1mb' })); app.use(express.urlencoded({ extended: true, limit: '1mb' })); // 请求日志中间件 app.use((req, res, next) => { const start = Date.now(); res.on('finish', () => { const ms = Date.now() - start; if (req.path.startsWith('/api/')) { console.log(`[${req.method}] ${req.path} → ${res.statusCode} (${ms}ms)`); } }); next(); }); app.use('/api/assistant-profile', assistantProfileRoutes); app.use('/api/voice', voiceRoutes); app.use('/api/chat', chatRoutes); app.use('/api/session', sessionRoutes); app.get('/api/health', (req, res) => { const envReady = !process.env.VOLC_S2S_APP_ID?.startsWith('your_'); res.json({ status: 'ok', mode: 's2s-hybrid', apiVersion: '2024-12-01', configured: envReady, features: { voiceChat: true, textChat: !!process.env.COZE_API_TOKEN && !process.env.COZE_API_TOKEN.startsWith('your_') && !!process.env.COZE_BOT_ID && !process.env.COZE_BOT_ID.startsWith('your_'), textChatProvider: 'coze', webSearch: !!process.env.VOLC_WEBSEARCH_API_KEY && !process.env.VOLC_WEBSEARCH_API_KEY.startsWith('your_'), customSpeaker: !!process.env.VOLC_S2S_SPEAKER_ID && !process.env.VOLC_S2S_SPEAKER_ID.startsWith('your_'), arkKnowledgeBase: !!process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS && !process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS.startsWith('your_'), }, }); }); // 静态文件服务 app.use(express.static(path.join(__dirname, '../client/dist'))); // 处理单页应用路由 app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '../client/dist/index.html')); }); // 统一错误处理中间件 app.use((err, req, res, _next) => { console.error(`[Error] ${req.method} ${req.path}:`, err.message); const status = err.status || 500; res.status(status).json({ success: false, error: err.message || 'Internal Server Error', ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }), }); }); // 404 处理 app.use((req, res) => { res.status(404).json({ success: false, error: `Route not found: ${req.method} ${req.path}` }); }); // 启动服务(先初始化数据库) async function start() { try { await db.initialize(); } catch (err) { console.error('[DB] MySQL initialization failed:', err.message); console.warn('[DB] Continuing without database — context switching will use in-memory fallback'); } if (process.env.ENABLE_NATIVE_VOICE_GATEWAY !== 'false') { setupNativeVoiceGateway(server); console.log('[NativeVoice] Gateway enabled at /ws/realtime-dialog'); } else { console.log('[NativeVoice] Gateway disabled (ENABLE_NATIVE_VOICE_GATEWAY=false)'); } server.listen(PORT, () => { console.log('\n========================================'); console.log(` 🚀 Voice Chat Backend`); console.log(` 📡 http://localhost:${PORT}`); console.log(` 🔧 Mode: S2S端到端 + LLM混合 (API v2024-12-01)`); console.log('========================================\n'); validateEnv(); console.log(''); }); } start();