| 1 | import type { TaskMessage } from '@/api/ai/ai.types' |
| 2 | import type { IUploadedMedia } from '@/components/Chat/MediaUpload' |
| 3 | import type { IDisplayMessage, IMessageStep, IWorkflowStep } from '@/store/agent' |
| 4 | import { parseUserMessageContent } from './parseMessageContent' |
| 5 | |
| 6 | /** |
| 7 | * 判断文本是否为占位符(不应该显示给用户) |
| 8 | */ |
| 9 | function isPlaceholderText(text: string | undefined): boolean { |
| 10 | if (!text) |
| 11 | return true |
| 12 | const trimmed = text.trim() |
| 13 | if (!trimmed) |
| 14 | return true |
| 15 | |
| 16 | // 过滤常见的占位符文本 |
| 17 | const placeholders = ['(no content)', 'no content', '(empty)', 'empty', '无内容', '(无内容)'] |
| 18 | |
| 19 | return placeholders.some(p => trimmed.toLowerCase() === p.toLowerCase()) |
| 20 | } |
| 21 | |
| 22 | /** |
| 23 | * 将后端消息转换为显示格式 |
| 24 | * 数据结构说明: |
| 25 | * - user: { type: 'user', content: [{ type: 'text', text: '...' }] } |
| 26 | * - assistant: { type: 'assistant', uuid: '...', message: { content: [{ type: 'text', text: '...' }] } } |
| 27 | * - stream_event: 流式事件,用于提取工具调用信息 |
| 28 | * - result: { type: 'result', message: '...', result: { action: '...', platform: '...', ... } } |
| 29 | * - message 字段包含文本内容 |
| 30 | * - result 字段(根级别)包含 action 数据(如 createChannel、updateChannel 等) |
| 31 | * |
| 32 | * 改进:解析多步骤和工作流步骤 |
| 33 | * - message_start 事件标识新步骤开始 |
| 34 | * - tool_use 事件标识工具调用 |
| 35 | * - tool_result 事件标识工具结果 |
| 36 | */ |
| 37 | export function convertMessages(messages: TaskMessage[]): IDisplayMessage[] { |
| 38 | const displayMessages: IDisplayMessage[] = [] |
| 39 | |
| 40 | // 临时存储当前 assistant 消息的步骤 |
| 41 | let currentSteps: IMessageStep[] = [] |
| 42 | let currentStepContent = '' |
| 43 | let currentStepWorkflow: IWorkflowStep[] = [] |
| 44 | let stepIndex = 0 |
| 45 | let lastAssistantMsgIndex = -1 |
| 46 | // 用于在 medias 处理后保留原始 content 供去重检查 |
| 47 | let contentBeforeMediasProcessing = '' |
| 48 | |
| 49 | // 用于追踪工具调用的 Map |
| 50 | const toolCallMap = new Map<string, string>() |
| 51 | |
| 52 | // 用于追踪当前消息轮次(通过 message.id 判断) |
| 53 | // 当 message.id 变化时,表示新的一轮消息开始,需要分割步骤 |
| 54 | let currentMessageId = '' |
| 55 | |
| 56 | /** 保存当前步骤到步骤列表 */ |
| 57 | const saveCurrentStep = () => { |
| 58 | if (currentStepContent.trim() || currentStepWorkflow.length > 0) { |
| 59 | currentSteps.push({ |
| 60 | id: `step-${stepIndex}`, |
| 61 | content: currentStepContent.trim(), |
| 62 | workflowSteps: [...currentStepWorkflow], |
| 63 | isActive: false, |
| 64 | timestamp: Date.now(), |
| 65 | }) |
| 66 | stepIndex++ |
| 67 | } |
| 68 | currentStepContent = '' |
| 69 | currentStepWorkflow = [] |
| 70 | } |
| 71 | |
| 72 | /** 将步骤保存到最后一个 assistant 消息 */ |
| 73 | const saveStepsToMessage = () => { |
| 74 | saveCurrentStep() |
| 75 | if (currentSteps.length > 0 && lastAssistantMsgIndex >= 0) { |
| 76 | const lastMsg = displayMessages[lastAssistantMsgIndex] |
| 77 | if (lastMsg && lastMsg.role === 'assistant') { |
| 78 | // 合并已有的 steps(保留之前可能由 result 附加的 media-only steps),避免覆盖 |
| 79 | lastMsg.steps = [...(lastMsg.steps || []), ...currentSteps] |
| 80 | } |
| 81 | } |
| 82 | currentSteps = [] |
| 83 | stepIndex = 0 |
| 84 | } |
| 85 | |
| 86 | messages.forEach((msg, index) => { |
| 87 | // 如果任意消息体里直接包含根级别的 result(有些 SSE 使用 stream_event 包裹 result),优先处理 medias 并按步骤位置插入 |
| 88 | const msgAnyCheck = msg as any |
| 89 | if (msgAnyCheck && msgAnyCheck.result) { |
| 90 | const resultData = msgAnyCheck.result |
| 91 | const resultArray = Array.isArray(resultData) ? resultData : [resultData] |
| 92 | |
| 93 | // 保存处理 medias 前的 currentStepContent,用于后续去重检查 |
| 94 | contentBeforeMediasProcessing = currentStepContent |
| 95 | |
| 96 | resultArray.forEach((item: any, arrIndex: number) => { |
| 97 | if (item && item.medias && Array.isArray(item.medias) && item.medias.length > 0) { |
| 98 | const convertedMedias = item.medias.map((m: any) => ({ |
| 99 | url: m.url || m.thumbUrl || '', |
| 100 | type: m.type === 'video' ? 'video' : 'image', |
| 101 | name: m.name, |
| 102 | })) |
| 103 | |
| 104 | // 如果当前有未保存的步骤内容,先保存该步骤,然后把 media step 放到 currentSteps(以便后续合并到最后 assistant 消息中,保证 media 出现在该步骤之后) |
| 105 | if (currentStepContent && currentStepContent.trim()) { |
| 106 | saveCurrentStep() |
| 107 | currentSteps.push({ |
| 108 | id: `media-step-${Date.now()}-${arrIndex}`, |
| 109 | content: '', |
| 110 | workflowSteps: [], |
| 111 | isActive: false, |
| 112 | timestamp: Date.now(), |
| 113 | medias: convertedMedias, |
| 114 | } as any) |
| 115 | } |
| 116 | else { |
| 117 | // 否则直接附加到最后一条 assistant 消息的 steps(如果存在),或新建一条 assistant 消息 |
| 118 | const lastMsg = displayMessages[displayMessages.length - 1] |
| 119 | const mediaStep = { |
| 120 | id: `media-step-${Date.now()}-${arrIndex}`, |
| 121 | content: '', |
| 122 | workflowSteps: [], |
| 123 | isActive: false, |
| 124 | timestamp: Date.now(), |
| 125 | medias: convertedMedias, |
| 126 | } |
| 127 | if (lastMsg && lastMsg.role === 'assistant') { |
| 128 | if (!lastMsg.steps) |
| 129 | lastMsg.steps = [] |
| 130 | // 尝试将 media 插入到最后一个有文本内容的 step 之后 |
| 131 | let inserted = false |
| 132 | for (let i = lastMsg.steps.length - 1; i >= 0; i--) { |
| 133 | const s = lastMsg.steps[i] as any |
| 134 | if (s && s.content && String(s.content).trim()) { |
| 135 | lastMsg.steps.splice(i + 1, 0, mediaStep as any) |
| 136 | inserted = true |
| 137 | break |
| 138 | } |
| 139 | } |
| 140 | if (!inserted) { |
| 141 | // 如果没有文本 step,但 message 层有 content(未拆分为 step),把 message.content 转为 step,放在前面 |
| 142 | if (lastMsg.content && String(lastMsg.content).trim()) { |
| 143 | const contentStep = { |
| 144 | id: `legacy-content-${Date.now()}`, |
| 145 | content: lastMsg.content, |
| 146 | workflowSteps: [], |
| 147 | isActive: false, |
| 148 | timestamp: Date.now(), |
| 149 | } |
| 150 | // 清空 message.content 并保留在 steps 中 |
| 151 | lastMsg.content = '' |
| 152 | lastMsg.steps.push(contentStep as any) |
| 153 | } |
| 154 | // 最后添加 mediaStep |
| 155 | lastMsg.steps.push(mediaStep as any) |
| 156 | } |
| 157 | } |
| 158 | else { |
| 159 | displayMessages.push({ |
| 160 | id: msgAnyCheck.uuid || `result-${index}-${arrIndex}`, |
| 161 | role: 'assistant', |
| 162 | content: '', |
| 163 | status: 'done', |
| 164 | steps: [mediaStep as any], |
| 165 | }) |
| 166 | } |
| 167 | } |
| 168 | } |
| 169 | }) |
| 170 | // 继续后续的 result 内容处理(不返回,仍需执行 processResultMessage 对文本/actions 解析) |
| 171 | } |
| 172 | if (msg.type === 'user') { |
| 173 | // 用户消息处理 |
| 174 | processUserMessage( |
| 175 | msg, |
| 176 | index, |
| 177 | displayMessages, |
| 178 | currentStepWorkflow, |
| 179 | toolCallMap, |
| 180 | saveStepsToMessage, |
| 181 | ) |
| 182 | } |
| 183 | else if (msg.type === 'stream_event') { |
| 184 | // 流式事件处理 |
| 185 | processStreamEvent(msg, currentStepWorkflow, toolCallMap, saveCurrentStep) |
| 186 | } |
| 187 | else if (msg.type === 'assistant') { |
| 188 | // AI 回复消息处理 |
| 189 | // 检测消息轮次变化:通过 message.id 判断是否是新的一轮 |
| 190 | // 在详情数据中,同一轮的多条 assistant 消息会有相同的 message.id |
| 191 | const messageData = (msg as any).message as any |
| 192 | const messageId = messageData?.id || '' |
| 193 | |
| 194 | // 检查是否包含 text 内容(非空文本) |
| 195 | const hasText = messageData?.content?.some( |
| 196 | (item: any) => item.type === 'text' && item.text?.trim(), |
| 197 | ) |
| 198 | |
| 199 | // 只在遇到包含 text 的新 message.id 时开始新步骤 |
| 200 | // 纯 tool_use 消息不触发新步骤,其工作流会合并到当前步骤 |
| 201 | if (hasText && messageId && currentMessageId && messageId !== currentMessageId) { |
| 202 | // 新的消息轮次开始(且有文本内容),保存当前步骤 |
| 203 | saveCurrentStep() |
| 204 | } |
| 205 | // 更新当前消息 ID |
| 206 | if (messageId) { |
| 207 | currentMessageId = messageId |
| 208 | } |
| 209 | |
| 210 | const result = processAssistantMessage( |
| 211 | msg, |
| 212 | index, |
| 213 | displayMessages, |
| 214 | currentStepWorkflow, |
| 215 | toolCallMap, |
| 216 | ) |
| 217 | if (result.contentToAdd) { |
| 218 | currentStepContent += (currentStepContent ? '\n\n' : '') + result.contentToAdd |
| 219 | } |
| 220 | if (result.newAssistantMsgIndex !== undefined) { |
| 221 | lastAssistantMsgIndex = result.newAssistantMsgIndex |
| 222 | } |
| 223 | } |
| 224 | else if (msg.type === 'result') { |
| 225 | // 结果消息处理 |
| 226 | const result = processResultMessage(msg, index, displayMessages) |
| 227 | // 更严格的去重:比较 trim 后的内容,避免因空白字符导致重复 |
| 228 | // 注意:如果 medias 处理时调用了 saveCurrentStep(),currentStepContent 可能被重置 |
| 229 | // 使用 contentBeforeMediasProcessing 作为备选来源进行去重检查 |
| 230 | const contentToAdd = result.contentToAdd?.trim() |
| 231 | const existingContent = currentStepContent.trim() |
| 232 | const previousContent = contentBeforeMediasProcessing.trim() |
| 233 | // 检查内容是否已存在于当前步骤或之前保存的步骤中 |
| 234 | const isDuplicate |
| 235 | = contentToAdd |
| 236 | && (existingContent.includes(contentToAdd) |
| 237 | || contentToAdd === existingContent |
| 238 | || previousContent.includes(contentToAdd) |
| 239 | || contentToAdd === previousContent) |
| 240 | if (contentToAdd && !isDuplicate) { |
| 241 | currentStepContent += (currentStepContent ? '\n\n' : '') + result.contentToAdd |
| 242 | } |
| 243 | // 重置 contentBeforeMediasProcessing,避免影响后续消息 |
| 244 | contentBeforeMediasProcessing = '' |
| 245 | if (result.newAssistantMsgIndex !== undefined) { |
| 246 | lastAssistantMsgIndex = result.newAssistantMsgIndex |
| 247 | } |
| 248 | } |
| 249 | else if (msg.type === 'error') { |
| 250 | if (msg.code === 12001) { |
| 251 | // 积分不足:创建 insufficientCredits action 卡片 |
| 252 | saveStepsToMessage() |
| 253 | displayMessages.push({ |
| 254 | id: `error-${index}`, |
| 255 | role: 'assistant', |
| 256 | content: '', |
| 257 | status: 'done', |
| 258 | actions: [{ type: 'insufficientCredits' }], |
| 259 | }) |
| 260 | lastAssistantMsgIndex = displayMessages.length - 1 |
| 261 | } |
| 262 | } |
| 263 | }) |
| 264 | |
| 265 | // 保存最后的步骤 |
| 266 | saveStepsToMessage() |
| 267 | |
| 268 | // 后处理:确保每条 assistant 消息都有正确的 content |
| 269 | displayMessages.forEach((msg) => { |
| 270 | if (msg.role === 'assistant' && msg.steps && msg.steps.length > 0) { |
| 271 | const totalContent = msg.steps |
| 272 | .map(s => s.content) |
| 273 | .filter(Boolean) |
| 274 | .join('\n\n') |
| 275 | if (totalContent && !msg.content) { |
| 276 | msg.content = totalContent |
| 277 | } |
| 278 | } |
| 279 | }) |
| 280 | |
| 281 | return displayMessages |
| 282 | } |
| 283 | |
| 284 | /** 处理用户消息 */ |
| 285 | function processUserMessage( |
| 286 | msg: TaskMessage, |
| 287 | index: number, |
| 288 | displayMessages: IDisplayMessage[], |
| 289 | currentStepWorkflow: IWorkflowStep[], |
| 290 | toolCallMap: Map<string, string>, |
| 291 | saveStepsToMessage: () => void, |
| 292 | ) { |
| 293 | let content = '' |
| 294 | const medias: IUploadedMedia[] = [] |
| 295 | let isToolResult = false |
| 296 | |
| 297 | if (Array.isArray(msg.content)) { |
| 298 | // 尝试使用新的解析器解析数组格式 |
| 299 | const parsed = parseUserMessageContent(msg.content) |
| 300 | if (parsed.hasSpecialFormat || parsed.medias.length > 0) { |
| 301 | content = parsed.text |
| 302 | medias.push(...parsed.medias) |
| 303 | } |
| 304 | else { |
| 305 | // 使用原有逻辑 |
| 306 | msg.content.forEach((item: any) => { |
| 307 | if (item.type === 'text') { |
| 308 | content = item.text || '' |
| 309 | } |
| 310 | else if (item.type === 'image') { |
| 311 | medias.push({ |
| 312 | url: item.source?.url || '', |
| 313 | type: 'image', |
| 314 | }) |
| 315 | } |
| 316 | else if (item.type === 'video') { |
| 317 | medias.push({ |
| 318 | url: item.source?.url || '', |
| 319 | type: 'video', |
| 320 | }) |
| 321 | } |
| 322 | else if (item.type === 'document') { |
| 323 | medias.push({ |
| 324 | url: item.source?.url || '', |
| 325 | type: 'document', |
| 326 | }) |
| 327 | } |
| 328 | else if (item.type === 'tool_result') { |
| 329 | isToolResult = true |
| 330 | } |
| 331 | }) |
| 332 | } |
| 333 | } |
| 334 | else if (typeof msg.content === 'string') { |
| 335 | // 尝试使用新的解析器解析字符串格式 |
| 336 | const parsed = parseUserMessageContent(msg.content) |
| 337 | content = parsed.text |
| 338 | if (parsed.medias.length > 0) { |
| 339 | medias.push(...parsed.medias) |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | // 只有非工具结果的用户消息才显示(包含文本或媒体) |
| 344 | if ((content || medias.length > 0) && !isToolResult) { |
| 345 | saveStepsToMessage() |
| 346 | |
| 347 | displayMessages.push({ |
| 348 | id: msg.uuid || `user-${index}`, |
| 349 | role: 'user', |
| 350 | content, |
| 351 | medias: medias.length > 0 ? medias : undefined, |
| 352 | status: 'done', |
| 353 | }) |
| 354 | } |
| 355 | |
| 356 | // 处理工具结果(合并到对应的 tool_call 的 result 字段) |
| 357 | // 这样 UI 渲染时可以通过 result 字段判断工具是否已完成 |
| 358 | if ((msg as any).message) { |
| 359 | const userMsg = (msg as any).message as any |
| 360 | const contentArray = userMsg?.content || userMsg?.message?.content |
| 361 | if (contentArray && Array.isArray(contentArray)) { |
| 362 | contentArray.forEach((item: any) => { |
| 363 | if (item.type === 'tool_result' && item.tool_use_id) { |
| 364 | // 提取结果文本 |
| 365 | let resultText = '' |
| 366 | if (Array.isArray(item.content)) { |
| 367 | item.content.forEach((rc: any) => { |
| 368 | if (rc.type === 'text') { |
| 369 | resultText = rc.text || '' |
| 370 | } |
| 371 | }) |
| 372 | } |
| 373 | else if (typeof item.content === 'string') { |
| 374 | resultText = item.content |
| 375 | } |
| 376 | |
| 377 | // 查找对应的 tool_call 并设置 result 字段 |
| 378 | if (resultText) { |
| 379 | const toolCall = currentStepWorkflow.find( |
| 380 | s => s.type === 'tool_call' && s.id === item.tool_use_id, |
| 381 | ) |
| 382 | if (toolCall) { |
| 383 | toolCall.result = resultText |
| 384 | } |
| 385 | } |
| 386 | } |
| 387 | }) |
| 388 | } |
| 389 | } |
| 390 | |
| 391 | // 从 tool_use_result 字段获取工具结果 |
| 392 | if ((msg as any).tool_use_result) { |
| 393 | const results = (msg as any).tool_use_result |
| 394 | if (Array.isArray(results)) { |
| 395 | results.forEach((result: any) => { |
| 396 | if (result.type === 'text' && result.text) { |
| 397 | // 查找最后一个没有 result 的 tool_call |
| 398 | const lastToolCall = [...currentStepWorkflow] |
| 399 | .reverse() |
| 400 | .find(s => s.type === 'tool_call' && !s.result) |
| 401 | if (lastToolCall) { |
| 402 | lastToolCall.result = result.text |
| 403 | } |
| 404 | } |
| 405 | }) |
| 406 | } |
| 407 | } |
| 408 | } |
| 409 | |
| 410 | /** 处理流式事件 */ |
| 411 | function processStreamEvent( |
| 412 | msg: TaskMessage, |
| 413 | currentStepWorkflow: IWorkflowStep[], |
| 414 | toolCallMap: Map<string, string>, |
| 415 | saveCurrentStep: () => void, |
| 416 | ) { |
| 417 | const streamEvent = msg as any |
| 418 | const event = streamEvent.event |
| 419 | |
| 420 | // message_start 表示新的一轮消息开始(新步骤) |
| 421 | if (event?.type === 'message_start') { |
| 422 | saveCurrentStep() |
| 423 | } |
| 424 | |
| 425 | // 工具调用开始 |
| 426 | if (event?.type === 'content_block_start' && event.content_block?.type === 'tool_use') { |
| 427 | const toolName = event.content_block.name || 'Unknown Tool' |
| 428 | const toolId = event.content_block.id || `tool-${Date.now()}` |
| 429 | |
| 430 | toolCallMap.set(toolId, toolName) |
| 431 | |
| 432 | currentStepWorkflow.push({ |
| 433 | id: toolId, |
| 434 | type: 'tool_call', |
| 435 | toolName, |
| 436 | content: '', |
| 437 | isActive: false, |
| 438 | timestamp: Date.now(), |
| 439 | }) |
| 440 | } |
| 441 | |
| 442 | // 工具调用参数 |
| 443 | if (event?.type === 'content_block_delta' && event.delta?.type === 'input_json_delta') { |
| 444 | const lastToolCall = currentStepWorkflow.findLast(s => s.type === 'tool_call') |
| 445 | if (lastToolCall) { |
| 446 | lastToolCall.content = (lastToolCall.content || '') + (event.delta.partial_json || '') |
| 447 | } |
| 448 | } |
| 449 | } |
| 450 | |
| 451 | /** 处理 assistant 消息 */ |
| 452 | function processAssistantMessage( |
| 453 | msg: TaskMessage, |
| 454 | index: number, |
| 455 | displayMessages: IDisplayMessage[], |
| 456 | currentStepWorkflow: IWorkflowStep[], |
| 457 | toolCallMap: Map<string, string>, |
| 458 | ): { contentToAdd?: string, newAssistantMsgIndex?: number } { |
| 459 | let content = '' |
| 460 | const messageData = (msg as any).message as any |
| 461 | |
| 462 | if (messageData?.content && Array.isArray(messageData.content)) { |
| 463 | messageData.content.forEach((item: any) => { |
| 464 | if (item.type === 'text') { |
| 465 | content += item.text || '' |
| 466 | } |
| 467 | else if (item.type === 'tool_use') { |
| 468 | const toolName = item.name || 'Unknown Tool' |
| 469 | const toolId = item.id || `tool-${Date.now()}` |
| 470 | const toolInput = item.input ? JSON.stringify(item.input, null, 2) : '' |
| 471 | |
| 472 | toolCallMap.set(toolId, toolName) |
| 473 | |
| 474 | const existingCall = currentStepWorkflow.find(s => s.id === toolId) |
| 475 | if (existingCall) { |
| 476 | existingCall.content = toolInput |
| 477 | existingCall.isActive = false |
| 478 | } |
| 479 | else { |
| 480 | currentStepWorkflow.push({ |
| 481 | id: toolId, |
| 482 | type: 'tool_call', |
| 483 | toolName, |
| 484 | content: toolInput, |
| 485 | isActive: false, |
| 486 | timestamp: Date.now(), |
| 487 | }) |
| 488 | } |
| 489 | } |
| 490 | }) |
| 491 | } |
| 492 | |
| 493 | // 检查是否需要创建新的 assistant 消息 |
| 494 | const lastMsg = displayMessages[displayMessages.length - 1] |
| 495 | if (!lastMsg || lastMsg.role !== 'assistant') { |
| 496 | displayMessages.push({ |
| 497 | id: (msg as any).uuid || `assistant-${index}`, |
| 498 | role: 'assistant', |
| 499 | content: '', |
| 500 | status: 'done', |
| 501 | steps: [], |
| 502 | }) |
| 503 | return { |
| 504 | contentToAdd: content || undefined, |
| 505 | newAssistantMsgIndex: displayMessages.length - 1, |
| 506 | } |
| 507 | } |
| 508 | |
| 509 | return { contentToAdd: content || undefined } |
| 510 | } |
| 511 | |
| 512 | /** 处理结果消息 */ |
| 513 | function processResultMessage( |
| 514 | msg: TaskMessage, |
| 515 | index: number, |
| 516 | displayMessages: IDisplayMessage[], |
| 517 | ): { contentToAdd?: string, newAssistantMsgIndex?: number, actions?: any[], publishFlows?: any[] } { |
| 518 | const msgAny = msg as any |
| 519 | const msgData = msgAny.message |
| 520 | let content = '' |
| 521 | let actions: any[] = [] |
| 522 | let publishFlows: any[] = [] |
| 523 | |
| 524 | // 处理 result 消息格式 |
| 525 | // 格式1: message 是字符串 |
| 526 | if (msgData && typeof msgData === 'string') { |
| 527 | content = msgData |
| 528 | } |
| 529 | // 格式2: message 是对象,包含 message 文本和 result 数组/对象 |
| 530 | else if (msgData && typeof msgData === 'object') { |
| 531 | // 获取文本消息 |
| 532 | if (msgData.message && typeof msgData.message === 'string') { |
| 533 | content = msgData.message |
| 534 | } |
| 535 | } |
| 536 | |
| 537 | // 过滤占位符文本 |
| 538 | if (isPlaceholderText(content)) { |
| 539 | content = '' |
| 540 | } |
| 541 | |
| 542 | // 解析 result 数据中的 action(支持数组和单个对象) |
| 543 | // 注意:后端保存的结构是 msg.result(根级别),不是 msg.message.result |
| 544 | // 同时兼容两种格式以防万一 |
| 545 | const resultData |
| 546 | = msgAny.result || (msgData && typeof msgData === 'object' ? msgData.result : null) |
| 547 | |
| 548 | if (resultData) { |
| 549 | // 统一转换为数组处理 |
| 550 | const resultArray = Array.isArray(resultData) ? resultData : [resultData] |
| 551 | |
| 552 | // Map actions (but do NOT attach medias to action cards to avoid duplicate rendering) |
| 553 | // Exception: navigateToPublish needs medias to pass to the publish page |
| 554 | // 过滤掉 action 为 "none" 的项,因为它们不需要显示为 action 卡片 |
| 555 | actions = resultArray |
| 556 | .filter((item: any) => item && item.action && item.action !== 'none') // 只处理有实际 action 的项 |
| 557 | .map((item: any) => ({ |
| 558 | type: item.action, // 映射 action -> type |
| 559 | platform: item.platform, |
| 560 | accountId: item.accountId, |
| 561 | title: item.title, |
| 562 | description: item.description, |
| 563 | // navigateToPublish 需要 medias 来传递到发布页面,其他类型不需要(避免重复渲染) |
| 564 | medias: item.action === 'navigateToPublish' ? item.medias : undefined, |
| 565 | tags: item.tags, |
| 566 | })) |
| 567 | |
| 568 | // 提取包含 flowId 的发布流程数据 |
| 569 | publishFlows = resultArray |
| 570 | .filter((item: any) => item && item.flowId) // 只处理有 flowId 的项 |
| 571 | .map((item: any) => ({ |
| 572 | flowId: item.flowId, |
| 573 | platform: item.platform, |
| 574 | initialData: { |
| 575 | title: item.title, |
| 576 | description: item.description, |
| 577 | medias: item.medias, |
| 578 | }, |
| 579 | })) |
| 580 | |
| 581 | // medias 的插入逻辑已在外层 convertMessages 的循环中处理,以保证插入顺序正确(避免覆盖或顺序错误) |
| 582 | } |
| 583 | |
| 584 | if (content || actions.length > 0 || publishFlows.length > 0) { |
| 585 | const lastMsg = displayMessages[displayMessages.length - 1] |
| 586 | if (lastMsg && lastMsg.role === 'assistant') { |
| 587 | // 将 actions 附加到最后一条 assistant 消息 |
| 588 | if (actions.length > 0) { |
| 589 | lastMsg.actions = [...(lastMsg.actions || []), ...actions] |
| 590 | } |
| 591 | // 将 publishFlows 附加到最后一条 assistant 消息 |
| 592 | if (publishFlows.length > 0) { |
| 593 | lastMsg.publishFlows = [...(lastMsg.publishFlows || []), ...publishFlows] |
| 594 | } |
| 595 | return { contentToAdd: content || undefined } |
| 596 | } |
| 597 | else { |
| 598 | displayMessages.push({ |
| 599 | id: msgAny.uuid || `result-${index}`, |
| 600 | role: 'assistant', |
| 601 | content: content || '', |
| 602 | status: 'done', |
| 603 | actions: actions.length > 0 ? actions : undefined, |
| 604 | publishFlows: publishFlows.length > 0 ? publishFlows : undefined, |
| 605 | }) |
| 606 | return { newAssistantMsgIndex: displayMessages.length - 1 } |
| 607 | } |
| 608 | } |
| 609 | |
| 610 | return {} |
| 611 | } |
| 612 |