Files
test1/components/nova-sdk/utils/event-render-props.ts
2026-03-20 07:33:46 +00:00

421 lines
13 KiB
TypeScript
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.
/**
* 事件渲染属性生成器
*
* 完整复刻 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<string>([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_namefallback 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<string, unknown>),
...(e.content?.metadata as Record<string, unknown>),
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<ApiEvent['content']>['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<string, unknown>
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<string, unknown> {
try {
const parsed = JSON.parse(str)
if (typeof parsed === 'object' && parsed !== null) return parsed
} catch {
// ignore
}
return {}
}
function createCommonProps(e: ApiEvent): Partial<MessageItemProps> {
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<MessageItemProps> = { ...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<string, unknown>).action_type = normalizeActionType(e)
}
const baseAction = { action: createAction(e) }
// MCP 工具事件
if (isMcpEvent(e)) {
const meta = e.content?.metadata as Record<string, unknown> | 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<string, unknown>[] }).list ?? [])
: []
return {
action: createAction(e),
...createCommonProps(e),
taskTodoConfig: {
list,
} as TaskTodoConfig,
}
}
function handleUserInteraction(e: ApiEvent): MessageItemProps {
if (e.content) {
(e.content as Record<string, unknown>).action_type = normalizeActionType(e)
}
const interactionContent = (
(e.content?.content as Record<string, unknown>) || {}
)
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),
}
}
}