From f96a59112351d80b0065463220739d51c0046d97 Mon Sep 17 00:00:00 2001 From: weichen Date: Tue, 13 Oct 2026 05:59:57 +0000 Subject: [PATCH] Add admin system settings panel for managed model and service config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: P2 前端入口——系统受管项(模型/密钥/Tavily/遥测/运行基础设施)需要唯一的管理员写入面;系统管理新增「模型与服务配置」分区消费 GET/PUT /api/settings/system。 Evidence: vitest design-guard 6/6 通过;npm run build 成功;面板仅渲染 scope=system 项,保存后重 GET 回填,403 如实提示。 Impact: contracts 加 SettingDesc.scope;api 加 get/putSystemSettings;AdminZone 注册新分区;无既有面板变更。 Refs: modules/documents/docs/proposals/2026-10-13-rdm-settings-members-duties-perms-increment.md P2;Backend PR 188 --- src/api.ts | 4 ++ src/components/AdminZone.tsx | 4 +- src/components/adminSections.tsx | 73 ++++++++++++++++++++++++++++++++ src/contracts.ts | 2 + 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/api.ts b/src/api.ts index e416742..6f24c64 100644 --- a/src/api.ts +++ b/src/api.ts @@ -126,6 +126,10 @@ export const api = { json<{ schema: SettingDesc[]; defaults: Record; user: Record; project: Record }>(r)), putUserSettings: (values: Record) => auth.fetch(`${BASE}/api/settings/user`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values) }), putProjectSettings: (projectId: string, values: Record) => auth.fetch(`${BASE}/api/settings/project/${projectId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values) }), + // P2 系统管理面:受管项(models/tavily/telemetry/运行基础设施)的唯一写入面。 + getSystemSettings: () => auth.fetch(`${BASE}/api/settings/system`).then(r => + json<{ schema: SettingDesc[]; defaults: Record; system: Record }>(r)), + putSystemSettings: (values: Record) => auth.fetch(`${BASE}/api/settings/system`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values) }), listTools: () => auth.fetch(`${BASE}/api/tools`).then(r => r.json()), listPlugins: () => auth.fetch(`${BASE}/api/plugins`).then(r => r.json()), diff --git a/src/components/AdminZone.tsx b/src/components/AdminZone.tsx index 87f1846..ae9762a 100644 --- a/src/components/AdminZone.tsx +++ b/src/components/AdminZone.tsx @@ -7,7 +7,7 @@ import { useState } from 'react' import { store, useStore } from '../store' import { EmptyState, Icon, type IconName } from '@ui' -import { UsersPanel, ProfilesSection, PluginsSection, PluginWorkspacesPanel } from './adminSections' +import { UsersPanel, ProfilesSection, PluginsSection, PluginWorkspacesPanel, SystemSettingsPanel } from './adminSections' import { RdmAuditWorkspace } from './RdmAuditWorkspace' import { RdmRolesPanel } from './adminRdmSections' import type { AdminZoneSection } from '../api' @@ -31,6 +31,8 @@ const SECTIONS: AdminSectionDef[] = [ // U6/KTD5:RDM 专业角色管理——命令面齐备(rdmRolesAvailable)才显示;普通基座零暴露。 { key: 'rdm-roles', label: 'RDM 角色权限', icon: 'shield', desc: '专业角色:动作默认/上限、账号绑定、停用(仅管理员)', render: () => }, { key: 'profiles', label: '智能体画像', icon: 'bot', desc: '系统级画像:系统提示、工具允许/确认/拒绝清单', render: () => }, + // P2 受管:模型/密钥/Tavily/遥测/运行基础设施的唯一写入面(Backend PR 188 系统设置 API)。 + { key: 'system-settings', label: '模型与服务配置', icon: 'settings', desc: '系统受管项:模型、密钥、Tavily、遥测与运行基础设施(仅管理员)', render: () => }, { key: 'plugins', label: '插件管理', icon: 'puzzle', desc: '已装载插件清单与热重载(装卸不影响核心)', render: () => }, { key: 'workspaces', label: '插件工作区', icon: 'grid', desc: '插件声明面板与服务端声明工作区', render: () => }, ] diff --git a/src/components/adminSections.tsx b/src/components/adminSections.tsx index 973047c..4e56ab8 100644 --- a/src/components/adminSections.tsx +++ b/src/components/adminSections.tsx @@ -6,6 +6,8 @@ import { api } from '../api' import { store, useStore } from '../store' import type { AgentProfile } from '../contracts' import { Btn, EmptyState, Icon, SettingRow, SettingsGroup, UIRenderer, type DocBridge } from '@ui' +import { SettingInput, flatten, unflatten } from './SettingsModal' +import type { SettingDesc } from '../contracts' import { docBridge } from './Workbench' import { randomPassword } from '../passwords' @@ -293,3 +295,74 @@ export function PluginWorkspacesPanel() { ) } + +// ---- 模型与服务配置(P2 系统受管项的唯一写入面)---- + +/** + * SystemSettingsPanel:消费 GET/PUT /api/settings/system。 + * - 仅渲染 schema 中 scope=system 的受管项(模型、密钥、Tavily、遥测、运行基础设施); + * 非受管项属于用户/项目偏好,不在本面板出现。 + * - 保存后重新 GET 以服务端结果回填(保存成功 ≠ 本地草稿即真相);掩码占位符 + * 原样传回,由服务端视为未修改。 + * - 权限不足(非管理员)由分区门控与 API 403 双层拦截,403 时如实提示。 + */ +export function SystemSettingsPanel() { + const [schema, setSchema] = useState(null) + const [draft, setDraft] = useState>({}) + const [loaded, setLoaded] = useState(false) + const [forbidden, setForbidden] = useState(false) + const [err, setErr] = useState('') + const [notice, setNotice] = useState('') + const [busy, setBusy] = useState(false) + + const load = async () => { + setErr(''); setForbidden(false) + try { + const out = await api.getSystemSettings() + setSchema(out.schema.filter(d => d.scope === 'system')) + setDraft(flatten(out.system)) + setLoaded(true) + } catch (e) { + if (String(e).includes('403')) { setForbidden(true); setLoaded(true) } + else setErr(String(e)) + } + } + useEffect(() => { load() }, []) + + const groups: Record = {} + for (const d of schema ?? []) (groups[d.group] ??= []).push(d) + + const save = async () => { + setErr(''); setNotice(''); setBusy(true) + try { + await api.putSystemSettings(unflatten(draft)) + setNotice('已保存;按各项标注的生效时机生效(需重启的项在重启后生效)') + await load() + } catch (e) { setErr(String(e)) } finally { setBusy(false) } + } + + if (!loaded) return 加载系统设置… + if (forbidden) return 系统设置仅管理员可配置 + + return ( +
+

+ 模型、密钥与运行基础设施为系统受管配置:仅此面板(管理员)可写,用户与项目设置中的同名项不再生效。 +

+ {Object.entries(groups).map(([group, descs]) => ( + + {descs.map(d => ( + + setDraft(prev => ({ ...prev, [d.key]: v }))} /> + + ))} + + ))} +
+ 保存系统配置 +
+ {notice &&
{notice}
} + {err &&
{err}
} +
+ ) +} diff --git a/src/contracts.ts b/src/contracts.ts index da69011..70348ef 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -270,6 +270,8 @@ export interface SettingDesc { default?: unknown; options?: string[]; effect: 'hot' | 'new-run' | 'restart'; secret?: boolean; + /** P2 受管:system = 系统受管项,仅系统管理面可写(用户/项目层拒绝)。 */ + scope?: 'system'; } // ---- 插件 ----