feat(web): 新增工具列表 API 客户端与 CheckboxList 勾选组件
- 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 已选值以可移除标签呈现
This commit is contained in:
parent
89c444ad3f
commit
3ea0c19262
@ -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',
|
||||
|
||||
@ -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<ExpertListResponse | null> {
|
||||
return apiGetSilent<ExpertListResponse>(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<Response> {
|
||||
export async function createExpert(payload: { name: string; description: string; body: string; scope: string; capability?: CapabilityPolicy }): Promise<Response> {
|
||||
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<Response> {
|
||||
export async function updateExpert(payload: { name: string; scope: string; description?: string; body?: string; capability?: CapabilityPolicy }): Promise<Response> {
|
||||
return fetch(API.expertsUpdate, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@ -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<SubagentListResponse | null> {
|
||||
return apiGetSilent<SubagentListResponse>(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<Response> {
|
||||
return fetch(API.subagentsUpdate, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
6
web/src/api/tools.ts
Normal file
6
web/src/api/tools.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { API, apiGetSilent } from './client'
|
||||
import type { ToolsListResponse } from '../components/Settings/types'
|
||||
|
||||
export function listTools(): Promise<ToolsListResponse | null> {
|
||||
return apiGetSilent<ToolsListResponse>(API.tools)
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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[]) => (
|
||||
<div className="space-y-1">
|
||||
{opts.map(option => (
|
||||
<div key={option.key} className="flex items-center justify-between py-1.5 gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-[var(--text-primary)] font-mono">{option.label}</div>
|
||||
{option.description && <div className="text-xs text-[var(--text-muted)] truncate">{option.description}</div>}
|
||||
</div>
|
||||
<Toggle checked={selectedSet.has(option.key)} onChange={() => toggle(option.key)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
let body: ReactNode
|
||||
if (options.length === 0) {
|
||||
body = <p className="text-xs text-[var(--text-muted)]">{emptyHint || '无可用选项'}</p>
|
||||
} else if (groupBy) {
|
||||
const groups = new Map<string, CheckboxListOption[]>()
|
||||
for (const opt of options) {
|
||||
const g = groupBy(opt)
|
||||
const arr = groups.get(g) ?? []
|
||||
arr.push(opt)
|
||||
groups.set(g, arr)
|
||||
}
|
||||
body = (
|
||||
<div className="space-y-3">
|
||||
{Array.from(groups.entries()).map(([g, opts]) => (
|
||||
<div key={g}>
|
||||
<div className="text-[10px] font-medium text-[var(--text-muted)] uppercase tracking-wider mb-1">{g}</div>
|
||||
{renderOptions(opts)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
body = renderOptions(options)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{body}
|
||||
{extra.length > 0 && (
|
||||
<div>
|
||||
<div className="text-[10px] font-medium text-[var(--text-muted)] uppercase tracking-wider mb-1">其他已选(不在当前选项中)</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{extra.map(key => (
|
||||
<span key={key} className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-[var(--accent-cyan)]/10 border border-[var(--accent-cyan)]/20 text-xs text-[var(--accent-cyan)] font-mono">
|
||||
{key}
|
||||
<button onClick={() => toggle(key)} className="hover:text-white transition-colors"><X className="h-3 w-3" /></button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MapEntryHeader({ name, onDelete, onRename }: { name: string; onDelete: () => void; onRename?: (n: string) => void }) {
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [val, setVal] = useState(name)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user