const axios = require('axios'); /** * Coze 智能体对话服务 * 通过 Coze v3 Chat API 与已配置知识库的 Bot 进行对话 * 支持流式和非流式两种模式,Coze 内部管理会话历史 */ class CozeChatService { constructor() { this.baseUrl = (process.env.COZE_BASE_URL || 'https://api.coze.cn') + '/v3'; } _getHeaders() { return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.COZE_API_TOKEN}`, }; } _getBotId() { return process.env.COZE_BOT_ID; } isConfigured() { const token = process.env.COZE_API_TOKEN; const botId = process.env.COZE_BOT_ID; return token && token !== 'your_coze_api_token' && botId && botId !== 'your_coze_bot_id'; } /** * 非流式对话 * @param {string} userId - 用户标识 * @param {string} message - 用户消息 * @param {string|null} conversationId - Coze 会话 ID(续接对话时传入) * @param {Array} extraMessages - 额外上下文消息(如语音字幕历史) * @returns {{ content: string, conversationId: string }} */ async chat(userId, message, conversationId = null, extraMessages = []) { const additionalMessages = [ ...extraMessages.map(m => ({ role: m.role, content: m.content || m.text, content_type: 'text', })), { role: 'user', content: message, content_type: 'text', }, ]; const body = { bot_id: this._getBotId(), user_id: userId, additional_messages: additionalMessages, stream: false, auto_save_history: true, }; if (conversationId) { body.conversation_id = conversationId; } console.log(`[CozeChat] Sending non-stream chat, userId=${userId}, convId=${conversationId || 'new'}`); const chatRes = await axios.post(`${this.baseUrl}/chat`, body, { headers: this._getHeaders(), timeout: 15000, }); const chatData = chatRes.data?.data; if (!chatData?.id || !chatData?.conversation_id) { throw new Error('Coze chat creation failed: ' + JSON.stringify(chatRes.data)); } const chatId = chatData.id; const convId = chatData.conversation_id; // 轮询等待完成(最多 60 秒) const maxAttempts = 30; for (let i = 0; i < maxAttempts; i++) { await new Promise(r => setTimeout(r, 2000)); const statusRes = await axios.get( `${this.baseUrl}/chat/retrieve?chat_id=${chatId}&conversation_id=${convId}`, { headers: this._getHeaders(), timeout: 10000 } ); const status = statusRes.data?.data?.status; if (status === 'completed') break; if (status === 'failed' || status === 'requires_action') { throw new Error(`Coze chat ended with status: ${status}`); } } // 获取消息列表 const msgRes = await axios.get( `${this.baseUrl}/chat/message/list?chat_id=${chatId}&conversation_id=${convId}`, { headers: this._getHeaders(), timeout: 10000 } ); const messages = msgRes.data?.data || []; const answerMsg = messages.find(m => m.role === 'assistant' && m.type === 'answer'); return { content: answerMsg?.content || '', conversationId: convId, }; } /** * 流式对话 * @param {string} userId - 用户标识 * @param {string} message - 用户消息 * @param {string|null} conversationId - Coze 会话 ID * @param {Array} extraMessages - 额外上下文消息 * @param {{ onChunk, onDone }} callbacks - 流式回调 * @returns {{ content: string, conversationId: string }} */ async chatStream(userId, message, conversationId = null, extraMessages = [], { onChunk, onDone }) { const additionalMessages = [ ...extraMessages.map(m => ({ role: m.role, content: m.content || m.text, content_type: 'text', })), { role: 'user', content: message, content_type: 'text', }, ]; const body = { bot_id: this._getBotId(), user_id: userId, additional_messages: additionalMessages, stream: true, auto_save_history: true, }; if (conversationId) { body.conversation_id = conversationId; } console.log(`[CozeChat] Sending stream chat, userId=${userId}, convId=${conversationId || 'new'}`); const response = await axios.post(`${this.baseUrl}/chat`, body, { headers: this._getHeaders(), timeout: 60000, responseType: 'stream', }); return new Promise((resolve, reject) => { let fullContent = ''; let resultConvId = conversationId; let buffer = ''; response.data.on('data', (chunk) => { buffer += chunk.toString(); const lines = buffer.split('\n'); buffer = lines.pop() || ''; let currentEvent = ''; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('event:')) { currentEvent = trimmed.slice(6).trim(); continue; } if (!trimmed.startsWith('data:')) continue; const data = trimmed.slice(5).trim(); if (data === '"[DONE]"' || data === '[DONE]') continue; try { const parsed = JSON.parse(data); if (currentEvent === 'conversation.chat.created') { resultConvId = parsed.conversation_id || resultConvId; } if (currentEvent === 'conversation.message.delta') { if (parsed.role === 'assistant' && parsed.type === 'answer') { const content = parsed.content || ''; fullContent += content; onChunk?.(content); } } } catch (e) { // skip malformed SSE lines } } }); response.data.on('end', () => { onDone?.(fullContent); resolve({ content: fullContent, conversationId: resultConvId }); }); response.data.on('error', (err) => { console.error('[CozeChat] Stream error:', err.message); reject(err); }); }); } } module.exports = new CozeChatService();