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
@@ -0,0 +1,97 @@
|
||||
import { labelFor } from "../../lib/enums";
|
||||
import type { AdminChannelRecord, EnumItem, OwnerAccount } from "../../lib/types";
|
||||
|
||||
/**
|
||||
* 主人档案「渠道账号白名单」的呈现层(主人 2026-10-05 要求):界面**按平台分组**
|
||||
* (一个平台一条 + 该平台下「+ 添加账号 ID」),**写盘时展平成扁平数组**
|
||||
* `accounts:[{platform,accountId}…]` —— 后端契约与 `configs/users/owner.yaml` 的
|
||||
* 文件格式**一个字都不改**,这里纯粹是呈现层。
|
||||
*
|
||||
* 为什么值得改:此前一行 = `{platform, accountId}`,同一平台多个账号要**重复填平台名**,
|
||||
* 且平台是纯文本输入框 —— 填错(`qq_obv11` / `QQ-OBV11`)会**静默失效**
|
||||
* (`owner.IsOwner` 匹配不上 actor 元数据 ⇒ 主人被当陌生人,界面上没有任何提示)。
|
||||
* 平台改为权威枚举下拉 + 账号 id 可从该平台已配置账号里选,消灭这类静默失效。
|
||||
*
|
||||
* 本模块是纯函数(无 DOM、无请求),因此分组/展平/下拉选项/候选都能在 node 下逐条测。
|
||||
*/
|
||||
|
||||
/** 一个平台分组:界面上的一条。 */
|
||||
export interface OwnerAccountGroup {
|
||||
platform: string;
|
||||
accountIds: string[];
|
||||
}
|
||||
|
||||
/** 把扁平数组按平台分组(保持首次出现顺序,同一平台的多条账号收进一组)。 */
|
||||
export function groupAccounts(accounts: OwnerAccount[]): OwnerAccountGroup[] {
|
||||
const groups: OwnerAccountGroup[] = [];
|
||||
const byPlatform = new Map<string, OwnerAccountGroup>();
|
||||
for (const a of accounts) {
|
||||
const platform = a?.platform ?? "";
|
||||
let g = byPlatform.get(platform);
|
||||
if (!g) {
|
||||
g = { platform, accountIds: [] };
|
||||
byPlatform.set(platform, g);
|
||||
groups.push(g);
|
||||
}
|
||||
g.accountIds.push(a?.accountId ?? "");
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/** 展平成后端契约要的扁平数组。**平台与账号 id 都非空才写**:任一为空都匹配不上任何
|
||||
* actor 元数据(写了也是噪声,还会让主人误以为已配置)。空的账号行/空平台组被忽略,
|
||||
* 界面上有对应提示,不是静默丢弃。 */
|
||||
export function flattenAccounts(groups: OwnerAccountGroup[]): OwnerAccount[] {
|
||||
const out: OwnerAccount[] = [];
|
||||
for (const g of groups) {
|
||||
const platform = g.platform.trim();
|
||||
for (const raw of g.accountIds) {
|
||||
const accountId = raw.trim();
|
||||
if (!platform || !accountId) continue;
|
||||
out.push({ platform, accountId });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 下拉选项:后端权威枚举(`GET /api/v1/meta/enums` 的 `platforms`)在前,随后补上
|
||||
* 存量里出现过的平台 —— 未列出的旧值**原样保留**(display-raw:绝不下拉吃掉它,
|
||||
* 否则一次保存就把主人已经生效的配置删了)。 */
|
||||
export function platformOptions(
|
||||
published: readonly EnumItem[] | undefined,
|
||||
used: readonly string[]
|
||||
): EnumItem[] {
|
||||
const out: EnumItem[] = [];
|
||||
const seen = new Set<string>();
|
||||
const push = (id: string, label?: string) => {
|
||||
if (!id || seen.has(id)) return;
|
||||
seen.add(id);
|
||||
out.push({ id, label: label || labelFor("platform", id) });
|
||||
};
|
||||
for (const p of published ?? []) push(p.id, p.label);
|
||||
for (const id of used) push(id);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 该平台**已配置账号**的候选(数据来自 `GET /api/v1/admin/channels` 的 accounts):
|
||||
* 让主人从真凭据里选,不再手打 id(手打错一个字符同样是静默失效)。 */
|
||||
export function accountCandidates(
|
||||
channels: AdminChannelRecord[] | null | undefined,
|
||||
platform: string
|
||||
): string[] {
|
||||
if (!platform) return [];
|
||||
const record = (channels ?? []).find((c) => c.name === platform);
|
||||
const ids = (record?.accounts ?? [])
|
||||
.map((a) => a?.id ?? "")
|
||||
.filter((id) => id !== "");
|
||||
return [...new Set(ids)];
|
||||
}
|
||||
|
||||
/** 该平台是否在「已配置账号」里出现过(用于给「候选为空」一句诚实的解释)。 */
|
||||
export function platformIsConfigured(
|
||||
channels: AdminChannelRecord[] | null | undefined,
|
||||
platform: string
|
||||
): boolean {
|
||||
if (!platform) return false;
|
||||
return (channels ?? []).some((c) => c.name === platform);
|
||||
}
|
||||
Reference in new issue
Block a user