/** * 产物(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