import type { ExtendedEvent, Attachment, ImageAttachment } from '../../types' // ---- 静默 action 类型:有 action 也不渲染 ToolCallAction ---- // 与 ToolCallAction.tsx 中的 SILENT_ACTION_TYPES 保持一致 export const SILENT_ACTION_TYPES = new Set([ 'agent_advance_phase', 'agent_end_task', 'agent_update_plan', 'finish_task', 'substep_complete', 'reback', 'agent_think', 'agent_schedule_task', 'text_json', 'message_notify_user', 'browser_use_takeover', 'mcp', 'mcp_tool', ]) // ---- MessageType ---- export interface MessageTypeState { isUserInput: boolean isTaskEnd: boolean isSummary: boolean showAction: boolean showContent: boolean showUserFile: boolean showSystemAttachments: boolean showMcpContent: boolean showOperation: boolean showPlanConfig: boolean showTaskTodoList: boolean showUserInteraction: boolean showTemplate: boolean /** 对应 Remix useMessageType.showTimestamp: !!base?.timestamp */ showTimestamp: boolean containerAlignment: 'items-start' | 'items-end' isFactCheck: boolean } /** * 根据事件计算消息显示类型标志位,对应 next-agent useMessageType 逻辑 */ export function getMessageType(event: ExtendedEvent): MessageTypeState { const props = event.renderProps || {} const { action, content, attachment, imageAttachment, mcpContent, planConfig, taskTodoConfig, operation, userInteraction, base, } = props const isUserInput = base?.metadata?.isUserInput ?? event.event_type === 'user_input' const isTaskEnd = event.event_type === 'task_end' // isSummary 对应 Remix: !!metadata?.is_summary const isSummary = !!base?.metadata?.is_summary const hasAttachment = !!(attachment?.length || imageAttachment?.length) const hasUserFile = isUserInput && hasAttachment const isFactCheck = !!content?.refer_content const showTemplate = !!( base?.metadata?.template_type && base?.metadata?.template_id ) // 完全对齐 Remix useMessageType const eventType = base?.event_type || event.event_type const actionType = action?.action_type || '' const isSummaryMessage = actionType === 'step_summary' || actionType === 'summary' const isStepCompleted = base?.plan_step_state === 'completed' return { isUserInput, isTaskEnd, isSummary, // 完全对齐 Remix: showAction: !!action && !mcpContent && !planConfig showAction: !!action && !mcpContent && !planConfig, showContent: !!content && !planConfig, showUserFile: hasUserFile, // 完全对齐 Remix: (hasAttachment && isStepCompleted && isSummaryMessage) || (hasAttachment && eventType === 'text') showSystemAttachments: (hasAttachment && isStepCompleted && isSummaryMessage) || (hasAttachment && eventType === 'text'), showMcpContent: !!mcpContent, showOperation: !!operation, showPlanConfig: !!planConfig, showTaskTodoList: !!(taskTodoConfig && taskTodoConfig.list?.length > 0), showUserInteraction: !!userInteraction, // 完全对齐 Remix: showTimestamp: !!base?.timestamp showTimestamp: !!base?.timestamp, showTemplate, containerAlignment: hasUserFile || isFactCheck || isUserInput ? 'items-end' : 'items-start', isFactCheck, } } /** * 判断是否是用户输入 */ export function isUserInput(event: ExtendedEvent): boolean { return event.event_type === 'user_input' || !!event.metadata?.isUserInput } /** * 提取文本内容 */ export function extractText(obj: unknown): string { if (typeof obj === 'string') { return obj } if (obj && typeof obj === 'object') { const o = obj as Record if (typeof o.text === 'string') { return o.text } if (o.content) { return extractText(o.content) } } return '' } /** * 获取消息内容 */ export function getMessageContent(event: ExtendedEvent): string { const renderContent = event.renderProps?.content?.content || event.renderProps?.content?.text if (renderContent) { return extractText(renderContent) } if (event.content) { return extractText(event.content) } return '' } /** 图片文件扩展名 */ export const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'] /** * 判断是否是图片文件 */ export function isImageFile(path: string): boolean { const ext = path.split('.').pop()?.toLowerCase() || '' return IMAGE_EXTENSIONS.includes(ext) } /** * 从 content 中提取附件列表 */ function extractAttachmentFiles(content: unknown): Array<{ path: string file_name: string file_type: string desc?: string url?: string }> { if (!content || typeof content !== 'object') return [] const c = content as Record if (Array.isArray(c.attachment_files)) { return c.attachment_files } if (c.content && typeof c.content === 'object') { return extractAttachmentFiles(c.content) } return [] } /** * 获取附件列表(非图片文件) */ export function getAttachments(event: ExtendedEvent): Attachment[] { if (event.renderProps?.attachment?.length) { return event.renderProps.attachment } const files = extractAttachmentFiles(event.content) return files .filter(f => !isImageFile(f.path || f.file_name)) .map(f => ({ file_id: f.path, file_name: f.file_name, file_type: f.file_type || f.file_name.split('.').pop() || '', file_url: f.url || f.path, })) } /** * 获取图片附件列表 */ export function getImageAttachments(event: ExtendedEvent): ImageAttachment[] { if (event.renderProps?.imageAttachment?.length) { return event.renderProps.imageAttachment } const files = extractAttachmentFiles(event.content) return files .filter(f => isImageFile(f.path || f.file_name)) .map(f => ({ url: f.url || f.path, file_name: f.file_name, })) } /** * 获取工具调用的 action 信息 */ export function getToolCallAction(event: ExtendedEvent): { name?: string arguments?: string[] action_type?: string } | null { if (event.event_type !== 'tool_call') return null const content = event.content as Record | undefined if (!content) return null const actionName = (content.action_name as string) || (content.action_type as string) || '' const actionType = (content.tool_name as string) || (content.action_type as string) || '' let args: string[] = [] if (Array.isArray(content.arguments)) { args = content.arguments as string[] } else if (typeof content.arguments === 'string') { try { const parsed = JSON.parse(content.arguments) if (Array.isArray(parsed)) { args = parsed } } catch { args = [content.arguments] } } if (!actionName && !actionType) return null return { name: actionName, arguments: args, action_type: actionType } } /** * 获取计划步骤状态 */ export function getPlanStepState(event: ExtendedEvent): string | null { const e = event as ExtendedEvent & { plan_step_state?: string } return e.plan_step_state || null }