|
|
@@ -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();
|
|
|
};
|