import type { Task, Message, CapsuleFileItem, CapsuleLinkItem, CapsuleAgentItem } from './types'; import { extractMarkdownLinks, groupStepsByAgent } from './utils'; // 胶囊聚合数据构建(纯函数):由主弹框的 computed 接线调用, // 与响应式状态管理解耦,便于单独测试 // 聚合文件:当前对话下用户上传的附件 + AI 返回的下载链接(含 Word 报告,按 url 去重) export const buildCapsuleFiles = (task: Task | undefined, messages: Message[]): CapsuleFileItem[] => { if (!task) return []; const items: CapsuleFileItem[] = []; const seenUrls = new Set(); task.attachedFiles.forEach((f) => { items.push({ kind: 'upload', name: f.name, size: f.size, file: f }); }); for (const msg of messages) { if (msg.type !== 'ai') continue; msg.downloadFiles?.forEach((d) => { if (!d.url || seenUrls.has(d.url)) return; seenUrls.add(d.url); items.push({ kind: 'download', name: d.filename || '文件', url: d.url }); }); if (msg.wordDownloadUrl && !seenUrls.has(msg.wordDownloadUrl)) { seenUrls.add(msg.wordDownloadUrl); items.push({ kind: 'download', name: '报告文件.docx', url: msg.wordDownloadUrl }); } } if (task.wordUrl && !seenUrls.has(task.wordUrl)) { seenUrls.add(task.wordUrl); items.push({ kind: 'download', name: '报告文件.docx', url: task.wordUrl }); } return items; }; // 聚合链接:AI 回复文本中的可跳转链接(与消息内 markdown 渲染共用同一套解析,按 href 去重)。 // 已在"文件"区展示的下载地址不再重复列入链接区; // 除精确匹配外,还按"路径+查询+哈希"匹配,覆盖正文相对路径与后端完整地址写法不一致的情况 export const buildCapsuleLinks = (files: CapsuleFileItem[], messages: Message[]): CapsuleLinkItem[] => { const normalize = (u: string) => u.trim().replace(/&/g, '&'); const pathKey = (u: string): string => { try { const parsed = new URL(u, window.location.href); return `${parsed.pathname}${parsed.search}${parsed.hash}`; } catch { return ''; } }; const seenDownload = new Set(); const seenDownloadPath = new Set(); for (const item of files) { if (!item.url) continue; const href = normalize(item.url); seenDownload.add(href); const key = pathKey(href); if (key) seenDownloadPath.add(key); } const seenLink = new Set(); const links: CapsuleLinkItem[] = []; for (const msg of messages) { if (msg.type !== 'ai' || !msg.content) continue; for (const link of extractMarkdownLinks(msg.content)) { const href = normalize(link.href); if (!href || seenLink.has(href)) continue; const key = pathKey(href); if (seenDownload.has(href) || (key && seenDownloadPath.has(key))) continue; seenLink.add(href); links.push({ title: link.title, href }); } } return links; }; // 聚合智能体:thinking 卡片中的智能体调用记录,按(消息、智能体)聚合, // 每次调用一个条目、状态独立(start→done 生命周期),条目身份与右侧详情面板对齐。 // 仅收录有实际执行活动的组(含 start/done/工具调用/执行中),纯思考 token 的"系统"组不进入。 // isAnswerVisible 由调用方传入(多回答折叠组中该消息是否为当前显示的回答) export const buildCapsuleAgents = (messages: Message[], isAnswerVisible?: (index: number) => boolean | undefined): CapsuleAgentItem[] => { const agents: CapsuleAgentItem[] = []; for (let i = 0; i < messages.length; i++) { const msg = messages[i]; if (msg.type !== 'ai' || !msg.thinkingSteps?.length) continue; // 该消息所属问答轮次:数到当前消息为止(含本轮提问)的用户消息条数, // 无前置提问的 AI 消息兜底为第 1 轮 let round = 0; for (let j = 0; j <= i; j++) { if (messages[j].type === 'user') round++; } round = Math.max(1, round); for (const group of groupStepsByAgent(msg.thinkingSteps)) { const hasActivity = group.steps.some((s) => s.type !== 'token'); if (!hasActivity) continue; const done = group.steps.some((s) => s.type === 'agent_done'); const toolCount = group.steps.filter((s) => s.type === 'tool_call').length; // 状态优先级:完成 > 审批等待(暂停) > 流式中 > 已中断 const status: CapsuleAgentItem['status'] = done ? 'done' : msg.isPendingApproval ? 'waiting' : msg.isLoading ? 'running' : 'interrupted'; agents.push({ messageIndex: i, agent: group.agent, agentId: group.agentId, status, round, toolCount, stepCount: group.steps.length, // 多回答折叠组中被隐藏的历史回答(重新生成产生),供胶囊面板标注 hiddenAnswer: isAnswerVisible ? isAnswerVisible(i) === false : false, }); } } return agents; };