require('dotenv').config(); const express = require('express'); const cors = require('cors'); const path = require('path'); const db = require('./db'); const voiceRoutes = require('./routes/voice'); const chatRoutes = require('./routes/chat'); const sessionRoutes = require('./routes/session'); // ========== 环境变量校验 ========== function validateEnv() { const required = [ { key: 'VOLC_RTC_APP_ID', desc: 'RTC 应用 ID' }, { key: 'VOLC_RTC_APP_KEY', desc: 'RTC 应用密钥' }, { key: 'VOLC_ACCESS_KEY_ID', desc: '火山引擎 AccessKey ID' }, { key: 'VOLC_SECRET_ACCESS_KEY', desc: '火山引擎 Secret Access Key' }, { 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: '方舟私域知识库(语音)' }, ]; 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 PORT = process.env.PORT || 3001; app.use(cors()); // RTC Function Calling 回调不带 Content-Type,必须在标准 body parser 之前手动读取 // 全局序列号:在 body 读取前同步分配,确保反映真实请求到达顺序 let fcCallbackSeq = 0; app.post('/api/voice/fc_callback', (req, res, next) => { const seq = ++fcCallbackSeq; // 同步分配,在任何异步操作之前 if (!req.headers['content-type']) { const chunks = []; req.on('data', (chunk) => chunks.push(chunk)); req.on('end', () => { const rawBuf = Buffer.concat(chunks); const raw = rawBuf.toString('utf-8'); console.log(`[RawBody] seq=${seq} Read ${rawBuf.length} bytes`); // 将所有回调原始内容追加到日志文件 try { const fs = require('fs'); fs.appendFileSync('fc_all_callbacks.log', `\n=== SEQ=${seq} TIME=${new Date().toISOString()} BYTES=${rawBuf.length} ===\n${raw}\n`); } catch(e) {} try { req.body = JSON.parse(raw); } catch (e) { console.error('[RawBody] JSON parse failed:', e.message); req.body = { _raw: raw }; } req.body._seq = seq; next(); }); req.on('error', (err) => { console.error('[RawBody] Error:', err.message); next(); }); } else { req.body = req.body || {}; req.body._seq = seq; next(); } }); 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/voice', voiceRoutes); app.use('/api/chat', chatRoutes); app.use('/api/session', sessionRoutes); // 静态文件服务 app.use(express.static('../client/dist')); // 处理单页应用路由 app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '../client/dist/index.html')); }); app.get('/api/health', (req, res) => { const envReady = !process.env.VOLC_RTC_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((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'); } app.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();