Browse Source

[Mod 0000]AI助手新对话时增加删除按钮

wangkeyi 2 days ago
parent
commit
5c6aca75ca
1 changed files with 59 additions and 7 deletions
  1. 59 7
      src/views/ventAI/manageAssistent/hooks/useChatSend.ts

+ 59 - 7
src/views/ventAI/manageAssistent/hooks/useChatSend.ts

@@ -170,6 +170,11 @@ export const useChatSend = (deps: ChatSendDeps) => {
           }
         }
       }
+
+      // 本轮问答刚由后端持久化,补一次数据库主键:删除按钮依赖它,不补的话刚发出的消息没有删除
+      // 入口(旧实现只在重新生成/编辑重发里补,正常发送出来的消息要刷新页面才补上)。
+      // 只能放在流正常结束之后——被中断的那一轮拿不到 session_id(见下面的 AbortError 分支)。
+      await hydrateMessageIds(originTask?.sessionId || '', originTask?.messages || []);
     } catch (error) {
       // 用户主动停止生成
       if ((error as Error)?.name === 'AbortError') {
@@ -244,9 +249,19 @@ export const useChatSend = (deps: ChatSendDeps) => {
   // 「当前会话 / 当前任务」,请求期间用户切走就会把别的会话的消息当成目标,回填出的 id 会让
   // 后续的删除打到另一条会话上。
   //
-  // 两边条数必须严格一致才回填。早期实现用「长度差」做尾部对齐,一旦本地存在仅前端的消息
-  // (例如发送失败的报错气泡,后端没有对应行),长度差为负,之后每条未回填的消息都会拿到
-  // 前一条的 id —— 删除按钮据此调 DELETE /messages/{id},删掉的是用户根本没想删的那一行。
+  // 两步配对,原则都是「宁可少补(那一条暂时没有删除按钮),不能补错(删除会删到别的行)」:
+  //
+  // 一、按「角色 + 文本」精确配对。条数不一致(本地多出仅前端的报错气泡,或后端还留着重发
+  //     产生的重复行)也能把对得上的逐条填上;只认文本完全一致的记录,同文本多条按先后顺序
+  //     一一消费。
+  // 二、剩下的没配上,且两边条数一致、角色顺序完全一致时,按位置补。
+  //     文本对不上是常态而不是异常:子智能体的输出在流式时被路由进思考卡片,主回答才进气泡,
+  //     而后端落库时把两段拼成同一条记录 —— 气泡文本只是整条记录的后半截,按文本永远配不上,
+  //     表现为「刚发出的 AI 消息没有删除按钮,刷新页面才出现」。条数与角色序列一致时,两条序列
+  //     就是同一串问答,正文差异不影响一一对应关系。
+  //
+  // 早期实现要么按条数严格相等整批回填(长度差为负时会整体放弃),要么用长度差做尾部对齐
+  // (每条未回填的消息都拿到前一条的 id,删除会删掉用户根本没想删的那一行),都已被这两种取代。
   const hydrateMessageIds = async (sessionId: string, msgs: Message[]) => {
     if (!sessionId || !msgs.length) return;
     try {
@@ -254,12 +269,49 @@ export const useChatSend = (deps: ChatSendDeps) => {
       // getDetail 已按真实契约标注 { messages, ... },不再猜 messages / data / 裸数组三种外壳
       const raw = res?.messages;
       if (!Array.isArray(raw)) return;
-      // 用 transformHistoryToMessages 归一化,避免 model_thinking 等记录导致条数与前端不对齐
+      // 用 transformHistoryToMessages 归一化,避免 model_thinking 等记录影响配对
       const history = transformHistoryToMessages(raw);
-      const ids = history.map((m) => m.id).filter((id): id is number => id != null);
-      if (ids.length !== msgs.length) return;
+
+      const normalize = (text: string) => (text || '').replace(/\s+/g, ' ').trim();
+      const keyOf = (type: Message['type'], text: string) => `${type}::${normalize(text)}`;
+
+      // 后端记录按 key 分桶,桶内保持先后顺序(同文本出现多次时按顺序对应)
+      const pool = new Map<string, number[]>();
+      for (const item of history) {
+        if (item.id == null) continue;
+        const key = keyOf(item.type, item.content);
+        const bucket = pool.get(key);
+        if (bucket) bucket.push(item.id);
+        else pool.set(key, [item.id]);
+      }
+
+      // 已发出去的主键:消息上本来就有的(历史加载回填的)也算,同一条后端记录不能落到两条前端消息上
+      const assigned = new Set<number>();
+      for (const msg of msgs) {
+        if (msg.id != null) assigned.add(msg.id);
+      }
+
+      for (const msg of msgs) {
+        if (msg.id != null) continue;
+        // 用户消息要拿「真正发出去的那段文本」比对:气泡里的 content 含注入的引用图标 HTML,
+        // 后端存的是纯文本(与编辑重发、重新生成取原始输入用的是同一个函数)
+        const text = msg.type === 'user' ? getUserInputText(msg) : msg.content;
+        const bucket = pool.get(keyOf(msg.type, text));
+        if (!bucket) continue;
+        // 桶内已被占用的主键(同文本的另一条已经被前面的消息领走)直接跳过
+        while (bucket.length && assigned.has(bucket[0])) bucket.shift();
+        if (!bucket.length) continue;
+        const id = bucket.shift() as number;
+        msg.id = id;
+        assigned.add(id);
+      }
+
+      const sameShape = history.length === msgs.length && history.every((h, i) => h.type === msgs[i].type);
+      if (!sameShape) return;
       for (let i = 0; i < msgs.length; i++) {
-        if (msgs[i].id == null) msgs[i].id = ids[i];
+        const id = history[i].id;
+        if (msgs[i].id != null || id == null || assigned.has(id)) continue;
+        msgs[i].id = id;
       }
     } catch (e) {
       console.error('补齐消息 id 失败:', e);