artifacts.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. /**
  2. * 产物(CSV 文件)在聊天里的提取与展示辅助。
  3. *
  4. * <p><b>为什么从消息派生</b>:产物元数据挂在工具结果 JSON 里,随
  5. * {@code agent_message.tool_events} 持久化,前端又把它还原成 {@code message.toolSteps}
  6. * —— 因此「实时流(tool_result 到达)」「刷新恢复」「打开历史会话」三条路径共用同一份
  7. * 提取逻辑(一个 computed 就够),不需要另开一套状态去同步。
  8. *
  9. * <p>数据口径(与后端一致):**预览限量、文件全量** —— 聊天里只展示元数据与前 N 行,
  10. * 完整行数在 {@code totalRows},要全量走下载。
  11. */
  12. import { ref } from 'vue';
  13. import type { ArtifactMeta } from '../api/types';
  14. import type { ChatMessage } from '../store/chatStream';
  15. /** 会话内产物:元数据 + 来源消息(预览记录归属用) */
  16. export interface SessionArtifact extends ArtifactMeta {
  17. /** 产生它的消息 ID */
  18. messageId: string;
  19. /** 产生它的工具名(进文件名与列表副标题) */
  20. toolName?: string;
  21. }
  22. /**
  23. * 数值字段归一化。
  24. *
  25. * 后端全局配置把 Long 序列化成字符串(防前端雪花 ID 精度丢失,见 `Json`),
  26. * 所以 `totalRows` / `sizeBytes` 到前端是字符串 —— 提取时统一转回 number,
  27. * 展示层就不必到处兼容两种类型。
  28. */
  29. function toCount(value: unknown): number | undefined {
  30. if (typeof value === 'number' && Number.isFinite(value)) return value;
  31. if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
  32. return Number(value);
  33. }
  34. return undefined;
  35. }
  36. /**
  37. * 解析单条消息里的产物(防御式:不是 JSON / 没有 artifact 字段的步骤直接跳过)。
  38. */
  39. export function extractMessageArtifacts(message: ChatMessage): SessionArtifact[] {
  40. const out: SessionArtifact[] = [];
  41. for (const step of message.toolSteps || []) {
  42. if (!step.result) continue;
  43. let parsed: any;
  44. try {
  45. parsed = JSON.parse(step.result);
  46. } catch {
  47. continue;
  48. }
  49. const meta = parsed?.artifact;
  50. if (meta && typeof meta.id === 'string' && meta.id) {
  51. out.push({
  52. ...meta,
  53. totalRows: toCount(meta.totalRows),
  54. sizeBytes: toCount(meta.sizeBytes),
  55. messageId: message.id,
  56. toolName: step.toolName,
  57. });
  58. }
  59. }
  60. return out;
  61. }
  62. /** 按消息顺序提取整段会话的产物(调用方自行决定要不要倒序展示) */
  63. export function extractArtifacts(messages: ChatMessage[]): SessionArtifact[] {
  64. return (messages || []).flatMap((m) => extractMessageArtifacts(m));
  65. }
  66. /** `38263 行`;没有行数时不占位 */
  67. export function artifactRowsText(artifact: SessionArtifact): string {
  68. return typeof artifact.totalRows === 'number' ? `${artifact.totalRows.toLocaleString()} 行` : '';
  69. }
  70. /** 文件大小:B / KB / MB,一位小数 */
  71. export function artifactSizeText(artifact: SessionArtifact): string {
  72. const bytes = artifact.sizeBytes;
  73. if (typeof bytes !== 'number' || bytes <= 0) return '';
  74. if (bytes < 1024) return `${bytes} B`;
  75. if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  76. return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
  77. }
  78. /* ------------------------------------------------------------------ *
  79. * 预览弹窗(全局单例)
  80. * ------------------------------------------------------------------ */
  81. /**
  82. * 当前待预览的产物。消息卡片、产物栏都往这里写,
  83. * 工作台挂一个 {@code ArtifactPreviewModal} 实例消费 —— 避免跨组件层层 emit。
  84. */
  85. export const previewArtifact = ref<SessionArtifact | null>(null);
  86. export function openArtifactPreview(artifact: SessionArtifact): void {
  87. previewArtifact.value = normalizeArtifact(artifact);
  88. }
  89. /** 数值字段兜底归一化(调用方传原始 artifact 时保证展示层拿到 number) */
  90. function normalizeArtifact(artifact: SessionArtifact): SessionArtifact {
  91. return { ...artifact, totalRows: toCount(artifact.totalRows), sizeBytes: toCount(artifact.sizeBytes) };
  92. }
  93. export function closeArtifactPreview(): void {
  94. previewArtifact.value = null;
  95. }