调研了 Claude Artifacts、ChatGPT Canvas、Manus、Dify/扣子、LibreChat/open-webui 后,业内共识 5 条,全部落到本方案:
artifact 元数据(id/行数/大小/列名),不把数据本体塞进 SSE 和模型上下文。x-token 请求头读,<a href> 直链必然 401,必须 fetch+blob。你已确认的两个取舍:每次表格类工具结果都自动生成 CSV;右栏仅展示当前会话产物。
ToolResultTable.store()(业务分析工具共用)和 SqlAnalysisTool.executeSql()(execute_sql)→ 落盘只改这两处即可覆盖全部工具。ToolResultTable.CELL_LIMIT,且污染缓存全量行)、工具行数上限(10k/100k 超了直接报错)、SSE 单帧 100k 字符、框架 eviction 只给模型留 2k 预览。要"全量输出",必须在进模型/SSE 之前就把数据写文件。agent_message.tool_events(jsonb)持久化未截断的工具结果,且前端 hydrateMessage 已还原成 toolSteps → 产物元数据零新增协议、零新表即可随消息持久化与刷新恢复。CaseContextHolder.get() 可拿到 caseId(SqlResultStore 已在用);workspaceId == caseId,Python 沙箱已有"落盘 + 白名单下载接口"成熟样例(AgentPythonFileController)。awb-body 只有左栏+中栏,右栏是空白插入点;折叠+拖宽面板在 cleaning.vue 有现成模式;codex 产物栏的列表/预览 UI 可参考(其数据层是 Electron IPC,不可复用)。工具执行(execute_sql / 业务分析工具)
├─ 全量行 ──stream──▶ ArtifactCsvWriter ──▶ QingJian/workspace/{caseId}/artifacts/{uuid}_{tool}_{ts}.csv
│ (UTF-8 BOM + 表头,逐行刷写,不截断单元格,无行数上限)
├─ 预览行(≤10万,沿用现状)─▶ SqlResultStore(聊天内表格分页不变)
└─ 工具返回 JSON 首部加 "artifact": {id, fileName, totalRows, sizeBytes, columns, previewRows}
│
├─▶ SSE tool_result 帧(体量小,不受 100k 截断影响)→ 前端即时上屏
└─▶ agent_message.tool_events 持久化 → 刷新后 hydrateMessage 还原
关键点:artifact 字段放在工具结果 JSON 的最前面(紧跟 resultId),这样即便大页结果触发 SSE 100k 截断,产物元数据也一定存活。
| # | 文件 | 改动 |
|---|---|---|
| 1 | 新 agent/artifact/ArtifactCsvWriter.java |
commons-csv(项目已有,CleanTask 在用)逐行写:UTF-8 带 BOM(Excel 打开中文不乱码)、首行表头、每 1000 行 flush、返回 {rows, bytes}。不做 500 字符截断——原始值全量入文件 |
| 2 | 新 agent/artifact/ArtifactService.java |
目录 QingJian/workspace/{caseId}/artifacts/(caseId 取 CaseContextHolder);uuid 命名;写完顺带清理 7 天前旧文件(照抄 PythonExecutor 的清理范式);preview(id, limit)(流式读前 N 条记录返回 JSON)、open(id)(路径 normalize + 必须仍在 artifacts 根内,防穿越) |
| 3 | 新 agent/controller/AgentArtifactController.java |
GET /agent/artifact/preview?id=&limit=(JSON:columns/rows/totalRows)、GET /agent/artifact/download?id=(text/csv + Content-Disposition)。不进白名单,走 sa-token header 鉴权,跨案件访问按 SqlResultStore 范式拒绝 |
| 4 | tools/ToolResultTable.java |
store() 增加导出:原始行→CSV,sanitize 后的行→SqlResultStore(预览),返回 JSON 前插 artifact 字段。签名不变(caseId 自取),各业务工具零改动即可受益 |
| 5 | tools/SqlAnalysisTool.java |
删除 MAX_TOTAL_ROWS>10万就报错:新增 MyBatis 流式查询重载(ResultHandler),边流边写 CSV,内存只留前 10 万行做预览分页;总行数 >预览时结果里标 previewTruncated:true,聊天表格照常翻页,完整数据在产物里。更新 @Tool description(去掉"不能超过 10 万行",改为"完整结果自动导出 CSV 产物") |
| 6 | tools/Call/Trans/Track/Person/FileAnalysisTool |
去掉 FETCH_LIMIT+1 探测报错(10k/5k 上限),全量交给 store() 落盘;留一个配置保险丝 artifact.max-rows(默认 0=不限制,满足你"不限条数"的要求,极端情况可运维侧兜底) |
| 7 | AgentService / AgentToolRegistry |
注入 ArtifactService 给上述工具 |
不做的事(有意为之):不新建 artifact 表——你选了"仅当前会话",元数据放 tool_events 已足够且与消息天然一致,免 DDL 迁移;跨会话"全部产物"视图以后要做时再补表。不改 SSE 协议(无新事件类型,三处协议文件都不用动)。
| # | 文件 | 改动 |
|---|---|---|
| 8 | ai/api/types.ts |
工具结果载荷类型加 artifact?: ArtifactMeta(id/fileName/totalRows/sizeBytes/columns/previewRows) |
| 9 | 新 ai/utils/artifacts.ts |
extractArtifacts(messages):从 message.toolSteps[].result 解析 artifact(防御式,解析失败跳过)——用 computed 从 messages 派生,实时(tool_result 到达)与刷新恢复共用一条路径,无状态同步问题;downloadArtifact():fetch 带 x-token → blob → 复用 core/utils/file/download.ts 的 saveBlobResponse |
| 10 | 新 ai/components/ArtifactPanel.vue |
右栏主体:头部「产物 N」+ 收起按钮;空态文案"查询结果会自动导出为 CSV 出现在这里";列表行 = CSV 图标 + 文件名 + N 行 · 大小 + hover 操作(预览/下载/定位到消息),最新一条带「新」角标;点击行定位回产生它的消息 |
| 11 | ai/views/aiWorkbench/index.vue |
.awb-body 内 awb-view 之后插 <ArtifactPanel v-if="view==='chat'">;可收起:收起后右缘留一条 6px 触发条(带产物数角标,悬停/点击展开),首次产出产物时自动展开;可拖宽:照抄 cleaning.vue 的 panel-resizer 模式,宽 340(min 280 / max 560)存 localStorage |
| 12 | ai/components/ChatMessageItem.vue |
消息尾部加产物行(面板收起时也能看到):文件卡片 + 下载/预览按钮 |
| 13 | 新 ai/components/ArtifactPreviewModal.vue |
预览弹窗:调 preview 接口取前 200 行 → a-table 展示 + 表头;底部「共 N 行」+「下载完整 CSV」。参考 codex 预览弹窗布局但独立实现(不引 codex 主题) |
| 14 | ai/styles/ai.less 或组件 scoped |
.awb-aside 用 --ai-* token:flex:0 0 auto + 左边框 + 自滚动(骨架照抄 .wb-aside) |
ArtifactCsvWriter + ArtifactService + Controller + ToolResultTable.store() 收口 → 单测(表头/逗号换行引号转义/BOM/全量行数/跨案件 404/路径穿越)。artifact.max-rows 配置。AgentScopeToolCallback),超大导出受此约束;预览内存上限 10 万行(可配),文件本身无上限。ToolResultEvictionMiddleware 只裁模型上下文,不影响文件——这正是"全量落盘、小载荷回模型"设计要利用的机制。