diff --git a/src/api.ts b/src/api.ts index 5175cc7..09407cb 100644 --- a/src/api.ts +++ b/src/api.ts @@ -20,7 +20,7 @@ export interface RoleRecord { roleId: string; name: string; description: string; enabled: boolean; roleVersion: number bindingCount: number; permissions: RolePermissionRecord[] } -export interface RoleListPage { roles: RoleRecord[]; nextCursor?: string } +interface RoleListPage { roles: RoleRecord[]; nextCursor?: string } export interface UserRoleBinding { bindingId: string; userId: string; roleId: string; active: boolean; bindingVersion: number assignedBy: string; assignedAt: string; userStateVersion: number; authorityEpoch: number @@ -31,7 +31,7 @@ export interface AccountRecord { userId: string; owner: string; profile: AccountProfile; profileVersion: number; enabled: boolean; stateVersion: number isAdmin: boolean; passwordSet: boolean; initializationState: string; initializationGeneration: number; createdAt?: string } -export interface AccountSearchPage { accounts: AccountRecord[]; nextCursor?: string; directoryVersion: string } +interface AccountSearchPage { accounts: AccountRecord[]; nextCursor?: string; directoryVersion: string } async function checked(res: Response): Promise { if (!res.ok) { diff --git a/src/components/adminRdmSections.tsx b/src/components/adminRdmSections.tsx index b8abad1..b52d57a 100644 --- a/src/components/adminRdmSections.tsx +++ b/src/components/adminRdmSections.tsx @@ -1,8 +1,5 @@ -// U6/KTD5:RDM 管理与项目页面组件。系统管理「RDM 角色权限」+ 项目设置「成员 / -// 文档职责 · 整文审签」。全部数据经 api.rdmCommand(命令总线),载荷构造与能力 -// 门控集中在 src/rdmManagement.ts;分区可见性由 AdminZone/ProjectSettingsModal -// 按 store 的 rdm*Available 门控(命令面不齐不渲染,普通基座零暴露)。样式只复用 -// 既有 ad-*/lg-*/settings-* 类(design-guard 棘轮:不新增裸色值/字号)。 +// RDM 项目设置组件:成员、文档职责与整文审签。全部数据经 api.rdmCommand +//(命令总线),载荷构造与能力门控集中在 src/rdmManagement.ts。 import { useEffect, useState, useRef, useCallback } from 'react' import { api } from '../api' import type { Project } from '../contracts' @@ -10,142 +7,11 @@ import { projectWorkError } from '../projectmeta' import { useStore } from '../store' import { Btn, Collapsible, EmptyState, Icon, Picker } from '@ui' import { - ROLE_ACTIONS, - roleCreatePayload, roleActionPayload, roleAssignPayload, roleDisablePayload, memberAddPayload, memberRemovePayload, memberSetRolePayload, assignDocDutiesPayload, docSubmitPayload, docApprovePayload, docRejectPayload, docFinalPayload, docStatusPayload, permCheckPayload, describeSource, - rolesListPlaceholder, describeRoleListError, } from '../rdmManagement' -// ---- 系统管理:RDM 角色权限(仅管理员 + rdmRolesAvailable) ---- - -interface RoleView { - roleId: string - name: string - disabled: boolean - version: number - actions: Record | null - accounts: string[] | null -} - -export function RdmRolesPanel() { - const me = useStore(s => s.me) - const [roles, setRoles] = useState(null) - const [err, setErr] = useState('') - const [notice, setNotice] = useState('') - const [busy, setBusy] = useState(false) - const [newName, setNewName] = useState('') - // 动作编辑器 - const [editRole, setEditRole] = useState('') - const [editAction, setEditAction] = useState(ROLE_ACTIONS[0]) - const [editDef, setEditDef] = useState(false) - const [editCap, setEditCap] = useState(true) - // 绑定 - const [assignOwner, setAssignOwner] = useState('') - - const refresh = async () => { - try { - const out = await api.rdmCommand<{ roles: RoleView[] }>('rdm.role.list') - setErr(''); setRoles(out.roles ?? []) - } catch (e) { setRoles(null); setErr(describeRoleListError(String(e))) } - } - useEffect(() => { refresh() }, []) - - const run = async (label: string, fn: () => Promise) => { - setErr(''); setNotice(''); setBusy(true) - try { await fn(); setNotice(label); await refresh() } - catch (e) { setErr(describeRoleListError(String(e))) } finally { setBusy(false) } - } - - if (!me?.isAdmin) return 仅平台管理员可管理专业角色 - - // 占位行单一出口:加载中仅在无错误时成立(拒绝后永远转圈是误导——见 - // rolesListPlaceholder 注释)。 - const placeholder = rolesListPlaceholder(roles, err) - - const sel = roles?.find(r => r.roleId === editRole) - const editCurrent = sel?.actions?.[editAction] - - return ( -
-
-
- setNewName(e.target.value)} /> - -
-

新角色动作未配置=全部拒绝(fail-closed),创建后经下方编辑器逐动作配置默认与上限。

-
- - - - - {(roles ?? []).map(r => ( - - - - - - - - - ))} - {roles !== null && roles.length === 0 && } - {placeholder && } - -
角色状态版本动作配置绑定账号
{r.name}
{r.roleId}
{r.disabled ? 已停用 : 生效}v{r.version}{Object.keys(r.actions ?? {}).length} 项{(r.accounts ?? []).join('、') || '—'}
暂无专业角色
{placeholder}
- -

动作配置(默认 ∩ 上限)

-
- - - - - -
- {sel && editCurrent &&

当前值:默认={String(editCurrent[0])},上限={String(editCurrent[1])}

} - -

绑定账号(每账号一个生效角色)

-
- setAssignOwner(e.target.value)} /> - - -
- - {notice &&
{notice}
} - {err &&
{err}
} -
- ) -} - // ---- 项目设置:成员(P7:可搜索人员选择器 + 专业角色/项目身份两列 + 显式身份调整)---- interface MemberRow { diff --git a/src/components/adminSections.tsx b/src/components/adminSections.tsx index 4e56ab8..a2be9a2 100644 --- a/src/components/adminSections.tsx +++ b/src/components/adminSections.tsx @@ -9,159 +9,7 @@ import { Btn, EmptyState, Icon, SettingRow, SettingsGroup, UIRenderer, type DocB import { SettingInput, flatten, unflatten } from './SettingsModal' import type { SettingDesc } from '../contracts' import { docBridge } from './Workbench' -import { randomPassword } from '../passwords' -// ---- 用户管理(原 AdminUsersModal 主体,R2 管理员发放制 + 2026-10-13 恢复 Key)---- -interface AdminUserRow { owner: string; name: string; isAdmin: boolean; mustChangePassword: boolean; hasPassword: boolean; passwordSetAt?: string; createdAt: string } -interface LoginKeyRow { id: string; owner: string; purpose: string; status: string; expiresAt: string; createdAt: string; createdBy?: string } - -/** Key 状态徽标:复用既有 chip 体系,不新增颜色。 */ -function keyStatusChip(status: string) { - if (status === 'unused') return 待使用 - if (status === 'consumed') return 已使用 - if (status === 'expired') return 已过期 - return 已撤销 -} - -export function UsersPanel() { - const me = useStore(s => s.me) - const [users, setUsers] = useState(null) - const [err, setErr] = useState('') - const [notice, setNotice] = useState('') - const [owner, setOwner] = useState('') - const [name, setName] = useState('') - const [pw, setPw] = useState('') - const [asAdmin, setAsAdmin] = useState(false) - const [busy, setBusy] = useState(false) - // 登录 Key 面板:keyFor 打开指定用户的签发/状态视图;issued 明文仅签发响应持有一次。 - const [keyFor, setKeyFor] = useState(null) - const [issued, setIssued] = useState<{ key: string; expiresAt: string } | null>(null) - const [keys, setKeys] = useState(null) - - const refresh = async () => { - try { setUsers(await api.listAdminUsers()) } catch (e) { setErr(String(e)) } - } - const refreshKeys = async (owner: string) => { - try { setKeys(await api.adminListLoginKeys(owner)) } catch (e) { setErr(String(e)) } - } - useEffect(() => { refresh() }, []) - - const openKeyPanel = async (owner: string) => { - setErr(''); setNotice(''); setIssued(null); setKeyFor(owner); setKeys(null) - await refreshKeys(owner) - } - - const create = async () => { - setErr(''); setNotice('') - if (pw.length < 8) { setErr('初始密码至少 8 位'); return } - setBusy(true) - try { - await api.createAdminUser(owner.trim(), name.trim(), pw, asAdmin) - setNotice(`已创建 ${owner.trim()}:首次登录须改密`) - setOwner(''); setName(''); setPw('') - await refresh() - } catch (e) { setErr(String(e).includes('409') ? '用户名已存在' : String(e)) } finally { setBusy(false) } - } - - const issue = async () => { - if (!keyFor) return - setErr(''); setNotice(''); setBusy(true) - try { - const out = await api.adminIssueLoginKey(keyFor) - setIssued({ key: out.key, expiresAt: out.expiresAt }) - await refreshKeys(keyFor) - } catch (e) { setErr(String(e)) } finally { setBusy(false) } - } - - const copyKey = async () => { - if (!issued) return - try { await navigator.clipboard.writeText(issued.key); setNotice('已复制到剪贴板,请安全交付给该用户') } catch { setErr('复制失败,请手动选择复制') } - } - - const revoke = async (id: string) => { - if (!keyFor) return - setErr(''); setBusy(true) - try { - await api.adminRevokeLoginKey(keyFor, id) - await refreshKeys(keyFor) - } catch (e) { setErr(String(e)) } finally { setBusy(false) } - } - - return ( -
- {me?.isAdmin && ( -
-
- setOwner(e.target.value)} /> - setName(e.target.value)} /> -
-
- setPw(e.target.value)} /> - - - -
-

创建后该用户首次登录必须修改此初始密码;忘记密码时为其生成一次性登录 Key。

-
- )} - {notice &&
{notice}
} - {err &&
{err}
} - {keyFor ? ( -
-
- {keyFor} 的一次性登录 Key(忘记密码恢复) - - -
- {issued && ( -
- e.currentTarget.select()} /> - -
- )} - {issued &&

明文仅此一次显示,关闭后不可再取;单次有效,{new Date(issued.expiresAt).toLocaleString()} 过期。请经安全渠道交付本人。

} - {keys !== null && keys.length > 0 && ( - - - - {keys.map(k => ( - - - - - - - - ))} - -
签发时间用途状态过期时间
{new Date(k.createdAt).toLocaleString()}{k.purpose === 'recover' ? '恢复' : '激活'}{keyStatusChip(k.status)}{new Date(k.expiresAt).toLocaleString()}{k.status === 'unused' && }
- )} -
- ) : ( - - - - {(users ?? []).map(u => ( - - - - - - - - - ))} - {users !== null && users.length === 0 && } - -
用户名显示名角色状态密码设置于
{u.owner}{u.name || '—'}{u.isAdmin ? ADMIN : USER}{u.mustChangePassword ? 待改密 : u.hasPassword ? '正常' : '未设密码'}{u.passwordSetAt ? new Date(u.passwordSetAt).toLocaleString() : '—'}{me?.isAdmin && ( - - )}
暂无账号
- )} -
- ) -} - -// ---- 智能体画像(原 SettingsModal ProfilesSection:数据化导入导出 + 编辑)---- export function ProfilesSection({ profiles }: { profiles: AgentProfile[] }) { const [editing, setEditing] = useState(null) const [text, setText] = useState('') diff --git a/src/rolePermissions.ts b/src/rolePermissions.ts index c764a17..53521c2 100644 --- a/src/rolePermissions.ts +++ b/src/rolePermissions.ts @@ -1,5 +1,5 @@ export type RolePermissionValue = { allowed: boolean; capAllowed: boolean } -export type RolePermissionChange = { actionId: string; allowed: boolean; capAllowed: boolean } +type RolePermissionChange = { actionId: string; allowed: boolean; capAllowed: boolean } export interface RolePermissionDraft { baseline: Record