259 lines
6.9 KiB
TypeScript
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
|
|
}
|