From 3ea0c19262ed5f61c0661c0e5102f242b2b4e985 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Thu, 30 Jul 2026 16:55:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=96=B0=E5=A2=9E=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=88=97=E8=A1=A8=20API=20=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E4=B8=8E=20CheckboxList=20=E5=8B=BE=E9=80=89=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api/client.ts: 注册 tools 与 subagentsUpdate 端点常量 - api/tools.ts: 新增 listTools() 调用 /api/tools - api/subagents.ts: 新增 updateSubagent() 调用 PUT /api/subagents/update - types.ts: 新增 ToolItem / ToolsListResponse 类型 - ui.tsx: 新增 CheckboxList 组件,支持分组展示、legacy 已选值以可移除标签呈现 --- web/src/api/client.ts | 2 + web/src/api/experts.ts | 6 +- web/src/api/subagents.ts | 15 ++++- web/src/api/tools.ts | 6 ++ web/src/components/Settings/types.ts | 23 ++++++- web/src/components/Settings/ui.tsx | 91 ++++++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 web/src/api/tools.ts diff --git a/web/src/api/client.ts b/web/src/api/client.ts index f32e320..8f195cb 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -6,8 +6,10 @@ export const API = { mcpStatus: '/api/mcp/status', skills: '/api/skills', skillsToggle: '/api/skills/toggle', + tools: '/api/tools', subagents: '/api/subagents', subagentsToggle: '/api/subagents/toggle', + subagentsUpdate: '/api/subagents/update', experts: '/api/experts', expertsToggle: '/api/experts/toggle', expertsCreate: '/api/experts/create', diff --git a/web/src/api/experts.ts b/web/src/api/experts.ts index 330e8a1..2ba08cd 100644 --- a/web/src/api/experts.ts +++ b/web/src/api/experts.ts @@ -1,5 +1,5 @@ import { API, apiGetSilent } from './client' -import type { ExpertListResponse, ExpertItem } from '../components/Settings/types' +import type { ExpertListResponse, ExpertItem, CapabilityPolicy } from '../components/Settings/types' export function listExperts(): Promise { return apiGetSilent(API.experts) @@ -13,7 +13,7 @@ export async function toggleExpert(name: string, scope: string, enabled: boolean }) } -export async function createExpert(payload: { name: string; description: string; body: string; scope: string }): Promise { +export async function createExpert(payload: { name: string; description: string; body: string; scope: string; capability?: CapabilityPolicy }): Promise { return fetch(API.expertsCreate, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -21,7 +21,7 @@ export async function createExpert(payload: { name: string; description: string; }) } -export async function updateExpert(payload: { name: string; scope: string; description?: string; body?: string }): Promise { +export async function updateExpert(payload: { name: string; scope: string; description?: string; body?: string; capability?: CapabilityPolicy }): Promise { return fetch(API.expertsUpdate, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, diff --git a/web/src/api/subagents.ts b/web/src/api/subagents.ts index 919036b..6eb880a 100644 --- a/web/src/api/subagents.ts +++ b/web/src/api/subagents.ts @@ -1,5 +1,5 @@ import { API, apiGetSilent } from './client' -import type { SubagentListResponse } from '../components/Settings/types' +import type { SubagentListResponse, CapabilityPolicy } from '../components/Settings/types' export function listSubagents(): Promise { return apiGetSilent(API.subagents) @@ -12,3 +12,16 @@ export async function toggleSubagent(name: string, scope: string, enabled: boole body: JSON.stringify({ name, scope, enabled }), }) } + +export async function updateSubagent(payload: { + name: string + description?: string + body?: string + capability?: CapabilityPolicy +}): Promise { + return fetch(API.subagentsUpdate, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }) +} diff --git a/web/src/api/tools.ts b/web/src/api/tools.ts new file mode 100644 index 0000000..133e10e --- /dev/null +++ b/web/src/api/tools.ts @@ -0,0 +1,6 @@ +import { API, apiGetSilent } from './client' +import type { ToolsListResponse } from '../components/Settings/types' + +export function listTools(): Promise { + return apiGetSilent(API.tools) +} diff --git a/web/src/components/Settings/types.ts b/web/src/components/Settings/types.ts index 3278ab1..481c4ea 100644 --- a/web/src/components/Settings/types.ts +++ b/web/src/components/Settings/types.ts @@ -40,13 +40,31 @@ export interface SkillListResponse { skills: SkillItem[] } +export interface ToolItem { + name: string + description: string + /** "builtin" 或 "mcp:{server_key}" */ + source: string +} + +export interface ToolsListResponse { + total: number + tools: ToolItem[] +} + +export interface CapabilityPolicy { + allowed_skills?: string[] + denied_skills: string[] + allowed_tools?: string[] + denied_tools: string[] +} + export interface SubagentItem { name: string description: string source: string disabled_in_scopes: string[] - allowed_tools?: string[] - denied_tools?: string[] + capability?: CapabilityPolicy } export interface SubagentListResponse { @@ -63,6 +81,7 @@ export interface ExpertItem { path?: string body?: string disabled_in_scopes: string[] + capability?: CapabilityPolicy } export interface ExpertListResponse { experts_system_enabled: boolean diff --git a/web/src/components/Settings/ui.tsx b/web/src/components/Settings/ui.tsx index d7f6558..754dc36 100644 --- a/web/src/components/Settings/ui.tsx +++ b/web/src/components/Settings/ui.tsx @@ -152,6 +152,97 @@ export function SourceEditor({ ) } +export interface CheckboxListOption { + key: string + label: string + description?: string + /** 可选分组标识,配合 groupBy 使用 */ + group?: string +} + +interface CheckboxListProps { + options: CheckboxListOption[] + selected: string[] + onChange: (selected: string[]) => void + /** 未在 options 中出现但已选中的值(legacy 数据),以可移除标签形式展示 */ + extraSelected?: string[] + emptyHint?: string + /** 可选:按返回的分组名分组展示(如 "builtin" / "mcp:xxx") */ + groupBy?: (option: CheckboxListOption) => string +} + +/** + * 通用勾选列表组件:用 Toggle 切换每个预设选项;额外的 legacy 已选值以可移除标签展示。 + */ +export function CheckboxList({ options, selected, onChange, extraSelected = [], emptyHint, groupBy }: CheckboxListProps) { + const toggle = (key: string) => + onChange(selected.includes(key) ? selected.filter(k => k !== key) : [...selected, key]) + + const selectedSet = new Set(selected) + const optionsInList = new Set(options.map(o => o.key)) + // 仅展示未出现在 options 中的额外已选值 + const extra = extraSelected.filter(k => !optionsInList.has(k)) + + // 分组渲染 + const renderOptions = (opts: CheckboxListOption[]) => ( +
+ {opts.map(option => ( +
+
+
{option.label}
+ {option.description &&
{option.description}
} +
+ toggle(option.key)} /> +
+ ))} +
+ ) + + let body: ReactNode + if (options.length === 0) { + body =

{emptyHint || '无可用选项'}

+ } else if (groupBy) { + const groups = new Map() + for (const opt of options) { + const g = groupBy(opt) + const arr = groups.get(g) ?? [] + arr.push(opt) + groups.set(g, arr) + } + body = ( +
+ {Array.from(groups.entries()).map(([g, opts]) => ( +
+
{g}
+ {renderOptions(opts)} +
+ ))} +
+ ) + } else { + body = renderOptions(options) + } + + return ( +
+ {body} + {extra.length > 0 && ( +
+
其他已选(不在当前选项中)
+
+ {extra.map(key => ( + + {key} + + + ))} +
+
+ )} +
+ ) +} + export function MapEntryHeader({ name, onDelete, onRename }: { name: string; onDelete: () => void; onRename?: (n: string) => void }) { const [editing, setEditing] = useState(false) const [val, setVal] = useState(name)