初始化模版工程
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
47
components/nova-sdk/utils/fileIcons.ts
Normal file
47
components/nova-sdk/utils/fileIcons.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
FileText,
|
||||
ImageIcon,
|
||||
Table2,
|
||||
FileSpreadsheet,
|
||||
Video,
|
||||
Music,
|
||||
Code,
|
||||
File,
|
||||
Globe,
|
||||
PresentationIcon,
|
||||
} from 'lucide-react'
|
||||
|
||||
export interface FileIconConfig {
|
||||
icon: React.ElementType
|
||||
color: string
|
||||
}
|
||||
|
||||
export function getFileIconConfig(fileType: string): FileIconConfig {
|
||||
const t = (fileType || '').toLowerCase()
|
||||
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(t))
|
||||
return { icon: ImageIcon, color: 'text-chart-2' }
|
||||
if (t === 'pdf')
|
||||
return { icon: FileText, color: 'text-destructive' }
|
||||
if (['xls', 'xlsx'].includes(t))
|
||||
return { icon: FileSpreadsheet, color: 'text-success' }
|
||||
if (t === 'csv')
|
||||
return { icon: Table2, color: 'text-success' }
|
||||
if (['ppt', 'pptx'].includes(t))
|
||||
return { icon: PresentationIcon, color: 'text-warning' }
|
||||
if (['doc', 'docx'].includes(t))
|
||||
return { icon: FileText, color: 'text-primary' }
|
||||
if (['mp4', 'mov', 'avi', 'webm'].includes(t))
|
||||
return { icon: Video, color: 'text-chart-3' }
|
||||
if (['mp3', 'm4a', 'wav', 'ogg'].includes(t))
|
||||
return { icon: Music, color: 'text-chart-4' }
|
||||
if (['js', 'ts', 'tsx', 'jsx', 'py', 'sh', 'bash', 'json'].includes(t))
|
||||
return { icon: Code, color: 'text-muted-foreground' }
|
||||
if (['html', 'htm'].includes(t))
|
||||
return { icon: Globe, color: 'text-brand' }
|
||||
if (['md', 'txt'].includes(t))
|
||||
return { icon: FileText, color: 'text-muted-foreground' }
|
||||
|
||||
return { icon: File, color: 'text-muted-foreground' }
|
||||
}
|
||||
87
components/nova-sdk/utils/slideEventHelpers.ts
Normal file
87
components/nova-sdk/utils/slideEventHelpers.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { ApiEvent } from '../types'
|
||||
|
||||
/**
|
||||
* 检查是否为 slide_create_in_batches 事件
|
||||
*/
|
||||
export function isSlideCreateInBatchesEvent(
|
||||
event: ApiEvent,
|
||||
state?: 'start' | 'running' | 'done'
|
||||
): boolean {
|
||||
if (event.content?.action_type !== 'slide_create_in_batches') {
|
||||
return false
|
||||
}
|
||||
if (!state) return true
|
||||
|
||||
const eventStatus = event.event_status as string | undefined
|
||||
const isStream = !!event.stream
|
||||
|
||||
if (state === 'start') return !isStream && eventStatus === 'running'
|
||||
if (state === 'running') return isStream && eventStatus === 'running'
|
||||
if (state === 'done') return eventStatus === 'success'
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Slide 流式数据累积结果
|
||||
*/
|
||||
export interface SlideStreamData {
|
||||
slideChunks: Map<string, string>
|
||||
slidePages?: Array<{ html: string; status: 'completed' }>
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 slide 事件的流式数据累积
|
||||
*/
|
||||
export function processSlideEvents(rawEvents: ApiEvent[]): SlideStreamData {
|
||||
const slideChunks = new Map<string, string>() // index -> content
|
||||
let slidePages: Array<{ html: string; status: 'completed' }> | undefined
|
||||
|
||||
for (const event of rawEvents) {
|
||||
// slide_create_in_batches 特殊处理:累积 chunks
|
||||
if (isSlideCreateInBatchesEvent(event, 'running')) {
|
||||
const chunk = event.content?.tool_input as { index?: number; content?: string } | undefined
|
||||
if (chunk?.content) {
|
||||
const key = (chunk.index || 1).toString()
|
||||
const existing = slideChunks.get(key) || ''
|
||||
slideChunks.set(key, existing + chunk.content)
|
||||
}
|
||||
}
|
||||
|
||||
if (isSlideCreateInBatchesEvent(event, 'done')) {
|
||||
const output = event.content?.tool_output as { generated_files?: Array<{ index: number; content: string }> } | undefined
|
||||
if (output?.generated_files) {
|
||||
slidePages = output.generated_files.map((slide) => ({
|
||||
html: slide.content,
|
||||
status: 'completed' as const,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { slideChunks, slidePages }
|
||||
}
|
||||
|
||||
/**
|
||||
* 为事件附加 slide 元数据
|
||||
*/
|
||||
export function attachSlideMetadata(
|
||||
event: ApiEvent,
|
||||
slideData: SlideStreamData
|
||||
): { pages?: Array<{ html: string; status: 'running' | 'completed' }>; isStreaming: boolean } | undefined {
|
||||
if (!isSlideCreateInBatchesEvent(event)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const { slideChunks, slidePages } = slideData
|
||||
|
||||
const streamPages = slideChunks.size > 0
|
||||
? Array.from(slideChunks.entries())
|
||||
.sort(([a], [b]) => Number(a) - Number(b))
|
||||
.map(([, content]) => ({ html: content, status: 'running' as const }))
|
||||
: undefined
|
||||
|
||||
return {
|
||||
pages: slidePages || streamPages,
|
||||
isStreaming: !slidePages && !!streamPages,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user