mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
Merge pull request #74 from Chiron-Brahm/main
fix: 修复模型注册表重复ID导致的唯一约束冲突
This commit is contained in:
@@ -20,3 +20,4 @@ Thumbs.db
|
||||
|
||||
# Kiro
|
||||
.kiro/
|
||||
.history
|
||||
|
||||
Generated
+26
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +248,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
|
||||
@@ -429,58 +441,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());
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [selectedTier, setSelectedTier] = useState<ModelTier | null>(null);
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false);
|
||||
const [displayLimit, setDisplayLimit] = useState(50); // 初始显示 50 个
|
||||
|
||||
// 防抖搜索
|
||||
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | 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"
|
||||
/>
|
||||
</div>
|
||||
@@ -195,7 +233,9 @@ export function EnhancedModelsTab() {
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">模型列表</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{filteredModels.length} 个模型
|
||||
{hasMore
|
||||
? `显示 ${displayedModels.length} / ${filteredModels.length} 个模型`
|
||||
: `${filteredModels.length} 个模型`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,19 +250,31 @@ export function EnhancedModelsTab() {
|
||||
<p>暂无模型数据</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y max-h-[600px] overflow-y-auto">
|
||||
{filteredModels.map((model) => (
|
||||
<ModelRow
|
||||
key={model.id}
|
||||
model={model}
|
||||
isFavorite={preferences.get(model.id)?.is_favorite || false}
|
||||
usageCount={preferences.get(model.id)?.usage_count || 0}
|
||||
copied={copied === model.id}
|
||||
onCopy={() => copyModelId(model.id)}
|
||||
onToggleFavorite={() => toggleFavorite(model.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
<div className="divide-y max-h-[600px] overflow-y-auto">
|
||||
{displayedModels.map((model) => (
|
||||
<ModelRow
|
||||
key={model.id}
|
||||
model={model}
|
||||
isFavorite={preferences.get(model.id)?.is_favorite || false}
|
||||
usageCount={preferences.get(model.id)?.usage_count || 0}
|
||||
copied={copied === model.id}
|
||||
onCopy={() => copyModelId(model.id)}
|
||||
onToggleFavorite={() => toggleFavorite(model.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{hasMore && (
|
||||
<div className="border-t px-4 py-3 flex justify-center">
|
||||
<button
|
||||
onClick={() => setDisplayLimit((prev) => prev + 50)}
|
||||
className="flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium hover:bg-muted"
|
||||
>
|
||||
加载更多 (还有 {filteredModels.length - displayLimit} 个)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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<ModelItemProps> = ({ model }) => {
|
||||
* ```
|
||||
*/
|
||||
export const ProviderModelList: React.FC<ProviderModelListProps> = ({
|
||||
providerId,
|
||||
providerType,
|
||||
className,
|
||||
maxItems,
|
||||
}) => {
|
||||
// 转换 Provider 类型为 registry ID
|
||||
// 转换 Provider ID 为 registry ID(优先使用 providerId,回退到 providerType)
|
||||
const registryProviderId = useMemo(
|
||||
() => mapProviderTypeToRegistryId(providerType),
|
||||
[providerType],
|
||||
() => mapProviderIdToRegistryId(providerId, providerType),
|
||||
[providerId, providerType],
|
||||
);
|
||||
|
||||
// 获取模型数据
|
||||
|
||||
@@ -238,7 +238,10 @@ export const ProviderSetting: React.FC<ProviderSettingProps> = ({
|
||||
|
||||
{/* 支持的模型列表 */}
|
||||
<section data-testid="supported-models-section">
|
||||
<ProviderModelList providerType={provider.type} />
|
||||
<ProviderModelList
|
||||
providerId={provider.id}
|
||||
providerType={provider.type}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<string, string> = {
|
||||
// 主流 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<string, string> = {
|
||||
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 {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user