Files
2026-03-20 07:33:46 +00:00

259 lines
6.9 KiB
TypeScript

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<string, unknown>
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<string, unknown>
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<string, unknown> | 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
}