From 5793b71441e729d720be815542432d8cfb0d71c0 Mon Sep 17 00:00:00 2001 From: DennyHo0917 <149746199+DennyHo0917@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:37:46 +0800 Subject: [PATCH] feat: add API Route provider Add API Route as a built-in OpenAI-compatible gateway using HyperChat existing generic provider runtime. --- .../src/zodSchemas/appSettingsSchema.mts | 323 +-- .../web/src/components/ProviderSettings.tsx | 2324 +++++++++-------- 2 files changed, 1325 insertions(+), 1322 deletions(-) diff --git a/packages/shared/src/zodSchemas/appSettingsSchema.mts b/packages/shared/src/zodSchemas/appSettingsSchema.mts index ad783ae4..cf1da575 100644 --- a/packages/shared/src/zodSchemas/appSettingsSchema.mts +++ b/packages/shared/src/zodSchemas/appSettingsSchema.mts @@ -1,163 +1,164 @@ -import { z } from "zod"; - -// 已知提供商类型 -export const KnownProviderSchema = z.enum([ - "openai", - "anthropic", - "gemini", +import { z } from "zod"; + +// 已知提供商类型 +export const KnownProviderSchema = z.enum([ + "openai", + "anthropic", + "gemini", "302", + "api-route", "openrouter", - "qwen", - "deepseek", - "doubao", - "xai", - "glm", - "ollama", - "kimi", - "unknown", // 用于未知或不支持的提供商,需要自己填baseURL和apiKey -]).describe("Supported AI model providers"); - -// 提供商配置 Schema -export const ProviderConfigSchema = z.object({ - key: KnownProviderSchema.describe("Provider unique identifier"), - label: z.string().describe("Display name"), - baseURL: z.string().url().describe("API base URL"), - icon: z.string().optional().describe("Icon"), - description: z.string().optional().describe("Description"), - hasApiKey: z.boolean().default(true).describe("Requires API Key"), - apiKey: z.string().optional().describe("API Key"), - isBuiltIn: z.boolean().default(false).describe("Built-in provider"), -}).describe("AI model provider configuration"); - -// AI模型配置项 Schema -export const AIModelConfigItemSchema = z.object({ - key: z.string().describe("Model unique identifier"), - name: z.string().describe("Model name"), - model: z.string().describe("Model identifier"), - provider: KnownProviderSchema.describe("Provider"), - supportImage: z.boolean().default(true).describe("Supports image"), - supportTool: z.boolean().default(true).describe("Supports tool calling"), - call_tool_step: z.number().optional().describe("Tool calling steps"), - type: z.enum(["llm", "embedding"]).default("llm").describe("Model type"), - toolMode: z.enum(["standard", "compatible"]).default("standard").describe("Tool mode"), - // 保留兼容性 - apiKey: z.string().default("").describe("API Key (deprecated, get from provider)"), - baseURL: z.string().default("").describe("Base URL (deprecated, get from provider)"), - fullName: z.string().optional().describe("Full name (provider:model name)"), -}).describe("AI model configuration item"); - -// AI配置 Schema -export const AIConfigSchema = z.object({ - models: z.array(AIModelConfigItemSchema).default([]).describe("AI model list"), - customProviders: z.array(ProviderConfigSchema).default([]).describe("Custom provider list"), - builtinApiKeys: z.record(z.object({ - apiKey: z.string().describe("API Key"), - baseURL: z.string().describe("Base URL"), - })).default({}).describe("Built-in provider API Key configuration"), - defaultModel: z.string().optional().describe("Default model"), -}).describe("AI related configuration"); - -// 外观设置 Schema -export const AppearanceSchema = z.object({ - darkTheme: z.boolean().default(false).describe("Enable dark mode"), -}); - -// 桌面应用设置 Schema -export const DesktopSchema = z.object({ - closeAction: z.enum(["minimize", "exit"]).default("exit").describe("Window close action"), - windowSize: z.object({ - width: z.number().min(800).max(4000).default(1440).describe("Window width"), - height: z.number().min(600).max(3000).default(900).describe("Window height"), - }).default({}), -}); - - -// MCP Gateway 配置 Schema -export const MCPGatewaySchema = z.object({ - name: z.string().describe("Gateway name"), - description: z.string().optional().describe("Gateway description"), - allowMCPs: z.array(z.string()).default([]).describe("Allowed MCP list"), - blockMCPTools: z.array(z.string()).default([]).describe("Blocked MCP tool display names"), -}).describe("MCP gateway configuration"); - -// 系统设置 Schema -export const SystemSchema = z.object({ - isDeveloper: z.boolean().default(false).describe("Developer mode"), -}); - - -// 完整的应用设置 Schema -export const AppSettingsSchema = z.object({ - // 系统信息(只读) - version: z.string().default("").describe("Application version"), - appDataDir: z.string().default("").describe("Application data directory"), - logFilePath: z.string().default("").describe("Log file path"), - PATH: z.string().default("").describe("System PATH"), - platform: z.string().default("").describe("Operating system platform"), - uuid: z.string().default("").describe("Application unique identifier"), - - // 用户可配置设置 - appearance: AppearanceSchema.default({}), - system: SystemSchema.default({}), - desktop: DesktopSchema.default({}), - ai: AIConfigSchema.default({}), - mcpGateWays: z.array(MCPGatewaySchema).default([]).describe("MCP gateway configuration list"), - -}); - -// 导出类型 -export type AppSettings = z.infer; -export type AppearanceSettings = z.infer; -export type SystemSettings = z.infer; -export type DesktopSettings = z.infer; -export type AISettings = z.infer; -export type AIModelConfigItem = z.infer; -export type ProviderConfig = z.infer; -export type KnownProvider = z.infer; -export type MCPGateway = z.infer; - -// 默认设置(不包含 UUID 生成,因为前端不能使用 uuid 库) -export const DEFAULT_APP_SETTINGS: Omit = (() => { - const result = AppSettingsSchema.safeParse({}); - if (result.success) { - const { uuid, ...rest } = result.data; - return rest; - } - // 如果解析失败,返回基础默认值 - throw new Error("Failed to generate default app settings from schema"); -})(); - -// 验证函数 -export function validateAppSettings(data: unknown): data is AppSettings { - return AppSettingsSchema.safeParse(data).success; -} - -export function validateAppearanceSettings(data: unknown): data is AppearanceSettings { - return AppearanceSchema.safeParse(data).success; -} - - -export function validateSystemSettings(data: unknown): data is SystemSettings { - return SystemSchema.safeParse(data).success; -} - -export function validateDesktopSettings(data: unknown): data is DesktopSettings { - return DesktopSchema.safeParse(data).success; -} - -export function validateAISettings(data: unknown): data is AISettings { - return AIConfigSchema.safeParse(data).success; -} - -export function validateAIModelConfigItem(data: unknown): data is AIModelConfigItem { - return AIModelConfigItemSchema.safeParse(data).success; -} - -export function validateProviderConfig(data: unknown): data is ProviderConfig { - return ProviderConfigSchema.safeParse(data).success; -} - -export function validateMCPGateway(data: unknown): data is MCPGateway { - return MCPGatewaySchema.safeParse(data).success; -} - + "qwen", + "deepseek", + "doubao", + "xai", + "glm", + "ollama", + "kimi", + "unknown", // 用于未知或不支持的提供商,需要自己填baseURL和apiKey +]).describe("Supported AI model providers"); + +// 提供商配置 Schema +export const ProviderConfigSchema = z.object({ + key: KnownProviderSchema.describe("Provider unique identifier"), + label: z.string().describe("Display name"), + baseURL: z.string().url().describe("API base URL"), + icon: z.string().optional().describe("Icon"), + description: z.string().optional().describe("Description"), + hasApiKey: z.boolean().default(true).describe("Requires API Key"), + apiKey: z.string().optional().describe("API Key"), + isBuiltIn: z.boolean().default(false).describe("Built-in provider"), +}).describe("AI model provider configuration"); + +// AI模型配置项 Schema +export const AIModelConfigItemSchema = z.object({ + key: z.string().describe("Model unique identifier"), + name: z.string().describe("Model name"), + model: z.string().describe("Model identifier"), + provider: KnownProviderSchema.describe("Provider"), + supportImage: z.boolean().default(true).describe("Supports image"), + supportTool: z.boolean().default(true).describe("Supports tool calling"), + call_tool_step: z.number().optional().describe("Tool calling steps"), + type: z.enum(["llm", "embedding"]).default("llm").describe("Model type"), + toolMode: z.enum(["standard", "compatible"]).default("standard").describe("Tool mode"), + // 保留兼容性 + apiKey: z.string().default("").describe("API Key (deprecated, get from provider)"), + baseURL: z.string().default("").describe("Base URL (deprecated, get from provider)"), + fullName: z.string().optional().describe("Full name (provider:model name)"), +}).describe("AI model configuration item"); + +// AI配置 Schema +export const AIConfigSchema = z.object({ + models: z.array(AIModelConfigItemSchema).default([]).describe("AI model list"), + customProviders: z.array(ProviderConfigSchema).default([]).describe("Custom provider list"), + builtinApiKeys: z.record(z.object({ + apiKey: z.string().describe("API Key"), + baseURL: z.string().describe("Base URL"), + })).default({}).describe("Built-in provider API Key configuration"), + defaultModel: z.string().optional().describe("Default model"), +}).describe("AI related configuration"); + +// 外观设置 Schema +export const AppearanceSchema = z.object({ + darkTheme: z.boolean().default(false).describe("Enable dark mode"), +}); + +// 桌面应用设置 Schema +export const DesktopSchema = z.object({ + closeAction: z.enum(["minimize", "exit"]).default("exit").describe("Window close action"), + windowSize: z.object({ + width: z.number().min(800).max(4000).default(1440).describe("Window width"), + height: z.number().min(600).max(3000).default(900).describe("Window height"), + }).default({}), +}); + + +// MCP Gateway 配置 Schema +export const MCPGatewaySchema = z.object({ + name: z.string().describe("Gateway name"), + description: z.string().optional().describe("Gateway description"), + allowMCPs: z.array(z.string()).default([]).describe("Allowed MCP list"), + blockMCPTools: z.array(z.string()).default([]).describe("Blocked MCP tool display names"), +}).describe("MCP gateway configuration"); + +// 系统设置 Schema +export const SystemSchema = z.object({ + isDeveloper: z.boolean().default(false).describe("Developer mode"), +}); + + +// 完整的应用设置 Schema +export const AppSettingsSchema = z.object({ + // 系统信息(只读) + version: z.string().default("").describe("Application version"), + appDataDir: z.string().default("").describe("Application data directory"), + logFilePath: z.string().default("").describe("Log file path"), + PATH: z.string().default("").describe("System PATH"), + platform: z.string().default("").describe("Operating system platform"), + uuid: z.string().default("").describe("Application unique identifier"), + + // 用户可配置设置 + appearance: AppearanceSchema.default({}), + system: SystemSchema.default({}), + desktop: DesktopSchema.default({}), + ai: AIConfigSchema.default({}), + mcpGateWays: z.array(MCPGatewaySchema).default([]).describe("MCP gateway configuration list"), + +}); + +// 导出类型 +export type AppSettings = z.infer; +export type AppearanceSettings = z.infer; +export type SystemSettings = z.infer; +export type DesktopSettings = z.infer; +export type AISettings = z.infer; +export type AIModelConfigItem = z.infer; +export type ProviderConfig = z.infer; +export type KnownProvider = z.infer; +export type MCPGateway = z.infer; + +// 默认设置(不包含 UUID 生成,因为前端不能使用 uuid 库) +export const DEFAULT_APP_SETTINGS: Omit = (() => { + const result = AppSettingsSchema.safeParse({}); + if (result.success) { + const { uuid, ...rest } = result.data; + return rest; + } + // 如果解析失败,返回基础默认值 + throw new Error("Failed to generate default app settings from schema"); +})(); + +// 验证函数 +export function validateAppSettings(data: unknown): data is AppSettings { + return AppSettingsSchema.safeParse(data).success; +} + +export function validateAppearanceSettings(data: unknown): data is AppearanceSettings { + return AppearanceSchema.safeParse(data).success; +} + + +export function validateSystemSettings(data: unknown): data is SystemSettings { + return SystemSchema.safeParse(data).success; +} + +export function validateDesktopSettings(data: unknown): data is DesktopSettings { + return DesktopSchema.safeParse(data).success; +} + +export function validateAISettings(data: unknown): data is AISettings { + return AIConfigSchema.safeParse(data).success; +} + +export function validateAIModelConfigItem(data: unknown): data is AIModelConfigItem { + return AIModelConfigItemSchema.safeParse(data).success; +} + +export function validateProviderConfig(data: unknown): data is ProviderConfig { + return ProviderConfigSchema.safeParse(data).success; +} + +export function validateMCPGateway(data: unknown): data is MCPGateway { + return MCPGatewaySchema.safeParse(data).success; +} + diff --git a/packages/web/src/components/ProviderSettings.tsx b/packages/web/src/components/ProviderSettings.tsx index 2a1abb5d..08c24261 100644 --- a/packages/web/src/components/ProviderSettings.tsx +++ b/packages/web/src/components/ProviderSettings.tsx @@ -1,1165 +1,1167 @@ -import React, { useState, useEffect } from 'react'; -import { - Card, - Form, - Input, - Button, - Space, - Typography, - Tag, - Row, - Col, - message, - Modal, - Table, - Popconfirm, - Select, - Switch, - Radio, -} from 'antd'; -import { - PlusOutlined, - CheckOutlined, - EditOutlined, - DeleteOutlined, - SettingOutlined, - ArrowLeftOutlined, - ReloadOutlined, -} from '@ant-design/icons'; - -import type { AIModelConfigItem, ProviderConfig, KnownProvider } from '@dadigua/hyperchat-shared'; -import { useAISettings } from "../contexts/AppSettingsContext"; -import { t } from '../i18n'; -import { call } from '../common/call'; - -const { Title, Text } = Typography; -const { Option } = Select; - -// 模型编辑接口 -interface ModelFormData { - name?: string; - model: string; - type: 'llm' | 'embedding'; - toolMode: 'standard' | 'compatible'; - supportImage: boolean; - supportTool: boolean; - // Unknown provider specific fields - apiKey?: string; - baseURL?: string; -} - -// 远程模型信息接口 -interface RemoteModel { - id: string; - object: string; - created?: number; - owned_by?: string; -} - -// 智能模型选择器组件 -const SmartModelSelector: React.FC<{ - value?: string; - onChange?: (value: string) => void; - placeholder?: string; - remoteModels: RemoteModel[]; - loadingModels: boolean; - canFetchModels: boolean; - onRefreshModels: () => void; -}> = ({ - value, - onChange, - placeholder = t`e.g., gpt-4.1`, - remoteModels, - loadingModels, - canFetchModels, - onRefreshModels -}) => { - if (canFetchModels && remoteModels.length > 0) { - // 显示选择器 - return ( - - ); - } else { - // 显示输入框 - return ( - onChange?.(e.target.value)} - placeholder={placeholder} - suffix={ - canFetchModels ? ( - - )} - - handleDeleteModel(record)} - okText={t`Yes`} - cancelText={t`No`} - > - - - - ), - }, - ]; - - // 渲染提供商视图 - const renderProvidersView = () => ( -
-
-
- {t`AI Provider Settings`} - - {t`Configure API keys for different AI providers. Click a provider to manage its models.`} - -
- {/* */} -
- - - {providers.map((provider) => ( - - - {!provider.isBuiltIn && ( -
- ); - - // 渲染模型管理视图 - const renderModelsView = () => ( -
-
-
-
- - {/* Unknown 提供商不显示 API Key 按钮,因为每个模型单独配置 */} - {selectedProvider?.key !== 'unknown' && ( - - )} - - -
- - - - ); - - return ( -
- {view === 'providers' ? renderProvidersView() : renderModelsView()} - - {/* API Key 配置 Modal */} - setIsApiKeyModalOpen(false)} - footer={null} - width={500} - > - {selectedProvider && ( -
-
-
- {selectedProvider.icon} - {selectedProvider.label} -
- {selectedProvider.description} -
- - - - - - {selectedProvider.isBuiltIn && ( - - - - )} - - {!selectedProvider.isBuiltIn && ( - - - - )} - - - - - -
- - -
- - )} -
- - {/* 模型编辑 Modal */} - setIsModelModalOpen(false)} - footer={null} - width={600} - > -
- - {/* Unknown provider specific fields */} - {selectedProvider?.key === 'unknown' && ( - <> - - - - - - - - )} - 0 - ? t`Select from ${remoteModels.length} available models` - : t`Enter model ID manually` - } - > - selectedProvider && fetchRemoteModels(selectedProvider)} - placeholder={t`e.g., gpt-4.1`} - /> - - - - - - - - LLM - Embedding - - - - - - - - -
- - - - - - - - - - - - -
- - -
- - - - {/* 提供商管理 Modal */} - setIsProviderModalOpen(false)} - footer={null} - width={500} - > -
- { - if (!value) return; - // 检查key是否唯一(编辑时排除自己) - const existingProvider = providers.find(p => - p.key === value && (!editingProvider || p.key !== editingProvider.key) - ); - if (existingProvider) { - throw new Error(t`Provider key already exists`); - } - } - } - ]} - > - - - - - - - - - - - - - -
- - -
- -
- - ); -} + { key: 'qwen', label: 'Qwen', baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', icon: 'qwen', description: 'Alibaba Qwen models', hasApiKey: true, isBuiltIn: true }, + { key: 'deepseek', label: 'DeepSeek', baseURL: 'https://api.deepseek.com', icon: 'deepseek', description: 'DeepSeek models', hasApiKey: true, isBuiltIn: true }, + { key: 'doubao', label: 'Doubao', baseURL: 'https://ark.cn-beijing.volces.com/api/v3', icon: 'doubao', description: 'ByteDance Doubao models', hasApiKey: true, isBuiltIn: true }, + { key: 'xai', label: 'xAI', baseURL: 'https://api.x.ai/v1', icon: 'xai', description: 'xAI Grok models', hasApiKey: true, isBuiltIn: true }, + { key: 'glm', label: 'GLM', baseURL: 'https://open.bigmodel.cn/api/paas/v4', icon: 'glm', description: 'Zhipu GLM models', hasApiKey: true, isBuiltIn: true }, + { key: 'ollama', label: 'Ollama', baseURL: 'http://localhost:11434/v1', icon: 'ollama', description: 'Local Ollama models', hasApiKey: false, isBuiltIn: true }, + { key: 'kimi', label: 'Kimi', baseURL: 'https://api.moonshot.cn/v1', icon: 'kimi', description: 'Moonshot Kimi models', hasApiKey: true, isBuiltIn: true }, + { key: 'unknown', label: 'Unknown Provider', baseURL: '', icon: 'custom', description: 'OpenAI compatible provider, configure apiKey and baseURL per model', hasApiKey: false, isBuiltIn: true } + ]; + }; + + useEffect(() => { + refresh(); + }, [aiSettings]); + + // 处理提供商点击 - 如果有API Key则进入模型管理,否则配置API Key + const handleProviderClick = (provider: ProviderConfig) => { + if (hasProviderApiKey(provider)) { + setSelectedProvider(provider); + setView('models'); + } else { + handleAddApiKey(provider); + } + }; + + // 添加自定义提供商 + const handleAddProvider = () => { + setEditingProvider(null); + providerForm.resetFields(); + setIsProviderModalOpen(true); + }; + + // 编辑提供商 + const handleEditProvider = (provider: ProviderConfig) => { + if (provider.isBuiltIn) { + message.error(t`Built-in providers cannot be edited`); + return; + } + setEditingProvider(provider); + providerForm.setFieldsValue({ + key: provider.key, + label: provider.label, + baseURL: provider.baseURL, + description: provider.description, + }); + setIsProviderModalOpen(true); + }; + + // 删除提供商(内置不能删除,非内置则彻底删除并移除相关模型) + const handleDeleteProvider = async (provider: ProviderConfig) => { + if (provider.isBuiltIn) { + // 内置提供商不能删除 + message.error(t`Built-in providers cannot be deleted`); + return; + } + + try { + // 删除自定义提供商及其下所有模型 + if (!aiSettings) return; + + const updatedCustomProviders = aiSettings.customProviders?.filter(p => p.key !== provider.key) || []; + const updatedModels = aiSettings.models?.filter(model => model.provider !== provider.key) || []; + + await updateAISettings({ + customProviders: updatedCustomProviders, + models: updatedModels + }); + + message.success(t`Provider and all related models deleted successfully`); + // Context 会自动更新 aiSettings,只需要更新 providers 状态 + refresh(); + } catch (error) { + message.error(t`Failed to delete provider`); + console.error('Delete provider failed:', error); + } + }; + + // 保存提供商(新增或编辑) + // values: 表单提交的提供商信息 + const handleSaveProvider = async (values: any) => { + setLoading(true); + try { + if (!aiSettings) return; + + if (editingProvider) { + // 编辑现有提供商 + const updatedCustomProviders = aiSettings.customProviders?.map(p => + p.key === editingProvider.key + ? { ...p, label: values.label, baseURL: values.baseURL, description: values.description } + : p + ) || []; + + await updateAISettings({ + customProviders: updatedCustomProviders + }); + + message.success(t`Provider updated successfully`); + } else { + // 添加新提供商 + const newProvider: ProviderConfig = { + key: values.key as KnownProvider, + label: values.label, + baseURL: values.baseURL, + description: values.description, + hasApiKey: true, + isBuiltIn: false + }; + + const updatedCustomProviders = [...(aiSettings.customProviders || []), newProvider]; + + await updateAISettings({ + customProviders: updatedCustomProviders + }); + + message.success(t`Provider added successfully`); + } + + refresh(); + setIsProviderModalOpen(false); + } catch (error) { + message.error(t`Failed to save provider`); + console.error('Save provider failed:', error); + } finally { + setLoading(false); + } + }; + + // 配置API Key + const handleAddApiKey = (provider: ProviderConfig) => { + setSelectedProvider(provider); + apiKeyForm.resetFields(); + + // 获取已保存的 API Key 信息 + let apiKeyInfo: { apiKey?: string; baseURL?: string } | null = null; + if (aiSettings) { + if (provider.isBuiltIn && provider.key) { + apiKeyInfo = aiSettings.builtinApiKeys?.[provider.key] || null; + } else { + apiKeyInfo = { apiKey: provider.apiKey, baseURL: provider.baseURL }; + } + } + + apiKeyForm.setFieldsValue({ + provider: provider.key, + baseURL: apiKeyInfo?.baseURL || provider.baseURL, + apiKey: apiKeyInfo?.apiKey || '', + }); + setIsApiKeyModalOpen(true); + }; + + /** + * 保存API Key配置到对应的Provider(而不是模型) + * @param values 表单提交的API Key和BaseURL + */ + const handleSaveApiKey = async (values: any) => { + if (!selectedProvider || !aiSettings) return; + setLoading(true); + try { + if (selectedProvider.isBuiltIn) { + // 内置提供商,保存到 builtinApiKeys + const finalBaseURL = values.baseURL || selectedProvider.baseURL; + const updatedBuiltinApiKeys = { + ...(aiSettings.builtinApiKeys || {}), + [selectedProvider.key!]: { + apiKey: values.apiKey, + baseURL: finalBaseURL + } + }; + + await updateAISettings({ + builtinApiKeys: updatedBuiltinApiKeys + }); + } else { + // 自定义提供商,更新提供商配置 + const updatedCustomProviders = aiSettings.customProviders?.map(p => + p.key === selectedProvider.key + ? { ...p, apiKey: values.apiKey } + : p + ) || []; + + await updateAISettings({ + customProviders: updatedCustomProviders + }); + } + + message.success(t`API Key configured successfully!`); + refresh(); + setIsApiKeyModalOpen(false); + } catch (error) { + message.error(t`Failed to save configuration`); + console.error('Save failed:', error); + } finally { + setLoading(false); + } + }; + + // 添加/编辑模型 + const handleAddModel = () => { + setEditingModel(null); + modelForm.resetFields(); + modelForm.setFieldsValue({ + type: 'llm', + toolMode: 'standard', + supportImage: true, + supportTool: true, + }); + setIsModelModalOpen(true); + + // 尝试获取远程模型列表 + if (selectedProvider) { + fetchRemoteModels(selectedProvider); + } + }; + + const handleEditModel = (model: AIModelConfigItem) => { + setEditingModel(model); + modelForm.resetFields(); + const formValues: any = { + name: model.name, + model: model.model, + type: model.type, + toolMode: model.toolMode, + supportImage: model.supportImage ?? true, // 默认值为 true + supportTool: model.supportTool ?? true, // 默认值为 true + }; + + // 对于 unknown 提供商,添加 apiKey 和 baseURL 字段 + if (model.provider === 'unknown') { + formValues.apiKey = model.apiKey || ''; + formValues.baseURL = model.baseURL || ''; + } + + modelForm.setFieldsValue(formValues); + setIsModelModalOpen(true); + + // 尝试获取远程模型列表 + if (selectedProvider) { + fetchRemoteModels(selectedProvider); + } + }; + + const handleSaveModel = async (values: ModelFormData) => { + if (!selectedProvider || !aiSettings) return; + + setLoading(true); + try { + // 如果名称为空,使用模型ID作为名称 + const finalName = values.name?.trim() || values.model; + + let updatedModels = [...(aiSettings.models || [])]; + + if (editingModel) { + // 编辑现有模型 + const index = updatedModels.findIndex(m => m.key === editingModel.key); + if (index >= 0) { + const existingModel = updatedModels[index]; + if (!existingModel) return; + const updatedModel: AIModelConfigItem = { + ...existingModel, + name: finalName, + model: values.model, + type: values.type, + toolMode: values.toolMode, + supportImage: values.supportImage, + supportTool: values.supportTool, + // Update apiKey and baseURL for unknown provider + apiKey: selectedProvider.key === 'unknown' ? (values.apiKey || '') : existingModel.apiKey, + baseURL: selectedProvider.key === 'unknown' ? (values.baseURL || '') : existingModel.baseURL, + }; + + updatedModels[index] = updatedModel; + } + } else { + // 添加新模型 + const newModel: AIModelConfigItem = { + key: selectedProvider.key + ':' + values.model, // 使用提供商key和模型id生成唯一key + name: finalName, + model: values.model, + apiKey: selectedProvider.key === 'unknown' ? (values.apiKey || '') : '', // Unknown provider uses per-model apiKey + baseURL: selectedProvider.key === 'unknown' ? (values.baseURL || '') : '', // Unknown provider uses per-model baseURL + provider: selectedProvider.key as KnownProvider, + supportImage: values.supportImage, + supportTool: values.supportTool, + type: values.type, + toolMode: values.toolMode, + }; + updatedModels.push(newModel); + } + + await updateAISettings({ + models: updatedModels + }); + + refresh(); + setIsModelModalOpen(false); + message.success(editingModel ? t`Model updated successfully!` : t`Model added successfully!`); + } catch (error) { + message.error(t`Failed to save model`); + console.error('Save failed:', error); + } finally { + setLoading(false); + } + }; + + // 删除模型 + const handleDeleteModel = async (model: AIModelConfigItem) => { + try { + if (!aiSettings) return; + + const updatedModels = aiSettings.models?.filter(m => m.key !== model.key) || []; + + await updateAISettings({ + models: updatedModels + }); + + refresh(); + message.success(t`Model deleted successfully!`); + } catch (error) { + message.error(t`Failed to delete model`); + console.error('Delete failed:', error); + } + }; + + // 设置默认模型 + const handleSetDefaultModel = async (model: AIModelConfigItem) => { + try { + if (!aiSettings) return; + + await updateAISettings({ + defaultModel: model.key + }); + + refresh(); + message.success(t`Default model set successfully!`); + } catch (error) { + message.error(t`Failed to set default model`); + console.error('Set default failed:', error); + } + }; + + // 模型表格列配置 + const modelColumns = [ + { + title: t`Name`, + dataIndex: 'name', + key: 'name', + render: (name: string, record: AIModelConfigItem) => ( + + {name} + {aiSettings?.defaultModel === record.key && {t`Default`}} + + ), + }, + { + title: t`Model`, + dataIndex: 'model', + key: 'model', + }, + { + title: t`Type`, + dataIndex: 'type', + key: 'type', + render: (type: string) => ( + + {type?.toUpperCase() || 'UNKNOWN'} + + ), + }, + { + title: t`Features`, + key: 'features', + render: (_: any, record: AIModelConfigItem) => ( + + {record.supportImage && {t`Image`}} + {record.supportTool && {t`Tools`}} + + ), + }, + { + title: t`Actions`, + key: 'actions', + render: (_: any, record: AIModelConfigItem) => ( + + {aiSettings?.defaultModel !== record.key && record.type === "llm" && ( + + )} + + handleDeleteModel(record)} + okText={t`Yes`} + cancelText={t`No`} + > + + + + ), + }, + ]; + + // 渲染提供商视图 + const renderProvidersView = () => ( +
+
+
+ {t`AI Provider Settings`} + + {t`Configure API keys for different AI providers. Click a provider to manage its models.`} + +
+ {/* */} +
+ + + {providers.map((provider) => ( +
+ + {!provider.isBuiltIn && ( + + )} + + + + +
+ + ); + + return ( +
+ {view === 'providers' ? renderProvidersView() : renderModelsView()} + + {/* API Key 配置 Modal */} + setIsApiKeyModalOpen(false)} + footer={null} + width={500} + > + {selectedProvider && ( +
+
+
+ {selectedProvider.icon} + {selectedProvider.label} +
+ {selectedProvider.description} +
+ + + + + + {selectedProvider.isBuiltIn && ( + + + + )} + + {!selectedProvider.isBuiltIn && ( + + + + )} + + + + + +
+ + +
+ + )} +
+ + {/* 模型编辑 Modal */} + setIsModelModalOpen(false)} + footer={null} + width={600} + > +
+ + {/* Unknown provider specific fields */} + {selectedProvider?.key === 'unknown' && ( + <> + + + + + + + + )} + 0 + ? t`Select from ${remoteModels.length} available models` + : t`Enter model ID manually` + } + > + selectedProvider && fetchRemoteModels(selectedProvider)} + placeholder={t`e.g., gpt-4.1`} + /> + + + + + + + + LLM + Embedding + + + + + + + + +
+ + + + + + + + + + + + +
+ + +
+ + + + {/* 提供商管理 Modal */} + setIsProviderModalOpen(false)} + footer={null} + width={500} + > +
+ { + if (!value) return; + // 检查key是否唯一(编辑时排除自己) + const existingProvider = providers.find(p => + p.key === value && (!editingProvider || p.key !== editingProvider.key) + ); + if (existingProvider) { + throw new Error(t`Provider key already exists`); + } + } + } + ]} + > + + + + + + + + + + + + + +
+ + +
+ +
+ + ); +}