From 25e76c5e01f41c2409707ba49a41f9b8d723002c Mon Sep 17 00:00:00 2001 From: weichen Date: Mon, 12 Oct 2026 02:34:04 +0000 Subject: [PATCH 1/2] Host plugin-declared sections in the admin zone Why: KTD4 requires the RDM audit partition to be reachable from the admin zone by RDM domain admins and base superadmins, without hardcoding RDM into the base AdminZone or merging roles in the frontend. Evidence: npx vitest run 101/101 green (incl. design-guard), tsc --noEmit clean, knip 42 <= 43; Dev composite CI is authoritative. Impact: AdminZone merges admin-only builtin sections with sections declared by compiled plugins (GET /api/admin-zone/sections, loaded in refreshPlugins); the entry menu shows when isAdmin OR declared sections exist; declared sections render via an id-keyed builtin component map (rdm.audit -> RdmAuditWorkspace) with a version-mismatch placeholder for unknown ids; unknown icons fall back to the generic grid icon. Token and color usage unchanged. Refs: WP3-I3/KTD4; task card feature-adminzone-plugin-sections. --- src/api.ts | 3 +- src/components/AdminZone.tsx | 58 ++++++++++++++++++++++++++++------ src/components/TaskSidebar.tsx | 3 +- src/contracts.ts | 7 ++++ src/store.ts | 9 +++++- 5 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/api.ts b/src/api.ts index c51c9fb..be0a0c9 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,7 +1,7 @@ // API 客户端(API-first:UI 只是第一个客户端)与 SSE 流客户端(游标重放 + 终态对账)。 // 认证:服务端开启 API key 时经 auth.fetch/withKey 注入凭证(嵌入式档无认证则透传)。 import { auth } from './auth' -import type { +import type { AdminZoneSection, Project, Conversation, Message, Run, Artifact, HITLCard, HITLResponse, AgentProfile, SettingDesc, StreamEvent, StreamEventType, PlanSnapshot, TodoList, UploadInfo, PromptModule, ActivityCard, @@ -113,6 +113,7 @@ export const api = { listTools: () => auth.fetch(`${BASE}/api/tools`).then(r => r.json()), listPlugins: () => auth.fetch(`${BASE}/api/plugins`).then(r => r.json()), + adminZoneSections: () => auth.fetch(`${BASE}/api/admin-zone/sections`).then(r => r.json()) as Promise, reloadPlugins: () => auth.fetch(`${BASE}/api/plugins/reload`, { method: 'POST' }).then(r => r.json()), listCommands: () => auth.fetch(`${BASE}/api/commands`).then(r => r.json()) as Promise<{ name?: string; id?: string; title?: string; description?: string; danger?: string }[]>, setSessionNote: (id: string, sessionNote: string) => auth.fetch(`${BASE}/api/conversations/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionNote }) }).then(r => { if (!r.ok) throw new Error(`${r.status}`) }), diff --git a/src/components/AdminZone.tsx b/src/components/AdminZone.tsx index bc83811..5061901 100644 --- a/src/components/AdminZone.tsx +++ b/src/components/AdminZone.tsx @@ -1,10 +1,15 @@ -// 系统管理(重设计方案 阶段②):管理员专属的全屏独立视图(带返回,非弹窗)。 +// 系统管理(重设计方案 阶段②):全屏独立视图(带返回,非弹窗)。 // 用户决策:系统管理 = 全屏独立视图;普通用户界面不暴露任何系统能力。 // 分区注册表:新增管理页只改 SECTIONS,不改 AdminZone 本体(声明式注册)。 +// WP3-I3/KTD4:编译插件可经 Registry.AddAdminZone 声明展示分区(服务端 +// /api/admin-zone/sections 下发);声明分区对全部认证主体开放,区内数据 +// 能力由分区组件自身的命令面 fail-closed——前端只消费、不合并角色。 import { useState } from 'react' import { store, useStore } from '../store' -import { Icon, type IconName } from '@ui' +import { EmptyState, Icon, type IconName } from '@ui' import { UsersPanel, ProfilesSection, PluginsSection, PluginWorkspacesPanel } from './adminSections' +import { RdmAuditWorkspace } from './RdmAuditWorkspace' +import type { AdminZoneSection } from '../contracts' interface AdminSectionDef { key: string @@ -27,12 +32,47 @@ const SECTIONS: AdminSectionDef[] = [ { key: 'workspaces', label: '插件工作区', icon: 'grid', desc: '插件声明面板与服务端声明工作区', render: () => }, ] +/** 声明分区 → 内置分区组件(按声明 ID 映射;AdminZone 不硬编码领域逻辑)。 */ +const DECLARED_COMPONENTS: Record React.ReactNode> = { + 'rdm.audit': () => , +} + +/** icon 走基座 IconName 词表;声明携带未知图标时回退通用图标(不渲染空白)。 */ +const ICONS: IconName[] = ['plus', 'search', 'panel-left', 'inbox', 'settings', 'sun', 'moon', 'pin', 'archive', 'undo', + 'chevron-right', 'chevron-down', 'folder', 'message', 'package', 'bot', 'puzzle', 'grid', 'arrow-up', 'paperclip', + 'more', 'logout', 'key', 'user', 'users', 'shield', 'x', 'check', 'alert', 'send', 'sparkles', 'clock', 'list', + 'file', 'image', 'download', 'trash', 'eye', 'maximize', 'minimize'] +function declaredIcon(name?: string): IconName { + return (ICONS as string[]).includes(name ?? '') ? (name as IconName) : 'grid' +} + +function DeclaredSection({ section }: { section: AdminZoneSection }) { + const render = DECLARED_COMPONENTS[section.id] + if (!render) { + return 分区「{section.title}」由内置插件声明,但当前前端版本未携带其组件
(部署前端与二进制版本不匹配时会出现;升级前端即可)
+ } + return <>{render()} +} + export function AdminZone() { const me = useStore(s => s.me) + const declared = useStore(s => s.adminSections) const [sectionKey, setSectionKey] = useState('users') - // 双保险:入口已在菜单层门控(me?.isAdmin),此处再拦一次(纵深防御)。 - if (!me?.isAdmin) return null - const section = SECTIONS.find(s => s.key === sectionKey) ?? SECTIONS[0] + const isAdmin = !!me?.isAdmin + // 门控:基座系统分区仅 isAdmin(纵深防御与菜单层一致);声明分区对全部 + // 认证主体开放(区内能力由组件命令面判定)。两者皆无时不渲染。 + if (!isAdmin && declared.length === 0) return null + const sections: AdminSectionDef[] = [ + ...(isAdmin ? SECTIONS : []), + ...declared.map(s => ({ + key: `decl:${s.id}`, label: s.title, icon: declaredIcon(s.icon), + desc: s.desc || '插件声明的管理分区(区内数据能力由插件命令面判定)', + render: () => , + })), + ] + const fallback = sections[0]?.key ?? '' + const section = sections.find(s => s.key === sectionKey) ?? sections[0] + if (!section) return null return (
@@ -40,14 +80,14 @@ export function AdminZone() { 返回工作台 系统管理 - 仅管理员可见 · 变更即时生效 + {isAdmin ? '仅管理员可见' : '按插件声明开放'} · 变更即时生效 - {me.owner} + {me?.owner ?? ''}