diff --git a/src/artifactGrouping.test.ts b/src/artifactGrouping.test.ts new file mode 100644 index 0000000..58ce6f9 --- /dev/null +++ b/src/artifactGrouping.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' +import { groupArtifactsByName } from './artifactGrouping' +import type { Artifact } from './contracts' + +// 2026-10-11 实测事故:一次 run 内 register_artifact 三次 → 文件面板三份同名 +// 转写报告并列。修复语义(用户裁定):存储保留版本、界面按名称分组去重。 + +function art(id: string, name: string, updatedAt: string, size = 40000): Artifact { + return { id, projectId: 'p1', name, kind: 'document', size, createdAt: updatedAt, updatedAt } +} + +describe('groupArtifactsByName', () => { + it('同名 3 行归一组:latest 为最新,versions 新→旧', () => { + const g = groupArtifactsByName([ + art('a1', '报告.docx', '2026-10-11T15:22:20Z'), + art('a2', '报告.docx', '2026-10-11T15:24:06Z'), + art('a3', '报告.docx', '2026-10-11T15:23:38Z'), + art('b1', '其他.md', '2026-10-11T15:20:00Z', 100), + ]) + expect(g).toHaveLength(2) + const grp = g.find(x => x.latest.name === '报告.docx')! + expect(grp.latest.id).toBe('a2') // 最新 15:24:06 + expect(grp.versions).toHaveLength(3) + expect(grp.versions.map(v => v.id)).toEqual(['a2', 'a3', 'a1']) + expect(g.find(x => x.latest.name === '其他.md')!.versions).toHaveLength(1) + }) + + it('不同名互不影响;空列表返回空', () => { + const g = groupArtifactsByName([art('x', 'a.md', 'T1'), art('y', 'b.md', 'T2')]) + expect(g).toHaveLength(2) + expect(groupArtifactsByName([])).toEqual([]) + }) +}) diff --git a/src/artifactGrouping.ts b/src/artifactGrouping.ts new file mode 100644 index 0000000..7ed406b --- /dev/null +++ b/src/artifactGrouping.ts @@ -0,0 +1,23 @@ +// 同名工件版本分组(2026-10-11 实测事故的修复语义,用户裁定:存储保留版本、 +// 界面去重):组内按 updated_at 新→旧,latest 为面板显示行,其余为历史版本。 +import type { Artifact } from './contracts' + +export interface ArtifactGroup { + latest: Artifact + versions: Artifact[] +} + +export function groupArtifactsByName(list: Artifact[]): ArtifactGroup[] { + const byName = new Map() + for (const a of list) { + const arr = byName.get(a.name) ?? [] + arr.push(a) + byName.set(a.name, arr) + } + const out: ArtifactGroup[] = [] + for (const versions of byName.values()) { + versions.sort((x, y) => (x.updatedAt < y.updatedAt ? 1 : x.updatedAt > y.updatedAt ? -1 : 0)) + out.push({ latest: versions[0], versions }) + } + return out +} diff --git a/src/components/ArtifactsPanel.tsx b/src/components/ArtifactsPanel.tsx index 8a2849a..3fdc9ac 100644 --- a/src/components/ArtifactsPanel.tsx +++ b/src/components/ArtifactsPanel.tsx @@ -3,8 +3,10 @@ import { useState } from 'react' import { store, useStore } from '../store' import { api } from '../api' -import { Icon } from '@ui' +import { Collapsible, Icon } from '@ui' import { artifactIcon, humanSize } from '../messageparts' +import type { Artifact } from '../contracts' +import { groupArtifactsByName } from '../artifactGrouping' export function ArtifactsPanel() { const artifacts = useStore(s => s.artifacts) @@ -15,6 +17,10 @@ export function ArtifactsPanel() { const kinds = ['all', 'document', 'data', 'image'] as const const visible = artifacts.filter(a => (kind === 'all' || a.kind === kind) && a.name.toLowerCase().includes(q.toLowerCase())) + // 同名工件 = 同一交付物的版本序列(存储保留全量):面板按名称分组去重, + // 只显示最新版;组>1 时可展开看历史版本。2026-10-11 实测 3 份同名转写报告 + // 并列的直接修复(存储与提示词语义「修订=新版本」保持不变)。 + const groups = groupArtifactsByName(visible) return (
@@ -52,22 +58,28 @@ export function ArtifactsPanel() { ?
暂无文件——上传附件或让模型产出文档(mock:#doc);这里可浏览、预览、下载
: visible.length === 0 ?
没有匹配「{q}」的文件
- : visible.map(a => ( - // 预览落位在第三栏「文件」入口:requestPreview 会切到预览子视图。 -
store.requestPreview(a)} title={`预览 ${a.name}`}> - - - {a.name} - {a.kind} · {humanSize(a.size) || '—'} · {shortTime(a.updatedAt)} - - - - e.stopPropagation()}> - -
- ))} + : groups.map(g => g.versions.length === 1 + ? + : + +
+ }> + {g.versions.slice(1).map(v => ( +
store.requestPreview(v)} title={`预览历史版本 ${v.name}`}> + + + v{shortTime(v.updatedAt)} · {humanSize(v.size) || '—'} + + + e.stopPropagation()}> + +
+ ))} + )} ) } @@ -81,3 +93,32 @@ function shortTime(iso: string): string { : d.toLocaleDateString([], { month: '2-digit', day: '2-digit' }) } catch { return '' } } + +function ArtifactRow({ a }: { a: Artifact }) { + return ( + // 预览落位在第三栏「文件」入口:requestPreview 会切到预览子视图。 +
store.requestPreview(a)} title={`预览 ${a.name}`}> + + + + e.stopPropagation()}> + +
+ ) +} + +function ArtifactRowInner({ a, badge }: { a: Artifact; badge?: string }) { + return ( + <> + + + {a.name}{badge ? {badge} : null} + {a.kind} · {humanSize(a.size) || '—'} · {shortTime(a.updatedAt)} + + + ) +} + +