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

+37 -51
View File
@@ -3,15 +3,23 @@ import { useAsyncData } from "../../lib/useAsyncData";
import {
adminCreateUser,
adminGetOwnerProfile,
adminListChannels,
adminListUsers,
adminIssueApiKey,
adminRevokeApiKey,
adminSaveOwnerProfile,
fetchEnums,
getStoredRole,
} from "../../lib/api";
import type { AdminUserRecord, OwnerAccount, OwnerProfile } from "../../lib/types";
import type {
AdminChannelRecord,
AdminUserRecord,
EnumItem,
OwnerProfile,
} from "../../lib/types";
import { useAuditStore } from "../audit";
import { AdminTable, Notice, PageHeader, type AdminColumn } from "../components";
import { OwnerAccountsEditor } from "./OwnerAccountsEditor";
import { Button, Surface, Tag } from "../../components/primitives";
const roleTone: Record<string, "accent" | "ok" | "neutral" | "warn"> = {
@@ -138,6 +146,10 @@ function UsersTableSection({
* 是一次真实 PUT:成功回读服务端档案,失败如实报错 —— 绝不假装保存成功。 */
function OwnerProfileSection({ isOwner }: { isOwner: boolean }): React.ReactNode {
const { data, loading, error } = useAsyncData(adminGetOwnerProfile, []);
// 平台下拉取权威枚举(未接线/失败时回落本仓镜像的标签表,取值仍原样呈现);
// 账号 id 候选取该平台已配置的渠道账号,避免手打错一位就静默失效。
const enums = useAsyncData(fetchEnums, []);
const channels = useAsyncData(adminListChannels, []);
const log = useAuditStore((s) => s.log);
const [profile, setProfile] = useState<OwnerProfile | null>(null);
const [notice, setNotice] = useState<string | null>(null);
@@ -181,7 +193,15 @@ function OwnerProfileSection({ isOwner }: { isOwner: boolean }): React.ReactNode
{fail ? <Notice tone="err">{fail}</Notice> : null}
{notice ? <Notice tone="ok">{notice}</Notice> : null}
{profile ? (
<OwnerProfileForm profile={profile} disabled={!isOwner} busy={busy} onChange={setProfile} onSave={save} />
<OwnerProfileForm
profile={profile}
disabled={!isOwner}
busy={busy}
platforms={enums.data?.platforms}
channels={channels.data}
onChange={setProfile}
onSave={save}
/>
) : (
<p className="text-xs text-[var(--mu-text-lo)]">{loading ? "读取中…" : "未加载到主人档案"}</p>
)}
@@ -193,17 +213,23 @@ interface OwnerProfileFormProps {
profile: OwnerProfile;
disabled: boolean;
busy: boolean;
platforms: EnumItem[] | undefined;
channels: AdminChannelRecord[] | null;
onChange: (next: OwnerProfile) => void;
onSave: () => void;
}
/** 档案表单本体(纯受控:所有改动都经 onChange 回到草稿)。 */
function OwnerProfileForm({ profile, disabled, busy, onChange, onSave }: OwnerProfileFormProps): React.ReactNode {
function OwnerProfileForm({
profile,
disabled,
busy,
platforms,
channels,
onChange,
onSave,
}: OwnerProfileFormProps): React.ReactNode {
const patch = (p: Partial<OwnerProfile>): void => onChange({ ...profile, ...p });
const patchAccount = (i: number, p: Partial<OwnerAccount>): void =>
patch({ accounts: profile.accounts.map((a, idx) => (idx === i ? { ...a, ...p } : a)) });
const removeAccount = (i: number): void => patch({ accounts: profile.accounts.filter((_, idx) => idx !== i) });
const appendAccount = (): void => patch({ accounts: [...profile.accounts, { platform: "", accountId: "" }] });
return (
<>
@@ -213,12 +239,12 @@ function OwnerProfileForm({ profile, disabled, busy, onChange, onSave }: OwnerPr
<Field label="默认人格" value={profile.defaultPersona} onChange={(v) => patch({ defaultPersona: v })} disabled={disabled} />
<Field label="身份说明书" value={profile.identityFile} onChange={(v) => patch({ identityFile: v })} disabled={disabled} />
</div>
<AccountWhitelistEditor
<OwnerAccountsEditor
accounts={profile.accounts}
disabled={disabled}
onPatch={patchAccount}
onRemove={removeAccount}
onAppend={appendAccount}
platforms={platforms}
channels={channels}
onChange={(accounts) => patch({ accounts })}
/>
<div className="space-y-1">
<span className="text-xs text-[var(--mu-text-lo)]">关系备注</span>
@@ -242,46 +268,6 @@ function OwnerProfileForm({ profile, disabled, busy, onChange, onSave }: OwnerPr
);
}
interface AccountWhitelistEditorProps {
accounts: OwnerAccount[];
disabled: boolean;
onPatch: (index: number, patch: Partial<OwnerAccount>) => void;
onRemove: (index: number) => void;
onAppend: () => void;
}
/** 渠道账号白名单行编辑器:命中此列表的账号走 owner 车道(元数据判定,非提示词)。 */
function AccountWhitelistEditor({
accounts,
disabled,
onPatch,
onRemove,
onAppend,
}: AccountWhitelistEditorProps): React.ReactNode {
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-[var(--mu-text-lo)]">渠道账号白名单(命中即走 owner 车道)</span>
<Button size="sm" variant="ghost" disabled={disabled} onClick={onAppend}>
添加账号
</Button>
</div>
{accounts.length === 0 ? (
<p className="text-xs text-[var(--mu-text-lo)]">尚未配置:现状为「无人是主人」(默认拒绝,fail-safe)。</p>
) : null}
{accounts.map((a, i) => (
<div key={`acct-${i}`} className="flex flex-wrap items-end gap-2">
<Field label="平台" value={a.platform} onChange={(v) => onPatch(i, { platform: v })} disabled={disabled} />
<Field label="账号 id" value={a.accountId} onChange={(v) => onPatch(i, { accountId: v })} disabled={disabled} />
<Button size="sm" variant="ghost" disabled={disabled} onClick={() => onRemove(i)}>
删除
</Button>
</div>
))}
</div>
);
}
/** API key 签发块(裁决 15.4.3):不选角色 —— 所有 key 一律超管。 */
function ApiKeySection({ isOwner, onIssued }: { isOwner: boolean; onIssued: () => void }): React.ReactNode {
const log = useAuditStore((s) => s.log);