Skip to content

feat: add table export asset management - #134

Merged
VLSMB merged 7 commits into
mainfrom
feature/add_csv_tool
Aug 29, 2026
Merged

feat: add table export asset management#134
VLSMB merged 7 commits into
mainfrom
feature/add_csv_tool

Conversation

@mengnankkkk

Copy link
Copy Markdown
Member
  • add CSV table export tool, API, storage, and history page
  • group reports and table exports under asset management
  • document MySQL streaming behavior and PostgreSQL export limits

- add CSV table export tool, API, storage, and history page
- group reports and table exports under asset management
- document MySQL streaming behavior and PostgreSQL export limits
@github-actions

Copy link
Copy Markdown

Thank you for your contribution! We will review your request as soon as possible. Please review the code yourself using ponytail https://github.com/DietrichGebert/ponytail.

- split semantic and system management tabs into sidebar subroutes
- restore semantic help tips beside page titles
- render spreadsheet download links as chat download cards
@lzq986

lzq986 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

@VLSMB 有空帮忙看一下

@lzq986
lzq986 requested a review from VLSMB August 23, 2026 15:10
@lzq986

lzq986 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

整体方向 👍,表格导出这个能力补得很实用,流式读取 + CSV 公式注入防护 + 路径穿越防护都到位,前端导航重构也理顺了。提几个点,按优先级排:

P1 — 必须修

1. create() 返回的 createTime 是 null

TableExportServiceImpl.create() 里 insert 只写了 id/title/row_count/session_idcreate_time 交给 DB 默认值,但 MyBatis 不会自动回读,导致 toResponse() 拿到的 createTime 为 null——export_table 工具返回给 Agent 的响应里时间字段是空的(列表页走 select 所以无碍)。

fix(二选一,推荐第一个):

// create() 里 insert 之前补一行
tableExport.setCreateTime(LocalDateTime.now());
<!-- TableExportMapper.xml 的 insert 增加 create_time -->
INSERT INTO table_export (id, title, row_count, session_id, create_time)
VALUES (#{id}, #{title}, #{rowCount}, #{sessionId}, #{createTime})

P2 — 建议修

2. deleteById 先删文件再删行,失败会留孤儿行

当前顺序:Files.deleteIfExists(file)deleteById(id)。若 DB 删除返回 0(记录已不在),会抛 exportNotFound,但文件已经被删了,留下「有行无文件」的反向孤儿。建议先删 DB 行、确认成功后再删文件:

if (tableExportMapper.deleteById(id) == 0) {
    throw exportNotFound(id);
}
try {
    Files.deleteIfExists(file);
} catch (IOException e) {
    throw BusinessException.of(ErrorCode.OPERATION_FAILED, "Failed to delete export file.", e);
}

P3 — 可维护性(不强求,建议顺手收一下)

3. 聊天端下载链接解析过度设计

系统里唯一产出下载 URL 的是 ExportTableTool.formatResult,固定输出 /api/table-exports/{uuid}/download。但 ChatMessage.vue 为这一种形态写了 4 个正则 + markdown 链接解析 + 文件名推导 + 外部 URL fetch 兜底,全是 speculative。

FILE_URL_REMARKDOWN_LINK_REDOWNLOAD_LABEL_REfileNameFromUrlfileNameOrFallback 都没有实际命中路径,ChatFileDownload.vuefetch(props.file.url) 的非 /api/ 分支也是死的。可以整个塌缩成:

const TABLE_EXPORT_RE = /\/api\/table-exports\/([0-9a-f-]{36})\/download/i;

function extractDownloads(text: string) {
  const files: { id: string; url: string; name: string }[] = [];
  const lines = text.split('\n').filter(line => {
    const m = line.match(TABLE_EXPORT_RE);
    if (!m) return true;
    files.push({ id: m[1], url: m[0], name: `${m[1]}.csv` });
    return false;
  });
  return { text: lines.join('\n').trim(), files };
}

4. ChatDownloadFile 接口重复定义

同样的 { id; url; name }ChatMessage.vueChatFileDownload.vue 各写了一份,提成一个共享类型 import 即可。

5. DomainManage.vue / TableSemanticManage.vuekeyword/sortOrder props 已无调用方

唯一传这两个 props 的父组件 SemanticManage.vue 在本 PR 删除后,这两个 props 永远落在默认值 ''/'asc',没人再传。可以去掉 props,直接内联常量。

@lzq986

lzq986 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

发现剩一个必须处理的架构问题。

P1 — 导出文件持久化在本地磁盘,多副本 / pod 漂移会丢

当前 export_table 把 CSV 写到 data-agent.export-dir(默认 ${user.dir}/exports),DB 的 table_export 表只存元数据(id/title/row_count/session_id),下载用 FileSystemResource 读本地文件。

这在我们当前 pod 部署下有两个真实故障模式:

  1. pod 漂移 / 重启 → 文件消失:容器文件系统是临时的,/exports 没挂持久卷时,pod 被驱逐、滚动更新、节点故障,目录就清空。此时 DB 记录还在 → 用户点下载 404(代码已兜底 Files.exists → 抛 404,不会崩,但体验是「导出成功却下载不到」)。
  2. 多副本 → 每次请求都可能 404:只要 replicas > 1,导出发生在 pod A(文件写在 A 本地盘),下载请求被 LB 打到 pod B(没这文件)→ 404。这比「偶发重启才丢」更致命。

另外这和 report 不一致——reportcontent LONGTEXT 正文直接进 DB,下载走前端 blob,从不落磁盘。table_export 是项目里第一个落本地磁盘的方案。

建议(按推荐度):

方案 适用 说明
① CSV 内容进 DB 数据量可控 table_export 加一列 content LONGTEXT,下载时后端从 DB 读内容流式返回。一次性消除「丢失 + 多副本」两个问题,且和 report 对齐。前提是给导出定个行数/字节上限(比如 MAX_EXPORT_ROWS),否则超大文件会撑爆 DB。
② 对象存储(S3/OSS/MinIO) 确有几百 MB 大文件 云原生正解,DB 存 key,下载走签名 URL 或后端代理。但项目目前没有任何对象存储基建,引入成本高。
③ PVC + 单副本 短期不想动 export-dir 挂持久卷 + 约束 replicas=1。能拖,但多副本死穴仍在,靠运维纪律而非架构保证。

我的判断:这是 AI 问答场景,导出的大表格通常几千~几万行、CSV 几 MB 量级,方案 ① 进 DB 最划算,零新基建、跟 report 统一、一次性解决全部隐患。只有确认会有上百 MB 的导出时才值得上对象存储。

另外一个必须补的事:docs/configuration.md 已经写了「PostgreSQL 需补 cursor」这类限制,却没写「导出文件存在本地磁盘、多副本/pod 漂移会丢」这个更现实的限制。无论选哪个方案,这条要么在文档里明确标注,要么直接改成方案 ①。


或者你看看有无更好的方案

@mengnankkkk

Copy link
Copy Markdown
Member Author

发现剩一个必须处理的架构问题。

P1 — 导出文件持久化在本地磁盘,多副本 / pod 漂移会丢

当前 export_table 把 CSV 写到 data-agent.export-dir(默认 ${user.dir}/exports),DB 的 table_export 表只存元数据(id/title/row_count/session_id),下载用 FileSystemResource 读本地文件。

这在我们当前 pod 部署下有两个真实故障模式:

  1. pod 漂移 / 重启 → 文件消失:容器文件系统是临时的,/exports 没挂持久卷时,pod 被驱逐、滚动更新、节点故障,目录就清空。此时 DB 记录还在 → 用户点下载 404(代码已兜底 Files.exists → 抛 404,不会崩,但体验是「导出成功却下载不到」)。
  2. 多副本 → 每次请求都可能 404:只要 replicas > 1,导出发生在 pod A(文件写在 A 本地盘),下载请求被 LB 打到 pod B(没这文件)→ 404。这比「偶发重启才丢」更致命。

另外这和 report 不一致——reportcontent LONGTEXT 正文直接进 DB,下载走前端 blob,从不落磁盘。table_export 是项目里第一个落本地磁盘的方案。

建议(按推荐度):

方案 适用 说明
① CSV 内容进 DB 数据量可控 table_export 加一列 content LONGTEXT,下载时后端从 DB 读内容流式返回。一次性消除「丢失 + 多副本」两个问题,且和 report 对齐。前提是给导出定个行数/字节上限(比如 MAX_EXPORT_ROWS),否则超大文件会撑爆 DB。
② 对象存储(S3/OSS/MinIO) 确有几百 MB 大文件 云原生正解,DB 存 key,下载走签名 URL 或后端代理。但项目目前没有任何对象存储基建,引入成本高。
③ PVC + 单副本 短期不想动 export-dir 挂持久卷 + 约束 replicas=1。能拖,但多副本死穴仍在,靠运维纪律而非架构保证。
我的判断:这是 AI 问答场景,导出的大表格通常几千~几万行、CSV 几 MB 量级,方案 ① 进 DB 最划算,零新基建、跟 report 统一、一次性解决全部隐患。只有确认会有上百 MB 的导出时才值得上对象存储。

另外一个必须补的事:docs/configuration.md 已经写了「PostgreSQL 需补 cursor」这类限制,却没写「导出文件存在本地磁盘、多副本/pod 漂移会丢」这个更现实的限制。无论选哪个方案,这条要么在文档里明确标注,要么直接改成方案 ①。

或者你看看有无更好的方案

也放进DB里面吧,感觉最好简单来。后面可以提供一个统一的云文件的存储的抽象接口

@VLSMB
VLSMB merged commit 9882302 into main Aug 29, 2026
3 checks passed
@mengnankkkk
mengnankkkk deleted the feature/add_csv_tool branch August 29, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants