/** * 事件渲染属性生成器 * * 完整复刻 remix next-agent 的 getEventRenderProps 逻辑: * event-types.ts → event-transformers.ts → event-processors.ts → event-handlers.ts * * 用于在 useEventProcessor 中将 ApiEvent 转换为 ExtendedEvent 时填充 renderProps。 */ import type { ApiEvent, MessageItemProps, ActionInfo, McpContent, PlanConfig, TaskTodoConfig, Operation, UserInteraction, Attachment, ImageAttachment, BaseEvent, MessageContent, } from '../types' // ───────────────────────────────────────────── // 事件类型 / 动作类型常量(对应 next-agent-chat.type.ts) // ───────────────────────────────────────────── const ET = { UserInput: 'user_input', Text: 'text', ToolCall: 'tool_call', TaskUpdate: 'task_update', TaskEnd: 'task_end', UserInteraction: 'user_interaction', MessageAskUser: 'message_ask_user', AskUserReply: 'ask_user_reply', StepSummary: 'step_summary', Summary: 'summary', } as const const EA = { TextJson: 'text_json', Mcp: 'mcp', McpTool: 'mcp_tool', BrowserUseTakeover: 'browser_use_takeover', SlideInit: 'slide_init', MessageNotifyUser: 'message_notify_user', StepSummary: 'step_summary', Summary: 'summary', AgentScheduleTask: 'agent_schedule_task', } as const const ES = { RUNNING: 'running', SUCCESS: 'success', FAILED: 'failed', } as const /** task_update 等无 content 的事件类型集合 */ const CONTENTLESS_EVENTS = new Set([ET.TaskUpdate]) // ───────────────────────────────────────────── // 事件类型判断(对应 event-types.ts) // ───────────────────────────────────────────── function isUserInputEvent(e: ApiEvent) { return e.event_type === ET.UserInput } function isToolCallEvent(e: ApiEvent) { return e.event_type === ET.ToolCall } function isPlanEvent(e: ApiEvent) { return e.event_type === ET.Text && e.content?.action_type === EA.TextJson } function isMcpEvent(e: ApiEvent) { return ( isToolCallEvent(e) && (e.content?.action_type === EA.Mcp || e.content?.action_type === EA.McpTool) ) } function isBrowserUseTakeoverEvent(e: ApiEvent) { return e.content?.action_type === EA.BrowserUseTakeover } function isSlideOutlineEvent(e: ApiEvent) { return isToolCallEvent(e) && e.content?.action_type === EA.SlideInit } function isAgentScheduleTaskEvent(e: ApiEvent) { return e.content?.action_type === EA.AgentScheduleTask } function isMessageNotifyUserEvent(e: ApiEvent) { return isToolCallEvent(e) && e.content?.action_type === EA.MessageNotifyUser } function isStepSummaryEvent(e: ApiEvent) { return ( e.event_type === ET.StepSummary && e.content?.action_type === EA.StepSummary ) } function isStepSummaryLegacy(e: ApiEvent) { return ( e.event_type === ET.Summary && e.content?.action_type === EA.Summary ) } /** 任务结束带文件(text 事件 + attachment_files) */ function isTaskEndWithFiles(e: ApiEvent) { return ( e.event_type === ET.Text && !!e.content?.attachment_files?.length ) } function isImageFile(fileType?: string) { return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes( (fileType || '').toLowerCase(), ) } // ───────────────────────────────────────────── // 数据转换工具(对应 event-transformers.ts) // ───────────────────────────────────────────── /** 规范化动作类型:优先取 tool_name,fallback action_type */ function normalizeActionType(e: ApiEvent): string { return (e.content?.tool_name || e.content?.action_type || '') as string } /** 创建 base 属性(对应 createBaseEventProps) */ function createBaseProps(e: ApiEvent): { base: BaseEvent } { const normalizedTimestamp = typeof e.timestamp === 'number' ? e.timestamp : typeof e.content?.timestamp === 'number' ? e.content.timestamp : undefined return { base: { event_id: e.event_id, event_type: e.event_type, event_status: e.event_status, stream: e.stream, plan_step_state: e.plan_step_state, action_type: normalizeActionType(e), timestamp: normalizedTimestamp, metadata: { ...(e.metadata as Record), ...(e.content?.metadata as Record), isUserInput: isUserInputEvent(e), isScheduleTask: isAgentScheduleTaskEvent(e), }, }, } } /** 创建 action 属性(对应 createActionProps) */ function createAction(e: ApiEvent): ActionInfo { return { name: (e.content?.action_name as string) || '', arguments: e.content?.arguments as string[] | undefined, action_type: normalizeActionType(e), block: isPlanEvent(e) || isSlideOutlineEvent(e), tool_input: e.content?.tool_input, tool_output: e.content?.tool_output, } } // ───────────────────────────────────────────── // 附件处理(对应 event-processors.ts processEventAttachments) // ───────────────────────────────────────────── function processAttachmentFiles( files: NonNullable['attachment_files'], ): Attachment[] { if (!files?.length) return [] return files .filter(f => f.file_name && f.file_type && f.path) .map(f => ({ file_name: f.file_name!, file_type: f.file_type!, file_url: f.path!, })) } function processEventAttachments(e: ApiEvent): [Attachment[], ImageAttachment[]] { const attachments: Attachment[] = [] const imageAttachments: ImageAttachment[] = [] const toolOutput = e.content?.tool_output // tool_output 中的单文件附件 if ( toolOutput && typeof toolOutput === 'object' && !Array.isArray(toolOutput) ) { const to = toolOutput as Record if (to.path && to.file_type && to.file_name) { attachments.push({ file_name: to.file_name as string, file_type: to.file_type as string, file_url: to.path as string, }) } } // user_input / message_notify_user / step_summary 的 attachment_files if ( (isUserInputEvent(e) || isMessageNotifyUserEvent(e) || isStepSummaryEvent(e) || isStepSummaryLegacy(e)) && e.content?.attachment_files?.length ) { attachments.push(...processAttachmentFiles(e.content.attachment_files)) } // 任务结束带文件(text + attachment_files) if (isTaskEndWithFiles(e) && e.content?.attachment_files?.length) { const all = processAttachmentFiles(e.content.attachment_files) const imageFiles = all.filter(a => isImageFile(a.file_type)) imageAttachments.push( ...imageFiles.map(a => ({ url: a.file_url, path: a.file_url, file_name: a.file_name, })), ) const rest = all.filter(a => !isImageFile(a.file_type)) if (rest.length > 0) attachments.push(...rest.slice(0, 3)) } return [attachments, imageAttachments] } // ───────────────────────────────────────────── // 通用事件属性(对应 event-processors.ts createCommonEventProps) // ───────────────────────────────────────────── function tryParseObject(str: string): Record { try { const parsed = JSON.parse(str) if (typeof parsed === 'object' && parsed !== null) return parsed } catch { // ignore } return {} } function createCommonProps(e: ApiEvent): Partial { const base = createBaseProps(e) const [attachments, imageAttachments] = processEventAttachments(e) // 计划事件(text + text_json) if (isPlanEvent(e)) { return { ...base, planConfig: { ...tryParseObject( typeof e.content?.content === 'string' ? e.content.content : '{}', ), fast_mode: e.content?.fast_mode, } as PlanConfig, } } const result: Partial = { ...base } if (attachments.length > 0) result.attachment = attachments if (imageAttachments.length > 0) result.imageAttachment = imageAttachments // 文本内容 if ( !CONTENTLESS_EVENTS.has(e.event_type) && e.content?.content && typeof e.content.content === 'string' ) { result.content = { content: e.content.content, refer_content: e.content.refer_content, } as MessageContent } return result } // ───────────────────────────────────────────── // 各事件类型处理器(对应 event-handlers.ts) // ───────────────────────────────────────────── function handleDefault(e: ApiEvent): MessageItemProps { return { action: createAction(e), ...createCommonProps(e), } } function handleToolCall(e: ApiEvent): MessageItemProps { // 规范化 action_type if (e.content) { (e.content as Record).action_type = normalizeActionType(e) } const baseAction = { action: createAction(e) } // MCP 工具事件 if (isMcpEvent(e)) { const meta = e.content?.metadata as Record | undefined return { ...createBaseProps(e), ...baseAction, mcpContent: { metadata: { ...meta, name: (meta?.name || meta?.mcp_name) as string | undefined, }, status: !(meta?.is_auto_executed) && e.event_status !== ES.SUCCESS && e.event_status !== ES.FAILED ? 'ready' : e.event_status, tool_call_id: e.content?.tool_call_id, action_type: e.content?.action_type, tool_input: e.content?.tool_input, tool_output: e.content?.tool_output, } as McpContent, } } // 浏览器接管事件 if (isBrowserUseTakeoverEvent(e)) { return { ...createBaseProps(e), ...baseAction, content: { content: '' }, operation: { operation_type: e.content?.action_type, content: typeof e.content?.content === 'string' ? e.content.content : '', } as Operation, } } return { ...baseAction, ...createCommonProps(e) } } function handleTaskUpdate(e: ApiEvent): MessageItemProps { const rawContent = e.content?.content const list = Array.isArray(rawContent) ? rawContent : rawContent && typeof rawContent === 'object' && Array.isArray((rawContent as { list?: unknown }).list) ? ((rawContent as { list: Record[] }).list ?? []) : [] return { action: createAction(e), ...createCommonProps(e), taskTodoConfig: { list, } as TaskTodoConfig, } } function handleUserInteraction(e: ApiEvent): MessageItemProps { if (e.content) { (e.content as Record).action_type = normalizeActionType(e) } const interactionContent = ( (e.content?.content as Record) || {} ) const { text, ...rest } = interactionContent return { action: createAction(e), ...createBaseProps(e), userInteraction: { content: text as string | undefined, ...rest } as UserInteraction, } } // ───────────────────────────────────────────── // 主入口(对应 event-handlers.ts getEventRenderProps) // ───────────────────────────────────────────── export function getEventRenderProps(event: ApiEvent): MessageItemProps { switch (event.event_type) { case ET.UserInput: case ET.Text: case ET.MessageAskUser: case ET.AskUserReply: return handleDefault(event) case ET.ToolCall: return handleToolCall(event) case ET.TaskUpdate: return handleTaskUpdate(event) case ET.UserInteraction: return handleUserInteraction(event) default: return { action: { action_type: normalizeActionType(event), name: (event.content?.action_name as string) || '', block: isSlideOutlineEvent(event), }, ...createCommonProps(event), } } }