feat: 添加realtime_dialog和realtime_dialog_external_rag_test项目,更新test2项目

This commit is contained in:
User
2026-03-13 13:06:46 +08:00
parent 9dab61345c
commit 5521b673f5
215 changed files with 7626 additions and 1876 deletions

View File

@@ -1,11 +1,133 @@
const express = require('express');
const router = express.Router();
const cozeChatService = require('../services/cozeChatService');
const arkChatService = require('../services/arkChatService');
const ToolExecutor = require('../services/toolExecutor');
const { shouldForceKnowledgeRoute } = require('../services/realtimeDialogRouting');
const db = require('../db');
// 存储文字对话的会话状态sessionId -> session
const chatSessions = new Map();
function normalizeAssistantText(text) {
return String(text || '')
.replace(/\r/g, ' ')
.replace(/\n{2,}/g, '。')
.replace(/\n/g, ' ')
.replace(/。{2,}/g, '。')
.replace(/([])\1+/g, '$1')
.replace(/([。!?;,])\s*([。!?;,])/g, '$2')
.replace(/\s+/g, ' ')
.trim();
}
async function loadHandoffMessages(sessionId, voiceSubtitles = []) {
let voiceMessages = [];
try {
const dbHistory = await db.getHistoryForLLM(sessionId, 20);
if (dbHistory.length > 0) {
voiceMessages = dbHistory;
console.log(`[Chat] Loaded ${dbHistory.length} messages from DB for session ${sessionId}`);
}
} catch (e) { console.warn('[DB] getHistoryForLLM failed:', e.message); }
if (voiceMessages.length === 0 && voiceSubtitles.length > 0) {
const recentSubtitles = voiceSubtitles.slice(-10);
for (const sub of recentSubtitles) {
voiceMessages.push({
role: sub.role === 'user' ? 'user' : 'assistant',
content: sub.text,
});
}
}
return voiceMessages;
}
async function buildChatSessionState(sessionId, voiceSubtitles = []) {
const voiceMessages = await loadHandoffMessages(sessionId, voiceSubtitles);
let handoffSummary = '';
try {
handoffSummary = await arkChatService.summarizeContextForHandoff(voiceMessages, 3);
} catch (error) {
console.warn('[Chat] summarizeContextForHandoff failed:', error.message);
}
return {
userId: `user_${sessionId.slice(0, 12)}`,
conversationId: null,
voiceMessages,
handoffSummary,
handoffSummaryUsed: false,
createdAt: Date.now(),
lastActiveAt: Date.now(),
fromVoice: voiceSubtitles.length > 0 || voiceMessages.length > 0,
};
}
function buildInitialContextMessages(session) {
const summary = String(session?.handoffSummary || '').trim();
const extraMessages = [];
if (summary && !session?.handoffSummaryUsed) {
extraMessages.push({ role: 'assistant', content: `会话交接摘要:${summary}` });
}
if (Array.isArray(session?.voiceMessages) && session.voiceMessages.length > 0) {
extraMessages.push(...session.voiceMessages.slice(-6));
}
return extraMessages;
}
async function buildKnowledgeContextMessages(sessionId, session) {
const dbHistory = await db.getHistoryForLLM(sessionId, 20).catch(() => []);
const summary = String(session?.handoffSummary || '').trim();
if (!summary || session?.handoffSummaryUsed) {
return dbHistory;
}
return [
{ role: 'assistant', content: `会话交接摘要:${summary}` },
...dbHistory,
];
}
function extractKnowledgeReply(result) {
if (result && result.results && Array.isArray(result.results)) {
return result.results.map((item) => item.content || JSON.stringify(item)).join('\n');
}
if (result && result.error) {
return result.error;
}
return typeof result === 'string' ? result : '';
}
async function tryKnowledgeReply(sessionId, session, message) {
const text = String(message || '').trim();
if (!text) return null;
const context = await buildKnowledgeContextMessages(sessionId, session);
if (!shouldForceKnowledgeRoute(text, context)) {
return null;
}
const result = await ToolExecutor.execute('search_knowledge', { query: text }, context);
const content = normalizeAssistantText(extractKnowledgeReply(result));
if (!content) {
return null;
}
return {
content,
meta: {
route: 'search_knowledge',
original_text: text,
tool_name: 'search_knowledge',
tool_args: { query: text },
source: result?.source || null,
original_query: result?.original_query || text,
rewritten_query: result?.rewritten_query || null,
hit: typeof result?.hit === 'boolean' ? result.hit : null,
reason: result?.reason || null,
error_type: result?.errorType || null,
latency_ms: result?.latency_ms || null,
},
};
}
/**
* POST /api/chat/start
* 创建文字对话会话,可选传入语音通话的历史字幕
@@ -21,46 +143,21 @@ router.post('/start', async (req, res) => {
return res.status(500).json({ success: false, error: 'Coze 智能体未配置,请设置 COZE_API_TOKEN 和 COZE_BOT_ID' });
}
// 优先从数据库加载完整历史(包含语音通话中的工具结果等)
let voiceMessages = [];
try {
const dbHistory = await db.getHistoryForLLM(sessionId, 20);
if (dbHistory.length > 0) {
voiceMessages = dbHistory;
console.log(`[Chat] Loaded ${dbHistory.length} messages from DB for session ${sessionId}`);
}
} catch (e) { console.warn('[DB] getHistoryForLLM failed:', e.message); }
// 如果数据库没有历史,回退到 voiceSubtitles
if (voiceMessages.length === 0 && voiceSubtitles.length > 0) {
const recentSubtitles = voiceSubtitles.slice(-10);
for (const sub of recentSubtitles) {
voiceMessages.push({
role: sub.role === 'user' ? 'user' : 'assistant',
content: sub.text,
});
}
}
const sessionState = await buildChatSessionState(sessionId, voiceSubtitles);
// 更新数据库会话模式为 chat
try { await db.createSession(sessionId, `user_${sessionId.slice(0, 12)}`, 'chat'); } catch (e) {}
chatSessions.set(sessionId, {
userId: `user_${sessionId.slice(0, 12)}`,
conversationId: null,
voiceMessages,
createdAt: Date.now(),
fromVoice: voiceSubtitles.length > 0 || voiceMessages.length > 0,
});
chatSessions.set(sessionId, sessionState);
console.log(`[Chat] Session started: ${sessionId}, fromVoice: ${voiceSubtitles.length > 0}, voiceMessages: ${voiceMessages.length}`);
console.log(`[Chat] Session started: ${sessionId}, fromVoice: ${sessionState.fromVoice}, voiceMessages: ${sessionState.voiceMessages.length}, summary: ${sessionState.handoffSummary ? 'yes' : 'no'}`);
res.json({
success: true,
data: {
sessionId,
messageCount: voiceMessages.length,
fromVoice: voiceSubtitles.length > 0 || voiceMessages.length > 0,
messageCount: sessionState.voiceMessages.length,
fromVoice: sessionState.fromVoice,
},
});
});
@@ -81,23 +178,30 @@ router.post('/send', async (req, res) => {
// 自动创建会话(如果不存在)
if (!session) {
session = {
userId: `user_${sessionId.slice(0, 12)}`,
conversationId: null,
voiceMessages: [],
createdAt: Date.now(),
fromVoice: false,
};
session = await buildChatSessionState(sessionId, []);
chatSessions.set(sessionId, session);
}
session.lastActiveAt = Date.now();
console.log(`[Chat] User(${sessionId}): ${message}`);
// 写入数据库:用户消息
db.addMessage(sessionId, 'user', message, 'chat_user').catch(e => console.warn('[DB] addMessage failed:', e.message));
const knowledgeReply = await tryKnowledgeReply(sessionId, session, message);
if (knowledgeReply) {
session.handoffSummaryUsed = true;
db.addMessage(sessionId, 'assistant', knowledgeReply.content, 'chat_bot', 'search_knowledge', knowledgeReply.meta).catch(e => console.warn('[DB] addMessage failed:', e.message));
return res.json({
success: true,
data: {
content: knowledgeReply.content,
},
});
}
// 首次对话时注入语音历史作为上下文,之后 Coze 自动管理会话历史
const extraMessages = !session.conversationId ? session.voiceMessages : [];
const extraMessages = !session.conversationId ? buildInitialContextMessages(session) : [];
const result = await cozeChatService.chat(
session.userId,
@@ -105,21 +209,23 @@ router.post('/send', async (req, res) => {
session.conversationId,
extraMessages
);
const normalizedContent = normalizeAssistantText(result.content);
// 保存 Coze 返回的 conversationId
session.conversationId = result.conversationId;
session.handoffSummaryUsed = true;
console.log(`[Chat] Assistant(${sessionId}): ${result.content?.substring(0, 100)}`);
console.log(`[Chat] Assistant(${sessionId}): ${normalizedContent?.substring(0, 100)}`);
// 写入数据库AI 回复
if (result.content) {
db.addMessage(sessionId, 'assistant', result.content, 'chat_bot').catch(e => console.warn('[DB] addMessage failed:', e.message));
if (normalizedContent) {
db.addMessage(sessionId, 'assistant', normalizedContent, 'chat_bot').catch(e => console.warn('[DB] addMessage failed:', e.message));
}
res.json({
success: true,
data: {
content: result.content,
content: normalizedContent,
},
});
} catch (error) {
@@ -160,15 +266,10 @@ router.post('/send-stream', async (req, res) => {
let session = chatSessions.get(sessionId);
if (!session) {
session = {
userId: `user_${sessionId.slice(0, 12)}`,
conversationId: null,
voiceMessages: [],
createdAt: Date.now(),
fromVoice: false,
};
session = await buildChatSessionState(sessionId, []);
chatSessions.set(sessionId, session);
}
session.lastActiveAt = Date.now();
console.log(`[Chat][SSE] User(${sessionId}): ${message}`);
@@ -182,9 +283,17 @@ router.post('/send-stream', async (req, res) => {
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders();
const knowledgeReply = await tryKnowledgeReply(sessionId, session, message);
if (knowledgeReply) {
session.handoffSummaryUsed = true;
db.addMessage(sessionId, 'assistant', knowledgeReply.content, 'chat_bot', 'search_knowledge', knowledgeReply.meta).catch(e => console.warn('[DB] addMessage failed:', e.message));
res.write(`data: ${JSON.stringify({ type: 'done', content: knowledgeReply.content })}\n\n`);
return res.end();
}
try {
// 首次对话时注入语音历史作为上下文
const extraMessages = !session.conversationId ? session.voiceMessages : [];
const extraMessages = !session.conversationId ? buildInitialContextMessages(session) : [];
const result = await cozeChatService.chatStream(
session.userId,
@@ -198,17 +307,19 @@ router.post('/send-stream', async (req, res) => {
onDone: () => {},
}
);
const normalizedContent = normalizeAssistantText(result.content);
// 保存 Coze 返回的 conversationId
session.conversationId = result.conversationId;
console.log(`[Chat][SSE] Assistant(${sessionId}): ${result.content?.substring(0, 100)}`);
session.handoffSummaryUsed = true;
console.log(`[Chat][SSE] Assistant(${sessionId}): ${normalizedContent?.substring(0, 100)}`);
// 写入数据库AI 回复
if (result.content) {
db.addMessage(sessionId, 'assistant', result.content, 'chat_bot').catch(e => console.warn('[DB] addMessage failed:', e.message));
if (normalizedContent) {
db.addMessage(sessionId, 'assistant', normalizedContent, 'chat_bot').catch(e => console.warn('[DB] addMessage failed:', e.message));
}
res.write(`data: ${JSON.stringify({ type: 'done', content: result.content })}\n\n`);
res.write(`data: ${JSON.stringify({ type: 'done', content: normalizedContent })}\n\n`);
res.end();
} catch (error) {
console.error('[Chat][SSE] Stream failed:', error.message);
@@ -231,7 +342,7 @@ setInterval(() => {
const now = Date.now();
const TTL = 30 * 60 * 1000;
for (const [id, session] of chatSessions) {
if (now - session.createdAt > TTL) {
if (now - (session.lastActiveAt || session.createdAt) > TTL) {
chatSessions.delete(id);
console.log(`[Chat] Session expired and cleaned: ${id}`);
}