From f8b4d67b212760ee2f2e4cc9bb21aa646f1e21f9 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 22 May 2026 11:20:12 +0800 Subject: [PATCH] fix(settings): run batch updates per-tool and lock actions while busy Update All previously sent every tool to the backend as one script, where set -e (or errorlevel on Windows) aborts the whole batch on the first failure, skipping the rest and not refreshing versions for tools that did succeed. Run each tool as its own call instead: failures are isolated, each tool's version refreshes as soon as it finishes, and the result is summarized in a toast (success / partial / failure) listing which tools failed. Also derive an isAnyBusy flag and disable all install/update buttons, the refresh button and the WSL shell selectors while any action is running, so users can't trigger concurrent global npm/pip writes. Add the toolActionPartial i18n key (zh/en/ja). --- src/components/settings/AboutSection.tsx | 135 +++++++++++++++-------- src/i18n/locales/en.json | 1 + src/i18n/locales/ja.json | 1 + src/i18n/locales/zh.json | 1 + 4 files changed, 90 insertions(+), 48 deletions(-) diff --git a/src/components/settings/AboutSection.tsx b/src/components/settings/AboutSection.tsx index bd67fce3a..61f3590ff 100644 --- a/src/components/settings/AboutSection.tsx +++ b/src/components/settings/AboutSection.tsx @@ -372,54 +372,91 @@ export function AboutSection({ isPortable }: AboutSectionProps) { async (toolNames: ToolName[], action: ToolLifecycleAction) => { if (toolNames.length === 0) return; - setToolActions((prev) => { - const next = { ...prev }; - for (const toolName of toolNames) { - next[toolName] = action; - } - return next; - }); - if (toolNames.length > 1) { + const isBatch = toolNames.length > 1; + if (isBatch) { setBatchAction(action); } - try { - await settingsApi.runToolLifecycleAction( - [...toolNames], - action, - wslShellByTool, - ); - // 静默执行已真正结束:刷新对应工具的版本号,让卡片立即反映安装结果。 - await refreshToolVersions([...toolNames], wslShellByTool); + // 逐工具串行执行:每个工具独立成败、独立刷新版本,一个失败不会连坐 + // 后续工具(后端把整批拼成单脚本 + set -e,会在首个失败处中止整批)。 + const failures: { toolName: ToolName; detail: string }[] = []; + let succeeded = 0; + + for (const toolName of toolNames) { + setToolActions((prev) => ({ ...prev, [toolName]: action })); + try { + await settingsApi.runToolLifecycleAction( + [toolName], + action, + wslShellByTool, + ); + // 静默执行真正结束后刷新该工具版本,卡片立即反映结果。 + await refreshToolVersions([toolName], wslShellByTool); + succeeded += 1; + } catch (error) { + console.error( + `[AboutSection] Failed to run tool action for ${toolName}`, + error, + ); + const detail = error instanceof Error ? error.message : String(error); + failures.push({ toolName, detail }); + } finally { + setToolActions((prev) => { + const next = { ...prev }; + delete next[toolName]; + return next; + }); + } + } + + if (isBatch) { + setBatchAction(null); + } + + const actionLabel = + action === "install" + ? t("settings.toolInstall") + : t("settings.toolUpdate"); + + if (failures.length === 0) { toast.success( t("settings.toolActionDone", { - count: toolNames.length, - action: - action === "install" - ? t("settings.toolInstall") - : t("settings.toolUpdate"), + count: succeeded, + action: actionLabel, }), { closeButton: true }, ); - } catch (error) { - console.error("[AboutSection] Failed to run tool action", error); - // 后端在命令失败时回传 stderr 末尾若干行,作为 toast 详情提示用户。 - const detail = error instanceof Error ? error.message : String(error); + return; + } + + // 批量场景每个失败只摘取错误末行(最相关),单工具场景给出完整详情。 + const lastLine = (text: string) => { + const lines = text.trim().split("\n").filter(Boolean); + return lines[lines.length - 1] ?? text; + }; + const failureDescription = isBatch + ? failures + .map( + (f) => `${TOOL_DISPLAY_NAMES[f.toolName]}: ${lastLine(f.detail)}`, + ) + .join("\n") + : failures[0]?.detail; + + if (succeeded === 0) { toast.error(t("settings.toolActionFailed"), { - description: detail || undefined, + description: failureDescription || undefined, closeButton: true, }); - } finally { - setToolActions((prev) => { - const next = { ...prev }; - for (const toolName of toolNames) { - delete next[toolName]; - } - return next; - }); - if (toolNames.length > 1) { - setBatchAction(null); - } + } else { + // 部分成功:用 warning 汇总成败数量,详情列出失败的工具。 + toast.warning( + t("settings.toolActionPartial", { + succeeded, + failed: failures.length, + action: actionLabel, + }), + { description: failureDescription || undefined, closeButton: true }, + ); } }, [t, wslShellByTool, refreshToolVersions], @@ -427,6 +464,10 @@ export function AboutSection({ isPortable }: AboutSectionProps) { const displayVersion = version ?? t("common.unknown"); + // 任一安装/升级进行中(批量或单工具)即视为忙碌:用于禁用所有操作按钮, + // 避免并发触发多个 npm/pip 全局写入造成冲突。 + const isAnyBusy = Boolean(batchAction) || Object.keys(toolActions).length > 0; + return ( loadAllToolVersions()} - disabled={isLoadingTools || Boolean(batchAction)} + disabled={isLoadingTools || isAnyBusy} > handleRunToolAction(updatableToolNames, "update")} disabled={ - isLoadingTools || - Boolean(batchAction) || - updatableToolNames.length === 0 + isLoadingTools || isAnyBusy || updatableToolNames.length === 0 } > {batchAction === "update" ? ( @@ -704,7 +743,9 @@ export function AboutSection({ isPortable }: AboutSectionProps) {