From 2eed76306ed53accbc669e22dacd76a82c7b84c2 Mon Sep 17 00:00:00 2001 From: Chiron <598621670@qq.com> Date: Wed, 7 Jan 2026 10:01:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E8=A1=A8=E9=87=8D=E5=A4=8DID=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E7=9A=84=E5=94=AF=E4=B8=80=E7=BA=A6=E6=9D=9F=E5=86=B2?= =?UTF-8?q?=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 fetch_models_from_repo 中添加去重逻辑 - 使用 INSERT OR REPLACE 作为双重保险 - 优化 provider 类型映射和模型列表组件 --- .gitignore | 1 + pnpm-lock.yaml | 26 +++++ src-tauri/src/agent/protocols/openai.rs | 12 +-- src-tauri/src/commands/orchestrator_cmd.rs | 5 + .../src/services/model_registry_service.rs | 98 +++++++++++-------- .../api-server/ApiServerPage.test.ts | 2 + src/components/api-server/ApiServerPage.tsx | 2 + .../api-server/EnhancedModelsTab.tsx | 92 +++++++++++++---- .../api-key/ProviderModelList.tsx | 13 ++- .../provider-pool/api-key/ProviderSetting.tsx | 5 +- .../api-key/providerTypeMapping.ts | 86 ++++++++++++++-- src/hooks/useModelRegistry.ts | 39 +++++--- 12 files changed, 281 insertions(+), 100 deletions(-) diff --git a/.gitignore b/.gitignore index 2b01a18b4..1e24d18e2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ Thumbs.db # Kiro .kiro/ +.history diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2877280d5..6764711c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: '@tauri-apps/api': specifier: ^2.9.1 version: 2.9.1 + '@tauri-apps/plugin-deep-link': + specifier: ^2.4.5 + version: 2.4.5 '@tauri-apps/plugin-dialog': specifier: ^2.4.2 version: 2.4.2 @@ -1243,56 +1246,67 @@ packages: resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.54.0': resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.54.0': resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.54.0': resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.54.0': resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.54.0': resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.54.0': resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.54.0': resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.54.0': resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.54.0': resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.54.0': resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.54.0': resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} @@ -1416,30 +1430,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-arm64-musl@2.9.6': resolution: {integrity: sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tauri-apps/cli-linux-riscv64-gnu@2.9.6': resolution: {integrity: sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-gnu@2.9.6': resolution: {integrity: sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-musl@2.9.6': resolution: {integrity: sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tauri-apps/cli-win32-arm64-msvc@2.9.6': resolution: {integrity: sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==} @@ -1464,6 +1483,9 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-deep-link@2.4.5': + resolution: {integrity: sha512-Zf2RTj1D9IQQ45/jqW8XTKvql24HqlPjcpv0mV/O2jHQkNe11HOTZBVj6IK37qs+MWV7xZzcmazx/QVZnhAwaQ==} + '@tauri-apps/plugin-dialog@2.4.2': resolution: {integrity: sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ==} @@ -4308,6 +4330,10 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.9.6 '@tauri-apps/cli-win32-x64-msvc': 2.9.6 + '@tauri-apps/plugin-deep-link@2.4.5': + dependencies: + '@tauri-apps/api': 2.9.1 + '@tauri-apps/plugin-dialog@2.4.2': dependencies: '@tauri-apps/api': 2.9.1 diff --git a/src-tauri/src/agent/protocols/openai.rs b/src-tauri/src/agent/protocols/openai.rs index 79dab6bd5..85bd2d336 100644 --- a/src-tauri/src/agent/protocols/openai.rs +++ b/src-tauri/src/agent/protocols/openai.rs @@ -403,14 +403,10 @@ impl Protocol for OpenAIProtocol { req_builder = req_builder.header("X-Provider-Id", pid); } - let response = req_builder - .json(&request) - .send() - .await - .map_err(|e| { - eprintln!("[OpenAIProtocol] 请求发送失败: {}", e); - format!("请求失败: {}", e) - })?; + let response = req_builder.json(&request).send().await.map_err(|e| { + eprintln!("[OpenAIProtocol] 请求发送失败: {}", e); + format!("请求失败: {}", e) + })?; let status = response.status(); eprintln!("[OpenAIProtocol] 响应状态: {}", status); diff --git a/src-tauri/src/commands/orchestrator_cmd.rs b/src-tauri/src/commands/orchestrator_cmd.rs index 31299b7dc..a239e522e 100644 --- a/src-tauri/src/commands/orchestrator_cmd.rs +++ b/src-tauri/src/commands/orchestrator_cmd.rs @@ -180,6 +180,10 @@ fn extract_supported_models( "gemini-3-flash-preview".to_string(), ] } + CredentialData::IFlowOAuth { .. } | CredentialData::IFlowCookie { .. } => { + // iFlow 是 DeepSeek 的代理服务 + vec!["deepseek-chat".to_string(), "deepseek-reasoner".to_string()] + } _ => vec![], } } @@ -194,6 +198,7 @@ fn map_pool_provider_type(pool_type: &str) -> ProviderType { "codex" => ProviderType::OpenAI, "qwen" => ProviderType::Custom, "antigravity" => ProviderType::Antigravity, + "iflow" | "deepseek" => ProviderType::Custom, // DeepSeek 及其代理 iFlow _ => ProviderType::Custom, } } diff --git a/src-tauri/src/services/model_registry_service.rs b/src-tauri/src/services/model_registry_service.rs index d4a33520c..042013c99 100644 --- a/src-tauri/src/services/model_registry_service.rs +++ b/src-tauri/src/services/model_registry_service.rs @@ -286,6 +286,18 @@ impl ModelRegistryService { } } + // 去重:使用 HashMap 按 id 去重,保留第一个出现的模型 + let mut seen_ids = std::collections::HashSet::new(); + let original_count = models.len(); + models.retain(|m| seen_ids.insert(m.id.clone())); + + if models.len() < original_count { + tracing::warn!( + "[ModelRegistry] 发现 {} 个重复 ID,已去重", + original_count - models.len() + ); + } + // 按 provider_id 和 display_name 排序 models.sort_by(|a, b| { a.provider_id @@ -439,58 +451,60 @@ impl ModelRegistryService { /// 保存模型到数据库 async fn save_models_to_db(&self, models: &[EnhancedModelMetadata]) -> Result<(), String> { - let conn = self.db.lock().map_err(|e| e.to_string())?; + let mut conn = self.db.lock().map_err(|e| e.to_string())?; - // 开始事务 - conn.execute("BEGIN TRANSACTION", []) - .map_err(|e| e.to_string())?; + // 使用 rusqlite 的事务 API + let tx = conn.transaction().map_err(|e| e.to_string())?; // 清空现有数据 - conn.execute("DELETE FROM model_registry", []) + tx.execute("DELETE FROM model_registry", []) .map_err(|e| e.to_string())?; - // 插入新数据 - let mut stmt = conn - .prepare( - "INSERT INTO model_registry ( - id, display_name, provider_id, provider_name, family, tier, - capabilities, pricing, limits, status, release_date, is_latest, - description, source, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - ) - .map_err(|e| e.to_string())?; + // 插入新数据(使用 INSERT OR REPLACE 处理可能的重复 ID) + { + let mut stmt = tx + .prepare( + "INSERT OR REPLACE INTO model_registry ( + id, display_name, provider_id, provider_name, family, tier, + capabilities, pricing, limits, status, release_date, is_latest, + description, source, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) + .map_err(|e| e.to_string())?; - for model in models { - let capabilities_json = serde_json::to_string(&model.capabilities).unwrap_or_default(); - let pricing_json = model - .pricing - .as_ref() - .map(|p| serde_json::to_string(p).unwrap_or_default()); - let limits_json = serde_json::to_string(&model.limits).unwrap_or_default(); + for model in models { + let capabilities_json = + serde_json::to_string(&model.capabilities).unwrap_or_default(); + let pricing_json = model + .pricing + .as_ref() + .map(|p| serde_json::to_string(p).unwrap_or_default()); + let limits_json = serde_json::to_string(&model.limits).unwrap_or_default(); - stmt.execute(params![ - model.id, - model.display_name, - model.provider_id, - model.provider_name, - model.family, - model.tier.to_string(), - capabilities_json, - pricing_json, - limits_json, - model.status.to_string(), - model.release_date, - model.is_latest as i32, - model.description, - model.source.to_string(), - model.created_at, - model.updated_at, - ]) - .map_err(|e| e.to_string())?; + stmt.execute(params![ + model.id, + model.display_name, + model.provider_id, + model.provider_name, + model.family, + model.tier.to_string(), + capabilities_json, + pricing_json, + limits_json, + model.status.to_string(), + model.release_date, + model.is_latest as i32, + model.description, + model.source.to_string(), + model.created_at, + model.updated_at, + ]) + .map_err(|e| e.to_string())?; + } } // 提交事务 - conn.execute("COMMIT", []).map_err(|e| e.to_string())?; + tx.commit().map_err(|e| e.to_string())?; tracing::info!("[ModelRegistry] 保存了 {} 个模型到数据库", models.len()); diff --git a/src/components/api-server/ApiServerPage.test.ts b/src/components/api-server/ApiServerPage.test.ts index dca1fe121..68d0a43b1 100644 --- a/src/components/api-server/ApiServerPage.test.ts +++ b/src/components/api-server/ApiServerPage.test.ts @@ -46,6 +46,8 @@ function getTestModel(provider: string): string { return "gpt-4o"; case "claude": return "claude-sonnet-4-20250514"; + case "deepseek": + return "deepseek-chat"; case "kiro": default: return "claude-opus-4-5-20251101"; diff --git a/src/components/api-server/ApiServerPage.tsx b/src/components/api-server/ApiServerPage.tsx index 30f78c806..d60f2056b 100644 --- a/src/components/api-server/ApiServerPage.tsx +++ b/src/components/api-server/ApiServerPage.tsx @@ -446,6 +446,8 @@ export function ApiServerPage() { return "gpt-4o"; case "claude": return "claude-sonnet-4-20250514"; + case "deepseek": + return "deepseek-chat"; case "kiro": default: return "claude-opus-4-5-20251101"; diff --git a/src/components/api-server/EnhancedModelsTab.tsx b/src/components/api-server/EnhancedModelsTab.tsx index 60226e837..d44efac1e 100644 --- a/src/components/api-server/EnhancedModelsTab.tsx +++ b/src/components/api-server/EnhancedModelsTab.tsx @@ -4,7 +4,7 @@ * 使用 model_registry 数据,支持搜索、收藏、分组等功能 */ -import { useState } from "react"; +import { useState, useMemo, useEffect, useRef, useCallback } from "react"; import { Cpu, RefreshCw, @@ -40,14 +40,33 @@ export function EnhancedModelsTab() { } = useModelRegistry(); const [searchQuery, setSearchQuery] = useState(""); + const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(""); const [selectedProvider, setSelectedProvider] = useState(null); const [selectedTier, setSelectedTier] = useState(null); const [copied, setCopied] = useState(null); const [showFavoritesOnly, setShowFavoritesOnly] = useState(false); + const [displayLimit, setDisplayLimit] = useState(50); // 初始显示 50 个 + + // 防抖搜索 + const debounceTimerRef = useRef | null>(null); + const handleSearchChange = useCallback((value: string) => { + setSearchQuery(value); + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + debounceTimerRef.current = setTimeout(() => { + setDebouncedSearchQuery(value); + }, 150); + }, []); + + // 当筛选条件变化时,重置显示数量 + useEffect(() => { + setDisplayLimit(50); + }, [debouncedSearchQuery, selectedProvider, selectedTier, showFavoritesOnly]); // 搜索和过滤 - const filteredModels = (() => { - let result = searchQuery ? search(searchQuery) : models; + const filteredModels = useMemo(() => { + let result = debouncedSearchQuery ? search(debouncedSearchQuery) : models; if (selectedProvider) { result = result.filter((m) => m.provider_id === selectedProvider); } @@ -58,9 +77,28 @@ export function EnhancedModelsTab() { result = result.filter((m) => preferences.get(m.id)?.is_favorite); } return result; - })(); + }, [ + debouncedSearchQuery, + models, + selectedProvider, + selectedTier, + showFavoritesOnly, + preferences, + search, + ]); - const providers = Array.from(groupedByProvider.keys()); + // 分页显示的模型 + const displayedModels = useMemo(() => { + return filteredModels.slice(0, displayLimit); + }, [filteredModels, displayLimit]); + + const hasMore = filteredModels.length > displayLimit; + + // 缓存 providers 列表,避免每次渲染都重新计算 + const providers = useMemo( + () => Array.from(groupedByProvider.keys()), + [groupedByProvider], + ); const copyModelId = (id: string) => { navigator.clipboard.writeText(id); @@ -106,7 +144,7 @@ export function EnhancedModelsTab() { type="text" placeholder="搜索模型名称、ID、Provider..." value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} + onChange={(e) => handleSearchChange(e.target.value)} className="w-full rounded-lg border bg-background pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20" /> @@ -195,7 +233,9 @@ export function EnhancedModelsTab() {
模型列表 - {filteredModels.length} 个模型 + {hasMore + ? `显示 ${displayedModels.length} / ${filteredModels.length} 个模型` + : `${filteredModels.length} 个模型`}
@@ -210,19 +250,31 @@ export function EnhancedModelsTab() {

暂无模型数据

) : ( -
- {filteredModels.map((model) => ( - copyModelId(model.id)} - onToggleFavorite={() => toggleFavorite(model.id)} - /> - ))} -
+ <> +
+ {displayedModels.map((model) => ( + copyModelId(model.id)} + onToggleFavorite={() => toggleFavorite(model.id)} + /> + ))} +
+ {hasMore && ( +
+ +
+ )} + )} diff --git a/src/components/provider-pool/api-key/ProviderModelList.tsx b/src/components/provider-pool/api-key/ProviderModelList.tsx index 26d99e48e..8b11af54a 100644 --- a/src/components/provider-pool/api-key/ProviderModelList.tsx +++ b/src/components/provider-pool/api-key/ProviderModelList.tsx @@ -9,14 +9,16 @@ import { cn } from "@/lib/utils"; import { useModelRegistry } from "@/hooks/useModelRegistry"; import { Eye, Wrench, Brain, Sparkles, Loader2 } from "lucide-react"; import type { EnhancedModelMetadata } from "@/lib/types/modelRegistry"; -import { mapProviderTypeToRegistryId } from "./providerTypeMapping"; +import { mapProviderIdToRegistryId } from "./providerTypeMapping"; // ============================================================================ // 类型定义 // ============================================================================ export interface ProviderModelListProps { - /** Provider 类型,如 "anthropic", "openai", "gemini" */ + /** Provider ID,如 "deepseek", "openai", "anthropic" */ + providerId: string; + /** Provider 类型(API 协议),如 "anthropic", "openai", "gemini" */ providerType: string; /** 额外的 CSS 类名 */ className?: string; @@ -104,14 +106,15 @@ const ModelItem: React.FC = ({ model }) => { * ``` */ export const ProviderModelList: React.FC = ({ + providerId, providerType, className, maxItems, }) => { - // 转换 Provider 类型为 registry ID + // 转换 Provider ID 为 registry ID(优先使用 providerId,回退到 providerType) const registryProviderId = useMemo( - () => mapProviderTypeToRegistryId(providerType), - [providerType], + () => mapProviderIdToRegistryId(providerId, providerType), + [providerId, providerType], ); // 获取模型数据 diff --git a/src/components/provider-pool/api-key/ProviderSetting.tsx b/src/components/provider-pool/api-key/ProviderSetting.tsx index 6c89cc5e5..99e76119c 100644 --- a/src/components/provider-pool/api-key/ProviderSetting.tsx +++ b/src/components/provider-pool/api-key/ProviderSetting.tsx @@ -238,7 +238,10 @@ export const ProviderSetting: React.FC = ({ {/* 支持的模型列表 */}
- +
diff --git a/src/components/provider-pool/api-key/providerTypeMapping.ts b/src/components/provider-pool/api-key/providerTypeMapping.ts index 7d5e0e4b0..84a0af739 100644 --- a/src/components/provider-pool/api-key/providerTypeMapping.ts +++ b/src/components/provider-pool/api-key/providerTypeMapping.ts @@ -1,26 +1,96 @@ +/* + * @Author: Chiron 598621670@qq.com + * @Date: 2026-01-06 17:34:03 + * @LastEditors: Chiron 598621670@qq.com + * @LastEditTime: 2026-01-07 00:53:05 + * @FilePath: /proxycast/src/components/provider-pool/api-key/providerTypeMapping.ts + * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + */ /** * @file Provider 类型映射工具 - * @description Provider 类型到 model_registry provider_id 的映射 + * @description Provider ID/类型到 model_registry provider_id 的映射 * @module components/provider-pool/api-key/providerTypeMapping */ /** - * Provider 类型到 model_registry provider_id 的映射 + * Provider ID 到 model_registry provider_id 的映射 + * 用于将系统 Provider ID(如 deepseek, moonshot)映射到模型注册表中的 provider_id + */ +const PROVIDER_ID_TO_REGISTRY_ID: Record = { + // 主流 AI + openai: "openai", + anthropic: "anthropic", + gemini: "google", + deepseek: "deepseek", + moonshot: "moonshot", + groq: "groq", + grok: "grok", + mistral: "mistral", + perplexity: "perplexity", + cohere: "cohere", + // 国内 AI + zhipu: "zhipu", + baichuan: "baichuan", + dashscope: "dashscope", + doubao: "doubao", + minimax: "minimax", + stepfun: "stepfun", + lingyi: "lingyi", + baidu: "baidu", + hunyuan: "hunyuan", + spark: "spark", + // 云服务 + "azure-openai": "openai", + vertexai: "google", + "aws-bedrock": "anthropic", + // 代理服务 + iflow: "deepseek", // iFlow 是 DeepSeek 的代理 + // 其他 + ollama: "ollama", + together: "together", + fireworks: "fireworks", + replicate: "replicate", +}; + +/** + * Provider 类型(API 协议)到 model_registry provider_id 的映射 + * 作为 Provider ID 映射的回退 */ const PROVIDER_TYPE_TO_REGISTRY_ID: Record = { anthropic: "anthropic", openai: "openai", "openai-response": "openai", gemini: "google", - "azure-openai": "openai", - vertexai: "google", - "aws-bedrock": "anthropic", - ollama: "ollama", - "new-api": "custom", - gateway: "custom", }; /** + * 将 Provider ID 转换为 model_registry 的 provider_id + * 优先使用 Provider ID 映射,回退到 Provider Type 映射 + * + * @param providerId Provider ID(如 "deepseek", "openai") + * @param providerType Provider 类型/API 协议(如 "openai", "anthropic") + * @returns model_registry 中的 provider_id + */ +export function mapProviderIdToRegistryId( + providerId: string, + providerType?: string, +): string { + // 优先使用 Provider ID 映射 + if (PROVIDER_ID_TO_REGISTRY_ID[providerId]) { + return PROVIDER_ID_TO_REGISTRY_ID[providerId]; + } + + // 回退到 Provider Type 映射 + if (providerType && PROVIDER_TYPE_TO_REGISTRY_ID[providerType]) { + return PROVIDER_TYPE_TO_REGISTRY_ID[providerType]; + } + + // 最后回退到原始 providerId + return providerId; +} + +/** + * @deprecated 使用 mapProviderIdToRegistryId 代替 * 将 Provider 类型转换为 model_registry 的 provider_id */ export function mapProviderTypeToRegistryId(providerType: string): string { diff --git a/src/hooks/useModelRegistry.ts b/src/hooks/useModelRegistry.ts index 308cafc1b..a7cd31228 100644 --- a/src/hooks/useModelRegistry.ts +++ b/src/hooks/useModelRegistry.ts @@ -100,36 +100,43 @@ function fuzzySearch( .map((model) => { let score = 0; - // 精确匹配 ID + // 精确匹配 ID(最高优先级) if (model.id.toLowerCase() === queryLower) { - score += 100; + score += 1000; + } else if (model.id.toLowerCase().startsWith(queryLower)) { + // ID 以搜索词开头 + score += 500; } else if (model.id.toLowerCase().includes(queryLower)) { - score += 50; + score += 100; } // 显示名称匹配 - if (model.display_name.toLowerCase().includes(queryLower)) { - score += 30; + if (model.display_name.toLowerCase().startsWith(queryLower)) { + score += 80; + } else if (model.display_name.toLowerCase().includes(queryLower)) { + score += 40; } // Provider 匹配 - if (model.provider_name.toLowerCase().includes(queryLower)) { - score += 20; + if (model.provider_id.toLowerCase() === queryLower) { + score += 200; + } else if (model.provider_name.toLowerCase().includes(queryLower)) { + score += 30; } // 家族匹配 if (model.family?.toLowerCase().includes(queryLower)) { - score += 15; + score += 20; } - // 最新版本加分 - if (model.is_latest) { - score += 5; - } - - // 活跃状态加分 - if (model.status === "active") { - score += 3; + // 只有在有匹配的情况下,才给最新版本和活跃状态加分 + if (score > 0) { + if (model.is_latest) { + score += 5; + } + if (model.status === "active") { + score += 3; + } } return { model, score };