Przeglądaj źródła

[Fix R3-9] 缩小重新生成去重删除范围(服务端时间戳基线,防误删历史同文案提问)

lizuo 1 dzień temu
rodzic
commit
c046275adb

+ 49 - 6
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -1870,7 +1870,17 @@
   };
 
   // 重新生成后,删除后端因重发而产生的重复用户消息(保留第一次,形成「一次提问、多次回答」)
-  const removeDuplicateRegenerateUserMessage = async (expectedContent: string) => {
+  // [审计-第三轮 R3-9] 缩小删除范围:只删本次重发产生的那一条
+  // 原行为:凡内容同文案的历史 user 消息(idx > 0 的全部匹配项)都会被 deleteMessage 静默删除,
+  //   "继续"、"总结一下"这类极常见的同文案历史提问会整批丢失。
+  // 现行为:以重发前服务端快照的最大 created_at 为基线 baselineMs(同一时钟域内比较),
+  //   仅删除 created_at 严格大于基线的消息(即本次重发由后端新落库的那条重复提问);历史同文案消息全部保留。
+  // 为什么:基线取自服务端 getDetail 快照而非浏览器 Date.now(),避免前后端时钟偏差(矿内网常见分钟级)
+  //   导致历史消息被误判为"晚于重发"而误删;用严格大于(不用 >=)是因为基线那条本身可能就是
+  //   上一次的同文案提问,且 created_at 若为秒级精度,同秒的新消息宁可漏删也不能误删。
+  // 退化:baselineMs 为 NaN(服务端时间戳不可得)时,只删匹配项中最后一条(本次重发产生的即最新一条),
+  //   仍不删全部;created_at 缺失/不可解析的消息一律不删,宁留重复、不误删历史。
+  const removeDuplicateRegenerateUserMessage = async (expectedContent: string, baselineMs: number) => {
     const sessionId = getCurrentSessionId();
     if (!sessionId) return;
     try {
@@ -1878,12 +1888,26 @@
       const raw = res?.messages || (Array.isArray(res) ? res : res?.data);
       if (!Array.isArray(raw)) return;
 
-      // 保留第一条 user(原始提问),删除其余与重发内容一致的重复 user,避免多次重新生成残留
+      // 保留第一条 user(原始提问),其余与重发内容一致的候选 user,避免多次重新生成残留
       const userMsgs = raw.filter((m: any) => m?.role === 'user' || m?.type === 'user_message');
-      const duplicates = userMsgs.filter(
-        (m: any, idx: number) => idx > 0 && normalizeLineBreaks(m?.content || '').trim() === expectedContent.trim() && m?.id != null
+      const matches = userMsgs.filter(
+        (m: any, idx: number) =>
+          idx > 0 &&
+          normalizeLineBreaks(m?.content || '').trim() === expectedContent.trim() &&
+          m?.id != null &&
+          // created_at 缺失/不可解析的候选一律不删,防误删历史
+          Number.isFinite(m?.created_at ? dayjs(m.created_at).valueOf() : NaN)
       );
-      for (const dup of duplicates) {
+
+      let toDelete: any[] = [];
+      if (Number.isFinite(baselineMs)) {
+        // 同一时钟域(服务端 created_at)内比较:只删晚于重发前快照基线的(本次重发新落库的那条)
+        toDelete = matches.filter((m: any) => dayjs(m.created_at).valueOf() > baselineMs);
+      } else if (matches.length > 0) {
+        // 服务端时间戳不可得:退化为只删最后一条匹配项(本次重发产生的即最新一条),仍不删全部
+        toDelete = [matches[matches.length - 1]];
+      }
+      for (const dup of toDelete) {
         await deleteMessage(dup.id);
       }
     } catch (e) {
@@ -1919,9 +1943,28 @@
     const attachedFile = userMsg.attachedFile;
 
     // 不删除旧 AI 消息、不动用户提问,直接重发同一条用户输入重新生成
+    // [审计-第三轮 R3-9] 重发前先取服务端消息快照,取其中最大 created_at 作为基线 baselineMs:
+    // 与去重函数在同一时钟域(服务端 created_at)内比较,避免前后端时钟偏差导致误删历史同文案提问。
+    // 快照无任何可解析时间戳时 baselineMs 为 NaN,去重函数退化为"只删最后一条匹配项"。
+    let baselineMs = NaN;
+    try {
+      const sessionId = getCurrentSessionId();
+      if (sessionId) {
+        const pre = await getDetail(sessionId);
+        const preRaw = pre?.messages || (Array.isArray(pre) ? pre : pre?.data);
+        if (Array.isArray(preRaw)) {
+          for (const m of preRaw) {
+            const ms = m?.created_at ? dayjs(m.created_at).valueOf() : NaN;
+            if (Number.isFinite(ms) && (!Number.isFinite(baselineMs) || ms > baselineMs)) baselineMs = ms;
+          }
+        }
+      }
+    } catch (e) {
+      console.error('取重发前消息快照基线失败:', e);
+    }
     await streamAiAnswer(userInput, attachedFile);
     // 删除重发产生的重复用户消息,形成「一次提问、多次回答」
-    await removeDuplicateRegenerateUserMessage(userInput);
+    await removeDuplicateRegenerateUserMessage(userInput, baselineMs);
     // 回填新生成消息的 id,使折叠组内各条消息都能正常显示删除按钮
     await hydrateMessageIds();
   };