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',
|
mcpStatus: '/api/mcp/status',
|
||||||
skills: '/api/skills',
|
skills: '/api/skills',
|
||||||
skillsToggle: '/api/skills/toggle',
|
skillsToggle: '/api/skills/toggle',
|
||||||
|
tools: '/api/tools',
|
||||||
subagents: '/api/subagents',
|
subagents: '/api/subagents',
|
||||||
subagentsToggle: '/api/subagents/toggle',
|
subagentsToggle: '/api/subagents/toggle',
|
||||||
|
subagentsUpdate: '/api/subagents/update',
|
||||||
experts: '/api/experts',
|
experts: '/api/experts',
|
||||||
expertsToggle: '/api/experts/toggle',
|
expertsToggle: '/api/experts/toggle',
|
||||||
expertsCreate: '/api/experts/create',
|
expertsCreate: '/api/experts/create',
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { API, apiGetSilent } from './client'
|
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> {
|
export function listExperts(): Promise<ExpertListResponse | null> {
|
||||||
return apiGetSilent<ExpertListResponse>(API.experts)
|
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, {
|
return fetch(API.expertsCreate, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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, {
|
return fetch(API.expertsUpdate, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { API, apiGetSilent } from './client'
|
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> {
|
export function listSubagents(): Promise<SubagentListResponse | null> {
|
||||||
return apiGetSilent<SubagentListResponse>(API.subagents)
|
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 }),
|
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[]
|
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 {
|
export interface SubagentItem {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
source: string
|
source: string
|
||||||
disabled_in_scopes: string[]
|
disabled_in_scopes: string[]
|
||||||
allowed_tools?: string[]
|
capability?: CapabilityPolicy
|
||||||
denied_tools?: string[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubagentListResponse {
|
export interface SubagentListResponse {
|
||||||
@ -63,6 +81,7 @@ export interface ExpertItem {
|
|||||||
path?: string
|
path?: string
|
||||||
body?: string
|
body?: string
|
||||||
disabled_in_scopes: string[]
|
disabled_in_scopes: string[]
|
||||||
|
capability?: CapabilityPolicy
|
||||||
}
|
}
|
||||||
export interface ExpertListResponse {
|
export interface ExpertListResponse {
|
||||||
experts_system_enabled: boolean
|
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 }) {
|
export function MapEntryHeader({ name, onDelete, onRename }: { name: string; onDelete: () => void; onRename?: (n: string) => void }) {
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false)
|
||||||
const [val, setVal] = useState(name)
|
const [val, setVal] = useState(name)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user