Skip to content

Commit d9ec0df

Browse files
committed
feat: 配置切换添加供应商支持从凭证池导入
- 新增'从凭证池导入'预设选项 - 选择后显示可用凭证列表(按 appType 筛选) - 选择凭证后自动配置 ProxyCast 代理设置 - 支持 Claude、Codex、Gemini 三种应用类型
1 parent 714c27c commit d9ec0df

1 file changed

Lines changed: 185 additions & 2 deletions

File tree

src/components/clients/ProviderForm.tsx

Lines changed: 185 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import React, { useState, useMemo, useCallback } from "react";
2-
import { X, ExternalLink, Wand2, Eye, EyeOff } from "lucide-react";
1+
import React, { useState, useMemo, useCallback, useEffect } from "react";
2+
import { X, ExternalLink, Wand2, Eye, EyeOff, Database } from "lucide-react";
33
import { Provider, AppType } from "@/lib/api/switch";
44
import { getConfig } from "@/hooks/useTauri";
55
import { cn } from "@/lib/utils";
6+
import {
7+
providerPoolApi,
8+
CredentialDisplay,
9+
PoolProviderType,
10+
} from "@/lib/api/providerPool";
611

712
interface ProviderFormProps {
813
appType: AppType;
@@ -20,6 +25,7 @@ type ProviderCategory =
2025
| "aggregator"
2126
| "third_party"
2227
| "proxy"
28+
| "credential_pool"
2329
| "custom";
2430

2531
// 预设供应商接口
@@ -137,6 +143,13 @@ const presets: Record<AppType, ProviderPreset[]> = {
137143
iconColor: "#3b82f6",
138144
defaultBaseUrl: "http://127.0.0.1:3001",
139145
},
146+
// 从凭证池导入
147+
{
148+
id: "credential_pool",
149+
name: "从凭证池导入",
150+
category: "credential_pool",
151+
iconColor: "#22c55e",
152+
},
140153
// 自定义
141154
{
142155
id: "custom",
@@ -181,6 +194,13 @@ model = "gpt-4o"
181194
api_base_url: "http://127.0.0.1:3001/v1",
182195
},
183196
},
197+
// 从凭证池导入
198+
{
199+
id: "credential_pool",
200+
name: "从凭证池导入",
201+
category: "credential_pool",
202+
iconColor: "#22c55e",
203+
},
184204
// 自定义
185205
{
186206
id: "custom",
@@ -211,6 +231,13 @@ model = "gpt-4o"
211231
GEMINI_MODEL: "gemini-2.0-flash",
212232
},
213233
},
234+
// 从凭证池导入
235+
{
236+
id: "credential_pool",
237+
name: "从凭证池导入",
238+
category: "credential_pool",
239+
iconColor: "#22c55e",
240+
},
214241
// 自定义
215242
{
216243
id: "custom",
@@ -228,6 +255,7 @@ const categoryLabels: Record<ProviderCategory, string> = {
228255
aggregator: "聚合服务",
229256
third_party: "第三方",
230257
proxy: "本地代理",
258+
credential_pool: "从凭证池导入",
231259
custom: "自定义",
232260
};
233261

@@ -450,6 +478,53 @@ export function ProviderForm({
450478
const [error, setError] = useState<string | null>(null);
451479
const [showApiKey, setShowApiKey] = useState(false);
452480

481+
// 凭证池相关状态
482+
const [poolCredentials, setPoolCredentials] = useState<CredentialDisplay[]>(
483+
[],
484+
);
485+
const [selectedCredentialUuid, setSelectedCredentialUuid] = useState<
486+
string | null
487+
>(null);
488+
const [loadingCredentials, setLoadingCredentials] = useState(false);
489+
490+
// 加载凭证池数据
491+
const loadPoolCredentials = useCallback(async () => {
492+
setLoadingCredentials(true);
493+
try {
494+
const overview = await providerPoolApi.getOverview();
495+
// 根据 appType 筛选相关的凭证类型
496+
const relevantTypes: PoolProviderType[] =
497+
appType === "claude"
498+
? ["claude", "kiro", "antigravity"]
499+
: appType === "codex"
500+
? ["openai", "codex"]
501+
: appType === "gemini"
502+
? ["gemini"]
503+
: [];
504+
505+
const credentials: CredentialDisplay[] = [];
506+
for (const pool of overview) {
507+
if (relevantTypes.includes(pool.provider_type as PoolProviderType)) {
508+
credentials.push(
509+
...pool.credentials.filter((c) => c.is_healthy && !c.is_disabled),
510+
);
511+
}
512+
}
513+
setPoolCredentials(credentials);
514+
} catch (e) {
515+
console.error("Failed to load pool credentials:", e);
516+
} finally {
517+
setLoadingCredentials(false);
518+
}
519+
}, [appType]);
520+
521+
// 当选择"从凭证池导入"时加载凭证
522+
useEffect(() => {
523+
if (selectedPresetId === "credential_pool") {
524+
loadPoolCredentials();
525+
}
526+
}, [selectedPresetId, loadPoolCredentials]);
527+
453528
// 当前选中的预设
454529
const selectedPreset = useMemo(() => {
455530
return appPresets.find((p) => p.id === selectedPresetId);
@@ -548,6 +623,55 @@ export function ProviderForm({
548623
setGeminiEnv(defaultGeminiEnv);
549624
}
550625
}
626+
627+
// 凭证池预设:重置选中的凭证
628+
if (preset.id === "credential_pool") {
629+
setSelectedCredentialUuid(null);
630+
}
631+
}
632+
};
633+
634+
// 处理凭证选择
635+
const handleCredentialSelect = async (credential: CredentialDisplay) => {
636+
setSelectedCredentialUuid(credential.uuid);
637+
setName(credential.name || `${credential.provider_type} 凭证`);
638+
setIconColor("#22c55e");
639+
640+
// 根据凭证类型和 appType 设置配置
641+
// 使用 ProxyCast 代理,凭证池中的凭证通过本地代理访问
642+
try {
643+
const config = await getConfig();
644+
const proxyApiKey = config.server.api_key || "";
645+
const proxyHost = config.server.host || "127.0.0.1";
646+
const proxyPort = config.server.port || 3001;
647+
const proxyBaseUrl = `http://${proxyHost}:${proxyPort}`;
648+
649+
if (appType === "claude") {
650+
setApiKey(proxyApiKey);
651+
setBaseUrl(proxyBaseUrl);
652+
setJsonManuallyEdited(false);
653+
} else if (appType === "codex") {
654+
setCodexAuth(
655+
JSON.stringify(
656+
{
657+
api_key: proxyApiKey,
658+
api_base_url: `${proxyBaseUrl}/v1`,
659+
},
660+
null,
661+
2,
662+
),
663+
);
664+
} else if (appType === "gemini") {
665+
setGeminiEnv(
666+
`GEMINI_API_KEY=${proxyApiKey}\nGOOGLE_GEMINI_BASE_URL=${proxyBaseUrl}\nGEMINI_MODEL=gemini-2.0-flash`,
667+
);
668+
}
669+
670+
setNotes(
671+
`使用凭证池: ${credential.provider_type} - ${credential.uuid.slice(0, 8)}`,
672+
);
673+
} catch (e) {
674+
console.error("Failed to load config:", e);
551675
}
552676
};
553677

@@ -680,6 +804,65 @@ export function ProviderForm({
680804
</div>
681805
)}
682806

807+
{/* 凭证池选择器(当选择"从凭证池导入"时显示) */}
808+
{!isEditMode && selectedPresetId === "credential_pool" && (
809+
<div className="space-y-3">
810+
<label className="block text-sm font-medium flex items-center gap-2">
811+
<Database className="h-4 w-4" />
812+
选择凭证
813+
</label>
814+
{loadingCredentials ? (
815+
<div className="flex items-center justify-center py-8 text-muted-foreground">
816+
<div className="animate-spin rounded-full h-5 w-5 border-2 border-primary border-t-transparent mr-2" />
817+
加载凭证中...
818+
</div>
819+
) : poolCredentials.length === 0 ? (
820+
<div className="text-center py-8 text-muted-foreground border border-dashed rounded-lg">
821+
<Database className="h-8 w-8 mx-auto mb-2 opacity-50" />
822+
<p>暂无可用凭证</p>
823+
<p className="text-xs mt-1">请先在凭证池中添加凭证</p>
824+
</div>
825+
) : (
826+
<div className="grid gap-2 max-h-48 overflow-y-auto">
827+
{poolCredentials.map((credential) => (
828+
<button
829+
key={credential.uuid}
830+
type="button"
831+
onClick={() => handleCredentialSelect(credential)}
832+
className={cn(
833+
"flex items-center gap-3 p-3 rounded-lg border text-left transition-all",
834+
selectedCredentialUuid === credential.uuid
835+
? "border-primary bg-primary/10"
836+
: "border-border hover:border-muted-foreground/50 hover:bg-muted/50",
837+
)}
838+
>
839+
<div
840+
className="w-3 h-3 rounded-full flex-shrink-0"
841+
style={{
842+
backgroundColor: credential.is_healthy
843+
? "#22c55e"
844+
: "#ef4444",
845+
}}
846+
/>
847+
<div className="flex-1 min-w-0">
848+
<p className="font-medium truncate">
849+
{credential.name || credential.uuid.slice(0, 8)}
850+
</p>
851+
<p className="text-xs text-muted-foreground">
852+
{credential.provider_type} ·{" "}
853+
{credential.uuid.slice(0, 8)}...
854+
</p>
855+
</div>
856+
{selectedCredentialUuid === credential.uuid && (
857+
<span className="text-xs text-primary">已选择</span>
858+
)}
859+
</button>
860+
))}
861+
</div>
862+
)}
863+
</div>
864+
)}
865+
683866
{/* 基础字段 */}
684867
<div className="space-y-4">
685868
<div>

0 commit comments

Comments
 (0)