From 9aa6aff3f501b109d2f6b0678532a30d9988d6f6 Mon Sep 17 00:00:00 2001 From: Bowl42 Date: Sun, 18 Jan 2026 03:50:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BA=20Provider=20=E5=92=8C=20Rout?= =?UTF-8?q?es=20=E9=A1=B5=E9=9D=A2=E6=B7=BB=E5=8A=A0=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Provider 列表页添加搜索框,支持按名称和显示信息过滤 - Client Routes 页面 (/routes/claude 等) 添加搜索框 - 搜索同时过滤已配置路由和可用 Provider - 添加 Custom Provider 模板的 Model Mapping 支持 - 添加 NVIDIA 模板 (OpenAI 兼容,默认模型映射) - Provider 创建页面增加 Model Mapping 编辑功能 - Provider 新增 Logo 字段,支持模板自动设置 - 添加 88code、aicodemirror、nvidia 图标资源 - 补充 i18n 翻译 --- internal/domain/model.go | 3 + web/src/assets/icons/88code.svg | 4 + web/src/assets/icons/aicodemirror.png | Bin 0 -> 6151 bytes web/src/assets/icons/nvidia.svg | 1 + .../routes/ClientTypeRoutesContent.tsx | 30 +++++- web/src/components/ui/model-input.tsx | 12 +++ web/src/lib/transport/types.ts | 1 + web/src/locales/en.json | 8 +- web/src/locales/zh.json | 8 +- web/src/pages/client-routes/index.tsx | 24 ++++- .../components/custom-config-step.tsx | 97 +++++++++++++++++- .../providers/components/select-type-step.tsx | 2 + web/src/pages/providers/index.tsx | 35 ++++++- web/src/pages/providers/types.ts | 28 +++++ 14 files changed, 238 insertions(+), 15 deletions(-) create mode 100644 web/src/assets/icons/88code.svg create mode 100644 web/src/assets/icons/aicodemirror.png create mode 100644 web/src/assets/icons/nvidia.svg diff --git a/internal/domain/model.go b/internal/domain/model.go index 4c43da93..65ef3903 100644 --- a/internal/domain/model.go +++ b/internal/domain/model.go @@ -88,6 +88,9 @@ type Provider struct { // 展示的名称 Name string `json:"name"` + // Logo URL 或 data URI + Logo string `json:"logo,omitempty"` + // 配置 Config *ProviderConfig `json:"config"` diff --git a/web/src/assets/icons/88code.svg b/web/src/assets/icons/88code.svg new file mode 100644 index 00000000..e0a10593 --- /dev/null +++ b/web/src/assets/icons/88code.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/src/assets/icons/aicodemirror.png b/web/src/assets/icons/aicodemirror.png new file mode 100644 index 0000000000000000000000000000000000000000..dbbd7548086929cda2869302b8f7d5f9969213e5 GIT binary patch literal 6151 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 literal 0 HcmV?d00001 diff --git a/web/src/assets/icons/nvidia.svg b/web/src/assets/icons/nvidia.svg new file mode 100644 index 00000000..e427e2ce --- /dev/null +++ b/web/src/assets/icons/nvidia.svg @@ -0,0 +1 @@ +NVIDIA \ No newline at end of file diff --git a/web/src/components/routes/ClientTypeRoutesContent.tsx b/web/src/components/routes/ClientTypeRoutesContent.tsx index 6966017b..4758a3a7 100644 --- a/web/src/components/routes/ClientTypeRoutesContent.tsx +++ b/web/src/components/routes/ClientTypeRoutesContent.tsx @@ -48,11 +48,13 @@ import { AntigravityQuotasProvider } from '@/contexts/antigravity-quotas-context interface ClientTypeRoutesContentProps { clientType: ClientType; projectID: number; // 0 for global routes + searchQuery?: string; // Optional search query from parent } export function ClientTypeRoutesContent({ clientType, projectID, + searchQuery = '', }: ClientTypeRoutesContentProps) { const [activeId, setActiveId] = useState(null) const { data: providerStats = {} } = useProviderStats(clientType, projectID || undefined) @@ -100,7 +102,16 @@ export function ClientTypeRoutesContent({ }); // Only show providers that have routes - const filteredItems = allItems.filter((item) => item.route); + let filteredItems = allItems.filter((item) => item.route); + + // Apply search filter + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + filteredItems = filteredItems.filter((item) => + item.provider.name.toLowerCase().includes(query) || + item.provider.type.toLowerCase().includes(query) + ); + } return filteredItems.sort((a, b) => { if (a.route && b.route) return a.route.position - b.route.position; @@ -110,15 +121,26 @@ export function ClientTypeRoutesContent({ if (!a.isNative && b.isNative) return 1; return a.provider.name.localeCompare(b.provider.name); }); - }, [providers, clientRoutes, clientType]); + }, [providers, clientRoutes, clientType, searchQuery]); // Get available providers (without routes yet) const availableProviders = useMemo((): Provider[] => { - return providers.filter((p) => { + let available = providers.filter((p) => { const hasRoute = clientRoutes.some((r) => Number(r.providerID) === Number(p.id)); return !hasRoute; }); - }, [providers, clientRoutes]); + + // Apply search filter + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + available = available.filter((p) => + p.name.toLowerCase().includes(query) || + p.type.toLowerCase().includes(query) + ); + } + + return available; + }, [providers, clientRoutes, searchQuery]); const activeItem = activeId ? items.find((item) => item.id === activeId) : null; diff --git a/web/src/components/ui/model-input.tsx b/web/src/components/ui/model-input.tsx index 36f33478..d1c581ba 100644 --- a/web/src/components/ui/model-input.tsx +++ b/web/src/components/ui/model-input.tsx @@ -48,6 +48,9 @@ const COMMON_MODELS = [ { id: '*gpt*', name: 'All GPT models', provider: 'OpenAI' }, { id: '*o1*', name: 'All o1 models', provider: 'OpenAI' }, { id: '*o3*', name: 'All o3 models', provider: 'OpenAI' }, + // NVIDIA/Meta wildcards + { id: '*llama*', name: 'All Llama models', provider: 'NVIDIA' }, + { id: 'meta/*', name: 'All Meta models', provider: 'NVIDIA' }, // OpenAI models { id: 'gpt-4o', name: 'GPT-4o', provider: 'OpenAI' }, { id: 'gpt-4o-mini', name: 'GPT-4o Mini', provider: 'OpenAI' }, @@ -58,6 +61,15 @@ const COMMON_MODELS = [ { id: 'o1-mini', name: 'o1 Mini', provider: 'OpenAI' }, { id: 'o1-pro', name: 'o1 Pro', provider: 'OpenAI' }, { id: 'o3-mini', name: 'o3 Mini', provider: 'OpenAI' }, + // NVIDIA models + { id: 'minimaxai/minimax-m2.1', name: 'MiniMax M2.1', provider: 'NVIDIA' }, + { id: 'z-ai/glm4.7', name: 'GLM 4.7', provider: 'NVIDIA' }, + { id: 'deepseek-ai/deepseek-rl', name: 'DeepSeek RL', provider: 'NVIDIA' }, + { id: 'qwen/qwen2.5-coder-32b-instruct', name: 'Qwen 2.5 Coder 32B', provider: 'NVIDIA' }, + { id: 'openai/gpt-oss-120b', name: 'GPT OSS 120B', provider: 'NVIDIA' }, + { id: 'google/gemma-3-27b-it', name: 'Gemma 3 27B', provider: 'NVIDIA' }, + { id: 'meta/llama-4-maverick-17b-128e-instruct', name: 'Llama 4 Maverick 17B', provider: 'NVIDIA' }, + { id: 'mistralai/devstral-2-123b-instruct-2512', name: 'Devstral 2 123B', provider: 'NVIDIA' }, // Antigravity supported target models (use these as mapping targets) { id: 'claude-opus-4-5-thinking', diff --git a/web/src/lib/transport/types.ts b/web/src/lib/transport/types.ts index 2b71488a..7aee4ea2 100644 --- a/web/src/lib/transport/types.ts +++ b/web/src/lib/transport/types.ts @@ -46,6 +46,7 @@ export interface Provider { updatedAt: string; type: string; name: string; + logo?: string; // Logo URL or data URI config: ProviderConfig | null; supportedClientTypes: ClientType[]; } diff --git a/web/src/locales/en.json b/web/src/locales/en.json index ef17ab04..d9aa6c76 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -36,7 +36,9 @@ "days": "days", "hours": "hours", "global": "Global", - "initFailed": "Failed to Initialize" + "initFailed": "Failed to Initialize", + "search": "Search", + "searchProviders": "Search providers..." }, "nav": { "dashboard": "Dashboard", @@ -568,6 +570,10 @@ "freeduck": { "name": "Free Duck", "description": "Free site · Claude Code only" + }, + "nvidia": { + "name": "NVIDIA NIM", + "description": "NVIDIA NIM · OpenAI Compatible" } } } diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json index eeb41827..91a8f2ba 100644 --- a/web/src/locales/zh.json +++ b/web/src/locales/zh.json @@ -36,7 +36,9 @@ "days": "天", "hours": "小时", "global": "全局", - "initFailed": "初始化失败" + "initFailed": "初始化失败", + "search": "搜索", + "searchProviders": "搜索 Provider..." }, "nav": { "dashboard": "仪表板", @@ -567,6 +569,10 @@ "freeduck": { "name": "Free Duck", "description": "免费站点 · 只有 Claude Code" + }, + "nvidia": { + "name": "NVIDIA NIM", + "description": "NVIDIA NIM · OpenAI 兼容" } } } diff --git a/web/src/pages/client-routes/index.tsx b/web/src/pages/client-routes/index.tsx index ad069198..6700f982 100644 --- a/web/src/pages/client-routes/index.tsx +++ b/web/src/pages/client-routes/index.tsx @@ -3,14 +3,20 @@ * 全局路由配置页面 - 显示当前 ClientType 的路由 */ +import { useState } from 'react'; import { useParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { Search } from 'lucide-react'; import { ClientIcon, getClientName } from '@/components/icons/client-icons'; import type { ClientType } from '@/lib/transport'; import { ClientTypeRoutesContent } from '@/components/routes/ClientTypeRoutesContent'; +import { Input } from '@/components/ui/input'; export function ClientRoutesPage() { + const { t } = useTranslation(); const { clientType } = useParams<{ clientType: string }>(); const activeClientType = (clientType as ClientType) || 'claude'; + const [searchQuery, setSearchQuery] = useState(''); return (
@@ -29,11 +35,27 @@ export function ClientRoutesPage() {

+
+ + setSearchQuery(e.target.value)} + className="pl-9 w-48" + /> +
{/* Content */}
- +
); diff --git a/web/src/pages/providers/components/custom-config-step.tsx b/web/src/pages/providers/components/custom-config-step.tsx index 9de020c6..b24c56fc 100644 --- a/web/src/pages/providers/components/custom-config-step.tsx +++ b/web/src/pages/providers/components/custom-config-step.tsx @@ -1,10 +1,11 @@ -import { Globe, ChevronLeft, Key, Check } from 'lucide-react'; +import { Globe, ChevronLeft, Key, Check, Plus, Trash2, ArrowRight } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { useCreateProvider } from '@/hooks/queries'; +import { useCreateProvider, useCreateModelMapping } from '@/hooks/queries'; import type { ClientType, CreateProviderData } from '@/lib/transport'; import { ClientsConfigSection } from './clients-config-section'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { ModelInput } from '@/components/ui/model-input'; import { useProviderForm } from '../context/provider-form-context'; import { useProviderNavigation } from '../hooks/use-provider-navigation'; @@ -13,6 +14,7 @@ export function CustomConfigStep() { const { formData, updateFormData, updateClient, isValid, isSaving, setSaving, saveStatus, setSaveStatus } = useProviderForm(); const { goToSelectType, goToProviders } = useProviderNavigation(); const createProvider = useCreateProvider(); + const createModelMapping = useCreateModelMapping(); const handleSave = async () => { if (!isValid()) return; @@ -32,6 +34,7 @@ export function CustomConfigStep() { const data: CreateProviderData = { type: 'custom', name: formData.name, + logo: formData.logo, config: { custom: { baseURL: formData.baseURL, @@ -42,7 +45,20 @@ export function CustomConfigStep() { supportedClientTypes, }; - await createProvider.mutateAsync(data); + const provider = await createProvider.mutateAsync(data); + + // Create model mappings if template has any + if (formData.modelMappings && formData.modelMappings.length > 0) { + for (const mapping of formData.modelMappings) { + await createModelMapping.mutateAsync({ + scope: 'provider', + providerID: provider.id, + pattern: mapping.pattern, + target: mapping.target, + }); + } + } + setSaveStatus('success'); setTimeout(() => goToProviders(), 500); } catch (error) { @@ -153,6 +169,81 @@ export function CustomConfigStep() { + {/* Model Mapping Section */} +
+
+

+ {t('modelMappings.title')} +

+ +
+ + {formData.modelMappings && formData.modelMappings.length > 0 ? ( +
+ {formData.modelMappings.map((mapping, index) => ( +
+
+ + { + const newMappings = [...(formData.modelMappings || [])]; + newMappings[index] = { ...newMappings[index], pattern: e.target.value }; + updateFormData({ modelMappings: newMappings }); + }} + placeholder="*claude*, *sonnet*, *" + className="font-mono text-sm" + /> +
+ +
+ + { + const newMappings = [...(formData.modelMappings || [])]; + newMappings[index] = { ...newMappings[index], target: value }; + updateFormData({ modelMappings: newMappings }); + }} + placeholder={t('modelInput.selectOrEnter')} + /> +
+ +
+ ))} +
+ ) : ( +
+ {t('modelMappings.noMappings')} +
+ )} +
+ {saveStatus === 'error' && (
diff --git a/web/src/pages/providers/components/select-type-step.tsx b/web/src/pages/providers/components/select-type-step.tsx index e07129a1..c8575399 100644 --- a/web/src/pages/providers/components/select-type-step.tsx +++ b/web/src/pages/providers/components/select-type-step.tsx @@ -41,6 +41,8 @@ export function SelectTypeStep() { selectedTemplate: templateId, name: template.name, clients: updatedClients, + modelMappings: template.modelMappings, + logo: template.logoUrl, }); goToCustomConfig(); diff --git a/web/src/pages/providers/index.tsx b/web/src/pages/providers/index.tsx index b9328648..46aa5a82 100644 --- a/web/src/pages/providers/index.tsx +++ b/web/src/pages/providers/index.tsx @@ -1,5 +1,5 @@ -import { useMemo, useRef } from 'react'; -import { Plus, Layers, Download, Upload } from 'lucide-react'; +import { useMemo, useRef, useState } from 'react'; +import { Plus, Layers, Download, Upload, Search } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { useProviders, useAllProviderStats } from '@/hooks/queries'; @@ -9,9 +9,9 @@ import { getTransport } from '@/lib/transport'; import { ProviderRow } from './components/provider-row'; import { useQueryClient } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; import { PageHeader } from '@/components/layout/page-header'; import { PROVIDER_TYPE_CONFIGS, type ProviderTypeKey } from './types'; -import { useState } from 'react'; import { AntigravityQuotasProvider } from '@/contexts/antigravity-quotas-context'; export function ProvidersPage() { @@ -21,6 +21,7 @@ export function ProvidersPage() { const { data: providerStats = {} } = useAllProviderStats(); const { countsByProvider } = useStreamingRequests(); const [importStatus, setImportStatus] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); const fileInputRef = useRef(null); const queryClient = useQueryClient(); @@ -32,7 +33,19 @@ export function ProvidersPage() { custom: [], }; - providers?.forEach((p) => { + // Filter providers by search query + const filteredProviders = providers?.filter((p) => { + if (!searchQuery.trim()) return true; + const query = searchQuery.toLowerCase(); + const config = PROVIDER_TYPE_CONFIGS[p.type as ProviderTypeKey]; + const displayInfo = config?.getDisplayInfo(p) || ''; + return ( + p.name.toLowerCase().includes(query) || + displayInfo.toLowerCase().includes(query) + ); + }); + + filteredProviders?.forEach((p) => { const type = p.type as ProviderTypeKey; if (groups[type]) { groups[type].push(p); @@ -43,7 +56,7 @@ export function ProvidersPage() { }); return groups; - }, [providers]); + }, [providers, searchQuery]); // Export providers as JSON file const handleExport = async () => { @@ -107,6 +120,18 @@ export function ProvidersPage() { count: providers?.length || 0, })} > +
+ + setSearchQuery(e.target.value)} + className="pl-9 w-48" + /> +
>; + modelMappings?: TemplateModelMapping[]; // 可选的模型映射 }; export const quickTemplates: QuickTemplate[] = [ @@ -95,6 +105,7 @@ export const quickTemplates: QuickTemplate[] = [ nameKey: 'addProvider.templates.88code.name', descriptionKey: 'addProvider.templates.88code.description', icon: 'grid', + logoUrl: logo88code, supportedClients: ['claude', 'codex', 'gemini'], clientBaseURLs: { claude: 'https://www.88code.ai/api', @@ -109,6 +120,7 @@ export const quickTemplates: QuickTemplate[] = [ nameKey: 'addProvider.templates.aicodemirror.name', descriptionKey: 'addProvider.templates.aicodemirror.description', icon: 'layers', + logoUrl: aicodemirrorLogo, supportedClients: ['claude', 'codex', 'gemini'], clientBaseURLs: { claude: 'https://api.aicodemirror.com/api/claudecode', @@ -144,6 +156,20 @@ export const quickTemplates: QuickTemplate[] = [ claude: 'https://free.duckcoding.com', }, }, + { + id: 'nvidia', + name: 'NVIDIA', + description: 'NVIDIA NIM · OpenAI 兼容', + nameKey: 'addProvider.templates.nvidia.name', + descriptionKey: 'addProvider.templates.nvidia.description', + icon: 'layers', + logoUrl: nvidiaLogo, + supportedClients: ['openai'], + clientBaseURLs: { + openai: 'https://integrate.api.nvidia.com', + }, + modelMappings: [{ pattern: '*', target: 'minimaxai/minimax-m2.1' }], + }, ]; // Client config @@ -169,6 +195,8 @@ export type ProviderFormData = { baseURL: string; apiKey: string; clients: ClientConfig[]; + modelMappings?: TemplateModelMapping[]; // 模型映射 + logo?: string; // Logo URL }; // Create step type