Browse Source

fix: 附件上传竞态补强(长超时 + 孤儿清理放宽)

针对「消息发出但文件未处理完」的竞态审查:
- 后端原子性已成立:buildBlocks(校验 READY)在 persistUserMessage 与开流之前,
  附件未就绪直接 400,消息不会带着半成品发出;上传同步处理完才返回 id
- 补缺口1:附件上传单独 10 分钟超时(默认 3 分钟会误杀大文档 RAG 索引/大表格
  流式扫描,用户重试产生重复附件)
- 补缺口2:孤儿清理 24h → 7 天(配置 orphan-hours)——
  输入框挂几天再发是正常行为,被清理后发送 400 属误伤
cc 1 tuần trước cách đây
mục cha
commit
c38cd7fec9

+ 4 - 0
ai-electron/frontend/src/ai/api/attachmentApi.ts

@@ -23,6 +23,10 @@ export async function uploadAttachment(
   const res: any = await defHttp.uploadFile<any>(
     {
       url: resolveApiUrl('/chat/attachments'),
+      // 附件上传单独给长超时:大文档 RAG 索引(几百块 embedding)与 200MB 表格
+      // 流式扫描都在上传请求内同步完成,3 分钟默认超时会把它们误杀成"上传失败",
+      // 用户重试就产生重复附件 —— 10 分钟封顶,正常情况远用不满
+      timeout: 10 * 60 * 1000,
       onUploadProgress: (e: { loaded: number; total?: number }) => {
         if (e.total) onProgress?.(Math.round((e.loaded * 100) / e.total));
       },

+ 3 - 0
ai-server/src/main/java/com/zsjz/ai/module/agent/attachment/ChatAttachmentProperties.java

@@ -53,6 +53,9 @@ public class ChatAttachmentProperties {
     /** RAG 索引并发度(embedding 调用) */
     private int indexConcurrency = 8;
 
+    /** 孤儿附件保留小时数:上传后这么久仍未随消息发出才清理(默认 7 天) */
+    private int orphanHours = 168;
+
     /** 接受的后缀白名单(小写;空 = 用默认清单) */
     private List<String> acceptSuffixes;
 

+ 7 - 3
ai-server/src/main/java/com/zsjz/ai/module/agent/attachment/ChatAttachmentService.java

@@ -362,13 +362,17 @@ public class ChatAttachmentService {
     // ==================== 清理 ====================
 
     /**
-     * 孤儿清理:24h 前上传且从未关联消息的附件 → 删文件 + sidecar + 向量行。
-     * 已关联的随会话留存(级联删除列为后续)。
+     * 孤儿清理:上传后超过 orphan-hours(默认 7 天)仍未关联消息的附件 →
+     * 删文件 + sidecar + 向量行。已关联的随会话留存(级联删除列为后续)。
+     *
+     * <p>时限特意宽(而不是 24h):用户把输入框挂几天再发是正常行为,
+     * 附件在发送时被清理掉(400「附件不存在或已清理」)属于误伤;
+     * 7 天足够覆盖任何真实使用节奏,孤儿又不会无限堆积。
      */
     @Scheduled(fixedDelayString = "${zsjz.chat-attachment.purge-interval-ms:3600000}",
             initialDelayString = "${zsjz.chat-attachment.purge-initial-delay-ms:600000}")
     public void purgeOrphans() {
-        LocalDateTime deadline = LocalDateTime.now().minusHours(24);
+        LocalDateTime deadline = LocalDateTime.now().minusHours(Math.max(24, props.getOrphanHours()));
         List<ChatAttachment> orphans = attachmentMapper.selectList(Wrappers.<ChatAttachment>lambdaQuery()
                 .isNull(ChatAttachment::getLinkMessageId)
                 .lt(ChatAttachment::getCreateTime, deadline));