import { useEffect, useState } from "react"; import { useAsyncData } from "../../lib/useAsyncData"; import { adminCreateUser, adminGetOwnerProfile, adminListChannels, adminListUsers, adminIssueApiKey, adminRevokeApiKey, adminSaveOwnerProfile, fetchEnums, getStoredRole, } from "../../lib/api"; 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 { accountsForSave } from "./ownerAccounts"; import { Button, Surface, Tag } from "../../components/primitives"; const roleTone: Record = { owner: "accent", admin: "ok", viewer: "neutral", }; export function UsersModule(): React.ReactNode { const isOwner = getStoredRole() === "owner"; const users = useAsyncData(adminListUsers, []); const log = useAuditStore((s) => s.log); const [busy, setBusy] = useState(false); const [fail, setFail] = useState(null); const createUser = () => { setBusy(true); setFail(null); adminCreateUser({ username: "new-user", role: "viewer" }) .then(() => { log("user.create", "ok"); users.reload(); }) .catch((err: unknown) => { log("user.create", "error"); setFail(err instanceof Error ? err.message : "新增用户失败"); }) .finally(() => setBusy(false)); }; const revokeKey = (id: string) => { setBusy(true); setFail(null); adminRevokeApiKey(id) .then(() => { log("apikey.revoke", "ok"); users.reload(); }) .catch((err: unknown) => { log("apikey.revoke", "error"); setFail(err instanceof Error ? err.message : "撤销密钥失败"); }) .finally(() => setBusy(false)); }; return (
新增用户 } /> {fail ? {fail} : null}
); } interface UsersTableSectionProps { rows: AdminUserRecord[] | null; loading: boolean; error: string | null; isOwner: boolean; busy: boolean; onRevoke: (id: string) => void; } /** 账号/RBAC 列表 + API key 行的真实撤销。 */ function UsersTableSection({ rows, loading, error, isOwner, busy, onRevoke, }: UsersTableSectionProps): React.ReactNode { const columns: AdminColumn[] = [ { key: "username", title: "用户", render: (u) => {u.username} }, { key: "role", title: "角色", render: (u) => {u.role} }, { key: "created", title: "创建时间", render: (u) => {u.createdAt ?? "—"} }, { key: "id", title: "ID", render: (u) => {u.id} }, { key: "actions", title: "操作", render: (u) => u.username === "apikey" && isOwner ? ( ) : ( — ), }, ]; return (

用户列表 · 角色(RBAC)

u.id} />
); } /** 主人档案编辑块(裁决 10.4.5)。它自带读取/草稿/保存状态,因此面板的「保存」 * 是一次真实 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(null); const [notice, setNotice] = useState(null); const [busy, setBusy] = useState(false); const [fail, setFail] = useState(null); // 后端是唯一事实源:每次成功读取都用服务端返回的档案覆盖草稿。 useEffect(() => { if (data) setProfile(data); }, [data]); const save = () => { if (!profile) return; setBusy(true); setFail(null); setNotice(null); // 草稿里允许有还没填完的空行(编辑期不能丢行),写盘前才收成写盘形态:只留平台与账号 ID // 都非空的条目,并去掉两侧空白。既不把空行写进 owner.yaml,也不会因为半成品条目被后端 400。 adminSaveOwnerProfile({ ...profile, accounts: accountsForSave(profile.accounts) }) .then((saved) => { log("owner.profile.edit", "ok"); setProfile(saved); setNotice("已保存并热生效:configs/users/owner.yaml(0600 原子写),白名单立即参与 owner 判定。"); }) .catch((err: unknown) => { log("owner.profile.edit", "error"); setFail(err instanceof Error ? err.message : "保存主人档案失败"); }) .finally(() => setBusy(false)); }; return (

主人档案({isOwner ? "可编辑" : "只读 · 需 owner"})

对应 configs/users/owner.yaml:各渠道确认的主人账号白名单(策略引擎据此比对 actor 元数据,非提示词/LLM 判断)+ 关系备注。身份说明书(给 LLM 的人格知识,非权限依据)见 identityFile。

{error ? ( 主人档案读取失败:{error}(未拿到真实档案,表单已禁用,不拿本地假数据冒充) ) : null} {fail ? {fail} : null} {notice ? {notice} : null} {profile ? ( ) : (

{loading ? "读取中…" : "未加载到主人档案"}

)}
); } 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, platforms, channels, onChange, onSave, }: OwnerProfileFormProps): React.ReactNode { const patch = (p: Partial): void => onChange({ ...profile, ...p }); return ( <>
patch({ id: v })} disabled={disabled} /> patch({ displayName: v })} disabled={disabled} /> patch({ defaultPersona: v })} disabled={disabled} /> patch({ identityFile: v })} disabled={disabled} />
patch({ accounts })} />
关系备注