/** * 产物(CSV 文件)在聊天里的提取与展示辅助。 * *

为什么从消息派生:产物元数据挂在工具结果 JSON 里,随 * {@code agent_message.tool_events} 持久化,前端又把它还原成 {@code message.toolSteps} * —— 因此「实时流(tool_result 到达)」「刷新恢复」「打开历史会话」三条路径共用同一份 * 提取逻辑(一个 computed 就够),不需要另开一套状态去同步。 * *

数据口径(与后端一致):**预览限量、文件全量** —— 聊天里只展示元数据与前 N 行, * 完整行数在 {@code totalRows},要全量走下载。 */ import { ref } from 'vue'; import type { ArtifactMeta } from '../api/types'; import type { ChatMessage } from '../store/chatStream'; /** 会话内产物:元数据 + 来源消息(预览记录归属用) */ export interface SessionArtifact extends ArtifactMeta { /** 产生它的消息 ID */ messageId: string; /** 产生它的工具名(进文件名与列表副标题) */ toolName?: string; } /** * 数值字段归一化。 * * 后端全局配置把 Long 序列化成字符串(防前端雪花 ID 精度丢失,见 `Json`), * 所以 `totalRows` / `sizeBytes` 到前端是字符串 —— 提取时统一转回 number, * 展示层就不必到处兼容两种类型。 */ function toCount(value: unknown): number | undefined { if (typeof value === 'number' && Number.isFinite(value)) return value; if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) { return Number(value); } return undefined; } /** * 解析单条消息里的产物(防御式:不是 JSON / 没有 artifact 字段的步骤直接跳过)。 */ export function extractMessageArtifacts(message: ChatMessage): SessionArtifact[] { const out: SessionArtifact[] = []; for (const step of message.toolSteps || []) { if (!step.result) continue; let parsed: any; try { parsed = JSON.parse(step.result); } catch { continue; } const meta = parsed?.artifact; if (meta && typeof meta.id === 'string' && meta.id) { out.push({ ...meta, totalRows: toCount(meta.totalRows), sizeBytes: toCount(meta.sizeBytes), messageId: message.id, toolName: step.toolName, }); } } return out; } /** 按消息顺序提取整段会话的产物(调用方自行决定要不要倒序展示) */ export function extractArtifacts(messages: ChatMessage[]): SessionArtifact[] { return (messages || []).flatMap((m) => extractMessageArtifacts(m)); } /** `38263 行`;没有行数时不占位 */ export function artifactRowsText(artifact: SessionArtifact): string { return typeof artifact.totalRows === 'number' ? `${artifact.totalRows.toLocaleString()} 行` : ''; } /** 文件大小:B / KB / MB,一位小数 */ export function artifactSizeText(artifact: SessionArtifact): string { const bytes = artifact.sizeBytes; if (typeof bytes !== 'number' || bytes <= 0) return ''; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(1)} MB`; } /* ------------------------------------------------------------------ * * 预览弹窗(全局单例) * ------------------------------------------------------------------ */ /** * 当前待预览的产物。消息卡片、产物栏都往这里写, * 工作台挂一个 {@code ArtifactPreviewModal} 实例消费 —— 避免跨组件层层 emit。 */ export const previewArtifact = ref(null); export function openArtifactPreview(artifact: SessionArtifact): void { previewArtifact.value = normalizeArtifact(artifact); } /** 数值字段兜底归一化(调用方传原始 artifact 时保证展示层拿到 number) */ function normalizeArtifact(artifact: SessionArtifact): SessionArtifact { return { ...artifact, totalRows: toCount(artifact.totalRows), sizeBytes: toCount(artifact.sizeBytes) }; } export function closeArtifactPreview(): void { previewArtifact.value = null; }