Files
bigwo/test2/server/routes/voice.js
User 9567eb7358 feat(server): KB prompt优化、字幕修复、S2S重连、助手配置API
- assistantProfileConfig: KB answer prompt改为分层策略(严格产品信息+灵活常识补充)
- nativeVoiceGateway: S2S upstream自动重连(最多50次)、event 351字幕debounce(800ms取最长文本)
- toolExecutor: 确定性query改写增强、KB查询传递session上下文
- contextKeywordTracker: 支持KB话题记忆优先enrichment
- contentSafeGuard: 新增品牌安全内容过滤服务
- assistantProfileService: 新增助手配置CRUD服务
- routes/assistantProfile: 新增助手配置API路由
- knowledgeKeywords: 扩展KB关键词词典
- fastAsrCorrector: ASR纠错规则更新
- tests/: KB prompt测试、保护窗口测试、Viking性能测试
- docs/: 助手配置API文档、系统提示词目录
2026-03-24 17:19:36 +08:00

185 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const express = require('express');
const router = express.Router();
const { v4: uuidv4 } = require('uuid');
const ToolExecutor = require('../services/toolExecutor');
const contextKeywordTracker = require('../services/contextKeywordTracker');
const { getRuleBasedDirectRouteDecision, shouldForceKnowledgeRoute } = require('../services/realtimeDialogRouting');
const DEFAULT_TOOLS = require('../config/tools');
const db = require('../db');
const directSessions = new Map();
router.get('/config', (req, res) => {
res.json({
success: true,
data: {
models: [
{ value: '1.2.1.0', label: 'O2.0(推荐,精品音质)' },
{ value: 'O', label: 'O基础版' },
{ value: '2.2.0.0', label: 'SC2.0(推荐,声音复刻)' },
{ value: 'SC', label: 'SC基础版' },
],
speakers: [
{ value: 'zh_female_vv_jupiter_bigtts', label: 'VV活泼女声', series: 'O' },
{ value: 'zh_female_xiaohe_jupiter_bigtts', label: '小禾(甜美女声·台湾口音)', series: 'O' },
{ value: 'zh_male_yunzhou_jupiter_bigtts', label: '云舟(沉稳男声)', series: 'O' },
{ value: 'zh_male_xiaotian_jupiter_bigtts', label: '小天(磁性男声)', series: 'O' },
{ value: 'saturn_common_female_1', label: 'Saturn 女声1', series: 'SC2.0' },
{ value: 'saturn_common_male_1', label: 'Saturn 男声1', series: 'SC2.0' },
{ value: 'ICL_common_female_1', label: 'ICL 女声1', series: 'SC' },
{ value: 'ICL_common_male_1', label: 'ICL 男声1', series: 'SC' },
],
tools: DEFAULT_TOOLS.map((t) => ({
name: t.function.name,
description: t.function.description,
})),
},
});
});
router.post('/direct/session', async (req, res) => {
try {
const { userId, sessionId } = req.body || {};
const sid = sessionId || uuidv4();
const directSession = {
sessionId: sid,
userId: userId || null,
startTime: Date.now(),
direct: true,
};
directSessions.set(sid, directSession);
await db.createSession(sid, userId || null, 'voice');
res.json({
success: true,
data: {
sessionId: sid,
userId: userId || null,
},
});
} catch (error) {
console.error('[DirectVoice] Create session failed:', error.message);
res.status(500).json({ success: false, error: error.message });
}
});
router.post('/direct/message', async (req, res) => {
try {
const { sessionId, role, text, source, toolName } = req.body || {};
if (!sessionId || !text || !source) {
return res.status(400).json({ success: false, error: 'sessionId, text and source are required' });
}
if (role === 'user') {
contextKeywordTracker.updateSession(sessionId, text);
}
await db.addMessage(sessionId, role === 'user' ? 'user' : 'assistant', text, source, toolName || null);
res.json({ success: true });
} catch (error) {
console.error('[DirectVoice] Add message failed:', error.message);
res.status(500).json({ success: false, error: error.message });
}
});
router.post('/diag', (req, res) => {
try {
const { sessionId, roomId, type, payload } = req.body || {};
console.log(`[Diag] type=${type || 'unknown'} session=${sessionId || '-'} room=${roomId || '-'} payload=${JSON.stringify(payload || {})}`);
res.json({ success: true });
} catch (error) {
console.error('[Diag] Error:', error.message);
res.status(500).json({ success: false, error: error.message });
}
});
router.post('/direct/query', async (req, res) => {
try {
const { sessionId, query, appendUserMessage } = req.body || {};
if (!sessionId) {
return res.status(400).json({ success: false, error: 'sessionId is required' });
}
const context = await db.getHistoryForLLM(sessionId, 20).catch(() => []);
const cleanQuery = (query || '').trim();
if (appendUserMessage && cleanQuery) {
contextKeywordTracker.updateSession(sessionId, cleanQuery);
await db.addMessage(sessionId, 'user', cleanQuery, 'voice_asr').catch(() => null);
}
if (!appendUserMessage && cleanQuery) {
contextKeywordTracker.updateSession(sessionId, cleanQuery);
}
const routeDecision = getRuleBasedDirectRouteDecision(cleanQuery);
const forceKb = shouldForceKnowledgeRoute(cleanQuery, context);
const shouldSearchKb = routeDecision.route === 'search_knowledge' || forceKb;
const directSession = directSessions.get(sessionId);
const result = shouldSearchKb
? await ToolExecutor.execute('search_knowledge', { query: cleanQuery, session_id: sessionId, _session: { userId: directSession?.userId || null } }, context)
: { hit: false, reason: 'route_skip', source: 'route_skip', error: '该问题不在知识库范围内,请咨询其他问题。' };
let contentText = JSON.stringify(result);
if (result && result.results && Array.isArray(result.results)) {
contentText = result.results.map((item) => item.content || JSON.stringify(item)).join('\n');
} else if (result && result.error) {
contentText = result.error;
} else if (typeof result === 'string') {
contentText = result;
}
const ragItems = result && result.results && Array.isArray(result.results) && result.results.length > 0
? result.results.map((item) => ({
title: item.title || '知识库结果',
content: item.content || JSON.stringify(item),
}))
: [{
title: '知识库结果',
content: contentText,
}];
await db.addMessage(sessionId, 'assistant', contentText, 'voice_tool', 'search_knowledge', {
route: 'search_knowledge',
original_text: cleanQuery,
tool_name: 'search_knowledge',
tool_args: { query: cleanQuery },
source: result?.source || null,
original_query: result?.original_query || cleanQuery,
rewritten_query: result?.rewritten_query || null,
selected_dataset_ids: result?.selected_dataset_ids || null,
selected_kb_routes: result?.selected_kb_routes || null,
hit: typeof result?.hit === 'boolean' ? result.hit : null,
reason: result?.reason || null,
error_type: result?.errorType || null,
latency_ms: result?.latency_ms || null,
}).catch(() => null);
res.json({
success: true,
data: {
sessionId,
query: cleanQuery,
contentText,
ragItems,
ragJson: JSON.stringify(ragItems),
},
});
} catch (error) {
console.error('[DirectVoice] Query failed:', error.message);
res.status(500).json({ success: false, error: error.message });
}
});
router.post('/direct/stop', async (req, res) => {
try {
const { sessionId } = req.body || {};
if (!sessionId) {
return res.status(400).json({ success: false, error: 'sessionId is required' });
}
directSessions.delete(sessionId);
const messages = await db.getMessages(sessionId).catch(() => []);
res.json({
success: true,
data: {
sessionId,
messageCount: messages.length,
},
});
} catch (error) {
console.error('[DirectVoice] Stop session failed:', error.message);
res.status(500).json({ success: false, error: error.message });
}
});
module.exports = router;