2026-03-12 12:47:56 +08:00
|
|
|
|
require('dotenv').config();
|
2026-03-13 13:06:46 +08:00
|
|
|
|
const http = require('http');
|
2026-03-12 12:47:56 +08:00
|
|
|
|
const express = require('express');
|
|
|
|
|
|
const cors = require('cors');
|
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
const db = require('./db');
|
2026-03-24 17:19:36 +08:00
|
|
|
|
const assistantProfileRoutes = require('./routes/assistantProfile');
|
2026-03-12 12:47:56 +08:00
|
|
|
|
const voiceRoutes = require('./routes/voice');
|
|
|
|
|
|
const chatRoutes = require('./routes/chat');
|
|
|
|
|
|
const sessionRoutes = require('./routes/session');
|
2026-03-13 13:06:46 +08:00
|
|
|
|
const { setupNativeVoiceGateway } = require('./services/nativeVoiceGateway');
|
2026-03-12 12:47:56 +08:00
|
|
|
|
|
|
|
|
|
|
// ========== 环境变量校验 ==========
|
|
|
|
|
|
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: '方舟私域知识库(语音)' },
|
2026-03-24 17:19:36 +08:00
|
|
|
|
{ key: 'ASSISTANT_PROFILE_API_URL', desc: '外接助手资料接口' },
|
2026-03-12 12:47:56 +08:00
|
|
|
|
];
|
|
|
|
|
|
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();
|
2026-03-13 13:06:46 +08:00
|
|
|
|
const server = http.createServer(app);
|
2026-03-12 12:47:56 +08:00
|
|
|
|
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();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-24 17:19:36 +08:00
|
|
|
|
app.use('/api/assistant-profile', assistantProfileRoutes);
|
2026-03-12 12:47:56 +08:00
|
|
|
|
app.use('/api/voice', voiceRoutes);
|
|
|
|
|
|
app.use('/api/chat', chatRoutes);
|
|
|
|
|
|
app.use('/api/session', sessionRoutes);
|
|
|
|
|
|
|
|
|
|
|
|
app.get('/api/health', (req, res) => {
|
2026-03-13 13:06:46 +08:00
|
|
|
|
const envReady = !process.env.VOLC_S2S_APP_ID?.startsWith('your_');
|
2026-03-12 12:47:56 +08:00
|
|
|
|
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_'),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-13 13:06:46 +08:00
|
|
|
|
// 静态文件服务
|
2026-03-17 11:00:09 +08:00
|
|
|
|
app.use(express.static(path.join(__dirname, '../client/dist')));
|
2026-03-13 13:06:46 +08:00
|
|
|
|
|
|
|
|
|
|
// 处理单页应用路由
|
|
|
|
|
|
app.get('*', (req, res) => {
|
|
|
|
|
|
res.sendFile(path.join(__dirname, '../client/dist/index.html'));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-12 12:47:56 +08:00
|
|
|
|
// 统一错误处理中间件
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 13:06:46 +08:00
|
|
|
|
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, () => {
|
2026-03-12 12:47:56 +08:00
|
|
|
|
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();
|