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,4 +1,4 @@
|
||||
import { MODES, type AdminChannelMessage, type AgentInfo, type BackthinkItem, type ChatMessage, type FlowGraph, type Mode, type ToolUsed } from "./types";
|
||||
import { MODES, type AdminChannelMessage, type AgentInfo, type BackthinkItem, type ChatMessage, type FlowGraph, type MessageDocument, type Mode, type ToolUsed } from "./types";
|
||||
import { now, uid } from "./util";
|
||||
|
||||
/**
|
||||
@@ -83,6 +83,24 @@ export function parseToolCall(v: unknown): ToolUsed | null {
|
||||
};
|
||||
}
|
||||
|
||||
/** Build a tool card from a `tool_execution.*` payload (契约 §3.4.2):
|
||||
* `{tool, phase, id?, delta?}`,`phase` ∈ `running/done/error`(**phase 绝不塞进
|
||||
* delta**)。注意这里的键名与 `tool_call.*` 不同:工具名在 `tool` 上(不是 `name`),
|
||||
* 成败由 `phase` 决定 —— 用 `parseToolCall` 读它只会得到 null,卡片就永远不会出现。 */
|
||||
export function parseToolExecution(v: unknown): ToolUsed | null {
|
||||
const r = toRecord(v);
|
||||
if (!r) return null;
|
||||
const name = typeof r.tool === "string" ? r.tool : "";
|
||||
if (!name) return null;
|
||||
const phase = typeof r.phase === "string" ? r.phase : "";
|
||||
return {
|
||||
name,
|
||||
summary: typeof r.detail === "string" ? r.detail : undefined,
|
||||
ok: phase === "done" ? true : phase === "error" ? false : undefined,
|
||||
at: now(),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseAgent(payload: unknown): AgentInfo | null {
|
||||
const r = toRecord(payload);
|
||||
if (!r) return null;
|
||||
@@ -159,6 +177,28 @@ export function parseGraph(payload: unknown): FlowGraph | null {
|
||||
};
|
||||
}
|
||||
|
||||
/** 解析消息 DTO 的 `documents` 数组(契约 §4.4)。**不含正文**,只在有附件时出现
|
||||
* (0 个附件时键缺席)。`status` 允许后端新增取值(display-raw:未知值原样带下去,
|
||||
* 渲染层按未知处理,绝不丢掉附件、也绝不把未知当错误)。`reason` 只在
|
||||
* `status:"unreadable"` 时下发,是服务端产出的逐字中文原因,客户端原样展示。 */
|
||||
export function parseDocuments(v: unknown): MessageDocument[] | undefined {
|
||||
if (!Array.isArray(v)) return undefined;
|
||||
const docs = v
|
||||
.map((d) => toRecord(d))
|
||||
.filter((d): d is Record<string, unknown> => d !== null)
|
||||
.map((d) => ({
|
||||
name: typeof d.name === "string" ? d.name : "",
|
||||
mime: typeof d.mime === "string" && d.mime ? d.mime : undefined,
|
||||
size: typeof d.size === "number" ? d.size : undefined,
|
||||
chars: typeof d.chars === "number" ? d.chars : undefined,
|
||||
truncated: typeof d.truncated === "boolean" ? d.truncated : undefined,
|
||||
status: typeof d.status === "string" && d.status ? d.status : undefined,
|
||||
reason: typeof d.reason === "string" && d.reason ? d.reason : undefined,
|
||||
}))
|
||||
.filter((d) => d.name !== "");
|
||||
return docs.length ? docs : undefined;
|
||||
}
|
||||
|
||||
/** Build a ChatMessage from a `message_persisted` payload (design/16 §3.3/§4.2):
|
||||
* the backend broadcast a fully-persisted message carrying the backend-generated
|
||||
* id. mode is validated against the known modes so a bad value never reaches the
|
||||
@@ -177,6 +217,11 @@ export function parsePersistedMessage(v: unknown): ChatMessage | null {
|
||||
mode: typeof r.mode === "string" && MODES.includes(r.mode as Mode) ? (r.mode as Mode) : undefined,
|
||||
mood: typeof r.mood === "string" ? r.mood : undefined,
|
||||
image: typeof r.image === "string" ? r.image : undefined,
|
||||
documents: parseDocuments(r.documents),
|
||||
// `chat_messages.turn_id`(契约 §4.4):忙时并行下靠它把回复与提问配对、把两条
|
||||
// 交错到达的回复分进两个气泡;旧行该键省略 ⇒ 回落按顺序渲染。
|
||||
turnId: typeof r.turnId === "string" && r.turnId ? r.turnId : undefined,
|
||||
clientMsgId: typeof r.clientMsgId === "string" && r.clientMsgId ? r.clientMsgId : undefined,
|
||||
platform: typeof r.platform === "string" && r.platform ? r.platform : undefined,
|
||||
deviceId: typeof r.deviceId === "string" && r.deviceId ? r.deviceId : undefined,
|
||||
senderName: typeof r.senderName === "string" && r.senderName ? r.senderName : undefined,
|
||||
|
||||
Reference in new issue
Block a user