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) {
|
|
|
|
|
if (item.type === 'tool' && item.tool_call_id) {
|
|
|
|
|
toolResponseMap.set(item.tool_call_id, item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 合并工具调用和响应
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 合并tool_calls
|
|
|
|
|
MessageProcessor._mergeToolCalls(result, chunk);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理AIMessageChunk类型
|
|
|
|
|
if (result.type === 'AIMessageChunk') {
|
|
|
|
|
result.type = 'ai';
|
|
|
|
|
if (result.additional_kwargs?.tool_calls) {
|
|
|
|
|
result.tool_calls = result.additional_kwargs.tool_calls;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 合并工具调用
|
|
|
|
|
* @private
|
|
|
|
|
* @param {Object} result - 结果对象
|
|
|
|
|
* @param {Object} chunk - 当前块
|
|
|
|
|
*/
|
|
|
|
|
static _mergeToolCalls(result, chunk) {
|
|
|
|
|
if (chunk.additional_kwargs?.tool_calls) {
|
|
|
|
|
if (!result.additional_kwargs) result.additional_kwargs = {};
|
|
|
|
|
if (!result.additional_kwargs.tool_calls) result.additional_kwargs.tool_calls = [];
|
|
|
|
|
|
|
|
|
|
for (const toolCall of chunk.additional_kwargs.tool_calls) {
|
|
|
|
|
const existingToolCall = result.additional_kwargs.tool_calls.find(
|
|
|
|
|
t => (t.id === toolCall.id || t.index === toolCall.index)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (existingToolCall) {
|
|
|
|
|
// 合并相同ID的tool call
|
|
|
|
|
if (existingToolCall.function && toolCall.function) {
|
|
|
|
|
existingToolCall.function.arguments += toolCall.function.arguments;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 添加新的tool call
|
|
|
|
|
result.additional_kwargs.tool_calls.push(JSON.parse(JSON.stringify(toolCall)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理流式响应数据块
|
|
|
|
|
* @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;
|