feat(web): 前端 P0 收尾(多路忙态收口 / 握手 history 落 store / 清理幽灵键)
This commit is contained in:
1 parent
e852e60af9
commit
012e9f3ad4
10 files changed
+365
-31
No files matched your search
@@ -28,6 +28,72 @@ export const BACKTHINK_SESSION_ID = "sess-backthink";
|
||||
/** 一个会话的消息列表;键是会话 id。 */
|
||||
export type SessionMessages = Record<string, ChatMessage[]>;
|
||||
|
||||
/** 握手 `history` 回放(契约 §3.2)的**整批**并入:一次最多 50 条**已落库**的权威行。
|
||||
*
|
||||
* 为什么不直接复用 `mergeMessage`:单条 `message_persisted` 允许用「同 role + 内容全等 /
|
||||
* `createdAt` 最近」的启发式去认领本端乐观气泡;整批回放套这条启发式,会把本端
|
||||
* **正在流式**的气泡错配到某条旧助手行上(正文被旧内容顶掉、流式态丢失)——
|
||||
* 回放越大、越容易配错。因此这里认领本端气泡只用**精确键**:
|
||||
* - 用户行:`clientMsgId`(本端生成、随 `chat.clientMsgId` 上行,回放 DTO 也带);
|
||||
* - 助手行:`turnId`(`turn_begin` 已把这个 id 钉在本端气泡上)。
|
||||
* 精确键对不上一律**追加**(猜错比多一条更糟),同 id 则 upsert。 */
|
||||
export function mergeHistory(list: ChatMessage[], msgs: ChatMessage[]): ChatMessage[] {
|
||||
let next = list;
|
||||
for (const msg of msgs) {
|
||||
const copy = next === list ? list.slice() : next;
|
||||
const idx = copy.findIndex((m) => m.id === msg.id);
|
||||
if (idx !== -1) {
|
||||
copy[idx] = { ...copy[idx], ...msg };
|
||||
next = copy;
|
||||
continue;
|
||||
}
|
||||
const adopt = historyAdoptIndex(copy, msg);
|
||||
if (adopt !== -1) {
|
||||
const m = copy[adopt];
|
||||
copy[adopt] = {
|
||||
...m,
|
||||
id: msg.id,
|
||||
sessionId: msg.sessionId ?? m.sessionId,
|
||||
content: msg.content || m.content,
|
||||
mode: msg.mode ?? m.mode,
|
||||
mood: msg.mood ?? m.mood,
|
||||
image: msg.image ?? m.image,
|
||||
documents: msg.documents ?? m.documents,
|
||||
turnId: msg.turnId ?? m.turnId,
|
||||
clientMsgId: msg.clientMsgId ?? m.clientMsgId,
|
||||
personaId: msg.personaId ?? m.personaId,
|
||||
createdAt: msg.createdAt ?? m.createdAt,
|
||||
// 回放里的**助手行** ⇒ 这一路已收尾落库:本端在飞气泡必须停止流式,
|
||||
// 否则那一帧 `stream_end` 丢在断线窗口里会让它永远转圈。
|
||||
...(msg.role === "assistant" ? { streaming: false } : {}),
|
||||
// 用户行落库 ⇒ 不再是「发送中」,转「已送达」;已被某路接手(answering)
|
||||
// 或已排队(queued)的不降级 —— 那些态来自比「落库」更晚的帧。
|
||||
...(msg.role === "user" && m.delivery === "sending"
|
||||
? { delivery: "sent" as const }
|
||||
: {}),
|
||||
};
|
||||
next = copy;
|
||||
continue;
|
||||
}
|
||||
copy.push(msg);
|
||||
next = copy;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
/** 回放行只按**精确键**认领本端乐观气泡:用户行认 `clientMsgId`、助手行认 `turnId`。
|
||||
* 两者都没有、或本端没有对应气泡时返回 -1(调用方改为追加)。 */
|
||||
function historyAdoptIndex(list: ChatMessage[], msg: ChatMessage): number {
|
||||
if (!msg.clientMsgId && !msg.turnId) return -1;
|
||||
for (let i = list.length - 1; i >= 0; i -= 1) {
|
||||
const m = list[i];
|
||||
if (m.sessionId || m.role !== msg.role) continue;
|
||||
if (msg.role === "user" && msg.clientMsgId && m.clientMsgId === msg.clientMsgId) return i;
|
||||
if (msg.role === "assistant" && msg.turnId && m.turnId === msg.turnId) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** 把一条后端消息并入消息列表:按 id upsert;没有同 id 时采纳本端同 role 的本地
|
||||
* 乐观气泡(本地 `uid` → 后端 `id`),使同一句话不会渲染两遍(design/16 §4.3 去重)。
|
||||
* 后端消息是内容的真源,本地气泡保留流式/工具/思考等本端状态。
|
||||
|
||||
Reference in new issue
Block a user