feat(deeplink): enhance dialog with config file preview

Add config file parsing and preview in deep link import dialog.
- Support Base64 encoded config display
- Add config file source indicator (embedded/remote)
- Parse and display config fields by app type (Claude/Codex/Gemini)
- Mask sensitive values in config preview
- Improve dialog layout and content organization
This commit is contained in:
YoVinchen
2025-11-22 14:02:22 +08:00
parent 939a2e4f2b
commit 2b0bc73276
3 changed files with 306 additions and 110 deletions

View File

@@ -281,7 +281,7 @@ function App() {
return <AgentsPanel onOpenChange={() => setCurrentView("providers")} />; return <AgentsPanel onOpenChange={() => setCurrentView("providers")} />;
default: default:
return ( return (
<div className="mx-auto max-w-[60rem] px-6 space-y-4"> <div className="mx-auto max-w-[56rem] px-6 space-y-4">
<ProviderList <ProviderList
providers={providers} providers={providers}
currentProviderId={currentProviderId} currentProviderId={currentProviderId}
@@ -345,7 +345,7 @@ function App() {
> >
<div className="h-4 w-full" aria-hidden data-tauri-drag-region /> <div className="h-4 w-full" aria-hidden data-tauri-drag-region />
<div <div
className="mx-auto max-w-[60rem] px-6 flex flex-wrap items-center justify-between gap-2" className="mx-auto max-w-[56rem] px-6 flex flex-wrap items-center justify-between gap-2"
data-tauri-drag-region data-tauri-drag-region
style={{ WebkitAppRegion: "drag" } as any} style={{ WebkitAppRegion: "drag" } as any}
> >
@@ -356,13 +356,12 @@ function App() {
{currentView !== "providers" ? ( {currentView !== "providers" ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
variant="ghost" variant="outline"
size="sm" size="icon"
onClick={() => setCurrentView("providers")} onClick={() => setCurrentView("providers")}
className="mr-1 hover:bg-black/5 dark:hover:bg-white/5 -ml-2" className="mr-2 rounded-lg"
> >
<ArrowLeft className="h-5 w-5 mr-1" /> <ArrowLeft className="h-4 w-4" />
{t("common.back")}
</Button> </Button>
<h1 className="text-lg font-semibold"> <h1 className="text-lg font-semibold">
{currentView === "settings" && t("settings.title")} {currentView === "settings" && t("settings.title")}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useMemo } from "react";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { DeepLinkImportRequest, deeplinkApi } from "@/lib/api/deeplink"; import { DeepLinkImportRequest, deeplinkApi } from "@/lib/api/deeplink";
import { import {
@@ -71,7 +71,6 @@ export function DeepLinkImportDialog() {
}); });
setIsOpen(false); setIsOpen(false);
setRequest(null);
} catch (error) { } catch (error) {
console.error("Failed to import provider from deep link:", error); console.error("Failed to import provider from deep link:", error);
toast.error(t("deeplink.importError"), { toast.error(t("deeplink.importError"), {
@@ -84,120 +83,314 @@ export function DeepLinkImportDialog() {
const handleCancel = () => { const handleCancel = () => {
setIsOpen(false); setIsOpen(false);
setRequest(null);
}; };
if (!request) return null;
// Mask API key for display (show first 4 chars + ***) // Mask API key for display (show first 4 chars + ***)
const maskedApiKey = const maskedApiKey =
request.apiKey.length > 4 request?.apiKey && request.apiKey.length > 4
? `${request.apiKey.substring(0, 4)}${"*".repeat(20)}` ? `${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<string, string>;
auth?: Record<string, string>;
tomlConfig?: string;
raw: Record<string, unknown>;
}
const parsedConfig = useMemo((): ParsedConfig | null => {
if (!request?.config) return null;
try {
const decoded = atob(request.config);
const parsed = JSON.parse(decoded) as Record<string, unknown>;
if (request.app === "claude") {
// Claude 格式: { env: { ANTHROPIC_AUTH_TOKEN: ..., ... } }
return {
type: "claude",
env: (parsed.env as Record<string, string>) || {},
raw: parsed,
};
} else if (request.app === "codex") {
// Codex 格式: { auth: { OPENAI_API_KEY: ... }, config: "TOML string" }
return {
type: "codex",
auth: (parsed.auth as Record<string, string>) || {},
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<string, string>,
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 ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen && !!request} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-[500px]"> <DialogContent className="sm:max-w-[500px]" zIndex="top">
{/* 标题显式左对齐,避免默认居中样式影响 */} {request && (
<DialogHeader className="text-left sm:text-left"> <>
<DialogTitle>{t("deeplink.confirmImport")}</DialogTitle> {/* 标题显式左对齐,避免默认居中样式影响 */}
<DialogDescription> <DialogHeader className="text-left sm:text-left">
{t("deeplink.confirmImportDescription")} <DialogTitle>{t("deeplink.confirmImport")}</DialogTitle>
</DialogDescription> <DialogDescription>
</DialogHeader> {t("deeplink.confirmImportDescription")}
</DialogDescription>
</DialogHeader>
{/* 主体内容整体右移,略大于标题内边距,让内容看起来不贴边 */} {/* 主体内容整体右移,略大于标题内边距,让内容看起来不贴边 */}
<div className="space-y-4 px-8 py-4"> <div className="space-y-4 px-8 py-4">
{/* App Type */} {/* App Type */}
<div className="grid grid-cols-3 items-center gap-4"> <div className="grid grid-cols-3 items-center gap-4">
<div className="font-medium text-sm text-muted-foreground"> <div className="font-medium text-sm text-muted-foreground">
{t("deeplink.app")} {t("deeplink.app")}
</div> </div>
<div className="col-span-2 text-sm font-medium capitalize"> <div className="col-span-2 text-sm font-medium capitalize">
{request.app} {request.app}
</div> </div>
</div>
{/* Provider Name */}
<div className="grid grid-cols-3 items-center gap-4">
<div className="font-medium text-sm text-muted-foreground">
{t("deeplink.providerName")}
</div>
<div className="col-span-2 text-sm font-medium">{request.name}</div>
</div>
{/* Homepage */}
<div className="grid grid-cols-3 items-center gap-4">
<div className="font-medium text-sm text-muted-foreground">
{t("deeplink.homepage")}
</div>
<div className="col-span-2 text-sm break-all text-blue-600 dark:text-blue-400">
{request.homepage}
</div>
</div>
{/* API Endpoint */}
<div className="grid grid-cols-3 items-center gap-4">
<div className="font-medium text-sm text-muted-foreground">
{t("deeplink.endpoint")}
</div>
<div className="col-span-2 text-sm break-all">
{request.endpoint}
</div>
</div>
{/* API Key (masked) */}
<div className="grid grid-cols-3 items-center gap-4">
<div className="font-medium text-sm text-muted-foreground">
{t("deeplink.apiKey")}
</div>
<div className="col-span-2 text-sm font-mono text-muted-foreground">
{maskedApiKey}
</div>
</div>
{/* Model (if present) */}
{request.model && (
<div className="grid grid-cols-3 items-center gap-4">
<div className="font-medium text-sm text-muted-foreground">
{t("deeplink.model")}
</div> </div>
<div className="col-span-2 text-sm font-mono">
{request.model} {/* Provider Name */}
<div className="grid grid-cols-3 items-center gap-4">
<div className="font-medium text-sm text-muted-foreground">
{t("deeplink.providerName")}
</div>
<div className="col-span-2 text-sm font-medium">
{request.name}
</div>
</div>
{/* Homepage */}
<div className="grid grid-cols-3 items-center gap-4">
<div className="font-medium text-sm text-muted-foreground">
{t("deeplink.homepage")}
</div>
<div className="col-span-2 text-sm break-all text-blue-600 dark:text-blue-400">
{request.homepage}
</div>
</div>
{/* API Endpoint */}
<div className="grid grid-cols-3 items-center gap-4">
<div className="font-medium text-sm text-muted-foreground">
{t("deeplink.endpoint")}
</div>
<div className="col-span-2 text-sm break-all">
{request.endpoint}
</div>
</div>
{/* API Key (masked) */}
<div className="grid grid-cols-3 items-center gap-4">
<div className="font-medium text-sm text-muted-foreground">
{t("deeplink.apiKey")}
</div>
<div className="col-span-2 text-sm font-mono text-muted-foreground">
{maskedApiKey}
</div>
</div>
{/* Model (if present) */}
{request.model && (
<div className="grid grid-cols-3 items-center gap-4">
<div className="font-medium text-sm text-muted-foreground">
{t("deeplink.model")}
</div>
<div className="col-span-2 text-sm font-mono">
{request.model}
</div>
</div>
)}
{/* Notes (if present) */}
{request.notes && (
<div className="grid grid-cols-3 items-start gap-4">
<div className="font-medium text-sm text-muted-foreground">
{t("deeplink.notes")}
</div>
<div className="col-span-2 text-sm text-muted-foreground">
{request.notes}
</div>
</div>
)}
{/* Config File Details (v3.8+) */}
{hasConfigFile && (
<div className="space-y-3 pt-2 border-t border-border-default">
<div className="grid grid-cols-3 items-center gap-4">
<div className="font-medium text-sm text-muted-foreground">
{t("deeplink.configSource")}
</div>
<div className="col-span-2 text-sm">
<span className="inline-flex items-center px-2 py-0.5 rounded-md bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-xs font-medium">
{configSource === "base64"
? t("deeplink.configEmbedded")
: t("deeplink.configRemote")}
</span>
{request.configFormat && (
<span className="ml-2 text-xs text-muted-foreground uppercase">
{request.configFormat}
</span>
)}
</div>
</div>
{/* Parsed Config Details */}
{parsedConfig && (
<div className="rounded-lg bg-muted/50 p-3 space-y-2">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
{t("deeplink.configDetails")}
</div>
{/* Claude config */}
{parsedConfig.type === "claude" && parsedConfig.env && (
<div className="space-y-1.5">
{Object.entries(parsedConfig.env).map(
([key, value]) => (
<div
key={key}
className="grid grid-cols-2 gap-2 text-xs"
>
<span className="font-mono text-muted-foreground truncate">
{key}
</span>
<span className="font-mono truncate">
{maskValue(key, String(value))}
</span>
</div>
),
)}
</div>
)}
{/* Codex config */}
{parsedConfig.type === "codex" && (
<div className="space-y-2">
{parsedConfig.auth &&
Object.keys(parsedConfig.auth).length > 0 && (
<div className="space-y-1.5">
<div className="text-xs text-muted-foreground">
Auth:
</div>
{Object.entries(parsedConfig.auth).map(
([key, value]) => (
<div
key={key}
className="grid grid-cols-2 gap-2 text-xs pl-2"
>
<span className="font-mono text-muted-foreground truncate">
{key}
</span>
<span className="font-mono truncate">
{maskValue(key, String(value))}
</span>
</div>
),
)}
</div>
)}
{parsedConfig.tomlConfig && (
<div className="space-y-1">
<div className="text-xs text-muted-foreground">
TOML Config:
</div>
<pre className="text-xs font-mono bg-background p-2 rounded overflow-x-auto max-h-24 whitespace-pre-wrap">
{parsedConfig.tomlConfig.substring(0, 300)}
{parsedConfig.tomlConfig.length > 300 && "..."}
</pre>
</div>
)}
</div>
)}
{/* Gemini config */}
{parsedConfig.type === "gemini" && parsedConfig.env && (
<div className="space-y-1.5">
{Object.entries(parsedConfig.env).map(
([key, value]) => (
<div
key={key}
className="grid grid-cols-2 gap-2 text-xs"
>
<span className="font-mono text-muted-foreground truncate">
{key}
</span>
<span className="font-mono truncate">
{maskValue(key, String(value))}
</span>
</div>
),
)}
</div>
)}
</div>
)}
{/* Config URL (if remote) */}
{request.configUrl && (
<div className="grid grid-cols-3 items-center gap-4">
<div className="font-medium text-sm text-muted-foreground">
{t("deeplink.configUrl")}
</div>
<div className="col-span-2 text-sm font-mono text-muted-foreground break-all">
{request.configUrl}
</div>
</div>
)}
</div>
)}
{/* Warning */}
<div className="rounded-lg bg-yellow-50 dark:bg-yellow-900/20 p-3 text-sm text-yellow-800 dark:text-yellow-200">
{t("deeplink.warning")}
</div> </div>
</div> </div>
)}
{/* Notes (if present) */} <DialogFooter>
{request.notes && ( <Button
<div className="grid grid-cols-3 items-start gap-4"> variant="outline"
<div className="font-medium text-sm text-muted-foreground"> onClick={handleCancel}
{t("deeplink.notes")} disabled={isImporting}
</div> >
<div className="col-span-2 text-sm text-muted-foreground"> {t("common.cancel")}
{request.notes} </Button>
</div> <Button onClick={handleImport} disabled={isImporting}>
</div> {isImporting ? t("deeplink.importing") : t("deeplink.import")}
)} </Button>
</DialogFooter>
{/* Warning */} </>
<div className="rounded-lg bg-yellow-50 dark:bg-yellow-900/20 p-3 text-sm text-yellow-800 dark:text-yellow-200"> )}
{t("deeplink.warning")}
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={handleCancel}
disabled={isImporting}
>
{t("common.cancel")}
</Button>
<Button onClick={handleImport} disabled={isImporting}>
{isImporting ? t("deeplink.importing") : t("deeplink.import")}
</Button>
</DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@@ -14,6 +14,10 @@ export interface DeepLinkImportRequest {
haikuModel?: string; haikuModel?: string;
sonnetModel?: string; sonnetModel?: string;
opusModel?: string; opusModel?: string;
// 配置文件导入字段 (v3.8+)
config?: string; // Base64 编码的配置内容
configFormat?: string; // json/toml
configUrl?: string; // 远程配置 URL
} }
export const deeplinkApi = { export const deeplinkApi = {