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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user