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:
13
src/App.tsx
13
src/App.tsx
@@ -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")}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user