From 2be77198e557a812d23ca2a90bc3c07f29480337 Mon Sep 17 00:00:00 2001 From: lin <1609924163@qq.com> Date: Tue, 9 Jun 2026 16:41:07 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/panel/UpstreamConfigModal.tsx | 162 ++++++---- .../components/panel/UpstreamProfileList.tsx | 12 +- .../src/lib/upstreamConfigTransfer.ts | 290 +++++++++++++++++- .../upstream/AndroidUpstreamConfigModal.tsx | 38 ++- .../upstream/AndroidUpstreamEmptyState.tsx | 14 +- .../upstream/AndroidUpstreamHeader.tsx | 8 +- .../upstream/useAndroidUpstreamConfig.ts | 49 +++ .../frontend/src/styles/_android-upstream.css | 58 ++++ .../test/upstreamConfigTransfer.test.mjs | 169 +++++++++- 9 files changed, 722 insertions(+), 78 deletions(-) diff --git a/image-studio/frontend/src/components/panel/UpstreamConfigModal.tsx b/image-studio/frontend/src/components/panel/UpstreamConfigModal.tsx index bc1c64ac1..1a351dd0b 100644 --- a/image-studio/frontend/src/components/panel/UpstreamConfigModal.tsx +++ b/image-studio/frontend/src/components/panel/UpstreamConfigModal.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useMemo } from "react"; -import { Eye, EyeOff, HelpCircle, Info, Plug, Plus, RefreshCw, Sparkles } from "lucide-react"; +import { ClipboardPaste, Eye, EyeOff, HelpCircle, Info, Plug, Plus, RefreshCw, Sparkles } from "lucide-react"; import { Modal } from "../common/Modal"; import { useStudioStore } from "../../state/studioStore"; import { @@ -19,6 +19,7 @@ import { UpstreamProfileList } from "./UpstreamProfileList"; import { usePlatform } from "../../platform/context"; import { buildUpstreamModelCatalog, type UpstreamModelCatalog } from "../../lib/upstreamModels"; import { + applyParsedUpstreamConfigImport, buildUpstreamConfigExportFile, parseUpstreamConfigImportFile, } from "../../lib/upstreamConfigTransfer"; @@ -50,6 +51,8 @@ export function UpstreamConfigModal({ const [savedKeyLoaded, setSavedKeyLoaded] = useState(false); const [faqOpen, setFaqOpen] = useState(false); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); + const [quickImportOpen, setQuickImportOpen] = useState(false); + const [quickImportText, setQuickImportText] = useState(""); const [syncingCodexConfig, setSyncingCodexConfig] = useState(false); const [loadingModels, setLoadingModels] = useState(false); const [modelCatalog, setModelCatalog] = useState(null); @@ -83,6 +86,8 @@ export function UpstreamConfigModal({ if (open) return; setFaqOpen(false); setDeleteConfirmOpen(false); + setQuickImportOpen(false); + setQuickImportText(""); }, [open]); // 列表切换 selected @@ -176,75 +181,45 @@ export function UpstreamConfigModal({ try { const raw = await ImportUpstreamConfigFromFile(); if (!raw) return; - const parsed = parseUpstreamConfigImportFile(raw); - const existingByName = new Map(useStudioStore.getState().profiles.map((profile) => [profile.name, profile])); - let importedCount = 0; - let nextActiveId = ""; + await handleImportFromRawJSON(raw, "已导入"); + } catch (error: any) { + pushToast(`导入上游配置失败:${error?.message ?? error}`, "error", 6000); + } + } - for (const incoming of parsed.profiles) { - const match = existingByName.get(incoming.name) ?? null; - if (match) { - await updateProfile(match.id, { - name: incoming.name, - apiMode: incoming.apiMode, - responsesTransport: incoming.responsesTransport ?? "sse", - requestPolicy: incoming.requestPolicy, - imagesNewAPICompat: incoming.imagesNewAPICompat, - baseURL: incoming.baseURL, - textModelID: incoming.textModelID, - imageModelID: incoming.imageModelID, - reasoningEffort: incoming.reasoningEffort, - concurrencyLimit: incoming.concurrencyLimit, - fallbackProfileId: incoming.fallbackProfileId, - lastUsedAt: incoming.lastUsedAt, - apiKey: incoming.apiKey ?? "", - }); - if (incoming.apiKey?.trim()) { - await SetStoredAPIKey(keyringUserFor(match.id), incoming.apiKey.trim()).catch(() => undefined); - } - if (parsed.activeProfileId && parsed.activeProfileId === incoming.id) nextActiveId = match.id; - } else { - const newId = await createProfile({ - name: incoming.name, - apiMode: incoming.apiMode, - responsesTransport: incoming.responsesTransport ?? "sse", - requestPolicy: incoming.requestPolicy, - imagesNewAPICompat: incoming.imagesNewAPICompat, - baseURL: incoming.baseURL, - textModelID: incoming.textModelID, - imageModelID: incoming.imageModelID, - reasoningEffort: incoming.reasoningEffort, - concurrencyLimit: incoming.concurrencyLimit, - apiKey: incoming.apiKey ?? "", - setActive: false, - }); - if (incoming.fallbackProfileId) { - await updateProfile(newId, { fallbackProfileId: incoming.fallbackProfileId }); - } - if (incoming.apiKey?.trim()) { - await SetStoredAPIKey(keyringUserFor(newId), incoming.apiKey.trim()).catch(() => undefined); - } - if (parsed.activeProfileId && parsed.activeProfileId === incoming.id) nextActiveId = newId; - } - importedCount += 1; - } + async function handleImportFromRawJSON(raw: string, successPrefix = "已导入") { + const parsed = parseUpstreamConfigImportFile(raw); + const result = await applyParsedUpstreamConfigImport(parsed, { + getProfiles: () => useStudioStore.getState().profiles, + createProfile, + updateProfile, + setActiveProfile, + }); + const targetId = result.activeProfileId || result.importedProfileIds[0] || selectedId; + const selectedProfile = useStudioStore.getState().profiles.find((profile) => profile.id === targetId) + ?? useStudioStore.getState().profiles[0] + ?? null; + if (selectedProfile) { + setSelectedId(selectedProfile.id); + setDraft(selectedProfile); + setDraftKey(await GetStoredAPIKey(keyringUserFor(selectedProfile.id)).catch(() => "")); + setSavedKeyLoaded(true); + } + pushToast(`${successPrefix} ${result.importedCount} 组上游配置`, "success"); + } - if (nextActiveId) { - await setActiveProfile(nextActiveId); - setSelectedId(nextActiveId); - } - const selectedProfile = useStudioStore.getState().profiles.find((profile) => profile.id === (nextActiveId || selectedId)) - ?? useStudioStore.getState().profiles[0] - ?? null; - if (selectedProfile) { - setSelectedId(selectedProfile.id); - setDraft(selectedProfile); - setDraftKey(await GetStoredAPIKey(keyringUserFor(selectedProfile.id)).catch(() => "")); - setSavedKeyLoaded(true); - } - pushToast(`已导入 ${importedCount} 组上游配置`, "success"); + async function handleQuickImport() { + const raw = quickImportText.trim(); + if (!raw) { + pushToast("先粘贴 JSON 模板", "warn"); + return; + } + try { + await handleImportFromRawJSON(raw, "已快捷导入"); + setQuickImportOpen(false); + setQuickImportText(""); } catch (error: any) { - pushToast(`导入上游配置失败:${error?.message ?? error}`, "error", 6000); + pushToast(`快捷导入失败:${error?.message ?? error}`, "error", 6000); } } @@ -391,12 +366,23 @@ export function UpstreamConfigModal({ className={`platform-card col-span-full flex items-center justify-between gap-3 border border-[color:var(--accent)]/25 bg-[var(--accent-soft)] px-4 py-3 text-left text-[13px] text-[var(--accent)] transition-colors hover:bg-[color:var(--accent)]/15 disabled:cursor-not-allowed disabled:opacity-60 ${usesFluentUI ? "rounded-[10px]" : "rounded-[18px]"}`} > - 同步 Codex 配置 + 同步 Codex 配置 自动读取当前电脑里的 Codex `base_url` 和 `OPENAI_API_KEY`。 ) : null} + {([ { id: "responses" as APIMode, @@ -466,6 +452,7 @@ export function UpstreamConfigModal({ onHandleDelete={() => setDeleteConfirmOpen(true)} onHandleExport={handleExportConfigs} onHandleImport={handleImportConfigs} + onHandleQuickImport={() => setQuickImportOpen(true)} onHandleSetActive={handleSetActive} onHandleSyncCodex={handleSyncCodex} /> @@ -504,6 +491,45 @@ export function UpstreamConfigModal({ setFaqOpen(false)} /> + setQuickImportOpen(false)} + title="快捷导入上游配置" + width={620} + > +
+

+ 直接粘贴对方提供的 JSON 模板。当前支持本应用导出文件、newapi_channel_conn、OpenCode provider 配置。 +

+