242 lines
7.1 KiB
JavaScript
242 lines
7.1 KiB
JavaScript
|
|
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;
|