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
@@ -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 } : {}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user