Files
bigwo/test2/server/services/cozeChatService.js

212 lines
6.0 KiB
JavaScript
Raw Normal View History

2026-03-12 12:47:56 +08:00
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();