diff --git a/dashboard/src/lib/api-helpers.ts b/dashboard/src/lib/api-helpers.ts index a9e429ce..9ae17ab5 100644 --- a/dashboard/src/lib/api-helpers.ts +++ b/dashboard/src/lib/api-helpers.ts @@ -52,4 +52,4 @@ export function throwIfError(result: ApiResponse): T { return result.data } throw new Error(result.error) -} +} \ No newline at end of file diff --git a/dashboard/src/lib/config-api.ts b/dashboard/src/lib/config-api.ts index 9f818f03..cfb51108 100644 --- a/dashboard/src/lib/config-api.ts +++ b/dashboard/src/lib/config-api.ts @@ -2,146 +2,96 @@ * 配置API客户端 */ +import { parseResponse } from '@/lib/api-helpers' import { fetchWithAuth } from '@/lib/fetch-with-auth' -import type { - ConfigSchema, - ConfigSchemaResponse, - ConfigDataResponse, - ConfigUpdateResponse, -} from '@/types/config-schema' +import type { ApiResponse } from '@/types/api' +import type { ConfigSchema } from '@/types/config-schema' const API_BASE = '/api/webui/config' /** * 获取麦麦主程序配置架构 */ -export async function getBotConfigSchema(): Promise { +export async function getBotConfigSchema(): Promise> { const response = await fetchWithAuth(`${API_BASE}/schema/bot`) - const data: ConfigSchemaResponse = await response.json() - - if (!data.success) { - throw new Error('获取配置架构失败') - } - - return data.schema + return parseResponse(response) } /** * 获取模型配置架构 */ -export async function getModelConfigSchema(): Promise { +export async function getModelConfigSchema(): Promise> { const response = await fetchWithAuth(`${API_BASE}/schema/model`) - const data: ConfigSchemaResponse = await response.json() - - if (!data.success) { - throw new Error('获取模型配置架构失败') - } - - return data.schema + return parseResponse(response) } /** * 获取指定配置节的架构 */ -export async function getConfigSectionSchema(sectionName: string): Promise { +export async function getConfigSectionSchema(sectionName: string): Promise> { const response = await fetchWithAuth(`${API_BASE}/schema/section/${sectionName}`) - const data: ConfigSchemaResponse = await response.json() - - if (!data.success) { - throw new Error(`获取配置节 ${sectionName} 架构失败`) - } - - return data.schema + return parseResponse(response) } /** * 获取麦麦主程序配置数据 */ -export async function getBotConfig(): Promise> { +export async function getBotConfig(): Promise>> { const response = await fetchWithAuth(`${API_BASE}/bot`) - const data: ConfigDataResponse = await response.json() - - if (!data.success) { - throw new Error('获取配置数据失败') - } - - return data.config + return parseResponse>(response) } /** * 获取模型配置数据 */ -export async function getModelConfig(): Promise> { +export async function getModelConfig(): Promise>> { const response = await fetchWithAuth(`${API_BASE}/model`) - const data: ConfigDataResponse = await response.json() - - if (!data.success) { - throw new Error('获取模型配置数据失败') - } - - return data.config + return parseResponse>(response) } /** * 更新麦麦主程序配置 */ -export async function updateBotConfig(config: Record): Promise { +export async function updateBotConfig( + config: Record +): Promise>> { const response = await fetchWithAuth(`${API_BASE}/bot`, { method: 'POST', body: JSON.stringify(config), }) - - const data: ConfigUpdateResponse = await response.json() - - if (!data.success) { - throw new Error(data.message || '保存配置失败') - } + return parseResponse>(response) } /** * 获取麦麦主程序配置的原始 TOML 内容 */ -export async function getBotConfigRaw(): Promise { +export async function getBotConfigRaw(): Promise> { const response = await fetchWithAuth(`${API_BASE}/bot/raw`) - const data: { success: boolean; content: string } = await response.json() - - if (!data.success) { - throw new Error('获取配置源代码失败') - } - - return data.content + return parseResponse(response) } /** * 更新麦麦主程序配置(原始 TOML 内容) */ -export async function updateBotConfigRaw(rawContent: string): Promise { +export async function updateBotConfigRaw(rawContent: string): Promise>> { const response = await fetchWithAuth(`${API_BASE}/bot/raw`, { method: 'POST', body: JSON.stringify({ raw_content: rawContent }), }) - - const data: ConfigUpdateResponse = await response.json() - - if (!data.success) { - throw new Error(data.message || '保存配置失败') - } + return parseResponse>(response) } /** * 更新模型配置 */ -export async function updateModelConfig(config: Record): Promise { +export async function updateModelConfig( + config: Record +): Promise>> { const response = await fetchWithAuth(`${API_BASE}/model`, { method: 'POST', body: JSON.stringify(config), }) - - const data: ConfigUpdateResponse = await response.json() - - if (!data.success) { - throw new Error(data.message || '保存配置失败') - } + return parseResponse>(response) } /** @@ -150,17 +100,12 @@ export async function updateModelConfig(config: Record): Promis export async function updateBotConfigSection( sectionName: string, sectionData: unknown -): Promise { +): Promise>> { const response = await fetchWithAuth(`${API_BASE}/bot/section/${sectionName}`, { method: 'POST', body: JSON.stringify(sectionData), }) - - const data: ConfigUpdateResponse = await response.json() - - if (!data.success) { - throw new Error(data.message || `保存配置节 ${sectionName} 失败`) - } + return parseResponse>(response) } /** @@ -169,17 +114,12 @@ export async function updateBotConfigSection( export async function updateModelConfigSection( sectionName: string, sectionData: unknown -): Promise { +): Promise>> { const response = await fetchWithAuth(`${API_BASE}/model/section/${sectionName}`, { method: 'POST', body: JSON.stringify(sectionData), }) - - const data: ConfigUpdateResponse = await response.json() - - if (!data.success) { - throw new Error(data.message || `保存配置节 ${sectionName} 失败`) - } + return parseResponse>(response) } /** @@ -211,28 +151,14 @@ export async function fetchProviderModels( providerName: string, parser: 'openai' | 'gemini' = 'openai', endpoint: string = '/models' -): Promise { +): Promise> { const params = new URLSearchParams({ provider_name: providerName, parser, endpoint, }) - const response = await fetchWithAuth(`/api/webui/models/list?${params}`) - - // 处理非 2xx 响应 - if (!response.ok) { - const errorData = await response.json().catch(() => ({})) - throw new Error(errorData.detail || `获取模型列表失败 (${response.status})`) - } - - const data: FetchModelsResponse = await response.json() - - if (!data.success) { - throw new Error('获取模型列表失败') - } - - return data.models + return parseResponse(response) } /** @@ -250,20 +176,14 @@ export interface TestConnectionResult { * 测试提供商连接状态(通过提供商名称) * @param providerName 提供商名称 */ -export async function testProviderConnection(providerName: string): Promise { +export async function testProviderConnection( + providerName: string +): Promise> { const params = new URLSearchParams({ provider_name: providerName, }) - const response = await fetchWithAuth(`/api/webui/models/test-connection-by-name?${params}`, { method: 'POST', }) - - // 处理非 2xx 响应 - if (!response.ok) { - const errorData = await response.json().catch(() => ({})) - throw new Error(errorData.detail || `测试连接失败 (${response.status})`) - } - - return await response.json() + return parseResponse(response) } diff --git a/dashboard/src/routes/config/bot.tsx b/dashboard/src/routes/config/bot.tsx index 72cb5783..ee923f9f 100644 --- a/dashboard/src/routes/config/bot.tsx +++ b/dashboard/src/routes/config/bot.tsx @@ -262,7 +262,16 @@ function BotConfigPageContent() { // 加载源代码 const loadSourceCode = useCallback(async () => { try { - const raw = await getBotConfigRaw() + const result = await getBotConfigRaw() + if (!result.success) { + toast({ + variant: 'destructive', + title: '加载失败', + description: result.error, + }) + return + } + const raw = result.data // 将 TOML 基本字符串中的转义序列转换为实际字符以便在编辑器中正确显示 // 使用正则表达式只处理双引号字符串内的转义序列,不影响单引号字符串 const unescaped = raw.replace(/"([^"]*)"/g, (_match, content) => { @@ -289,8 +298,17 @@ function BotConfigPageContent() { const loadConfig = useCallback(async () => { try { setLoading(true) - const config = await getBotConfig() - parseAndSetConfig(config) + const result = await getBotConfig() + if (!result.success) { + toast({ + title: '加载失败', + description: result.error, + variant: 'destructive', + }) + setLoading(false) + return + } + parseAndSetConfig(result.data) setHasUnsavedChanges(false) initialLoadRef.current = false @@ -382,7 +400,18 @@ function BotConfigPageContent() { .replace(/\r/g, '\\r') // 回车符 return `"${encoded}"` }) - await updateBotConfigRaw(escaped) + const result = await updateBotConfigRaw(escaped) + if (!result.success) { + setHasTomlError(true) + const errorMsg = result.error + setTomlErrorMessage(errorMsg) + toast({ + variant: 'destructive', + title: '保存失败', + description: errorMsg, + }) + return + } setHasUnsavedChanges(false) setHasTomlError(false) setTomlErrorMessage('') @@ -423,8 +452,16 @@ function BotConfigPageContent() { } else { // 切换回可视化时,直接重新加载配置但不显示全局 loading try { - const config = await getBotConfig() - parseAndSetConfig(config) + const result = await getBotConfig() + if (!result.success) { + toast({ + title: '加载失败', + description: result.error, + variant: 'destructive', + }) + return + } + parseAndSetConfig(result.data) setHasUnsavedChanges(false) } catch (error) { console.error('加载配置失败:', error) @@ -444,7 +481,16 @@ function BotConfigPageContent() { // 取消待处理的自动保存 cancelPendingAutoSave() - await updateBotConfig(buildFullConfig()) + const result = await updateBotConfig(buildFullConfig()) + if (!result.success) { + toast({ + title: '保存失败', + description: result.error, + variant: 'destructive', + }) + setSaving(false) + return + } setHasUnsavedChanges(false) toast({ title: '保存成功', @@ -474,7 +520,16 @@ function BotConfigPageContent() { // 取消待处理的自动保存 cancelPendingAutoSave() - await updateBotConfig(buildFullConfig()) + const result = await updateBotConfig(buildFullConfig()) + if (!result.success) { + toast({ + title: '保存失败', + description: result.error, + variant: 'destructive', + }) + setSaving(false) + return + } setHasUnsavedChanges(false) toast({ title: '保存成功', @@ -759,4 +814,4 @@ function BotConfigPageContent() { ) -} \ No newline at end of file +} diff --git a/dashboard/src/routes/config/bot/hooks/useAutoSave.ts b/dashboard/src/routes/config/bot/hooks/useAutoSave.ts index 27bfbb70..b6871011 100644 --- a/dashboard/src/routes/config/bot/hooks/useAutoSave.ts +++ b/dashboard/src/routes/config/bot/hooks/useAutoSave.ts @@ -187,6 +187,13 @@ export function useAutoSave( const saveSection = useCallback( async (sectionName: ConfigSectionName, sectionData: unknown) => { try { + setAutoSaving(true) + const result = await updateBotConfigSection(sectionName, sectionData) + if (!result.success) { + throw new Error(result.error) + } + setHasUnsavedChanges(false) + onSaveSuccess?.() setAutoSaving(true) await updateBotConfigSection(sectionName, sectionData) setHasUnsavedChanges(false) diff --git a/dashboard/src/routes/config/model.tsx b/dashboard/src/routes/config/model.tsx index ddba7681..8d39dc1c 100644 --- a/dashboard/src/routes/config/model.tsx +++ b/dashboard/src/routes/config/model.tsx @@ -179,7 +179,17 @@ function ModelConfigPageContent() { const loadConfig = useCallback(async () => { try { setLoading(true) - const config = await getModelConfig() + const result = await getModelConfig() + if (!result.success) { + toast({ + title: '加载失败', + description: result.error, + variant: 'destructive', + }) + setLoading(false) + return + } + const config = result.data const modelList = (config.models as ModelInfo[]) || [] setModels(modelList) @@ -288,11 +298,30 @@ function ModelConfigPageContent() { try { setSaving(true) clearAutoSaveTimers() - const config = await getModelConfig() + const resultGet = await getModelConfig() + if (!resultGet.success) { + toast({ + title: '保存失败', + description: resultGet.error, + variant: 'destructive', + }) + setSaving(false) + return + } + const config = resultGet.data // 清理每个模型中的 null 值 config.models = models.map(cleanModelForSave) config.model_task_config = taskConfig - await updateModelConfig(config) + const resultUpdate = await updateModelConfig(config) + if (!resultUpdate.success) { + toast({ + title: '保存失败', + description: resultUpdate.error, + variant: 'destructive', + }) + setSaving(false) + return + } setHasUnsavedChanges(false) toast({ title: '保存成功', @@ -318,11 +347,30 @@ function ModelConfigPageContent() { // 先取消自动保存定时器 clearAutoSaveTimers() - const config = await getModelConfig() + const resultGet = await getModelConfig() + if (!resultGet.success) { + toast({ + title: '保存失败', + description: resultGet.error, + variant: 'destructive', + }) + setSaving(false) + return + } + const config = resultGet.data // 清理每个模型中的 null 值 config.models = models.map(cleanModelForSave) config.model_task_config = taskConfig - await updateModelConfig(config) + const resultUpdate = await updateModelConfig(config) + if (!resultUpdate.success) { + toast({ + title: '保存失败', + description: resultUpdate.error, + variant: 'destructive', + }) + setSaving(false) + return + } setHasUnsavedChanges(false) toast({ title: '保存成功', diff --git a/dashboard/src/routes/config/model/hooks/useModelAutoSave.ts b/dashboard/src/routes/config/model/hooks/useModelAutoSave.ts index fdb2d57a..3bec158a 100644 --- a/dashboard/src/routes/config/model/hooks/useModelAutoSave.ts +++ b/dashboard/src/routes/config/model/hooks/useModelAutoSave.ts @@ -3,6 +3,7 @@ * 监听 models 和 taskConfig 变化,自动保存到服务器 */ import { useRef, useEffect, useCallback } from 'react' +import type { RefObject } from 'react' import { updateModelConfigSection } from '@/lib/config-api' import type { ModelInfo, ModelTaskConfig } from '../types' @@ -23,7 +24,7 @@ interface UseModelAutoSaveReturn { /** 清除所有待执行的保存定时器 */ clearTimers: () => void /** 初始加载状态标记引用 (用于设置初始加载完成) */ - initialLoadRef: React.MutableRefObject + initialLoadRef: RefObject } /** @@ -84,7 +85,10 @@ export function useModelAutoSave( onSavingChange?.(true) // 清理每个模型中的 null 值 const cleanedModels = newModels.map(cleanModelForSave) - await updateModelConfigSection('models', cleanedModels) + const result = await updateModelConfigSection('models', cleanedModels) + if (!result.success) { + throw new Error(result.error) + } onUnsavedChange?.(false) } catch (error) { console.error('自动保存模型列表失败:', error) @@ -98,7 +102,10 @@ export function useModelAutoSave( const autoSaveTaskConfig = useCallback(async (newTaskConfig: ModelTaskConfig) => { try { onSavingChange?.(true) - await updateModelConfigSection('model_task_config', newTaskConfig) + const result = await updateModelConfigSection('model_task_config', newTaskConfig) + if (!result.success) { + throw new Error(result.error) + } onUnsavedChange?.(false) } catch (error) { console.error('自动保存任务配置失败:', error) diff --git a/dashboard/src/routes/config/model/hooks/useModelFetcher.ts b/dashboard/src/routes/config/model/hooks/useModelFetcher.ts index 2073b95d..073c6bfa 100644 --- a/dashboard/src/routes/config/model/hooks/useModelFetcher.ts +++ b/dashboard/src/routes/config/model/hooks/useModelFetcher.ts @@ -88,11 +88,15 @@ export function useModelFetcher(options: UseModelFetcherOptions): UseModelFetche setModelFetchError(null) try { - const models = await fetchProviderModels( + const result = await fetchProviderModels( providerName, template.modelFetcher.parser, template.modelFetcher.endpoint ) + if (!result.success) { + throw new Error(result.error) + } + const models = result.data setAvailableModels(models) // 更新缓存 modelListCache.set(cacheKey, { models, timestamp: Date.now() }) diff --git a/dashboard/src/routes/config/modelProvider.tsx b/dashboard/src/routes/config/modelProvider.tsx index 9dd675e4..d42f8ba0 100644 --- a/dashboard/src/routes/config/modelProvider.tsx +++ b/dashboard/src/routes/config/modelProvider.tsx @@ -206,7 +206,17 @@ function ModelProviderConfigPageContent() { const loadConfig = async () => { try { setLoading(true) - const config = await getModelConfig() + const result = await getModelConfig() + if (!result.success) { + toast({ + title: '加载失败', + description: result.error, + variant: 'destructive', + }) + setLoading(false) + return + } + const config = result.data setProviders((config.api_providers as APIProvider[]) || []) setHasUnsavedChanges(false) initialLoadRef.current = false @@ -246,7 +256,17 @@ function ModelProviderConfigPageContent() { return } - const config = await getModelConfig() + const resultGet = await getModelConfig() + if (!resultGet.success) { + toast({ + title: '保存失败', + description: resultGet.error, + variant: 'destructive', + }) + setSaving(false) + return + } + const config = resultGet.data // 获取所有有效的 provider 名称 const validProviderNames = new Set(cleanedProviders.map(p => p.name)) @@ -260,7 +280,16 @@ function ModelProviderConfigPageContent() { config.api_providers = cleanedProviders config.models = filteredModels - await updateModelConfig(config) + const resultUpdate = await updateModelConfig(config) + if (!resultUpdate.success) { + toast({ + title: '保存失败', + description: resultUpdate.error, + variant: 'destructive', + }) + setSaving(false) + return + } setHasUnsavedChanges(false) toast({ title: '保存成功', @@ -284,7 +313,12 @@ function ModelProviderConfigPageContent() { context: 'auto' | 'manual' | 'restart' = 'auto' ) => { try { - const config = await getModelConfig() + const result = await getModelConfig() + if (!result.success) { + console.error('加载配置失败:', result.error) + return { shouldProceed: true, providers: newProviders } + } + const config = result.data const oldProviderNames = new Set(providers.map(p => p.name)) const newProviderNames = new Set(newProviders.map(p => p.name)) @@ -334,7 +368,17 @@ function ModelProviderConfigPageContent() { setDeleteConfirmState(prev => ({ ...prev, isOpen: false })) - const config = await getModelConfig() + const resultGet = await getModelConfig() + if (!resultGet.success) { + toast({ + title: '加载失败', + description: resultGet.error, + variant: 'destructive', + }) + savingFlag(false) + return + } + const config = resultGet.data // 清理 providers 数据 const cleanedProviders = deleteConfirmState.pendingProviders.map(cleanProviderData) @@ -348,7 +392,7 @@ function ModelProviderConfigPageContent() { return validProviderNames.has(model.api_provider) }) - // 获取被删除的模型名称 + // 获取被削除的模型名称 const deletedModelNames = new Set( deleteConfirmState.affectedModels.map((m: any) => m.name) ) @@ -371,7 +415,16 @@ function ModelProviderConfigPageContent() { config.models = filteredModels config.model_task_config = modelTaskConfig - await updateModelConfig(config) + const resultUpdate = await updateModelConfig(config) + if (!resultUpdate.success) { + toast({ + title: '保存失败', + description: resultUpdate.error, + variant: 'destructive', + }) + savingFlag(false) + return + } // 更新本地状态 setProviders(deleteConfirmState.pendingProviders) @@ -449,7 +502,17 @@ function ModelProviderConfigPageContent() { setAutoSaving(true) // 清理 providers 数据:将 null 值转换为默认值 const cleanedProviders = newProviders.map(cleanProviderData) - await updateModelConfigSection('api_providers', cleanedProviders) + const result = await updateModelConfigSection('api_providers', cleanedProviders) + if (!result.success) { + console.error('自动保存失败:', result.error) + toast({ + title: '自动保存失败', + description: result.error, + variant: 'destructive', + }) + setHasUnsavedChanges(true) + return + } setHasUnsavedChanges(false) } catch (error) { console.error('自动保存失败:', error) @@ -509,7 +572,17 @@ function ModelProviderConfigPageContent() { return } - const config = await getModelConfig() + const resultGet = await getModelConfig() + if (!resultGet.success) { + toast({ + title: '保存失败', + description: resultGet.error, + variant: 'destructive', + }) + setSaving(false) + return + } + const config = resultGet.data // 获取所有有效的 provider 名称 const validProviderNames = new Set(cleanedProviders.map(p => p.name)) @@ -519,12 +592,12 @@ function ModelProviderConfigPageContent() { const filteredModels = originalModels.filter((model: any) => { const isValid = validProviderNames.has(model.api_provider) if (!isValid) { - console.warn(`模型 "${model.name}" 引用了已删除的提供商 "${model.api_provider}",将被移除`) + console.warn(`模型 "${model.name}" 引用了已删除的提供商 "${model.api_provider}"、将被移除`) } return isValid }) - // 如果有模型被移除,显示警告 + // 如果有模型被移除、显示警告 if (originalModels.length !== filteredModels.length) { const removedCount = originalModels.length - filteredModels.length toast({ @@ -539,7 +612,16 @@ function ModelProviderConfigPageContent() { config.models = filteredModels console.log('完整配置数据:', config) - await updateModelConfig(config) + const resultUpdate = await updateModelConfig(config) + if (!resultUpdate.success) { + toast({ + title: '保存失败', + description: resultUpdate.error, + variant: 'destructive', + }) + setSaving(false) + return + } setHasUnsavedChanges(false) toast({ title: '保存成功', @@ -804,31 +886,40 @@ function ModelProviderConfigPageContent() { try { const result = await testProviderConnection(providerName) - setTestResults(prev => new Map(prev).set(providerName, result)) - + if (!result.success) { + toast({ + title: '测试失败', + description: result.error, + variant: 'destructive', + }) + return + } + const testResult = result.data + setTestResults(prev => new Map(prev).set(providerName, testResult)) + // 显示结果 toast - if (result.network_ok) { - if (result.api_key_valid === true) { + if (testResult.network_ok) { + if (testResult.api_key_valid === true) { toast({ title: '连接正常', - description: `${providerName} 网络连接正常,API Key 有效 (${result.latency_ms}ms)`, + description: `${providerName} 网络连接正常、API Key 有效 (${testResult.latency_ms}ms)`, }) - } else if (result.api_key_valid === false) { + } else if (testResult.api_key_valid === false) { toast({ title: '连接正常但 Key 无效', - description: `${providerName} 网络连接正常,但 API Key 无效或已过期`, + description: `${providerName} 网络连接正常、但 API Key 无效或已过期`, variant: 'destructive', }) } else { toast({ title: '网络连接正常', - description: `${providerName} 可以访问 (${result.latency_ms}ms)`, + description: `${providerName} 可以访问 (${testResult.latency_ms}ms)`, }) } } else { toast({ title: '连接失败', - description: result.error || '无法连接到提供商', + description: testResult.error || '无法连接到提供商', variant: 'destructive', }) } diff --git a/dashboard/src/types/api.ts b/dashboard/src/types/api.ts index 8727727f..03d11739 100644 --- a/dashboard/src/types/api.ts +++ b/dashboard/src/types/api.ts @@ -5,4 +5,4 @@ export type ApiResponse = | { success: true; data: T } - | { success: false; error: string } + | { success: false; error: string } \ No newline at end of file