feat(web): 前端与后端同步一批(帧壳 turnId / 并行六态与两级分桶 / documents 渲染 / System 键名 / 主人档案分组)
- 帧壳 `turnId` 在 socket 层不再被丢(`lib/ws.ts` 的 dispatchFrame 补 turnId/seq)—— 按路分桶的前提 - 并行帧组接入:`turn_begin` / `steer_ack` / `pending_ack` / `pending_drain` / `pending_full` / `chat_duplicate` ⇒ 六态 + 灰态 + `(sessionId, turnId)` 两级分桶;`appendChunk` 不再永远写尾气泡 - 多路忙态:`session_interrupted` 不再无条件当空闲(看 mode/activeTurns);`stream_end` 不再只按会话过滤 - `documents` 渲染(含 status/chars/truncated 与**逐字中文 reason**);`turnId` 历史行分组小标 - `System.tsx` 键名改正(真键 HOST/PORT/ADMIN_USERNAME/ADMIN_PASSWORD,避免照抄静默失效) - 主人档案**按平台分组** + 平台下拉 + 账号候选;**写盘仍展平数组**(后端契约不变) - 验证:tsc --noEmit ✅、npm run build ✅、22 个前端测试文件全通过(本次新增 3 文件 108 条断言) - 未做(如实):握手 history handler 与断线补拉、上行 interrupt 按钮、proactive_message/subagent_result 等帧落点、文档上传入口与图片预检、voice 管线、System 的 env 读端点
This commit is contained in:
1 parent
6a545badbd
commit
eaada3af5d
34 files changed
+2457
-223
No files matched your search
@@ -30,7 +30,13 @@ export type SessionMessages = Record<string, ChatMessage[]>;
|
||||
|
||||
/** 把一条后端消息并入消息列表:按 id upsert;没有同 id 时采纳本端同 role 的本地
|
||||
* 乐观气泡(本地 `uid` → 后端 `id`),使同一句话不会渲染两遍(design/16 §4.3 去重)。
|
||||
* 后端消息是内容的真源,本地气泡保留流式/工具/思考等本端状态。 */
|
||||
* 后端消息是内容的真源,本地气泡保留流式/工具/思考等本端状态。
|
||||
*
|
||||
* 采纳顺序(忙时并行后很重要,规范草案 §1.7):
|
||||
* ① 同 `turnId` 且尚未落库的气泡 —— `turn_begin` 已把本端气泡绑到这一路,turnId
|
||||
* 才是权威配对(并行时两条回复内容可能相似,靠内容判等会**配错路**);
|
||||
* ② 内容完全相同的那条(旧启发式,保持老后端行为);
|
||||
* ③ createdAt 最近的那条。 */
|
||||
export function mergeMessage(list: ChatMessage[], msg: ChatMessage): ChatMessage[] {
|
||||
const idx = list.findIndex((m) => m.id === msg.id);
|
||||
if (idx !== -1) {
|
||||
@@ -38,6 +44,14 @@ export function mergeMessage(list: ChatMessage[], msg: ChatMessage): ChatMessage
|
||||
next[idx] = { ...next[idx], ...msg };
|
||||
return next;
|
||||
}
|
||||
if (msg.turnId) {
|
||||
for (let i = list.length - 1; i >= 0; i -= 1) {
|
||||
const m = list[i];
|
||||
if (!m.sessionId && m.role === msg.role && m.turnId === msg.turnId) {
|
||||
return adopt(list, i, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 没有同 id:找一个本端尚未落库的同 role 气泡(无 sessionId)来归一化。
|
||||
// 优先内容完全相同的那条,否则取 createdAt 最近的;从新到旧扫描。
|
||||
let best = -1;
|
||||
@@ -52,9 +66,14 @@ export function mergeMessage(list: ChatMessage[], msg: ChatMessage): ChatMessage
|
||||
}
|
||||
}
|
||||
if (best === -1) return [...list, msg];
|
||||
const m = list[best];
|
||||
return adopt(list, best, msg);
|
||||
}
|
||||
|
||||
/** 把本端乐观气泡归一为后端消息(id 换成服务端的,其余保留本端状态)。 */
|
||||
function adopt(list: ChatMessage[], at: number, msg: ChatMessage): ChatMessage[] {
|
||||
const m = list[at];
|
||||
const next = list.slice();
|
||||
next[best] = {
|
||||
next[at] = {
|
||||
...m,
|
||||
id: msg.id,
|
||||
sessionId: msg.sessionId ?? m.sessionId,
|
||||
@@ -62,6 +81,9 @@ export function mergeMessage(list: ChatMessage[], msg: ChatMessage): ChatMessage
|
||||
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,
|
||||
platform: msg.platform ?? m.platform,
|
||||
deviceId: msg.deviceId ?? m.deviceId,
|
||||
personaId: msg.personaId ?? m.personaId,
|
||||
|
||||
Reference in new issue
Block a user