feat(web): 前端与后端同步一批(帧壳 turnId / 并行六态与两级分桶 / documents 渲染 / System 键名 / 主人档案分组)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 帧壳 `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:
AskaEth committed 2026-10-05 11:09:24 +08:00
1 parent 6a545badbd
commit eaada3af5d
34 files changed
+2457 -223

No files matched your search

+29 -4
View File
@@ -15,19 +15,42 @@ export function chatSync(
});
}
/** `POST /api/v1/chat`(`stream:true`)的响应面(契约 §4.6)。202/200 都属成功,
* 载荷必须**如实读出**:`queued` 表示被排队(不是流式),`parallel`/`turnId` 给出
* 这一路的权威回合标识(`turn_begin` 帧也会带同一个值)。 */
export interface ChatTriggerResult {
stream?: true;
/** 显式声明 `parallel:true` 且服务端照准 ⇒ 独立一路并行回合。 */
parallel?: boolean;
/** 会话忙且落回排队(HTTP 202):`queueDepth` = 仍在 pending 的条数。 */
queued?: boolean;
queueDepth?: number;
/** 本回合的权威标识(`turn_begin` 帧的帧壳 `turnId` 同值)。 */
turnId?: string;
/** `clientMsgId` 判重命中(HTTP 200):本条没有新回合,`messageId` 是既有消息的
* 权威 id(与 WS 的 `chat_duplicate` 帧同一条事实)。 */
duplicate?: boolean;
messageId?: string;
}
/** POST /api/v1/chat with stream:true — triggers streaming over the WS channel.
* `image` (a base64 data: URI or URL) is attached for the vision path; the
* gateway's chat body currently has no image field, so an unconfigured backend
* simply ignores it. `deviceId` mirrors the client's per-browser id onto the
* persisted message (design/16 §4.5). `mode` is the behavior mode this turn is
* sent in (design/01 §3); omitted when empty so the backend falls back to chat. */
* sent in (design/01 §3); omitted when empty so the backend falls back to chat.
* `clientMsgId` 是本条的幂等键(契约 §4.3):服务端判重命中时回
* `chat_duplicate{clientMsgId,messageId}`,本端据此把乐观气泡对齐到既有 id。
* `parallel` 显式请求独立一路(忙时并行规范草案 §2.2);省略 = 行为与今天逐字相同。 */
export function chatStream(
sessionId: string,
text: string,
image?: string,
mode?: Mode
): Promise<{ stream: true }> {
return request<{ stream: true }>("/chat", {
mode?: Mode,
clientMsgId?: string,
parallel?: boolean
): Promise<ChatTriggerResult> {
return request<ChatTriggerResult>("/chat", {
method: "POST",
body: JSON.stringify({
sessionId,
@@ -36,6 +59,8 @@ export function chatStream(
stream: true,
deviceId: getClientId(),
...(mode ? { mode } : {}),
...(clientMsgId ? { clientMsgId } : {}),
...(parallel ? { parallel: true } : {}),
}),
});
}
+19
View File
@@ -0,0 +1,19 @@
import type { EnumItem } from "../types";
import { request } from "./core";
/** GET /api/v1/meta/enums 的载荷(design/07 §6,Android 协作 B4):后端发布的权威
* 枚举表 + 未列出取值的回退策略(`unknown: "display-raw"`)。
*
* 客户端优先读这里而不是硬编码映射:表由后端一处维护,来源徽标/情绪标签才不会逐
* 客户端漂移。取不到(未接线 / 离线)时调用方回落到本仓镜像的 `lib/enums.ts`
* 标签表 —— 回落也只影响标签文字,不影响取值的原样呈现。 */
export interface MetaEnums {
platforms?: EnumItem[];
moods?: EnumItem[];
modes?: EnumItem[];
unknown?: string;
}
export function fetchEnums(): Promise<MetaEnums> {
return request<MetaEnums>("/meta/enums");
}
+16 -1
View File
@@ -1,4 +1,4 @@
import type { ChatMessage, Mode, SessionMeta } from "../types";
import type { ChatMessage, MessageDocument, Mode, SessionMeta } from "../types";
import { request } from "./core";
/** One chat message as returned by GET /api/v1/sessions/{id}/messages. Mirrors
@@ -11,6 +11,11 @@ export interface MessageDto {
mode?: Mode;
mood?: string;
image?: string;
/** 文档附件(契约 §4.4):`{name,mime,size,chars,truncated,status,reason?}`,
* ≤5 条/回合,0 个附件时键缺席。**不含正文**;`status:"unreadable"` 时带逐字
* 中文 `reason`。`chars`/`truncated` 是**注入侧**语义(注入上下文的字符数与是否
* 被预算截断),不是文件字数/截断。 */
documents?: MessageDocument[];
thinking?: string;
/** Source-platform tag (design/16 §4.4): "web" or an IM channel platform. */
platform?: string;
@@ -18,6 +23,13 @@ export interface MessageDto {
deviceId?: string;
/** Sender display name of a third-party message (design/16 §4.6). Blank for the owner. */
senderName?: string;
/** IM 来源适配器(`platform:account`);Web 来源为空。 */
adapterId?: string;
/** 本回合的权威回合标识(`chat_messages.turn_id`):用户行 = 吃进/起它的那一路,
* 助手行 = 产出它的那一路。旧行为空缺席。 */
turnId?: string;
/** 幂等键(契约 §4.3);空则省略。 */
clientMsgId?: string;
/** The role that spoke this message (design/22 §7-5, backing column
* `chat_messages.persona_id`); `''` means "inherit the current default role". */
personaId?: string;
@@ -34,10 +46,13 @@ function toChatMessage(m: MessageDto): ChatMessage {
mode: m.mode,
mood: m.mood,
image: m.image,
documents: m.documents,
thinking: m.thinking,
platform: m.platform,
deviceId: m.deviceId,
senderName: m.senderName,
turnId: m.turnId,
clientMsgId: m.clientMsgId,
personaId: m.personaId,
createdAt: m.createdAt,
};