From a97949b649d2147da57468288e65e228101351bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E5=BE=B7=E6=A3=AE?= Date: Wed, 28 Oct 2026 09:24:56 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E6=A8=A1=E7=BB=84=E5=AF=BC=E5=85=A5=E4=B8=8E=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=8F=96=E6=B6=88=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/modules/import-service.test.ts | 50 ++++++++++++++++++- .../src/main/modules/import-service.ts | 23 +++++---- .../desktop/src/main/modules/module-router.ts | 6 +-- apps/desktop/src/main/native-dialog.ts | 2 +- .../features/modules/ImportModuleButton.tsx | 41 ++++++++++++--- .../features/modules/ModuleImportNotice.tsx | 6 ++- apps/desktop/src/shared/module-import.ts | 2 +- ...07\346\241\243\345\256\236\347\216\260.md" | 2 + 8 files changed, 108 insertions(+), 24 deletions(-) diff --git a/apps/desktop/src/main/modules/import-service.test.ts b/apps/desktop/src/main/modules/import-service.test.ts index 6e93e39..c47097b 100644 --- a/apps/desktop/src/main/modules/import-service.test.ts +++ b/apps/desktop/src/main/modules/import-service.test.ts @@ -12,6 +12,50 @@ import { windowsProtocolCommand } from "../import-protocol"; import { ExistingGitModuleError } from "./import-git-module"; import { ModuleImportService } from "./import-service"; +test("本地来源选择期间不开始导入,取消后可重选,选定后才进入导入状态", async () => { + let directoryReads = 0; + const service = new ModuleImportService(async () => { + directoryReads++; + return "/library"; + }); + let select!: (path: string | null) => void; + let finish!: () => void; + let imports = 0; + const run = async (directory: string, path: string) => { + imports++; + assert.equal(directory, "/library"); + assert.equal(path, "/tmp/module.zip"); + await new Promise((resolve) => { + finish = resolve; + }); + return { path: "/library/module", name: "测试模组" }; + }; + const choose = () => new Promise((resolve) => (select = resolve)); + const canceled = service.local("ZIP 文件", choose, run); + assert.equal(service.get().status, "selecting"); + assert.equal(directoryReads, 0); + assert.equal(imports, 0); + assert.throws(() => service.start({ url: "https://github.com/a/b.git" }), /稍后重试/); + await assert.rejects(service.local("文件夹", choose, run), /稍后重试/); + service.dismiss(service.get().id); + service.report(new Error("不能打断选择")); + assert.equal(service.get().status, "selecting"); + select(null); + assert.equal(await canceled, null); + assert.equal(service.get().status, "idle"); + assert.equal(directoryReads, 0); + assert.equal(imports, 0); + + const importing = service.local("ZIP 文件", choose, run); + select("/tmp/module.zip"); + await waitFor(() => assert.equal(imports, 1)); + assert.equal(service.get().status, "running"); + assert.equal(directoryReads, 1); + finish(); + assert.deepEqual(await importing, { path: "/library/module", name: "测试模组" }); + assert.equal(service.get().status, "success"); +}); + test("官网链接仅接受 GitHub 仓库与提醒版本,不能注入路径、分支或命令", () => { const revision = "a".repeat(40); assert.deepEqual( @@ -84,7 +128,11 @@ test("导入互斥、替换确认取消、失败重试与官网版本提醒", as assert.throws(() => service.start(request), /稍后重试/); assert.equal(service.get().busyNotice, 1); await assert.rejects( - service.local("ZIP", async () => assert.fail("忙碌时不能打开选择器")), + service.local( + "ZIP", + async () => assert.fail("忙碌时不能打开选择器"), + async () => assert.fail("忙碌时不能导入"), + ), /稍后重试/, ); complete(); diff --git a/apps/desktop/src/main/modules/import-service.ts b/apps/desktop/src/main/modules/import-service.ts index 626483e..a99e481 100644 --- a/apps/desktop/src/main/modules/import-service.ts +++ b/apps/desktop/src/main/modules/import-service.ts @@ -20,7 +20,7 @@ export class ModuleImportService { } private available(): void { - if (this.state.status === "running" || this.state.status === "confirm") { + if (["selecting", "running", "confirm"].includes(this.state.status)) { this.state = { ...this.state, busyNotice: (this.state.busyNotice ?? 0) + 1 }; throw new Error("正在导入模组,请稍后重试。"); } @@ -34,7 +34,7 @@ export class ModuleImportService { } report(error: unknown): void { - if (this.state.status === "running" || this.state.status === "confirm") return; + if (["selecting", "running", "confirm"].includes(this.state.status)) return; this.request = undefined; this.state = { id: this.state.id + 1, @@ -89,23 +89,28 @@ export class ModuleImportService { } dismiss(id: number): void { - if (id !== this.state.id || this.state.status === "running") return; + if (id !== this.state.id || ["selecting", "running"].includes(this.state.status)) return; this.state = { id, status: "idle" }; this.request = undefined; } async local( source: string, - run: (directory: string) => Promise, + select: () => Promise, + run: (directory: string, path: string) => Promise, ): Promise { this.available(); this.request = undefined; - this.state = { id: this.state.id + 1, status: "running", source }; + this.state = { id: this.state.id + 1, status: "selecting", source }; try { - const result = await run(await this.directory()); - this.state = result - ? { ...this.state, status: "success", result } - : { id: this.state.id, status: "idle" }; + const path = await select(); + if (!path) { + this.state = { id: this.state.id, status: "idle" }; + return null; + } + this.state = { ...this.state, status: "running" }; + const result = await run(await this.directory(), path); + this.state = { ...this.state, status: "success", result }; return result; } catch (error) { this.state = { diff --git a/apps/desktop/src/main/modules/module-router.ts b/apps/desktop/src/main/modules/module-router.ts index 7f1cf11..c28328b 100644 --- a/apps/desktop/src/main/modules/module-router.ts +++ b/apps/desktop/src/main/modules/module-router.ts @@ -70,7 +70,7 @@ export const moduleRouter = { try { return await context.moduleImports.local( input.kind === "zip" ? "ZIP 文件" : "本地文件夹", - async (directory) => { + async () => { const selection = await showPathPicker({ title: input.kind === "zip" ? "选择模组 ZIP 文件" : "选择模组文件夹", properties: [input.kind === "zip" ? "openFile" : "openDirectory"], @@ -78,9 +78,9 @@ export const moduleRouter = { ? { filters: [{ name: "ZIP 压缩包", extensions: ["zip"] }] } : {}), }); - if (selection.canceled || !selection.filePaths[0]) return null; - return await importLocalModule(directory, selection.filePaths[0], input.kind); + return selection.canceled ? null : (selection.filePaths[0] ?? null); }, + (directory, path) => importLocalModule(directory, path, input.kind), ); } catch (error) { throw new ORPCError("BAD_REQUEST", { diff --git a/apps/desktop/src/main/native-dialog.ts b/apps/desktop/src/main/native-dialog.ts index 5f5b6e1..7792135 100644 --- a/apps/desktop/src/main/native-dialog.ts +++ b/apps/desktop/src/main/native-dialog.ts @@ -8,7 +8,7 @@ export interface PathPickerOptions { } export async function showPathPicker(options: PathPickerOptions) { const filePaths = await Utils.openFileDialog({ - startingFolder: options.defaultPath, + startingFolder: options.defaultPath ?? "~/", canChooseFiles: options.properties.includes("openFile"), canChooseDirectory: options.properties.includes("openDirectory"), allowsMultipleSelection: false, diff --git a/apps/desktop/src/renderer/src/features/modules/ImportModuleButton.tsx b/apps/desktop/src/renderer/src/features/modules/ImportModuleButton.tsx index fad060c..084fc97 100644 --- a/apps/desktop/src/renderer/src/features/modules/ImportModuleButton.tsx +++ b/apps/desktop/src/renderer/src/features/modules/ImportModuleButton.tsx @@ -1,4 +1,4 @@ -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { FileArchive, Folder, FolderInput, GitBranch, LoaderCircle } from "lucide-react"; import { useState } from "react"; @@ -19,6 +19,7 @@ type ImportSource = { kind: "zip" | "folder" } | { kind: "git"; url: string }; export function ImportModuleButton({ directory }: { directory?: string }) { const [open, setOpen] = useState(false); + const [sourceKind, setSourceKind] = useState("zip"); const [url, setUrl] = useState(""); const [importedName, setImportedName] = useState(null); const importer = useMutation({ @@ -26,7 +27,8 @@ export function ImportModuleButton({ directory }: { directory?: string }) { source.kind === "git" ? await desktopRpc.modules.importGit({ url: source.url }) : await desktopRpc.modules.importLocal(source), - onSuccess: async (module) => { + onSuccess: async (module, source) => { + if (source.kind !== "git" && !module) return; if (!module) { setOpen(false); return; @@ -37,6 +39,16 @@ export function ImportModuleButton({ directory }: { directory?: string }) { setUrl(""); }, }); + const importState = useQuery({ + ...desktopOrpc.modules.importState.queryOptions(), + enabled: importer.isPending && importer.variables?.kind !== "git", + staleTime: 0, + refetchInterval: 250, + }); + const importing = + importer.isPending && + importState.dataUpdatedAt > importer.submittedAt && + importState.data?.status === "running"; return (
{importedName ? ( @@ -72,7 +84,14 @@ export function ImportModuleButton({ directory }: { directory?: string }) { 选择 ZIP、文件夹或 Git 仓库,将模组保存到你的模组库。 - importer.reset()}> + { + if (importer.isPending || !value) return; + setSourceKind(value as ImportSource["kind"]); + importer.reset(); + }} + >