2025-08-25 13:57:47 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 消息处理工具类
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class MessageProcessor {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将工具结果与消息合并
|
|
|
|
|
|
* @param {Array} msgs - 消息数组
|
|
|
|
|
|
* @returns {Array} 合并后的消息数组
|
|
|
|
|
|
*/
|
|
|
|
|
|
static convertToolResultToMessages(msgs) {
|
|
|
|
|
|
const toolResponseMap = new Map();
|
2025-10-04 22:21:30 +08:00
|
|
|
|
|
2025-08-25 13:57:47 +08:00
|
|
|
|
// 构建工具响应映射
|
|
|
|
|
|
for (const item of msgs) {
|
2025-10-27 18:41:35 +08:00
|
|
|
|
if (item.type === 'tool') {
|
|
|
|
|
|
// 使用多种可能的ID字段来匹配工具调用
|
|
|
|
|
|
const toolCallId = item.tool_call_id || item.id;
|
|
|
|
|
|
if (toolCallId) {
|
|
|
|
|
|
toolResponseMap.set(toolCallId, item);
|
|
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 合并工具调用和响应
|
|
|
|
|
|
const convertedMsgs = msgs.map(item => {
|
|
|
|
|
|
if (item.type === 'ai' && item.tool_calls && item.tool_calls.length > 0) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...item,
|
|
|
|
|
|
tool_calls: item.tool_calls.map(toolCall => {
|
|
|
|
|
|
const toolResponse = toolResponseMap.get(toolCall.id);
|
|
|
|
|
|
return {
|
|
|
|
|
|
...toolCall,
|
|
|
|
|
|
tool_call_result: toolResponse || null
|
|
|
|
|
|
};
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return item;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return convertedMsgs;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将服务器历史记录转换为对话格式
|
|
|
|
|
|
* @param {Array} serverHistory - 服务器历史记录
|
|
|
|
|
|
* @returns {Array} 对话数组
|
|
|
|
|
|
*/
|
|
|
|
|
|
static convertServerHistoryToMessages(serverHistory) {
|
2025-10-04 22:21:30 +08:00
|
|
|
|
// Filter out standalone 'tool' messages since tool results are already in AI messages' tool_calls
|
|
|
|
|
|
// Backend new storage: tool results are embedded in AI messages' tool_calls array with tool_call_result field
|
|
|
|
|
|
const filteredHistory = serverHistory.filter(item => item.type !== 'tool');
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-04 22:21:30 +08:00
|
|
|
|
// 按照对话分组
|
2025-08-25 13:57:47 +08:00
|
|
|
|
const conversations = [];
|
|
|
|
|
|
let currentConv = null;
|
|
|
|
|
|
|
2025-10-04 22:21:30 +08:00
|
|
|
|
for (const item of filteredHistory) {
|
2025-08-25 13:57:47 +08:00
|
|
|
|
if (item.type === 'human') {
|
2025-10-04 22:21:30 +08:00
|
|
|
|
// Start new conversation, finalize previous one
|
|
|
|
|
|
if (currentConv) {
|
|
|
|
|
|
// Find the last AI message and mark it as final
|
|
|
|
|
|
for (let i = currentConv.messages.length - 1; i >= 0; i--) {
|
|
|
|
|
|
if (currentConv.messages[i].type === 'ai') {
|
|
|
|
|
|
currentConv.messages[i].isLast = true;
|
|
|
|
|
|
currentConv.status = 'finished';
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
currentConv = {
|
|
|
|
|
|
messages: [item],
|
|
|
|
|
|
status: 'loading'
|
|
|
|
|
|
};
|
|
|
|
|
|
conversations.push(currentConv);
|
|
|
|
|
|
} else if (item.type === 'ai' && currentConv) {
|
|
|
|
|
|
currentConv.messages.push(item);
|
2025-10-04 22:21:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
|
2025-10-04 22:21:30 +08:00
|
|
|
|
// Mark the last conversation as finished
|
|
|
|
|
|
if (currentConv && currentConv.messages.length > 0) {
|
|
|
|
|
|
// Find the last AI message and mark it as final
|
|
|
|
|
|
for (let i = currentConv.messages.length - 1; i >= 0; i--) {
|
|
|
|
|
|
if (currentConv.messages[i].type === 'ai') {
|
|
|
|
|
|
currentConv.messages[i].isLast = true;
|
2025-08-25 13:57:47 +08:00
|
|
|
|
currentConv.status = 'finished';
|
2025-10-04 22:21:30 +08:00
|
|
|
|
break;
|
2025-08-25 13:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return conversations;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 合并消息块
|
|
|
|
|
|
* @param {Array} chunks - 消息块数组
|
|
|
|
|
|
* @returns {Object|null} 合并后的消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
static mergeMessageChunk(chunks) {
|
|
|
|
|
|
if (chunks.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
|
|
// 深拷贝第一个chunk作为结果
|
|
|
|
|
|
const result = JSON.parse(JSON.stringify(chunks[0]));
|
|
|
|
|
|
result.content = result.content || '';
|
|
|
|
|
|
|
|
|
|
|
|
// 合并后续chunks
|
|
|
|
|
|
for (let i = 1; i < chunks.length; i++) {
|
|
|
|
|
|
const chunk = chunks[i];
|
2025-10-04 22:21:30 +08:00
|
|
|
|
|
2025-08-25 13:57:47 +08:00
|
|
|
|
// 合并内容
|
|
|
|
|
|
if (chunk.content) {
|
|
|
|
|
|
result.content += chunk.content;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 合并reasoning_content
|
|
|
|
|
|
if (chunk.reasoning_content) {
|
|
|
|
|
|
if (!result.reasoning_content) {
|
|
|
|
|
|
result.reasoning_content = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
result.reasoning_content += chunk.reasoning_content;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 合并additional_kwargs中的reasoning_content
|
|
|
|
|
|
if (chunk.additional_kwargs?.reasoning_content) {
|
|
|
|
|
|
if (!result.additional_kwargs) result.additional_kwargs = {};
|
|
|
|
|
|
if (!result.additional_kwargs.reasoning_content) {
|
|
|
|
|
|
result.additional_kwargs.reasoning_content = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
result.additional_kwargs.reasoning_content += chunk.additional_kwargs.reasoning_content;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-27 18:41:35 +08:00
|
|
|
|
// 合并tool_calls (处理新的数据结构)
|
2025-08-25 13:57:47 +08:00
|
|
|
|
MessageProcessor._mergeToolCalls(result, chunk);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理AIMessageChunk类型
|
|
|
|
|
|
if (result.type === 'AIMessageChunk') {
|
|
|
|
|
|
result.type = 'ai';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 合并工具调用
|
|
|
|
|
|
* @private
|
|
|
|
|
|
* @param {Object} result - 结果对象
|
|
|
|
|
|
* @param {Object} chunk - 当前块
|
|
|
|
|
|
*/
|
|
|
|
|
|
static _mergeToolCalls(result, chunk) {
|
2025-11-01 21:34:16 +08:00
|
|
|
|
if (chunk.tool_call_chunks && chunk.tool_call_chunks.length > 0) {
|
|
|
|
|
|
// 确保 result 有 tool_calls 数组
|
2025-10-27 18:41:35 +08:00
|
|
|
|
if (!result.tool_calls) result.tool_calls = [];
|
|
|
|
|
|
|
2025-11-01 21:34:16 +08:00
|
|
|
|
for (const toolCallChunk of chunk.tool_call_chunks) {
|
|
|
|
|
|
// 使用 index 来标识工具调用(因为可能有多个工具调用)
|
|
|
|
|
|
const existingToolCallIndex = result.tool_calls.findIndex(
|
|
|
|
|
|
t => t.index === toolCallChunk.index
|
2025-08-25 13:57:47 +08:00
|
|
|
|
);
|
2025-10-27 18:41:35 +08:00
|
|
|
|
|
2025-11-01 21:34:16 +08:00
|
|
|
|
if (existingToolCallIndex !== -1) {
|
|
|
|
|
|
// 合并相同index的tool call
|
|
|
|
|
|
const existingToolCall = result.tool_calls[existingToolCallIndex];
|
2025-10-27 18:41:35 +08:00
|
|
|
|
|
2025-11-01 21:34:16 +08:00
|
|
|
|
// 更新名称和ID(如果存在)
|
|
|
|
|
|
if (toolCallChunk.name && !existingToolCall.function?.name) {
|
|
|
|
|
|
if (!existingToolCall.function) existingToolCall.function = {};
|
|
|
|
|
|
existingToolCall.function.name = toolCallChunk.name;
|
2025-10-27 18:41:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-01 21:34:16 +08:00
|
|
|
|
if (toolCallChunk.id && !existingToolCall.id) {
|
|
|
|
|
|
existingToolCall.id = toolCallChunk.id;
|
2025-10-27 18:41:35 +08:00
|
|
|
|
}
|
2025-11-01 21:34:16 +08:00
|
|
|
|
|
|
|
|
|
|
// 合并参数
|
|
|
|
|
|
if (toolCallChunk.args) {
|
|
|
|
|
|
if (!existingToolCall.function) existingToolCall.function = {};
|
|
|
|
|
|
if (!existingToolCall.function.arguments) existingToolCall.function.arguments = '';
|
|
|
|
|
|
existingToolCall.function.arguments += toolCallChunk.args;
|
2025-10-27 18:41:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2025-08-25 13:57:47 +08:00
|
|
|
|
// 添加新的tool call
|
2025-11-01 21:34:16 +08:00
|
|
|
|
const newToolCall = {
|
|
|
|
|
|
index: toolCallChunk.index,
|
|
|
|
|
|
id: toolCallChunk.id,
|
|
|
|
|
|
function: {
|
|
|
|
|
|
name: toolCallChunk.name || null,
|
|
|
|
|
|
arguments: toolCallChunk.args || ''
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
result.tool_calls.push(newToolCall);
|
2025-10-27 18:41:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-25 13:57:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理流式响应数据块
|
|
|
|
|
|
* @param {Object} data - 响应数据
|
|
|
|
|
|
* @param {Object} onGoingConv - 进行中的对话对象
|
|
|
|
|
|
* @param {Object} state - 状态对象
|
|
|
|
|
|
* @param {Function} getAgentHistory - 获取历史记录函数
|
|
|
|
|
|
* @param {Function} handleError - 错误处理函数
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async processResponseChunk(data, onGoingConv, state, getAgentHistory, handleError) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
switch (data.status) {
|
|
|
|
|
|
case 'init':
|
|
|
|
|
|
// 代表服务端收到请求并返回第一个响应
|
|
|
|
|
|
state.waitingServerResponse = false;
|
|
|
|
|
|
onGoingConv.msgChunks[data.request_id] = [data.msg];
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 'loading':
|
|
|
|
|
|
if (data.msg.id) {
|
|
|
|
|
|
if (!onGoingConv.msgChunks[data.msg.id]) {
|
|
|
|
|
|
onGoingConv.msgChunks[data.msg.id] = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
onGoingConv.msgChunks[data.msg.id].push(data.msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 'error':
|
|
|
|
|
|
console.error("流式处理出错:", data.message);
|
|
|
|
|
|
handleError(new Error(data.message), 'stream');
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case 'finished':
|
|
|
|
|
|
await getAgentHistory();
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
console.warn('未知的响应状态:', data.status);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
handleError(error, 'stream');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理流式响应
|
|
|
|
|
|
* @param {Response} response - 响应对象
|
|
|
|
|
|
* @param {Function} processChunk - 处理块的函数
|
|
|
|
|
|
* @param {Function} scrollToBottom - 滚动到底部函数
|
|
|
|
|
|
* @param {Function} handleError - 错误处理函数
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async handleStreamResponse(response, processChunk, scrollToBottom, handleError) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const reader = response.body.getReader();
|
|
|
|
|
|
let buffer = '';
|
|
|
|
|
|
const decoder = new TextDecoder();
|
|
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
|
const { done, value } = await reader.read();
|
|
|
|
|
|
if (done) break;
|
|
|
|
|
|
|
|
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
|
|
const lines = buffer.split('\n');
|
|
|
|
|
|
buffer = lines.pop() || ''; // 保留最后一行可能不完整的内容
|
|
|
|
|
|
|
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
|
if (line.trim()) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = JSON.parse(line.trim());
|
|
|
|
|
|
await processChunk(data);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.debug('解析JSON出错:', e.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
await scrollToBottom();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理缓冲区中可能剩余的内容
|
|
|
|
|
|
if (buffer.trim()) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = JSON.parse(buffer.trim());
|
|
|
|
|
|
await processChunk(data);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('最终缓冲区内容无法解析:', buffer);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
handleError(error, 'stream');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default MessageProcessor;
|