ForcePilot/web/src/utils/messageProcessor.js
2026-06-03 18:55:34 +08:00

425 lines
14 KiB
JavaScript
Raw 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.

/**
* 消息处理工具类
*/
export class MessageProcessor {
/**
* 将工具结果与消息合并
* @param {Array} msgs - 消息数组
* @returns {Array} 合并后的消息数组
*/
static convertToolResultToMessages(msgs) {
const toolResponseMap = new Map()
// 构建工具响应映射
for (const item of msgs) {
if (item.type === 'tool') {
// 使用多种可能的ID字段来匹配工具调用
const toolCallId = item.tool_call_id || item.id
if (toolCallId) {
toolResponseMap.set(toolCallId, 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) {
// 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' &&
!(item.type === 'human' && item.extra_metadata?.source === 'ask_user_question_resume')
)
// 按照对话分组
const conversations = []
let currentConv = null
for (const item of filteredHistory) {
if (item.type === 'human') {
// 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
}
}
}
currentConv = {
messages: [item],
status: 'loading'
}
conversations.push(currentConv)
} else if (item.type === 'ai' && currentConv) {
currentConv.messages.push(item)
}
}
// 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
currentConv.status = 'finished'
break
}
}
}
return conversations
}
/**
* 提取一轮对话中所有知识库检索块
* @param {Object} conv - 单轮对话
* @param {Array} databases - 知识库列表
* @returns {Array} 归一化后的检索块
*/
static extractKnowledgeChunksFromConversation(conv, databases = []) {
if (!conv || !Array.isArray(conv.messages) || conv.messages.length === 0) return []
const databaseNames = new Set(
(databases || [])
.map((db) => db?.name)
.filter((name) => typeof name === 'string' && name.trim())
)
if (databaseNames.size === 0) return []
const normalizedChunks = []
const dedupSet = new Set()
const appendChunk = (chunk, kbName) => {
if (!chunk || typeof chunk !== 'object') return
const content = typeof chunk.content === 'string' ? chunk.content.trim() : ''
if (!content) return
const metadata = chunk.metadata && typeof chunk.metadata === 'object' ? chunk.metadata : {}
const dedupKey =
metadata.chunk_id && typeof metadata.chunk_id === 'string'
? `${kbName}::${metadata.chunk_id}`
: `${kbName}::${content}`
if (dedupSet.has(dedupKey)) return
dedupSet.add(dedupKey)
const score = typeof chunk.score === 'number' ? chunk.score : null
normalizedChunks.push({
kb_name: kbName,
content,
score,
metadata: {
source: metadata.source || '',
file_id: metadata.file_id || '',
chunk_id: metadata.chunk_id || '',
chunk_index: metadata.chunk_index
}
})
}
const parseToolResultContent = (content) => {
if (Array.isArray(content)) return content
if (content && typeof content === 'object') return content
if (typeof content === 'string') {
try {
return JSON.parse(content)
} catch {
return null
}
}
return null
}
for (const msg of conv.messages) {
if (!msg || msg.type !== 'ai' || !Array.isArray(msg.tool_calls)) continue
for (const toolCall of msg.tool_calls) {
const kbName = toolCall?.name || toolCall?.function?.name
if (!databaseNames.has(kbName)) continue
const content = toolCall?.tool_call_result?.content
const parsed = parseToolResultContent(content)
if (!parsed) continue
// Milvus / Dify: 直接是 chunks 数组
if (Array.isArray(parsed)) {
for (const chunk of parsed) appendChunk(chunk, kbName)
continue
}
const wrappedChunks = parsed?.data?.chunks
if (Array.isArray(wrappedChunks)) {
for (const chunk of wrappedChunks) appendChunk(chunk, kbName)
}
}
}
normalizedChunks.sort((a, b) => {
const scoreA = typeof a.score === 'number' ? a.score : Number.NEGATIVE_INFINITY
const scoreB = typeof b.score === 'number' ? b.score : Number.NEGATIVE_INFINITY
return scoreB - scoreA
})
return normalizedChunks
}
/**
* 提取一轮对话中的网络搜索来源
* @param {Object} conv - 单轮对话
* @returns {Array} 归一化后的网络来源
*/
static extractWebSourcesFromConversation(conv) {
if (!conv || !Array.isArray(conv.messages) || conv.messages.length === 0) return []
const webSources = []
const dedupSet = new Set()
const parseToolResultContent = (content) => {
if (Array.isArray(content)) return content
if (content && typeof content === 'object') return content
if (typeof content === 'string') {
try {
return JSON.parse(content)
} catch {
return null
}
}
return null
}
for (const msg of conv.messages) {
if (!msg || msg.type !== 'ai' || !Array.isArray(msg.tool_calls)) continue
for (const toolCall of msg.tool_calls) {
const toolName = (toolCall?.name || toolCall?.function?.name || '').toLowerCase()
if (!toolName.includes('tavily_search')) continue
const content = toolCall?.tool_call_result?.content
const parsed = parseToolResultContent(content)
const results = Array.isArray(parsed?.results) ? parsed.results : []
if (results.length === 0) continue
for (const item of results) {
const title = typeof item?.title === 'string' ? item.title.trim() : ''
const url = typeof item?.url === 'string' ? item.url.trim() : ''
if (!title || !url) continue
if (dedupSet.has(url)) continue
dedupSet.add(url)
webSources.push({
tool_name: toolCall?.name || toolCall?.function?.name || '网络搜索',
title,
url,
score: typeof item?.score === 'number' ? item.score : null,
content: typeof item?.content === 'string' ? item.content : '',
published_date: typeof item?.published_date === 'string' ? item.published_date : ''
})
}
}
}
webSources.sort((a, b) => {
const scoreA = typeof a.score === 'number' ? a.score : Number.NEGATIVE_INFINITY
const scoreB = typeof b.score === 'number' ? b.score : Number.NEGATIVE_INFINITY
return scoreB - scoreA
})
return webSources
}
/**
* 提取单个消息中的来源
* @param {Object} message - 消息对象
* @param {Array} databases - 知识库列表
* @returns {{knowledgeChunks: Array, webSources: Array}}
*/
static extractSourcesFromMessage(message, databases = []) {
if (!message || message.type !== 'ai') return { knowledgeChunks: [], webSources: [] }
// 复用提取逻辑,通过构建临时对话对象
const mockConv = { messages: [message] }
return {
knowledgeChunks: MessageProcessor.extractKnowledgeChunksFromConversation(mockConv, databases),
webSources: MessageProcessor.extractWebSourcesFromConversation(mockConv)
}
}
/**
* 提取一轮对话中的全部来源(知识库+网络搜索)
* @param {Object} conv - 单轮对话
* @param {Array} databases - 知识库列表
* @returns {{knowledgeChunks: Array, webSources: Array}}
*/
static extractSourcesFromConversation(conv, databases = []) {
return {
knowledgeChunks: MessageProcessor.extractKnowledgeChunksFromConversation(conv, databases),
webSources: MessageProcessor.extractWebSourcesFromConversation(conv)
}
}
/**
* 解析助手消息正文与推理内容,保持渲染和列表拆分使用同一套规则。
* @param {Object} message - AI 消息对象
* @returns {{content: string, reasoningContent: string}}
*/
static parseAssistantMessageBody(message) {
let content = typeof message?.content === 'string' ? message.content.trim() : ''
let reasoningContent = message?.additional_kwargs?.reasoning_content || ''
if (!reasoningContent && content) {
const thinkRegex = /<think>(.*?)<\/think>|<think>(.*?)$/s
const thinkMatch = content.match(thinkRegex)
if (thinkMatch) {
reasoningContent = (thinkMatch[1] || thinkMatch[2] || '').trim()
content = content.replace(thinkMatch[0], '').trim()
}
}
return { content, reasoningContent }
}
/**
* 合并消息块
* @param {Array} chunks - 消息块数组
* @returns {Object|null} 合并后的消息
*/
static mergeMessageChunk(chunks) {
if (chunks.length === 0) return null
// 深拷贝第一个chunk作为结果
const result = JSON.parse(JSON.stringify(chunks[0]))
// 处理用户消息的内容格式 - 确保显示纯文本
if (result.type === 'human' || result.role === 'user') {
// 如果content是数组格式LangChain多模态消息提取文本部分
if (Array.isArray(result.content)) {
const textPart = result.content.find((item) => item.type === 'text')
result.content = textPart ? textPart.text : ''
} else {
result.content = result.content || ''
}
} else {
result.content = result.content || ''
}
// 合并后续chunks
for (let i = 1; i < chunks.length; i++) {
const chunk = chunks[i]
// 合并内容
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'
}
return result
}
/**
* 合并工具调用
* @private
* @param {Object} result - 结果对象
* @param {Object} chunk - 当前块
*/
static _mergeToolCalls(result, chunk) {
if (chunk.tool_call_chunks && chunk.tool_call_chunks.length > 0) {
// 确保 result 有 tool_calls 数组
if (!result.tool_calls) result.tool_calls = []
for (const toolCallChunk of chunk.tool_call_chunks) {
// 使用 index 来标识工具调用(因为可能有多个工具调用)
const existingToolCallIndex = result.tool_calls.findIndex(
(t) => t.index === toolCallChunk.index
)
if (existingToolCallIndex !== -1) {
// 合并相同index的tool call
const existingToolCall = result.tool_calls[existingToolCallIndex]
// 更新名称和ID如果存在
if (toolCallChunk.name && !existingToolCall.function?.name) {
if (!existingToolCall.function) existingToolCall.function = {}
existingToolCall.function.name = toolCallChunk.name
}
if (toolCallChunk.id && !existingToolCall.id) {
existingToolCall.id = toolCallChunk.id
}
// 合并参数
if (toolCallChunk.args) {
if (!existingToolCall.function) existingToolCall.function = {}
if (!existingToolCall.function.arguments) existingToolCall.function.arguments = ''
existingToolCall.function.arguments += toolCallChunk.args
}
} else {
// 添加新的tool call
const newToolCall = {
index: toolCallChunk.index,
id: toolCallChunk.id,
function: {
name: toolCallChunk.name || null,
arguments: toolCallChunk.args || ''
}
}
result.tool_calls.push(newToolCall)
}
}
}
}
}
export default MessageProcessor