Add admin system settings panel for managed model and service config

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
This commit is contained in:
2026-10-13 05:59:57 +00:00
parent 44b964682c
commit f96a591123
4 changed files with 82 additions and 1 deletions
+4
View File
@@ -126,6 +126,10 @@ export const api = {
json<{ schema: SettingDesc[]; defaults: Record<string, unknown>; user: Record<string, unknown>; project: Record<string, unknown> }>(r)),
putUserSettings: (values: Record<string, unknown>) => auth.fetch(`${BASE}/api/settings/user`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values) }),
putProjectSettings: (projectId: string, values: Record<string, unknown>) => 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<string, unknown>; system: Record<string, unknown> }>(r)),
putSystemSettings: (values: Record<string, unknown>) => 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()),
+3 -1
View File
@@ -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: () => <RdmRolesPanel /> },
{ key: 'profiles', label: '智能体画像', icon: 'bot', desc: '系统级画像:系统提示、工具允许/确认/拒绝清单', render: () => <ProfilesPanelWrapper /> },
// P2 受管:模型/密钥/Tavily/遥测/运行基础设施的唯一写入面(Backend PR 188 系统设置 API)。
{ key: 'system-settings', label: '模型与服务配置', icon: 'settings', desc: '系统受管项:模型、密钥、Tavily、遥测与运行基础设施(仅管理员)', render: () => <SystemSettingsPanel /> },
{ key: 'plugins', label: '插件管理', icon: 'puzzle', desc: '已装载插件清单与热重载(装卸不影响核心)', render: () => <PluginsSection /> },
{ key: 'workspaces', label: '插件工作区', icon: 'grid', desc: '插件声明面板与服务端声明工作区', render: () => <PluginWorkspacesPanel /> },
]
+73
View File
@@ -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() {
</div>
)
}
// ---- 模型与服务配置(P2 系统受管项的唯一写入面)----
/**
* SystemSettingsPanel:消费 GET/PUT /api/settings/system。
* - 仅渲染 schema 中 scope=system 的受管项(模型、密钥、Tavily、遥测、运行基础设施);
* 非受管项属于用户/项目偏好,不在本面板出现。
* - 保存后重新 GET 以服务端结果回填(保存成功 ≠ 本地草稿即真相);掩码占位符
* 原样传回,由服务端视为未修改。
* - 权限不足(非管理员)由分区门控与 API 403 双层拦截,403 时如实提示。
*/
export function SystemSettingsPanel() {
const [schema, setSchema] = useState<SettingDesc[] | null>(null)
const [draft, setDraft] = useState<Record<string, any>>({})
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<string, SettingDesc[]> = {}
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 <EmptyState icon="settings">加载系统设置…</EmptyState>
if (forbidden) return <EmptyState icon="shield">系统设置仅管理员可配置</EmptyState>
return (
<div>
<p className="ad-hint" style={{ marginBottom: 14 }}>
模型、密钥与运行基础设施为系统受管配置:仅此面板(管理员)可写,用户与项目设置中的同名项不再生效。
</p>
{Object.entries(groups).map(([group, descs]) => (
<SettingsGroup key={group} title={group === 'models' ? '模型' : group === 'tools' ? '工具服务' : group === 'telemetry' ? '遥测' : group === 'runtime' ? '运行基础设施' : group}>
{descs.map(d => (
<SettingRow key={d.key} label={d.label} desc={d.description} effect={d.effect}>
<SettingInput desc={d} value={draft[d.key]} onChange={v => setDraft(prev => ({ ...prev, [d.key]: v }))} />
</SettingRow>
))}
</SettingsGroup>
))}
<div className="ad-row" style={{ marginTop: 12 }}>
<Btn variant="primary" icon="check" disabled={busy} onClick={save}>保存系统配置</Btn>
</div>
{notice && <div className="ad-notice"><Icon name="check" size={14} />{notice}</div>}
{err && <div className="lg-err"><Icon name="alert" size={14} />{err}</div>}
</div>
)
}
+2
View File
@@ -270,6 +270,8 @@ export interface SettingDesc {
default?: unknown; options?: string[];
effect: 'hot' | 'new-run' | 'restart';
secret?: boolean;
/** P2 受管:system = 系统受管项,仅系统管理面可写(用户/项目层拒绝)。 */
scope?: 'system';
}
// ---- 插件 ----