Files
bigwo/test2/server/app.js

181 lines
6.3 KiB
JavaScript
Raw Permalink Normal View History

2026-03-12 12:47:56 +08:00
require('dotenv').config();
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');
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');
const { setupNativeVoiceGateway } = require('./services/nativeVoiceGateway');
const redisClient = require('./services/redisClient');
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: '方舟私域知识库(语音)' },
{ key: 'ASSISTANT_PROFILE_API_URL', desc: '外接助手资料接口' },
{ key: 'REDIS_URL', desc: 'Redis对话上下文+KB缓存' },
{ key: 'ENABLE_RERANKER', 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();
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();
});
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) => {
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_'),
redis: redisClient.isAvailable(),
reranker: process.env.ENABLE_RERANKER === 'true' ? (process.env.VOLC_ARK_RERANKER_MODEL || 'doubao-seed-rerank') : false,
kbRetrievalMode: process.env.VOLC_ARK_KB_RETRIEVAL_MODE || 'answer',
2026-03-12 12:47:56 +08:00
},
});
});
// 静态文件服务
app.use(express.static(path.join(__dirname, '../client/dist')));
// 处理单页应用路由
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');
}
// Redis 初始化(可选,失败不阻塞启动)
try {
redisClient.createClient();
console.log('[Redis] Client created, connecting...');
} catch (err) {
console.warn('[Redis] Initialization failed:', err.message);
console.warn('[Redis] Continuing without Redis — 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, () => {
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();