/**
* 历史消息适配器
* 将PC端历史会话消息中的自定义HTML标签转换为移动端支持的Markdown容器语法
*
* PC端格式:
* 移动端格式: :::container executeId="xxx" type="xxx" status="xxx" name="xxx"\n:::\n
*/
import type { MessageInfo, ProcessingInfo, ExecuteResultInfo } from '@/types/interfaces/conversationInfo'
import { ProcessingEnum } from '@/types/enums/common'
import { AssistantRoleEnum, MessageTypeEnum, AgentComponentTypeEnum } from '@/types/enums/agent'
/**
* 组件执行列表项接口(来自API返回)
*/
export interface ComponentExecutedItem {
name: string
result: {
executeId: string
id: number
name: string
type: string // "Plan", "ToolCall", "Skill", "Workflow", "Page", etc.
startTime: number
endTime?: number
success?: boolean
data?: any
input?: any
}
status: string // "FINISHED" | "FAILED" | "EXECUTING"
targetId: number
type: string // "Plan", "ToolCall", "Skill", "Workflow", "Page", etc.
}
/**
* 解析的自定义标签信息
*/
interface ParsedCustomTag {
executeId: string
type: string
status: string
name: string
fullMatch: string // 完整的匹配字符串,用于替换
}
/**
* 解析PC端的markdown-custom-process HTML标签
* @param text 包含HTML标签的文本
* @returns 解析出的标签信息数组
*/
export function parseMarkdownCustomProcess(text: string): ParsedCustomTag[] {
if (!text) return []
const results: ParsedCustomTag[] = []
// 匹配
格式
// 以及 格式(无div包裹)
const regex = /[\s\n]*]*)><\/markdown-custom-process>[\s\n]*<\/div>|]*)><\/markdown-custom-process>/g
let match: RegExpExecArray | null
while ((match = regex.exec(text)) !== null) {
const attrString = match[1] || match[2] || ''
const fullMatch = match[0]
// 解析属性
const executeId = extractAttribute(attrString, 'executeId')
const type = extractAttribute(attrString, 'type')
const status = extractAttribute(attrString, 'status')
const name = extractAttribute(attrString, 'name')
if (executeId) {
results.push({
executeId,
type: type || '',
status: status || '',
name: name || '',
fullMatch
})
}
}
return results
}
/**
* 从属性字符串中提取指定属性的值
* @param attrString 属性字符串,如 ' executeId="xxx" type="yyy"'
* @param attrName 属性名
* @returns 属性值
*/
function extractAttribute(attrString: string, attrName: string): string {
const regex = new RegExp(`${attrName}="([^"]*)"`)
const match = attrString.match(regex)
return match ? match[1] : ''
}
/**
* 将PC端HTML标签转换为移动端Markdown容器语法
* @param tag 解析的标签信息
* @returns Markdown容器语法字符串
*/
export function convertToMobileContainerSyntax(tag: ParsedCustomTag): string {
const attrs = [
`executeId="${tag.executeId}"`,
tag.type ? `type="${tag.type}"` : '',
tag.status ? `status="${tag.status}"` : '',
tag.name ? `name="${tag.name}"` : ''
].filter(Boolean).join(' ')
return `\n\n:::container ${attrs}\n:::\n\n`
}
/**
* 解析状态字符串为ProcessingEnum
* @param status 状态字符串
* @returns ProcessingEnum值
*/
function parseStatus(status: string): ProcessingEnum {
switch (status) {
case 'FINISHED':
return ProcessingEnum.FINISHED
case 'FAILED':
return ProcessingEnum.FAILED
case 'EXECUTING':
default:
return ProcessingEnum.EXECUTING
}
}
/**
* 解析组件类型字符串为AgentComponentTypeEnum
* @param type 类型字符串
* @returns AgentComponentTypeEnum值
*/
function parseComponentType(type: string): AgentComponentTypeEnum {
switch (type) {
case 'Plan':
return AgentComponentTypeEnum.Plan
case 'ToolCall':
return AgentComponentTypeEnum.ToolCall
case 'Skill':
return AgentComponentTypeEnum.Skill
case 'Workflow':
return AgentComponentTypeEnum.Workflow
case 'Knowledge':
return AgentComponentTypeEnum.Knowledge
case 'Page':
return AgentComponentTypeEnum.Page
case 'Plugin':
return AgentComponentTypeEnum.Plugin
case 'Event':
return AgentComponentTypeEnum.Event
case 'Agent':
return AgentComponentTypeEnum.Agent
case 'Mcp':
case 'MCP':
return AgentComponentTypeEnum.MCP
default:
// 默认返回 ToolCall,因为这是最常见的类型
return AgentComponentTypeEnum.ToolCall
}
}
/**
* 将componentExecutedList转换为processingList
* @param componentExecutedList 组件执行列表
* @returns ProcessingInfo数组
*/
export function convertComponentExecutedListToProcessingList(
componentExecutedList: ComponentExecutedItem[] | null | undefined
): ProcessingInfo[] {
if (!componentExecutedList || !Array.isArray(componentExecutedList)) {
return []
}
return componentExecutedList.map((item): ProcessingInfo => {
const result = item.result || {}
const status = parseStatus(item.status)
const componentType = parseComponentType(item.type)
// 构建ExecuteResultInfo
const executeResult: ExecuteResultInfo = {
kind: result?.kind || '',
id: result.id || -1,
icon: '',
name: result.name || item.name || '',
type: componentType,
success: result.success ?? (item.status === 'FINISHED'),
error: '',
data: result.data || null,
startTime: result.startTime || 0,
endTime: result.endTime || 0,
input: result.input || null
}
return {
executeId: result.executeId || '',
name: item.name || result.name || '',
type: componentType,
status: status,
targetId: item.targetId || result.id || -1,
result: executeResult,
cardBindConfig: null as any
} as ProcessingInfo
})
}
/**
* 适配单条历史消息
* 将PC端格式的自定义标签转换为移动端支持的格式
* @param message 原始消息
* @returns 适配后的消息
*/
export function adaptHistoryMessage(message: MessageInfo): MessageInfo {
if (!message) return message
// 只处理ASSISTANT消息,非ASSISTANT消息确保text有值后直接返回
if (message.role !== AssistantRoleEnum.ASSISTANT && message.messageType !== MessageTypeEnum.ASSISTANT) {
// 确保 text 字段有值
if (message.text === undefined || message.text === null) {
return { ...message, text: '' }
}
return message
}
let adaptedText = message.text || ''
let processingList = message.processingList || []
// 1. 从componentExecutedList转换processingList
const componentExecutedList = (message as any).componentExecutedList as ComponentExecutedItem[] | null
if (componentExecutedList && componentExecutedList.length > 0) {
const convertedProcessingList = convertComponentExecutedListToProcessingList(componentExecutedList)
// 合并已有的processingList(如果有的话)
processingList = [...processingList, ...convertedProcessingList]
}
// 2. 解析并转换PC端HTML标签为移动端Markdown容器语法
const parsedTags = parseMarkdownCustomProcess(adaptedText)
for (const tag of parsedTags) {
const containerSyntax = convertToMobileContainerSyntax(tag)
// 替换HTML标签为Markdown容器语法
adaptedText = adaptedText.replace(tag.fullMatch, containerSyntax)
}
// 3. 清理多余的空行(超过2个连续换行符的情况)
adaptedText = adaptedText.replace(/\n{3,}/g, '\n\n')
return {
...message,
text: adaptedText || '', // 确保 text 始终有值
processingList
}
}
/**
* 批量适配历史消息列表
* @param messageList 原始消息列表
* @returns 适配后的消息列表
*/
export function adaptHistoryMessageList(messageList: MessageInfo[]): MessageInfo[] {
if (!messageList || !Array.isArray(messageList)) {
return []
}
return messageList.map(message => adaptHistoryMessage(message))
}