refactor: 建立前端 API 客户端层,消除硬编码路径与重复 fetch
- 新增 web/src/api/ 目录(6 个文件):client.ts 基础设施 + 5 个领域模块 - 封装 18 处 fetch 调用为强类型函数,消除 17 处硬编码 /api/ 路径 - 统一 13 处重复的 fetch+json+错误处理模式 - 删除旧 web/src/components/Settings/api/expert.ts,合并到新 api/experts.ts - ConfigPage.tsx 替换 15 处 fetch 调用,ExpertSelector.tsx 改用新 API - 保持现有组件行为不变(静默失败仍静默,错误处理等价)
This commit is contained in:
parent
a8267631b8
commit
ffc1f79de4
56
web/src/api/client.ts
Normal file
56
web/src/api/client.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// 统一的 API 端点常量,消除硬编码字符串
|
||||||
|
export const API = {
|
||||||
|
config: '/api/config',
|
||||||
|
restart: '/api/restart',
|
||||||
|
health: '/health',
|
||||||
|
mcpStatus: '/api/mcp/status',
|
||||||
|
skills: '/api/skills',
|
||||||
|
skillsToggle: '/api/skills/toggle',
|
||||||
|
subagents: '/api/subagents',
|
||||||
|
subagentsToggle: '/api/subagents/toggle',
|
||||||
|
experts: '/api/experts',
|
||||||
|
expertsToggle: '/api/experts/toggle',
|
||||||
|
expertsCreate: '/api/experts/create',
|
||||||
|
expertsUpdate: '/api/experts/update',
|
||||||
|
expertsDelete: '/api/experts/delete',
|
||||||
|
expertsSelected: '/api/experts/selected',
|
||||||
|
expertsSelect: '/api/experts/select',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础 fetch 封装:自动添加 JSON headers,解析响应。
|
||||||
|
* 返回 [data, error] 元组,不抛异常。
|
||||||
|
*/
|
||||||
|
export async function apiFetch<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options?: { method?: string; body?: unknown; signal?: AbortSignal }
|
||||||
|
): Promise<[T | null, { status: number; message: string } | null]> {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(endpoint, {
|
||||||
|
method: options?.method ?? 'GET',
|
||||||
|
headers: options?.body ? { 'Content-Type': 'application/json' } : undefined,
|
||||||
|
body: options?.body ? JSON.stringify(options.body) : undefined,
|
||||||
|
signal: options?.signal,
|
||||||
|
})
|
||||||
|
const data = await resp.json().catch(() => null)
|
||||||
|
if (!resp.ok) {
|
||||||
|
return [null, { status: resp.status, message: data?.message || data?.error || `HTTP ${resp.status}` }]
|
||||||
|
}
|
||||||
|
return [data as T, null]
|
||||||
|
} catch (e) {
|
||||||
|
return [null, { status: 0, message: e instanceof Error ? e.message : 'Network error' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET 请求,静默失败返回 null(用于列表加载等场景)。
|
||||||
|
*/
|
||||||
|
export async function apiGetSilent<T>(endpoint: string): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(endpoint)
|
||||||
|
if (!resp.ok) return null
|
||||||
|
return await resp.json() as T
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
27
web/src/api/config.ts
Normal file
27
web/src/api/config.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { API, apiFetch } from './client'
|
||||||
|
import type { AppConfig } from '../components/Settings/types'
|
||||||
|
|
||||||
|
export async function getAppConfig(): Promise<[AppConfig | null, string | null]> {
|
||||||
|
const [data, err] = await apiFetch<AppConfig>(API.config)
|
||||||
|
return [data, err?.message ?? null]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAppConfig(config: AppConfig): Promise<[true, null] | [false, string]> {
|
||||||
|
const [, err] = await apiFetch<{ success: boolean }>(API.config, { method: 'PUT', body: { config } })
|
||||||
|
return err ? [false, err.message] : [true, null]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restartGateway(): Promise<{ status: number; data: any }> {
|
||||||
|
const resp = await fetch(API.restart, { method: 'POST' })
|
||||||
|
const data = await resp.json().catch(() => ({}))
|
||||||
|
return { status: resp.status, data }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkHealth(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(API.health)
|
||||||
|
return resp.ok
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
53
web/src/api/experts.ts
Normal file
53
web/src/api/experts.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { API, apiGetSilent } from './client'
|
||||||
|
import type { ExpertListResponse, ExpertItem } from '../components/Settings/types'
|
||||||
|
|
||||||
|
export function listExperts(): Promise<ExpertListResponse | null> {
|
||||||
|
return apiGetSilent<ExpertListResponse>(API.experts)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function toggleExpert(name: string, scope: string, enabled: boolean): Promise<Response> {
|
||||||
|
return fetch(API.expertsToggle, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name, scope, enabled }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createExpert(payload: { name: string; description: string; body: string; scope: string }): Promise<Response> {
|
||||||
|
return fetch(API.expertsCreate, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateExpert(payload: { name: string; scope: string; description?: string; body?: string }): Promise<Response> {
|
||||||
|
return fetch(API.expertsUpdate, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteExpert(name: string, scope: string): Promise<Response> {
|
||||||
|
const params = new URLSearchParams({ name, scope })
|
||||||
|
return fetch(`${API.expertsDelete}?${params}`, { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSelectedExpert(sessionId: string): Promise<{ expert_name: string | null; expert: ExpertItem | null }> {
|
||||||
|
const params = new URLSearchParams({ session_id: sessionId })
|
||||||
|
const resp = await fetch(`${API.expertsSelected}?${params}`)
|
||||||
|
if (!resp.ok) return { expert_name: null, expert: null }
|
||||||
|
return resp.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function selectExpert(sessionId: string, expertName: string | null): Promise<{ success: boolean; error?: string }> {
|
||||||
|
const resp = await fetch(API.expertsSelect, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ session_id: sessionId, expert_name: expertName }),
|
||||||
|
})
|
||||||
|
const data = await resp.json().catch(() => ({}))
|
||||||
|
if (!resp.ok || !data.success) return { success: false, error: data.error || '切换专家失败' }
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
6
web/src/api/mcp.ts
Normal file
6
web/src/api/mcp.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { API, apiGetSilent } from './client'
|
||||||
|
import type { McpStatusResponse } from '../components/Settings/types'
|
||||||
|
|
||||||
|
export function getMcpStatus(): Promise<McpStatusResponse | null> {
|
||||||
|
return apiGetSilent<McpStatusResponse>(API.mcpStatus)
|
||||||
|
}
|
||||||
14
web/src/api/skills.ts
Normal file
14
web/src/api/skills.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { API, apiGetSilent } from './client'
|
||||||
|
import type { SkillListResponse } from '../components/Settings/types'
|
||||||
|
|
||||||
|
export function listSkills(): Promise<SkillListResponse | null> {
|
||||||
|
return apiGetSilent<SkillListResponse>(API.skills)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function toggleSkill(name: string, scope: string, enabled: boolean): Promise<Response> {
|
||||||
|
return fetch(API.skillsToggle, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name, scope, enabled }),
|
||||||
|
})
|
||||||
|
}
|
||||||
14
web/src/api/subagents.ts
Normal file
14
web/src/api/subagents.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { API, apiGetSilent } from './client'
|
||||||
|
import type { SubagentListResponse } from '../components/Settings/types'
|
||||||
|
|
||||||
|
export function listSubagents(): Promise<SubagentListResponse | null> {
|
||||||
|
return apiGetSilent<SubagentListResponse>(API.subagents)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function toggleSubagent(name: string, scope: string, enabled: boolean): Promise<Response> {
|
||||||
|
return fetch(API.subagentsToggle, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name, scope, enabled }),
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { UserCheck, ChevronDown, Loader2, Settings, Check } from 'lucide-react'
|
import { UserCheck, ChevronDown, Loader2, Settings, Check } from 'lucide-react'
|
||||||
import { getSelectedExpert, selectExpert } from '../Settings/ConfigPage'
|
import { getSelectedExpert, selectExpert, listExperts } from '../../api/experts'
|
||||||
|
|
||||||
interface ExpertItem {
|
interface ExpertItem {
|
||||||
name: string
|
name: string
|
||||||
@ -81,18 +81,15 @@ export function ExpertSelector({ sessionId, onManageExperts, onSelectionChange }
|
|||||||
|
|
||||||
const fetchExpertList = useCallback(async () => {
|
const fetchExpertList = useCallback(async () => {
|
||||||
setListLoading(true)
|
setListLoading(true)
|
||||||
try {
|
const data = await listExperts()
|
||||||
const resp = await fetch('/api/experts')
|
if (data) {
|
||||||
if (resp.ok) {
|
// Only show enabled experts (disabled_in_scopes.length === 0)
|
||||||
const data = await resp.json()
|
const enabled = (data.experts ?? []).filter(
|
||||||
// Only show enabled experts (disabled_in_scopes.length === 0)
|
(e: ExpertItem) => e.disabled_in_scopes.length === 0
|
||||||
const enabled = (data.experts ?? []).filter(
|
)
|
||||||
(e: ExpertItem) => e.disabled_in_scopes.length === 0
|
setExpertList(enabled)
|
||||||
)
|
}
|
||||||
setExpertList(enabled)
|
setListLoading(false)
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
finally { setListLoading(false) }
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleToggleOpen = () => {
|
const handleToggleOpen = () => {
|
||||||
|
|||||||
@ -16,7 +16,12 @@ import type {
|
|||||||
} from './types'
|
} from './types'
|
||||||
import { TABS, inputCls, selectCls, TIMEZONE_OPTIONS } from './constants'
|
import { TABS, inputCls, selectCls, TIMEZONE_OPTIONS } from './constants'
|
||||||
import { Field, Toggle, TagEditor, SectionCard, SourceEditor, MapEntryHeader } from './ui'
|
import { Field, Toggle, TagEditor, SectionCard, SourceEditor, MapEntryHeader } from './ui'
|
||||||
export { getSelectedExpert, selectExpert } from './api/expert'
|
import { getAppConfig, updateAppConfig, restartGateway, checkHealth } from '../../api/config'
|
||||||
|
import { listSkills, toggleSkill } from '../../api/skills'
|
||||||
|
import { listSubagents, toggleSubagent } from '../../api/subagents'
|
||||||
|
import { listExperts, toggleExpert, createExpert, updateExpert, deleteExpert } from '../../api/experts'
|
||||||
|
import { getMcpStatus } from '../../api/mcp'
|
||||||
|
export { getSelectedExpert, selectExpert } from '../../api/experts'
|
||||||
|
|
||||||
// ── Main Component ─────────────────────────────────────
|
// ── Main Component ─────────────────────────────────────
|
||||||
export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPageProps) {
|
export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPageProps) {
|
||||||
@ -57,89 +62,53 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
const [savingExpert, setSavingExpert] = useState(false)
|
const [savingExpert, setSavingExpert] = useState(false)
|
||||||
|
|
||||||
const fetchMcpStatus = useCallback(async () => {
|
const fetchMcpStatus = useCallback(async () => {
|
||||||
try {
|
const data = await getMcpStatus()
|
||||||
const resp = await fetch('/api/mcp/status')
|
if (data) setMcpStatus(data)
|
||||||
if (resp.ok) setMcpStatus(await resp.json())
|
|
||||||
} catch { /* ignore fetch errors */ }
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fetchSkillList = useCallback(async () => {
|
const fetchSkillList = useCallback(async () => {
|
||||||
setSkillListLoading(true)
|
setSkillListLoading(true)
|
||||||
try {
|
const data = await listSkills()
|
||||||
const resp = await fetch('/api/skills')
|
if (data) setSkillList(data)
|
||||||
if (resp.ok) setSkillList(await resp.json())
|
setSkillListLoading(false)
|
||||||
} catch { /* ignore fetch errors */ }
|
|
||||||
finally { setSkillListLoading(false) }
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const toggleSkill = useCallback(async (name: string, scope: string, enabled: boolean) => {
|
const toggleSkillCb = useCallback(async (name: string, scope: string, enabled: boolean) => {
|
||||||
const resp = await fetch('/api/skills/toggle', {
|
return toggleSkill(name, scope, enabled)
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ name, scope, enabled }),
|
|
||||||
})
|
|
||||||
return resp
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fetchSubagentList = useCallback(async () => {
|
const fetchSubagentList = useCallback(async () => {
|
||||||
setSubagentListLoading(true)
|
setSubagentListLoading(true)
|
||||||
try {
|
const data = await listSubagents()
|
||||||
const resp = await fetch('/api/subagents')
|
if (data) setSubagentList(data)
|
||||||
if (resp.ok) setSubagentList(await resp.json())
|
setSubagentListLoading(false)
|
||||||
} catch { /* ignore fetch errors */ }
|
|
||||||
finally { setSubagentListLoading(false) }
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const toggleSubagent = useCallback(async (name: string, scope: string, enabled: boolean) => {
|
const toggleSubagentCb = useCallback(async (name: string, scope: string, enabled: boolean) => {
|
||||||
const resp = await fetch('/api/subagents/toggle', {
|
return toggleSubagent(name, scope, enabled)
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ name, scope, enabled }),
|
|
||||||
})
|
|
||||||
return resp
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fetchExpertList = useCallback(async () => {
|
const fetchExpertList = useCallback(async () => {
|
||||||
setExpertListLoading(true)
|
setExpertListLoading(true)
|
||||||
try {
|
const data = await listExperts()
|
||||||
const resp = await fetch('/api/experts')
|
if (data) setExpertList(data)
|
||||||
if (resp.ok) setExpertList(await resp.json())
|
setExpertListLoading(false)
|
||||||
} catch { /* ignore fetch errors */ }
|
|
||||||
finally { setExpertListLoading(false) }
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const toggleExpert = useCallback(async (name: string, scope: string, enabled: boolean) => {
|
const toggleExpertCb = useCallback(async (name: string, scope: string, enabled: boolean) => {
|
||||||
const resp = await fetch('/api/experts/toggle', {
|
return toggleExpert(name, scope, enabled)
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ name, scope, enabled }),
|
|
||||||
})
|
|
||||||
return resp
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const createExpert = useCallback(async (payload: { name: string; description: string; body: string; scope: string }) => {
|
const createExpertCb = useCallback(async (payload: { name: string; description: string; body: string; scope: string }) => {
|
||||||
const resp = await fetch('/api/experts/create', {
|
return createExpert(payload)
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
})
|
|
||||||
return resp
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const updateExpert = useCallback(async (payload: { name: string; scope: string; description?: string; body?: string }) => {
|
const updateExpertCb = useCallback(async (payload: { name: string; scope: string; description?: string; body?: string }) => {
|
||||||
const resp = await fetch('/api/experts/update', {
|
return updateExpert(payload)
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
})
|
|
||||||
return resp
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const deleteExpert = useCallback(async (name: string, scope: string) => {
|
const deleteExpertCb = useCallback(async (name: string, scope: string) => {
|
||||||
const resp = await fetch(`/api/experts/delete?name=${encodeURIComponent(name)}&scope=${encodeURIComponent(scope)}`, {
|
return deleteExpert(name, scope)
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
return resp
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
@ -149,10 +118,11 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
|
|
||||||
// Load config
|
// Load config
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/config').then(r => r.json()).then(data => {
|
getAppConfig().then(([data, err]) => {
|
||||||
setConfig(data)
|
if (data) setConfig(data)
|
||||||
|
if (err) setError('加载配置失败: ' + err)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}).catch(e => { setError('加载配置失败: ' + e.message); setLoading(false) })
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Fetch MCP status when MCP tab is selected
|
// Fetch MCP status when MCP tab is selected
|
||||||
@ -190,55 +160,44 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!config) return
|
if (!config) return
|
||||||
setSaving(true); setError('')
|
setSaving(true); setError('')
|
||||||
try {
|
const [ok, err] = await updateAppConfig(config)
|
||||||
const resp = await fetch('/api/config', {
|
if (!ok) {
|
||||||
method: 'PUT',
|
setError(err || '保存失败')
|
||||||
headers: { 'Content-Type': 'application/json' },
|
} else {
|
||||||
body: JSON.stringify({ config }),
|
|
||||||
})
|
|
||||||
const data = await resp.json()
|
|
||||||
if (!resp.ok) throw new Error(data.message || data.error || '保存失败')
|
|
||||||
// Config is now synced to both disk and in-memory state,
|
// Config is now synced to both disk and in-memory state,
|
||||||
// so the local state is already correct. No need to re-fetch.
|
// so the local state is already correct. No need to re-fetch.
|
||||||
setDirty(false)
|
setDirty(false)
|
||||||
// Show restart confirmation dialog
|
// Show restart confirmation dialog
|
||||||
setShowRestartDialog(true)
|
setShowRestartDialog(true)
|
||||||
} catch (e: any) {
|
|
||||||
setError(e.message || '保存失败')
|
|
||||||
} finally {
|
|
||||||
setSaving(false)
|
|
||||||
}
|
}
|
||||||
|
setSaving(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRestart = async () => {
|
const handleRestart = async () => {
|
||||||
setShowRestartDialog(false)
|
setShowRestartDialog(false)
|
||||||
setRestarting(true)
|
setRestarting(true)
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/restart', { method: 'POST' })
|
const { status, data } = await restartGateway()
|
||||||
const data = await resp.json()
|
if (status === 409) {
|
||||||
if (resp.status === 409) {
|
|
||||||
setToast(data.message || '有任务运行中,请等待完成后再试')
|
setToast(data.message || '有任务运行中,请等待完成后再试')
|
||||||
setRestarting(false)
|
setRestarting(false)
|
||||||
setTimeout(() => setToast(''), 5000)
|
setTimeout(() => setToast(''), 5000)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!resp.ok) throw new Error(data.message || '重启失败')
|
if (status < 200 || status >= 300) throw new Error(data.message || '重启失败')
|
||||||
setToast('服务正在重启,页面将自动重连...')
|
setToast('服务正在重启,页面将自动重连...')
|
||||||
// Poll /health until gateway is back
|
// Poll /health until gateway is back
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
for (let i = 0; i < 30; i++) {
|
for (let i = 0; i < 30; i++) {
|
||||||
await new Promise(r => setTimeout(r, 1000))
|
await new Promise(r => setTimeout(r, 1000))
|
||||||
try {
|
if (await checkHealth()) {
|
||||||
const r = await fetch('/health')
|
const [refreshed] = await getAppConfig()
|
||||||
if (r.ok) {
|
if (refreshed) setConfig(refreshed)
|
||||||
const refreshed = await fetch('/api/config').then(r => r.json())
|
setToast('服务已重启,配置已生效')
|
||||||
setConfig(refreshed)
|
setRestarting(false)
|
||||||
setToast('服务已重启,配置已生效')
|
setTimeout(() => setToast(''), 3000)
|
||||||
setRestarting(false)
|
return
|
||||||
setTimeout(() => setToast(''), 3000)
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch { /* gateway not ready yet */ }
|
|
||||||
}
|
}
|
||||||
setToast('重启超时,请手动刷新页面')
|
setToast('重启超时,请手动刷新页面')
|
||||||
setRestarting(false)
|
setRestarting(false)
|
||||||
@ -486,7 +445,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await toggleSkill(name, 'project', !currentlyEnabled)
|
const resp = await toggleSkillCb(name, 'project', !currentlyEnabled)
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
if (!resp.ok || !data.success) {
|
if (!resp.ok || !data.success) {
|
||||||
// Rollback
|
// Rollback
|
||||||
@ -629,7 +588,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await toggleSubagent(name, 'project', !currentlyEnabled)
|
const resp = await toggleSubagentCb(name, 'project', !currentlyEnabled)
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
if (!resp.ok || !data.success) {
|
if (!resp.ok || !data.success) {
|
||||||
setSubagentList(prevList)
|
setSubagentList(prevList)
|
||||||
@ -732,7 +691,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await toggleExpert(name, 'project', !currentlyEnabled)
|
const resp = await toggleExpertCb(name, 'project', !currentlyEnabled)
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
if (!resp.ok || !data.success) {
|
if (!resp.ok || !data.success) {
|
||||||
setExpertList(prevList)
|
setExpertList(prevList)
|
||||||
@ -770,7 +729,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
const handleDelete = async (name: string) => {
|
const handleDelete = async (name: string) => {
|
||||||
if (!confirm(`确定删除专家 "${name}" 吗?此操作将删除对应文件。`)) return
|
if (!confirm(`确定删除专家 "${name}" 吗?此操作将删除对应文件。`)) return
|
||||||
try {
|
try {
|
||||||
const resp = await deleteExpert(name, 'project')
|
const resp = await deleteExpertCb(name, 'project')
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
if (!resp.ok || !data.success) {
|
if (!resp.ok || !data.success) {
|
||||||
setToast(data.error || '删除专家失败')
|
setToast(data.error || '删除专家失败')
|
||||||
@ -842,13 +801,13 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
|||||||
setEditingExpertError('')
|
setEditingExpertError('')
|
||||||
try {
|
try {
|
||||||
const resp = isEdit
|
const resp = isEdit
|
||||||
? await updateExpert({
|
? await updateExpertCb({
|
||||||
name: editingExpert.nameField,
|
name: editingExpert.nameField,
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
description: editingExpert.description,
|
description: editingExpert.description,
|
||||||
body: editingExpert.body,
|
body: editingExpert.body,
|
||||||
})
|
})
|
||||||
: await createExpert({
|
: await createExpertCb({
|
||||||
name: editingExpert.nameField,
|
name: editingExpert.nameField,
|
||||||
description: editingExpert.description,
|
description: editingExpert.description,
|
||||||
body: editingExpert.body,
|
body: editingExpert.body,
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
// Expert API helpers extracted from ConfigPage.tsx
|
|
||||||
// Used by both ConfigPage and ExpertSelector
|
|
||||||
import type { ExpertItem } from '../types'
|
|
||||||
|
|
||||||
export async function getSelectedExpert(sessionId: string): Promise<{ expert_name: string | null; expert: ExpertItem | null }> {
|
|
||||||
const resp = await fetch(`/api/experts/selected?session_id=${encodeURIComponent(sessionId)}`)
|
|
||||||
if (!resp.ok) return { expert_name: null, expert: null }
|
|
||||||
return resp.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function selectExpert(sessionId: string, expertName: string | null): Promise<{ success: boolean; error?: string }> {
|
|
||||||
const resp = await fetch('/api/experts/select', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ session_id: sessionId, expert_name: expertName }),
|
|
||||||
})
|
|
||||||
const data = await resp.json().catch(() => ({}))
|
|
||||||
if (!resp.ok || !data.success) return { success: false, error: data.error || '切换专家失败' }
|
|
||||||
return { success: true }
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user