| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- /**
- * 产物(CSV 文件)在聊天里的提取与展示辅助。
- *
- * <p><b>为什么从消息派生</b>:产物元数据挂在工具结果 JSON 里,随
- * {@code agent_message.tool_events} 持久化,前端又把它还原成 {@code message.toolSteps}
- * —— 因此「实时流(tool_result 到达)」「刷新恢复」「打开历史会话」三条路径共用同一份
- * 提取逻辑(一个 computed 就够),不需要另开一套状态去同步。
- *
- * <p>数据口径(与后端一致):**预览限量、文件全量** —— 聊天里只展示元数据与前 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<SessionArtifact | null>(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;
- }
|