Files
bigwo/test2/server/routes/chat.js
2026-03-12 12:47:56 +08:00

242 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 cozeChatService = require('../services/cozeChatService');
const db = require('../db');
// 存储文字对话的会话状态sessionId -> session
const chatSessions = new Map();
/**
* POST /api/chat/start
* 创建文字对话会话,可选传入语音通话的历史字幕
*/
router.post('/start', async (req, res) => {
const { sessionId, voiceSubtitles = [] } = req.body;
if (!sessionId) {
return res.status(400).json({ success: false, error: 'sessionId is required' });
}
if (!cozeChatService.isConfigured()) {
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,
});
}
}
// 更新数据库会话模式为 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,
});
console.log(`[Chat] Session started: ${sessionId}, fromVoice: ${voiceSubtitles.length > 0}, voiceMessages: ${voiceMessages.length}`);
res.json({
success: true,
data: {
sessionId,
messageCount: voiceMessages.length,
fromVoice: voiceSubtitles.length > 0 || voiceMessages.length > 0,
},
});
});
/**
* POST /api/chat/send
* 发送文字消息并获取 Coze 智能体回复(非流式)
*/
router.post('/send', async (req, res) => {
try {
const { sessionId, message } = req.body;
if (!sessionId || !message) {
return res.status(400).json({ success: false, error: 'sessionId and message are required' });
}
let session = chatSessions.get(sessionId);
// 自动创建会话(如果不存在)
if (!session) {
session = {
userId: `user_${sessionId.slice(0, 12)}`,
conversationId: null,
voiceMessages: [],
createdAt: Date.now(),
fromVoice: false,
};
chatSessions.set(sessionId, session);
}
console.log(`[Chat] User(${sessionId}): ${message}`);
// 写入数据库:用户消息
db.addMessage(sessionId, 'user', message, 'chat_user').catch(e => console.warn('[DB] addMessage failed:', e.message));
// 首次对话时注入语音历史作为上下文,之后 Coze 自动管理会话历史
const extraMessages = !session.conversationId ? session.voiceMessages : [];
const result = await cozeChatService.chat(
session.userId,
message,
session.conversationId,
extraMessages
);
// 保存 Coze 返回的 conversationId
session.conversationId = result.conversationId;
console.log(`[Chat] Assistant(${sessionId}): ${result.content?.substring(0, 100)}`);
// 写入数据库AI 回复
if (result.content) {
db.addMessage(sessionId, 'assistant', result.content, 'chat_bot').catch(e => console.warn('[DB] addMessage failed:', e.message));
}
res.json({
success: true,
data: {
content: result.content,
},
});
} catch (error) {
console.error('[Chat] Send failed:', error.message);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* GET /api/chat/history/:sessionId
* 获取会话状态
*/
router.get('/history/:sessionId', (req, res) => {
const session = chatSessions.get(req.params.sessionId);
if (!session) {
return res.json({ success: true, data: [] });
}
res.json({
success: true,
data: {
conversationId: session.conversationId,
fromVoice: session.fromVoice,
},
});
});
/**
* POST /api/chat/send-stream
* 流式发送文字消息SSE逐字输出 Coze 智能体回复
*/
router.post('/send-stream', async (req, res) => {
const { sessionId, message } = req.body;
if (!sessionId || !message) {
return res.status(400).json({ success: false, error: 'sessionId and message are required' });
}
let session = chatSessions.get(sessionId);
if (!session) {
session = {
userId: `user_${sessionId.slice(0, 12)}`,
conversationId: null,
voiceMessages: [],
createdAt: Date.now(),
fromVoice: false,
};
chatSessions.set(sessionId, session);
}
console.log(`[Chat][SSE] User(${sessionId}): ${message}`);
// 写入数据库:用户消息
db.addMessage(sessionId, 'user', message, 'chat_user').catch(e => console.warn('[DB] addMessage failed:', e.message));
// 设置 SSE 响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders();
try {
// 首次对话时注入语音历史作为上下文
const extraMessages = !session.conversationId ? session.voiceMessages : [];
const result = await cozeChatService.chatStream(
session.userId,
message,
session.conversationId,
extraMessages,
{
onChunk: (text) => {
res.write(`data: ${JSON.stringify({ type: 'chunk', content: text })}\n\n`);
},
onDone: () => {},
}
);
// 保存 Coze 返回的 conversationId
session.conversationId = result.conversationId;
console.log(`[Chat][SSE] Assistant(${sessionId}): ${result.content?.substring(0, 100)}`);
// 写入数据库AI 回复
if (result.content) {
db.addMessage(sessionId, 'assistant', result.content, 'chat_bot').catch(e => console.warn('[DB] addMessage failed:', e.message));
}
res.write(`data: ${JSON.stringify({ type: 'done', content: result.content })}\n\n`);
res.end();
} catch (error) {
console.error('[Chat][SSE] Stream failed:', error.message);
res.write(`data: ${JSON.stringify({ type: 'error', error: error.message })}\n\n`);
res.end();
}
});
/**
* DELETE /api/chat/:sessionId
* 删除对话会话
*/
router.delete('/:sessionId', (req, res) => {
chatSessions.delete(req.params.sessionId);
res.json({ success: true });
});
// 定时清理过期会话30 分钟无活动)
setInterval(() => {
const now = Date.now();
const TTL = 30 * 60 * 1000;
for (const [id, session] of chatSessions) {
if (now - session.createdAt > TTL) {
chatSessions.delete(id);
console.log(`[Chat] Session expired and cleaned: ${id}`);
}
}
}, 5 * 60 * 1000);
module.exports = router;