Update code

This commit is contained in:
User
2026-03-12 12:47:56 +08:00
parent 92e7fc5bda
commit 9dab61345c
9383 changed files with 1463454 additions and 1 deletions

188
test2/server/app.js Normal file
View File

@@ -0,0 +1,188 @@
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();