初始化模版工程
This commit is contained in:
420
components/nova-sdk/utils/event-render-props.ts
Normal file
420
components/nova-sdk/utils/event-render-props.ts
Normal file
@@ -0,0 +1,420 @@
|
||||
/**
|
||||
* 事件渲染属性生成器
|
||||
*
|
||||
* 完整复刻 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_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<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),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user