import { useState, useEffect, useMemo } from "react"; import { listen } from "@tauri-apps/api/event"; import { DeepLinkImportRequest, deeplinkApi } from "@/lib/api/deeplink"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import { useQueryClient } from "@tanstack/react-query"; interface DeeplinkError { url: string; error: string; } export function DeepLinkImportDialog() { const { t } = useTranslation(); const queryClient = useQueryClient(); const [request, setRequest] = useState(null); const [isImporting, setIsImporting] = useState(false); const [isOpen, setIsOpen] = useState(false); useEffect(() => { // Listen for deep link import events const unlistenImport = listen( "deeplink-import", async (event) => { console.log("Deep link import event received:", event.payload); // If config is present, merge it to get the complete configuration if (event.payload.config || event.payload.configUrl) { try { const mergedRequest = await deeplinkApi.mergeDeeplinkConfig( event.payload, ); console.log("Config merged successfully:", mergedRequest); setRequest(mergedRequest); } catch (error) { console.error("Failed to merge config:", error); toast.error(t("deeplink.configMergeError"), { description: error instanceof Error ? error.message : String(error), }); // Fall back to original request setRequest(event.payload); } } else { setRequest(event.payload); } setIsOpen(true); }, ); // Listen for deep link error events const unlistenError = listen("deeplink-error", (event) => { console.error("Deep link error:", event.payload); toast.error(t("deeplink.parseError"), { description: event.payload.error, }); }); return () => { unlistenImport.then((fn) => fn()); unlistenError.then((fn) => fn()); }; }, [t]); const handleImport = async () => { if (!request) return; setIsImporting(true); try { await deeplinkApi.importFromDeeplink(request); // Invalidate provider queries to refresh the list await queryClient.invalidateQueries({ queryKey: ["providers", request.app], }); toast.success(t("deeplink.importSuccess"), { description: t("deeplink.importSuccessDescription", { name: request.name, }), }); setIsOpen(false); } catch (error) { console.error("Failed to import provider from deep link:", error); toast.error(t("deeplink.importError"), { description: error instanceof Error ? error.message : String(error), }); } finally { setIsImporting(false); } }; const handleCancel = () => { setIsOpen(false); }; // Mask API key for display (show first 4 chars + ***) const maskedApiKey = request?.apiKey && request.apiKey.length > 4 ? `${request.apiKey.substring(0, 4)}${"*".repeat(20)}` : "****"; // Check if config file is present const hasConfigFile = !!(request?.config || request?.configUrl); const configSource = request?.config ? "base64" : request?.configUrl ? "url" : null; // Parse config file content for display interface ParsedConfig { type: "claude" | "codex" | "gemini"; env?: Record; auth?: Record; tomlConfig?: string; raw: Record; } // Helper to decode base64 with UTF-8 support const b64ToUtf8 = (str: string): string => { try { const binString = atob(str); const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0) || 0); return new TextDecoder().decode(bytes); } catch (e) { console.error("Failed to decode base64:", e); return atob(str); } }; const parsedConfig = useMemo((): ParsedConfig | null => { if (!request?.config) return null; try { const decoded = b64ToUtf8(request.config); const parsed = JSON.parse(decoded) as Record; if (request.app === "claude") { // Claude 格式: { env: { ANTHROPIC_AUTH_TOKEN: ..., ... } } return { type: "claude", env: (parsed.env as Record) || {}, raw: parsed, }; } else if (request.app === "codex") { // Codex 格式: { auth: { OPENAI_API_KEY: ... }, config: "TOML string" } return { type: "codex", auth: (parsed.auth as Record) || {}, tomlConfig: (parsed.config as string) || "", raw: parsed, }; } else if (request.app === "gemini") { // Gemini 格式: 扁平结构 { GEMINI_API_KEY: ..., GEMINI_BASE_URL: ... } return { type: "gemini", env: parsed as Record, raw: parsed, }; } return null; } catch (e) { console.error("Failed to parse config:", e); return null; } }, [request?.config, request?.app]); // Helper to mask sensitive values const maskValue = (key: string, value: string): string => { const sensitiveKeys = ["TOKEN", "KEY", "SECRET", "PASSWORD"]; const isSensitive = sensitiveKeys.some((k) => key.toUpperCase().includes(k), ); if (isSensitive && value.length > 8) { return `${value.substring(0, 8)}${"*".repeat(12)}`; } return value; }; return ( {request && ( <> {/* 标题显式左对齐,避免默认居中样式影响 */} {t("deeplink.confirmImport")} {t("deeplink.confirmImportDescription")} {/* 主体内容整体右移,略大于标题内边距,让内容看起来不贴边 */}
{/* App Type */}
{t("deeplink.app")}
{request.app}
{/* Provider Name */}
{t("deeplink.providerName")}
{request.name}
{/* Homepage */}
{t("deeplink.homepage")}
{request.homepage}
{/* API Endpoint */}
{t("deeplink.endpoint")}
{request.endpoint}
{/* API Key (masked) */}
{t("deeplink.apiKey")}
{maskedApiKey}
{/* Model Fields - 根据应用类型显示不同的模型字段 */} {request.app === "claude" ? ( <> {/* Claude 四种模型字段 */} {request.haikuModel && (
{t("deeplink.haikuModel")}
{request.haikuModel}
)} {request.sonnetModel && (
{t("deeplink.sonnetModel")}
{request.sonnetModel}
)} {request.opusModel && (
{t("deeplink.opusModel")}
{request.opusModel}
)} {request.model && (
{t("deeplink.multiModel")}
{request.model}
)} ) : ( <> {/* Codex 和 Gemini 使用通用 model 字段 */} {request.model && (
{t("deeplink.model")}
{request.model}
)} )} {/* Notes (if present) */} {request.notes && (
{t("deeplink.notes")}
{request.notes}
)} {/* Config File Details (v3.8+) */} {hasConfigFile && (
{t("deeplink.configSource")}
{configSource === "base64" ? t("deeplink.configEmbedded") : t("deeplink.configRemote")} {request.configFormat && ( {request.configFormat} )}
{/* Parsed Config Details */} {parsedConfig && (
{t("deeplink.configDetails")}
{/* Claude config */} {parsedConfig.type === "claude" && parsedConfig.env && (
{Object.entries(parsedConfig.env).map( ([key, value]) => (
{key} {maskValue(key, String(value))}
), )}
)} {/* Codex config */} {parsedConfig.type === "codex" && (
{parsedConfig.auth && Object.keys(parsedConfig.auth).length > 0 && (
Auth:
{Object.entries(parsedConfig.auth).map( ([key, value]) => (
{key} {maskValue(key, String(value))}
), )}
)} {parsedConfig.tomlConfig && (
TOML Config:
                                {parsedConfig.tomlConfig.substring(0, 300)}
                                {parsedConfig.tomlConfig.length > 300 && "..."}
                              
)}
)} {/* Gemini config */} {parsedConfig.type === "gemini" && parsedConfig.env && (
{Object.entries(parsedConfig.env).map( ([key, value]) => (
{key} {maskValue(key, String(value))}
), )}
)}
)} {/* Config URL (if remote) */} {request.configUrl && (
{t("deeplink.configUrl")}
{request.configUrl}
)}
)} {/* Warning */}
{t("deeplink.warning")}
)}
); }