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
@@ -1,23 +1,38 @@
|
||||
import { frameIsActive, frameSessionId, type FrameEvent, type FramePayload, type StoreState } from "./shared";
|
||||
import { turnsInSession } from "../../../store/turnCache";
|
||||
import {
|
||||
frameIsActive,
|
||||
frameSessionId,
|
||||
frameTurnId,
|
||||
type FrameEvent,
|
||||
type FramePayload,
|
||||
type StoreState,
|
||||
} from "./shared";
|
||||
|
||||
/** 流式正文帧处理组:stream_chunk / stream_end / message.start / message.delta /
|
||||
* message.end。命中(含组内主动丢弃)返回 true,未命中返回 false 交给下一组。
|
||||
*
|
||||
* 裁决 16.4.5:帧按自己的 `sessionId` 落进对应会话的消息桶,**不再**因为不属于
|
||||
* 激活会话而被丢掉 —— 后台会话的正文继续累积,切过去就能看到完整的这一轮。 */
|
||||
* 激活会话而被丢掉 —— 后台会话的正文继续累积,切过去就能看到完整的这一轮。
|
||||
*
|
||||
* 忙时并行(design/15 §2.5):帧壳 `turnId` 决定它属于**哪一路**,增量按
|
||||
* `(sessionId, turnId)` 二级分桶写进那一路的助手气泡;两条回复交错到达时不会糊成
|
||||
* 一条(此前只按「列表尾气泡」写,正是把两条回复糊在一起的成因)。 */
|
||||
export function handleStreamEvent(ev: FrameEvent, r: FramePayload, s: StoreState): boolean {
|
||||
switch (ev.type) {
|
||||
case "stream_chunk": {
|
||||
if (r && typeof r.delta === "string") s.appendChunk(r.delta, frameSessionId(ev, r));
|
||||
if (r && typeof r.delta === "string") {
|
||||
s.appendChunk(r.delta, frameSessionId(ev, r), frameTurnId(ev, r));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case "stream_end": {
|
||||
const sid = frameSessionId(ev, r);
|
||||
s.endStream(sid);
|
||||
const turnId = frameTurnId(ev, r);
|
||||
s.endStream(sid, turnId);
|
||||
// 忙态 / 上下文是**单槽全局**状态(design/15 §5),所以只有屏幕上这个会话的
|
||||
// 回合结束才收口;别的会话结束只能收它自己气泡的流,不能清掉这里的徽标。
|
||||
if (frameIsActive(sid, s.activeSessionId)) {
|
||||
s.setSessionBusy(false, 0);
|
||||
if (frameIsActive(sid, s.activeSessionId) && !otherLegsRunning(s, sid, turnId)) {
|
||||
s.setSessionBusy(false, 0, turnId ? 0 : undefined);
|
||||
if (r && typeof r.stopReason === "string") {
|
||||
s.setContext({ lastAgentMode: s.context.lastAgentMode });
|
||||
}
|
||||
@@ -31,7 +46,9 @@ export function handleStreamEvent(ev: FrameEvent, r: FramePayload, s: StoreState
|
||||
return true;
|
||||
}
|
||||
case "message.delta": {
|
||||
if (r && typeof r.delta === "string") s.appendChunk(r.delta, frameSessionId(ev, r));
|
||||
if (r && typeof r.delta === "string") {
|
||||
s.appendChunk(r.delta, frameSessionId(ev, r), frameTurnId(ev, r));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case "message.end": {
|
||||
@@ -43,3 +60,12 @@ export function handleStreamEvent(ev: FrameEvent, r: FramePayload, s: StoreState
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 除了刚收尾的这一路,该会话还有别的路在跑吗?有就**不能**把忙态置闲 —— 并行时
|
||||
* 先答完的那一路会把「她正在忙」徽标误关掉(v0.65 §4.6 的 `session_idle` 时点收紧
|
||||
* 就是为这个)。没有 `turnId` 的旧帧(单路老后端)逐字沿用旧行为:照样清忙。 */
|
||||
function otherLegsRunning(s: StoreState, sessionId: string | undefined, turnId: string | undefined): boolean {
|
||||
if (!turnId) return false;
|
||||
const sid = sessionId ?? s.activeSessionId;
|
||||
return turnsInSession(s.turnBindings, sid).some((b) => b.turnId !== turnId);
|
||||
}
|
||||
Reference in new issue
Block a user