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

212 lines
6.0 KiB
JavaScript
Raw Permalink 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 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();