From 8dd33a9649c5d72b71517e92288324fd6b0a8f6f Mon Sep 17 00:00:00 2001 From: forceOfPx <3239735744@qq.com> Date: Fri, 16 Jan 2026 11:13:50 +0800 Subject: [PATCH 01/22] =?UTF-8?q?=E5=85=B1=E5=BB=BA=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- icons/joint_build.png | Bin 0 -> 1173 bytes package-lock.json | 91 ++++++++------------ src/renderer/app.tsx | 2 + src/renderer/pages/hello/index.scss | 34 ++++++++ src/renderer/pages/hello/index.tsx | 8 ++ src/renderer/pages/welcome/WelcomeModal.css | 75 ++++++++++++++++ src/renderer/pages/welcome/WelcomeModal.tsx | 59 +++++++++++++ 7 files changed, 215 insertions(+), 54 deletions(-) create mode 100644 icons/joint_build.png create mode 100644 src/renderer/pages/welcome/WelcomeModal.css create mode 100644 src/renderer/pages/welcome/WelcomeModal.tsx diff --git a/icons/joint_build.png b/icons/joint_build.png new file mode 100644 index 0000000000000000000000000000000000000000..967f3a0b487522fb53d063d7c1c72faa6adf8c51 GIT binary patch literal 1173 zcmV;G1Zw+u5b59P9_yM#fSc5ne^wyb0aBXJRc8^X;GYDnk3;?r z$?2D z@5C$u^`-^>_W+g;D-w31o{1Q?)Un#SWEKG~^mDPujn*zUXX2|e7SW-LZWaM}fc;G& z$!DpYi`=FXejEc3iL5`35NPfpihN#7A(5NxLayhsv39Pt0gpthcjY4lxB%b|p2Ny; zGse$bxhJnXMrY5w%a{=Y*)4$2zMIR8unR(3V@&N)DTYz0E8wF#;eEG(ZyXJbxkDj4rHUR*v zx~Ft~I@}>H|0;o=Iaex12>f%UVOjY3d&{zX5F&RD5Q*x|R1bZ9lH616W|1kgN1{)as#Kdn?7T?KDEb>RR=z)k;=($o;i!tewj3Lh8{)7&-%tddnc9OdiEQ zH?xo$#oc7(jWwqYq|4c9_3nIR2Pn5v)gVBAR%t(ES!H7hS82expt0JU&_!pfcju!7 z7yyJ}%GSBVaNSB<@uGX&V+$3EP-fcgp_Lo@NMcLmVrzF&4*}aw zIq@YXL{ctHI)rPXao1?-G}kTeVgnj@B-pY@^rzwXi#-G=yFYVTTvY9HV!UT}=mHQ> za{#wMh%76AH_uBC0kPWQT+x4N0&Ht{fZJN-i-MH`@S@gA*Q=%;0$ip=5fP^6&7ODX z)+)b3vn(ytiddo;+socx4}mtHaxvZogpdcF=dYD&V~*zL0!bIZfO9Ffx^x{OkQFkl z-g4zp;eOsr^20ig5E%94Jy1Yzot?dTm%bhXVsTHzxF}t&eVJQ~oE@Q>?X34+xrYFs zv{Lsbrv|RH<;(ne=14.17" - } - }, "node_modules/@svgr/hast-util-to-babel-ast": { "version": "8.0.0", "resolved": "https://registry.npmmirror.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", @@ -5431,22 +5420,6 @@ } } }, - "node_modules/@svgr/plugin-svgo/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/@svgr/webpack": { "version": "8.1.0", "resolved": "https://registry.npmmirror.com/@svgr/webpack/-/webpack-8.1.0.tgz", @@ -5626,6 +5599,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5787,6 +5761,7 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5828,6 +5803,7 @@ "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6020,6 +5996,7 @@ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -6431,6 +6408,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6520,6 +6498,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6627,7 +6606,6 @@ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -6641,6 +6619,7 @@ "integrity": "sha512-TTFVbpKbyL6cPfEoKq6Ya3BIjTUr7uDW9+7Z+1oysRv1gpcN7kQ4luH8r/+rXXwz4n6BIz1iBJ1ezKCdsdNW0w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ant-design/colors": "^7.2.1", "@ant-design/cssinjs": "^1.23.0", @@ -7249,6 +7228,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -8514,7 +8494,8 @@ "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -8539,7 +8520,6 @@ "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9692,17 +9672,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmmirror.com/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -9996,6 +9965,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -10052,6 +10022,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -10564,6 +10535,7 @@ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -14087,7 +14059,6 @@ "resolved": "https://registry.npmmirror.com/mustache/-/mustache-4.2.0.tgz", "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", "license": "MIT", - "peer": true, "bin": { "mustache": "bin/mustache" } @@ -15154,6 +15125,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -15289,6 +15261,7 @@ "integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -16214,6 +16187,7 @@ "resolved": "https://registry.npmmirror.com/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -16223,6 +16197,7 @@ "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -18435,6 +18410,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -18903,6 +18879,7 @@ "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -19207,6 +19184,7 @@ "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -19280,6 +19258,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -19397,6 +19376,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -19489,6 +19469,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -19780,6 +19761,7 @@ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -19812,7 +19794,8 @@ "integrity": "sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==", "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/xterm-addon-fit": { "version": "0.5.0", @@ -19983,6 +19966,7 @@ "resolved": "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -19992,7 +19976,6 @@ "resolved": "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", "license": "ISC", - "peer": true, "peerDependencies": { "zod": "^3.25 || ^4" } diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index c8247c4..4859dc1 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -15,10 +15,12 @@ import PdfConvert from './pages/pdf-convert'; import PdfConfig from './pages/pdf-config'; import LLMConfig from './pages/llm-api-config'; import VoiceRTCConfig from './pages/voice-rtc-config'; +import WelcomeModal from './pages/welcome/WelcomeModal'; export default function App() { return ( + } /> diff --git a/src/renderer/pages/hello/index.scss b/src/renderer/pages/hello/index.scss index e3bc9f6..fffe4d2 100644 --- a/src/renderer/pages/hello/index.scss +++ b/src/renderer/pages/hello/index.scss @@ -667,6 +667,40 @@ body{ transform: translateY(-2px); } } + + .joint-build-button { + box-sizing: border-box; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 2px 8px; + gap: 4px; + width: auto; + min-width: 90px; + min-height: 22px; + background: #52c41a; + border: 1px solid #52c41a; + border-radius: 6px; + color: #fff; + font-family: 'Inter', sans-serif; + font-style: normal; + font-weight: 500; + font-size: clamp(10px, 2vw, 12px); + line-height: 16px; + cursor: pointer; + + .joint-build-icon { + width: 16px; + height: 16px; + } + + &:hover { + background: #73d13d; + border-color: #73d13d; + transform: translateY(-2px); + } + } .dev-example-link-inline { text-decoration: none; diff --git a/src/renderer/pages/hello/index.tsx b/src/renderer/pages/hello/index.tsx index 1e4c27f..95e4379 100644 --- a/src/renderer/pages/hello/index.tsx +++ b/src/renderer/pages/hello/index.tsx @@ -12,6 +12,7 @@ import wslLogo from './wslLogo.png'; // 新增导入Frame 3和Frame 8图片 import frame3 from './Frame 3.png'; import frame8 from './Frame 8.png'; +import jointBuildIcon from '../../../../icons/joint_build.png'; import './index.scss'; import { LeftOutlined, RightOutlined } from '@ant-design/icons'; import { useTrainingServiceShortcut } from '../../containers/use-training-service-shortcut'; @@ -581,6 +582,13 @@ export default function Hello() { 版本号:{__NPM_PACKAGE_VERSION__} 源码版本:{__COMMIT_HASH__}
+ + +
+ + + ); +} From 688587db9098da0598746b26a7093d081d623950 Mon Sep 17 00:00:00 2001 From: forceOfPx <3239735744@qq.com> Date: Fri, 16 Jan 2026 11:21:20 +0800 Subject: [PATCH 02/22] =?UTF-8?q?=E5=85=B1=E5=BB=BA=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/app.tsx | 2 + src/renderer/pages/hello/index.scss | 4 + src/renderer/pages/hello/index.tsx | 13 +- src/renderer/pages/joint-build/index.scss | 159 +++++++++++++++++ src/renderer/pages/joint-build/index.tsx | 200 ++++++++++++++++++++++ 5 files changed, 371 insertions(+), 7 deletions(-) create mode 100644 src/renderer/pages/joint-build/index.scss create mode 100644 src/renderer/pages/joint-build/index.tsx diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index 4859dc1..06081f9 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -16,6 +16,7 @@ import PdfConfig from './pages/pdf-config'; import LLMConfig from './pages/llm-api-config'; import VoiceRTCConfig from './pages/voice-rtc-config'; import WelcomeModal from './pages/welcome/WelcomeModal'; +import JointBuild from './pages/joint-build'; export default function App() { return ( @@ -23,6 +24,7 @@ export default function App() { + } /> } /> } /> } /> diff --git a/src/renderer/pages/hello/index.scss b/src/renderer/pages/hello/index.scss index fffe4d2..484dea7 100644 --- a/src/renderer/pages/hello/index.scss +++ b/src/renderer/pages/hello/index.scss @@ -668,6 +668,10 @@ body{ } } + .joint-build-link { + text-decoration: none; + } + .joint-build-button { box-sizing: border-box; display: flex; diff --git a/src/renderer/pages/hello/index.tsx b/src/renderer/pages/hello/index.tsx index 95e4379..0c712ff 100644 --- a/src/renderer/pages/hello/index.tsx +++ b/src/renderer/pages/hello/index.tsx @@ -582,13 +582,12 @@ export default function Hello() { 版本号:{__NPM_PACKAGE_VERSION__} 源码版本:{__COMMIT_HASH__}
- + + + +
+ + + +
+

共建参与模块设置

+

管理参与哪些模块的共建计划,请根据您电脑的磁盘空余选择开启

+
+ {modules.map((mod) => ( +
+
+
+ {mod.name} + + 推荐指数 {renderRecommendLevel(mod.recommendLevel)} + +
+ 磁盘占用 {mod.diskUsage} +
+ handleModuleToggle(mod.id, checked)} + disabled={!masterSwitch} + /> +
+ ))} +
+
+ + ); +} From 6fc2e189b671c8d90b5223aeaf207c4e87f73e40 Mon Sep 17 00:00:00 2001 From: forceOfPx <3239735744@qq.com> Date: Fri, 16 Jan 2026 11:24:19 +0800 Subject: [PATCH 03/22] =?UTF-8?q?=E5=85=B1=E5=BB=BA=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E6=80=BB=E5=BC=80=E5=85=B3=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/pages/joint-build/index.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/renderer/pages/joint-build/index.tsx b/src/renderer/pages/joint-build/index.tsx index 5b178a4..0037119 100644 --- a/src/renderer/pages/joint-build/index.tsx +++ b/src/renderer/pages/joint-build/index.tsx @@ -17,6 +17,7 @@ interface ModuleItem { const STORAGE_KEY_MASTER = 'joint_build_master_switch'; const STORAGE_KEY_MODULES = 'joint_build_modules'; const STORAGE_KEY_DISK_PATH = 'joint_build_disk_path'; +const STORAGE_KEY_WELCOME = 'ai_learning_assistant_welcome_shown'; // 欢迎弹窗用户选择 export default function JointBuild() { const [masterSwitch, setMasterSwitch] = useState(false); @@ -52,9 +53,16 @@ export default function JointBuild() { // 从 localStorage 加载配置 useEffect(() => { + // 优先读取共建开关的设置,如果没有则读取欢迎弹窗的用户选择 const savedMasterSwitch = localStorage.getItem(STORAGE_KEY_MASTER); if (savedMasterSwitch !== null) { setMasterSwitch(savedMasterSwitch === 'true'); + } else { + // 如果没有单独设置过共建开关,则读取欢迎弹窗的选择 + const welcomeChoice = localStorage.getItem(STORAGE_KEY_WELCOME); + if (welcomeChoice === 'true') { + setMasterSwitch(true); + } } const savedModules = localStorage.getItem(STORAGE_KEY_MODULES); From 80443b8ee337fcddf0737c63d3ec8563bbe12e48 Mon Sep 17 00:00:00 2001 From: forceOfPx <3239735744@qq.com> Date: Fri, 16 Jan 2026 11:36:05 +0800 Subject: [PATCH 04/22] =?UTF-8?q?=E8=AF=BB=E5=8F=96=E7=A3=81=E7=9B=98?= =?UTF-8?q?=E5=89=A9=E4=BD=99=E7=A9=BA=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/index.ts | 2 + src/main/joint-build/index.ts | 68 ++++++++++++++++++++++++ src/main/joint-build/type-info.ts | 11 ++++ src/main/preload.ts | 12 +++++ src/renderer/pages/joint-build/index.tsx | 56 ++++++++++++++----- 5 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 src/main/joint-build/index.ts create mode 100644 src/main/joint-build/type-info.ts diff --git a/src/main/index.ts b/src/main/index.ts index 2e400b2..c64ef7c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -13,6 +13,7 @@ import initPdfConvert from './pdf-convert'; import initTrainingService from './training-service'; import initLogService from './backup'; import initExternalUrl from './external-url'; +import { setupJointBuildHandlers } from './joint-build'; import path from 'node:path'; import { appPath, autoAdaptEncodingForWindows } from './exec'; import { logDeviceInfo } from './logger/log-device-info'; @@ -77,6 +78,7 @@ initExampleMain(ipcMain); initPdfConvert(ipcMain); initTrainingService(ipcMain); initExternalUrl(ipcMain); +setupJointBuildHandlers(ipcMain); updateTemplate(); const createWindow = async () => { diff --git a/src/main/joint-build/index.ts b/src/main/joint-build/index.ts new file mode 100644 index 0000000..2a37d53 --- /dev/null +++ b/src/main/joint-build/index.ts @@ -0,0 +1,68 @@ +import { dialog, IpcMain } from 'electron'; +import { selectFolderHandle, getDiskInfoHandle, DiskInfo } from './type-info'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import path from 'path'; +import { ipcHandle } from '../ipc-util'; + +const execPromise = promisify(exec); + +// 获取Windows磁盘信息 +async function getWindowsDiskInfo(diskPath: string): Promise { + try { + // 获取盘符,例如 "C:" 或 "D:" + const driveLetter = path.parse(diskPath).root.replace('\\', ''); + + // 使用 wmic 命令获取磁盘信息 + const { stdout } = await execPromise( + `wmic logicaldisk where "DeviceID='${driveLetter}'" get Size,FreeSpace /format:csv`, + { encoding: 'utf8' } + ); + + const lines = stdout.trim().split('\n').filter(line => line.trim()); + if (lines.length < 2) { + throw new Error('无法获取磁盘信息'); + } + + // CSV格式: Node,FreeSpace,Size + const values = lines[1].split(','); + const freeSpace = parseInt(values[1], 10) || 0; + const totalSize = parseInt(values[2], 10) || 0; + + return { + total: totalSize, + free: freeSpace, + used: totalSize - freeSpace, + }; + } catch (error) { + console.error('获取磁盘信息失败:', error); + // 返回默认值 + return { + total: 500 * 1024 * 1024 * 1024, // 500GB + free: 200 * 1024 * 1024 * 1024, // 200GB + used: 300 * 1024 * 1024 * 1024, // 300GB + }; + } +} + +export function setupJointBuildHandlers(ipcMain: IpcMain): void { + // 处理选择文件夹 + ipcHandle(ipcMain, selectFolderHandle, async () => { + const result = await dialog.showOpenDialog({ + properties: ['openDirectory'], + defaultPath: 'C:\\', // Windows默认从C盘开始 + title: '选择共建计划存储路径', + }); + + if (result.canceled || !result.filePaths || result.filePaths.length === 0) { + return null; + } + + return result.filePaths[0]; + }); + + // 处理获取磁盘信息 + ipcHandle(ipcMain, getDiskInfoHandle, async (_event, diskPath: string) => { + return getWindowsDiskInfo(diskPath); + }); +} diff --git a/src/main/joint-build/type-info.ts b/src/main/joint-build/type-info.ts new file mode 100644 index 0000000..5530bd7 --- /dev/null +++ b/src/main/joint-build/type-info.ts @@ -0,0 +1,11 @@ +export type ActionName = 'select-folder' | 'get-disk-info'; +export type ServiceName = 'joint-build'; + +export const selectFolderHandle = 'joint-build:select-folder'; +export const getDiskInfoHandle = 'joint-build:get-disk-info'; + +export interface DiskInfo { + total: number; // bytes + free: number; // bytes + used: number; // bytes +} diff --git a/src/main/preload.ts b/src/main/preload.ts index c4c20cf..5c87604 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -19,6 +19,11 @@ import { removeTrainingServiceHandle, startTrainingServiceHandle, } from './training-service/type-info'; +import { + selectFolderHandle, + getDiskInfoHandle, + DiskInfo, +} from './joint-build/type-info'; const electronHandler = { ipcRenderer: { @@ -108,6 +113,13 @@ const mainHandle = { logsTrainingServiceHandle, ); }, + // 共建计划相关 + selectJointBuildFolder: async (): Promise => { + return ipcInvoke(selectFolderHandle); + }, + getJointBuildDiskInfo: async (diskPath: string): Promise => { + return ipcInvoke(getDiskInfoHandle, diskPath); + }, }; export type MainHandle = typeof mainHandle; diff --git a/src/renderer/pages/joint-build/index.tsx b/src/renderer/pages/joint-build/index.tsx index 0037119..0a9e092 100644 --- a/src/renderer/pages/joint-build/index.tsx +++ b/src/renderer/pages/joint-build/index.tsx @@ -1,6 +1,6 @@ import { Switch, Button, Progress, message } from 'antd'; import { NavLink } from 'react-router-dom'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { LeftOutlined } from '@ant-design/icons'; import jointBuildIcon from '../../../../icons/joint_build.png'; import './index.scss'; @@ -19,11 +19,18 @@ const STORAGE_KEY_MODULES = 'joint_build_modules'; const STORAGE_KEY_DISK_PATH = 'joint_build_disk_path'; const STORAGE_KEY_WELCOME = 'ai_learning_assistant_welcome_shown'; // 欢迎弹窗用户选择 +// 字节转GB +const bytesToGB = (bytes: number): number => { + return Math.round((bytes / (1024 * 1024 * 1024)) * 10) / 10; +}; + export default function JointBuild() { const [masterSwitch, setMasterSwitch] = useState(false); const [diskPath, setDiskPath] = useState('C:\\'); - const [diskUsed, setDiskUsed] = useState(120); // GB - const [diskTotal, setDiskTotal] = useState(500); // GB + const [diskUsed, setDiskUsed] = useState(0); + const [diskTotal, setDiskTotal] = useState(0); + const [diskLoading, setDiskLoading] = useState(false); + // todo: 这些数据从哪获取?还是写死 const [modules, setModules] = useState([ { id: 'launcher', @@ -51,6 +58,20 @@ export default function JointBuild() { }, ]); + // 获取磁盘信息 + const fetchDiskInfo = useCallback(async (path: string) => { + setDiskLoading(true); + try { + const diskInfo = await window.mainHandle.getJointBuildDiskInfo(path); + setDiskTotal(bytesToGB(diskInfo.total)); + setDiskUsed(bytesToGB(diskInfo.used)); + } catch (error) { + console.error('获取磁盘信息失败:', error); + } finally { + setDiskLoading(false); + } + }, []); + // 从 localStorage 加载配置 useEffect(() => { // 优先读取共建开关的设置,如果没有则读取欢迎弹窗的用户选择 @@ -81,10 +102,12 @@ export default function JointBuild() { } const savedDiskPath = localStorage.getItem(STORAGE_KEY_DISK_PATH); - if (savedDiskPath) { - setDiskPath(savedDiskPath); - } - }, []); + const initialPath = savedDiskPath || 'C:\\'; + setDiskPath(initialPath); + + // 初始化时获取磁盘信息 + fetchDiskInfo(initialPath); + }, [fetchDiskInfo]); // 保存配置到 localStorage const saveConfig = (master: boolean, mods: ModuleItem[]) => { @@ -114,13 +137,18 @@ export default function JointBuild() { }; const handleChangePath = async () => { - // 这里可以调用 electron 的对话框选择文件夹 - // 暂时用 prompt 模拟 - const newPath = window.prompt('请输入新的磁盘路径:', diskPath); - if (newPath) { - setDiskPath(newPath); - localStorage.setItem(STORAGE_KEY_DISK_PATH, newPath); - message.success('路径已更新'); + try { + const newPath = await window.mainHandle.selectJointBuildFolder(); + if (newPath) { + setDiskPath(newPath); + localStorage.setItem(STORAGE_KEY_DISK_PATH, newPath); + message.success('路径已更新'); + // 获取新路径的磁盘信息 + fetchDiskInfo(newPath); + } + } catch (error) { + console.error('选择文件夹失败:', error); + message.error('选择文件夹失败'); } }; From d49c61ffeba560a52939ba5d3b2fc78067d127bb Mon Sep 17 00:00:00 2001 From: forceOfPx <3239735744@qq.com> Date: Fri, 16 Jan 2026 11:49:47 +0800 Subject: [PATCH 05/22] =?UTF-8?q?=E6=89=98=E7=9B=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/index.ts | 9 +- src/main/joint-build/index.ts | 118 ++++++++++++++++++++++- src/main/joint-build/type-info.ts | 3 +- src/main/preload.ts | 4 + src/renderer/pages/hello/index.tsx | 23 +++++ src/renderer/pages/joint-build/index.tsx | 22 ++++- 6 files changed, 171 insertions(+), 8 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index c64ef7c..2b00f42 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -13,7 +13,7 @@ import initPdfConvert from './pdf-convert'; import initTrainingService from './training-service'; import initLogService from './backup'; import initExternalUrl from './external-url'; -import { setupJointBuildHandlers } from './joint-build'; +import { setupJointBuildHandlers, setupWindowCloseHandler, isTrayEnabled } from './joint-build'; import path from 'node:path'; import { appPath, autoAdaptEncodingForWindows } from './exec'; import { logDeviceInfo } from './logger/log-device-info'; @@ -93,6 +93,9 @@ const createWindow = async () => { autoHideMenuBar: true, }); + // 设置窗口关闭行为(托盘最小化) + setupWindowCloseHandler(mainWindow); + // 最大化窗口 mainWindow.maximize(); @@ -119,6 +122,10 @@ app.on('ready', createWindow); // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { + // 如果托盘启用,不退出应用 + if (isTrayEnabled()) { + return; + } if (process.platform !== 'darwin') { app.quit(); } diff --git a/src/main/joint-build/index.ts b/src/main/joint-build/index.ts index 2a37d53..c84e718 100644 --- a/src/main/joint-build/index.ts +++ b/src/main/joint-build/index.ts @@ -1,5 +1,5 @@ -import { dialog, IpcMain } from 'electron'; -import { selectFolderHandle, getDiskInfoHandle, DiskInfo } from './type-info'; +import { dialog, IpcMain, Tray, Menu, BrowserWindow, app, nativeImage } from 'electron'; +import { selectFolderHandle, getDiskInfoHandle, DiskInfo, setTrayEnabledHandle } from './type-info'; import { exec } from 'child_process'; import { promisify } from 'util'; import path from 'path'; @@ -7,6 +7,13 @@ import { ipcHandle } from '../ipc-util'; const execPromise = promisify(exec); +// 托盘实例 +let tray: Tray | null = null; +// 是否启用托盘(共建开关状态) +let trayEnabled = false; +// 是否正在强制退出 +let forceQuit = false; + // 获取Windows磁盘信息 async function getWindowsDiskInfo(diskPath: string): Promise { try { @@ -65,4 +72,111 @@ export function setupJointBuildHandlers(ipcMain: IpcMain): void { ipcHandle(ipcMain, getDiskInfoHandle, async (_event, diskPath: string) => { return getWindowsDiskInfo(diskPath); }); + + // 处理设置托盘启用状态 + ipcHandle(ipcMain, setTrayEnabledHandle, async (_event, enabled: boolean) => { + trayEnabled = enabled; + if (enabled) { + createTray(); + } else { + destroyTray(); + } + return true; + }); +} + +// 获取图标路径 +function getIconPath(): string { + // 开发环境和生产环境的路径不同 + if (app.isPackaged) { + return path.join(process.resourcesPath, 'icons', 'icon.png'); + } else { + return path.join(__dirname, '..', '..', 'icons', 'icon.png'); + } +} + +// 创建托盘 +function createTray(): void { + if (tray) return; + + try { + const iconPath = getIconPath(); + const icon = nativeImage.createFromPath(iconPath); + tray = new Tray(icon.resize({ width: 16, height: 16 })); + + const contextMenu = Menu.buildFromTemplate([ + { + label: '显示主窗口', + click: () => { + const windows = BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const mainWindow = windows[0]; + mainWindow.show(); + mainWindow.focus(); + } + }, + }, + { type: 'separator' }, + { + label: '退出', + click: () => { + forceQuit = true; + app.quit(); + }, + }, + ]); + + // todo: 信息识别 + const version = app.getVersion(); + const isJointBuilding = trayEnabled ? '正在共建中(ON)' : '共建已关闭(OFF)'; + const currentUploadSpeed = '0KB/s'; + const linkedTo = 0; + tray.setToolTip(`AI学习助手 ${version}\n${isJointBuilding}\n当前上传速度: ${currentUploadSpeed}\n已连接伙伴${linkedTo}人`); + tray.setContextMenu(contextMenu); + + // 双击托盘图标显示窗口 + tray.on('double-click', () => { + const windows = BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const mainWindow = windows[0]; + mainWindow.show(); + mainWindow.focus(); + } + }); + } catch (error) { + console.error('创建托盘失败:', error); + } +} + +// 销毁托盘 +function destroyTray(): void { + if (tray) { + tray.destroy(); + tray = null; + } +} + +// 设置窗口关闭行为 +export function setupWindowCloseHandler(mainWindow: BrowserWindow): void { + mainWindow.on('close', (event) => { + if (trayEnabled && !forceQuit) { + event.preventDefault(); + mainWindow.hide(); + } + }); +} + +// 检查是否强制退出 +export function isForceQuit(): boolean { + return forceQuit; +} + +// 设置强制退出标志 +export function setForceQuit(value: boolean): void { + forceQuit = value; +} + +// 获取托盘启用状态 +export function isTrayEnabled(): boolean { + return trayEnabled; } diff --git a/src/main/joint-build/type-info.ts b/src/main/joint-build/type-info.ts index 5530bd7..ac7f09c 100644 --- a/src/main/joint-build/type-info.ts +++ b/src/main/joint-build/type-info.ts @@ -1,8 +1,9 @@ -export type ActionName = 'select-folder' | 'get-disk-info'; +export type ActionName = 'select-folder' | 'get-disk-info' | 'set-tray-enabled'; export type ServiceName = 'joint-build'; export const selectFolderHandle = 'joint-build:select-folder'; export const getDiskInfoHandle = 'joint-build:get-disk-info'; +export const setTrayEnabledHandle = 'joint-build:set-tray-enabled'; export interface DiskInfo { total: number; // bytes diff --git a/src/main/preload.ts b/src/main/preload.ts index 5c87604..e5e9f74 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -22,6 +22,7 @@ import { import { selectFolderHandle, getDiskInfoHandle, + setTrayEnabledHandle, DiskInfo, } from './joint-build/type-info'; @@ -120,6 +121,9 @@ const mainHandle = { getJointBuildDiskInfo: async (diskPath: string): Promise => { return ipcInvoke(getDiskInfoHandle, diskPath); }, + setTrayEnabled: async (enabled: boolean): Promise => { + return ipcInvoke(setTrayEnabledHandle, enabled); + }, }; export type MainHandle = typeof mainHandle; diff --git a/src/renderer/pages/hello/index.tsx b/src/renderer/pages/hello/index.tsx index 0c712ff..841dd82 100644 --- a/src/renderer/pages/hello/index.tsx +++ b/src/renderer/pages/hello/index.tsx @@ -49,6 +49,29 @@ export default function Hello() { }; }, [setupBackupListener]); + // 初始化托盘状态(根据共建开关) + useEffect(() => { + const initTrayStatus = async () => { + const masterSwitch = localStorage.getItem('joint_build_master_switch'); + const welcomeChoice = localStorage.getItem('ai_learning_assistant_welcome_shown'); + + let trayEnabled = false; + if (masterSwitch !== null) { + trayEnabled = masterSwitch === 'true'; + } else if (welcomeChoice === 'true') { + trayEnabled = true; + } + + try { + await window.mainHandle.setTrayEnabled(trayEnabled); + } catch (error) { + console.error('初始化托盘状态失败:', error); + } + }; + + initTrayStatus(); + }, []); + const [currentSlide, setCurrentSlide] = useState(0); const slides = [ diff --git a/src/renderer/pages/joint-build/index.tsx b/src/renderer/pages/joint-build/index.tsx index 0a9e092..4a7c32e 100644 --- a/src/renderer/pages/joint-build/index.tsx +++ b/src/renderer/pages/joint-build/index.tsx @@ -76,15 +76,21 @@ export default function JointBuild() { useEffect(() => { // 优先读取共建开关的设置,如果没有则读取欢迎弹窗的用户选择 const savedMasterSwitch = localStorage.getItem(STORAGE_KEY_MASTER); + let initialMasterSwitch = false; + if (savedMasterSwitch !== null) { - setMasterSwitch(savedMasterSwitch === 'true'); + initialMasterSwitch = savedMasterSwitch === 'true'; } else { // 如果没有单独设置过共建开关,则读取欢迎弹窗的选择 const welcomeChoice = localStorage.getItem(STORAGE_KEY_WELCOME); if (welcomeChoice === 'true') { - setMasterSwitch(true); + initialMasterSwitch = true; } } + + setMasterSwitch(initialMasterSwitch); + // 初始化时同步托盘状态到主进程 + window.mainHandle.setTrayEnabled(initialMasterSwitch).catch(console.error); const savedModules = localStorage.getItem(STORAGE_KEY_MODULES); if (savedModules) { @@ -118,11 +124,19 @@ export default function JointBuild() { ); }; - const handleMasterSwitchChange = (checked: boolean) => { + const handleMasterSwitchChange = async (checked: boolean) => { setMasterSwitch(checked); saveConfig(checked, modules); + + // 同步托盘状态到主进程 + try { + await window.mainHandle.setTrayEnabled(checked); + } catch (error) { + console.error('设置托盘状态失败:', error); + } + if (checked) { - message.success('共建计划已开启'); + message.success('共建计划已开启,关闭窗口后将最小化到托盘'); } else { message.info('共建计划已关闭'); } From 2002c012a9972572c600db8a1492ff881bc204f6 Mon Sep 17 00:00:00 2001 From: forceOfPx <3239735744@qq.com> Date: Fri, 16 Jan 2026 11:54:55 +0800 Subject: [PATCH 06/22] =?UTF-8?q?=E6=89=98=E7=9B=98=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/joint-build/index.ts | 44 +++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/src/main/joint-build/index.ts b/src/main/joint-build/index.ts index c84e718..5cb94a8 100644 --- a/src/main/joint-build/index.ts +++ b/src/main/joint-build/index.ts @@ -73,14 +73,13 @@ export function setupJointBuildHandlers(ipcMain: IpcMain): void { return getWindowsDiskInfo(diskPath); }); - // 处理设置托盘启用状态 + // 处理设置托盘启用状态(只更新状态,不销毁托盘) ipcHandle(ipcMain, setTrayEnabledHandle, async (_event, enabled: boolean) => { trayEnabled = enabled; - if (enabled) { - createTray(); - } else { - destroyTray(); - } + // 确保托盘始终存在 + createTray(); + // 更新托盘提示信息 + updateTrayTooltip(); return true; }); } @@ -126,12 +125,7 @@ function createTray(): void { }, ]); - // todo: 信息识别 - const version = app.getVersion(); - const isJointBuilding = trayEnabled ? '正在共建中(ON)' : '共建已关闭(OFF)'; - const currentUploadSpeed = '0KB/s'; - const linkedTo = 0; - tray.setToolTip(`AI学习助手 ${version}\n${isJointBuilding}\n当前上传速度: ${currentUploadSpeed}\n已连接伙伴${linkedTo}人`); + updateTrayTooltip(); tray.setContextMenu(contextMenu); // 双击托盘图标显示窗口 @@ -148,6 +142,22 @@ function createTray(): void { } } +// 更新托盘提示信息 +function updateTrayTooltip(): void { + if (!tray) return; + + const version = app.getVersion(); + const isJointBuilding = trayEnabled ? '正在共建中(ON)' : '共建已关闭(OFF)'; + if (!trayEnabled) { + tray.setToolTip(`AI学习助手 ${version}\n共建已关闭(OFF)`); + return; + } + // todo:获取动态数据 + const currentUploadSpeed = '0KB/s'; + const linkedTo = 0; + tray.setToolTip(`AI学习助手 ${version}\n${isJointBuilding}\n当前上传速度: ${currentUploadSpeed}\n已连接伙伴${linkedTo}人`); +} + // 销毁托盘 function destroyTray(): void { if (tray) { @@ -158,8 +168,12 @@ function destroyTray(): void { // 设置窗口关闭行为 export function setupWindowCloseHandler(mainWindow: BrowserWindow): void { + // 确保托盘已创建 + createTray(); + mainWindow.on('close', (event) => { - if (trayEnabled && !forceQuit) { + // 始终最小化到托盘,除非是强制退出 + if (!forceQuit) { event.preventDefault(); mainWindow.hide(); } @@ -176,7 +190,7 @@ export function setForceQuit(value: boolean): void { forceQuit = value; } -// 获取托盘启用状态 +// 获取托盘启用状态(始终启用托盘最小化) export function isTrayEnabled(): boolean { - return trayEnabled; + return true; } From 3cb825048b6bb57075bf9fea4c0567126079c457 Mon Sep 17 00:00:00 2001 From: forceOfPx <3239735744@qq.com> Date: Fri, 16 Jan 2026 12:10:09 +0800 Subject: [PATCH 07/22] =?UTF-8?q?=E6=89=98=E7=9B=98=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/joint-build/index.ts | 82 +++++++++++++++++------- src/main/preload.ts | 11 ++++ src/renderer/app.tsx | 31 ++++++++- src/renderer/pages/joint-build/index.tsx | 13 ++++ 4 files changed, 113 insertions(+), 24 deletions(-) diff --git a/src/main/joint-build/index.ts b/src/main/joint-build/index.ts index 5cb94a8..cf606e0 100644 --- a/src/main/joint-build/index.ts +++ b/src/main/joint-build/index.ts @@ -103,30 +103,8 @@ function createTray(): void { const icon = nativeImage.createFromPath(iconPath); tray = new Tray(icon.resize({ width: 16, height: 16 })); - const contextMenu = Menu.buildFromTemplate([ - { - label: '显示主窗口', - click: () => { - const windows = BrowserWindow.getAllWindows(); - if (windows.length > 0) { - const mainWindow = windows[0]; - mainWindow.show(); - mainWindow.focus(); - } - }, - }, - { type: 'separator' }, - { - label: '退出', - click: () => { - forceQuit = true; - app.quit(); - }, - }, - ]); - + updateTrayMenu(); updateTrayTooltip(); - tray.setContextMenu(contextMenu); // 双击托盘图标显示窗口 tray.on('double-click', () => { @@ -142,6 +120,64 @@ function createTray(): void { } } +// 更新托盘菜单 +function updateTrayMenu(): void { + if (!tray) return; + + const contextMenu = Menu.buildFromTemplate([ + { + label: '打开AI学习助手', + click: () => { + const windows = BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const mainWindow = windows[0]; + mainWindow.show(); + mainWindow.focus(); + // 发送路由事件到渲染进程,导航到主界面 + mainWindow.webContents.send('navigate-to', '/hello'); + } + }, + }, + { + label: '打开共建设置', + click: () => { + const windows = BrowserWindow.getAllWindows(); + if (windows.length > 0) { + const mainWindow = windows[0]; + mainWindow.show(); + mainWindow.focus(); + // 发送路由事件到渲染进程,导航到共建设置界面 + mainWindow.webContents.send('navigate-to', '/joint-build'); + } + }, + }, + { + label: trayEnabled ? '停止共建计划' : '运行共建计划', + click: () => { + trayEnabled = !trayEnabled; + // 更新菜单显示 + updateTrayMenu(); + updateTrayTooltip(); + // 通知渲染进程更新状态 + const windows = BrowserWindow.getAllWindows(); + if (windows.length > 0) { + windows[0].webContents.send('joint-build-status-changed', trayEnabled); + } + }, + }, + { type: 'separator' }, + { + label: '退出AI学习助手', + click: () => { + forceQuit = true; + app.quit(); + }, + }, + ]); + + tray.setContextMenu(contextMenu); +} + // 更新托盘提示信息 function updateTrayTooltip(): void { if (!tray) return; diff --git a/src/main/preload.ts b/src/main/preload.ts index e5e9f74..2e493ba 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -124,6 +124,17 @@ const mainHandle = { setTrayEnabled: async (enabled: boolean): Promise => { return ipcInvoke(setTrayEnabledHandle, enabled); }, + // 托盘菜单事件监听 + onNavigateTo: (callback: (route: string) => void) => { + const handler = (_event: IpcRendererEvent, route: string) => callback(route); + ipcRenderer.on('navigate-to', handler); + return () => ipcRenderer.removeListener('navigate-to', handler); + }, + onJointBuildStatusChanged: (callback: (enabled: boolean) => void) => { + const handler = (_event: IpcRendererEvent, enabled: boolean) => callback(enabled); + ipcRenderer.on('joint-build-status-changed', handler); + return () => ipcRenderer.removeListener('joint-build-status-changed', handler); + }, }; export type MainHandle = typeof mainHandle; diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index 06081f9..3906c57 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -1,4 +1,4 @@ -import { MemoryRouter as Router, Routes, Route } from 'react-router-dom'; +import { MemoryRouter as Router, Routes, Route, useNavigate } from 'react-router-dom'; import '@ant-design/v5-patch-for-react-19'; import Hello from './pages/hello'; import './app.css'; @@ -17,12 +17,41 @@ import LLMConfig from './pages/llm-api-config'; import VoiceRTCConfig from './pages/voice-rtc-config'; import WelcomeModal from './pages/welcome/WelcomeModal'; import JointBuild from './pages/joint-build'; +import { useEffect } from 'react'; + +// 托盘菜单导航监听组件 +function TrayNavigationHandler() { + const navigate = useNavigate(); + + useEffect(() => { + // 监听托盘菜单的导航事件 + const unsubscribeNavigate = window.mainHandle.onNavigateTo((route: string) => { + navigate(route); + }); + + // 监听共建计划状态变化事件 + const unsubscribeStatus = window.mainHandle.onJointBuildStatusChanged((enabled: boolean) => { + // 更新 localStorage + localStorage.setItem('joint_build_master_switch', String(enabled)); + // 触发页面刷新或状态更新 + window.dispatchEvent(new CustomEvent('joint-build-status-changed', { detail: enabled })); + }); + + return () => { + unsubscribeNavigate(); + unsubscribeStatus(); + }; + }, [navigate]); + + return null; +} export default function App() { return ( + } /> } /> diff --git a/src/renderer/pages/joint-build/index.tsx b/src/renderer/pages/joint-build/index.tsx index 4a7c32e..2e4efc3 100644 --- a/src/renderer/pages/joint-build/index.tsx +++ b/src/renderer/pages/joint-build/index.tsx @@ -115,6 +115,19 @@ export default function JointBuild() { fetchDiskInfo(initialPath); }, [fetchDiskInfo]); + // 监听托盘菜单的状态变化 + useEffect(() => { + const handleStatusChange = (event: CustomEvent) => { + setMasterSwitch(event.detail); + }; + + window.addEventListener('joint-build-status-changed', handleStatusChange as EventListener); + + return () => { + window.removeEventListener('joint-build-status-changed', handleStatusChange as EventListener); + }; + }, []); + // 保存配置到 localStorage const saveConfig = (master: boolean, mods: ModuleItem[]) => { localStorage.setItem(STORAGE_KEY_MASTER, String(master)); From 6c70518a53d85e23ed66ee7992d0d05ea02143d2 Mon Sep 17 00:00:00 2001 From: xiaoshen Date: Mon, 19 Jan 2026 11:56:39 +0800 Subject: [PATCH 08/22] feat: add launcher and P2P download updates for Obsidian and LLMstudio --- .gitignore | 6 +- external-resources/dlc/dlc-index.json | 39 ++- package-lock.json | 25 +- package.json | 2 + src/main/cmd/index.ts | 174 +++++++++++ src/main/dlc/index.ts | 34 +- src/main/dlc/type-info.ts | 3 + src/main/index.ts | 2 + src/main/ipc-data-type.ts | 6 +- src/main/launcher-update/index.ts | 342 +++++++++++++++++++++ src/main/launcher-update/type-info.ts | 9 + src/main/preload.ts | 30 +- src/main/training-service/index.ts | 11 +- src/renderer/pages/hello/index.scss | 9 + src/renderer/pages/hello/index.tsx | 64 +++- src/renderer/pages/lm-service/index.scss | 51 ++- src/renderer/pages/lm-service/index.tsx | 194 ++++++++++-- src/renderer/pages/obsidian-app/index.scss | 49 +++ src/renderer/pages/obsidian-app/index.tsx | 218 ++++++++++++- 19 files changed, 1219 insertions(+), 49 deletions(-) create mode 100644 src/main/launcher-update/index.ts create mode 100644 src/main/launcher-update/type-info.ts diff --git a/.gitignore b/.gitignore index 990de72..fbbb803 100644 --- a/.gitignore +++ b/.gitignore @@ -109,4 +109,8 @@ external-resources/dlc/**/*.mp4 external-resources/dlc/**/*.tar external-resources/dlc/**/*.zip external-resources/dlc/**/*.sql -external-resources/dlc/**/*.torrent \ No newline at end of file +external-resources/dlc/**/*.torrent + +# 忽略启动器更新临时目录 +update-temp/ +update.bat diff --git a/external-resources/dlc/dlc-index.json b/external-resources/dlc/dlc-index.json index 8edbf75..4c2b326 100644 --- a/external-resources/dlc/dlc-index.json +++ b/external-resources/dlc/dlc-index.json @@ -6,12 +6,12 @@ "2.0.0": { "magnet": "magnet:?xt=urn:btih:a7f8c85dcef68ae7958f0f3548603ea9016897df&dn=ala-training-v2.0.0.tar&tr=wss%3A%2F%2Flearning.panchuantech.cn%2Fannounce", "comment": "第一个版本", - "riquire":{} + "riquire": {} }, "2.0.2": { "magnet": "magnet:?xt=urn:btih:98995512a1cf581a1baea19132b1a6550ec424fc&dn=ala-training-v2.0.2.tar&tr=wss%3A%2F%2Flearning.panchuantech.cn%2Fannounce", "comment": "相比上一个版本修复了一些问题", - "riquire":{} + "riquire": {} } } }, @@ -22,10 +22,43 @@ "2.0.2": { "magnet": "magnet:?xt=urn:btih:cff0fb96747c5fc4696af4c9fa8cd30330050250&dn=ai_learning_assistant.sql&tr=wss%3A%2F%2Flearning.panchuantech.cn%2Fannounce&tr=wss%3A%2F%2F114.66.58.95%3A17853%2Fannounce", "comment": "第一个版本", - "riquire":{ + "riquire": { "TRAINING_TAR": "^2.0.0" } } } + }, + { + "id": "AI_LEARNING_ASSISTANT_LAUNCHER", + "name": "AI学习助手启动器", + "versions": { + "2.0.3": { + "magnet": "magnet:?xt=urn:btih:5e685a7cbe4045efc40f7a593dddfdf56ae7c59d&dn=AI-Learning-Assistant-Launcher-win32-x64-2.0.3.zip&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.btorrent.xyz", + "comment": "第一个版本", + "riquire": {} + } + } + }, + { + "id": "OBSIDIAN_SETUP_EXE", + "name": "obsidian 安装程序", + "versions": { + "1.11.4": { + "magnet": "magnet:?xt=urn:btih:0e35e31fcbfa86153cd155f24c5d835e977d7d8b&dn=Obsidian-1.11.4.exe&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.btorrent.xyz", + "comment": "第一个版本", + "riquire": {} + } + } + }, + { + "id": "LM_STUDIO_SETUP_EXE", + "name": "lm studio 安装程序", + "versions": { + "0.3.39": { + "magnet": "magnet:?xt=urn:btih:f99095d4fc565a5e7fcf971f80817d8dbf617c97&dn=LM-Studio-0.3.39-x64.exe&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.btorrent.xyz", + "comment": "第一个版本", + "riquire": {} + } + } } ] diff --git a/package-lock.json b/package-lock.json index 2f2c1f3..1b17429 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,8 @@ "langchain": "^0.3.35", "pdf-lib": "^1.17.1", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "semver": "^7.7.3" }, "devDependencies": { "@ant-design/v5-patch-for-react-19": "^1.0.3", @@ -34,6 +35,7 @@ "@electron/fuses": "^1.8.0", "@stdlib/utils-convert-path": "^0.2.2", "@svgr/webpack": "^8.1.0", + "@types/adm-zip": "^0.5.7", "@types/cross-spawn": "^6.0.6", "@types/dockerode": "^3.3.40", "@types/form-data": "^2.2.1", @@ -46,6 +48,7 @@ "@typescript-eslint/parser": "^5.62.0", "@vercel/webpack-asset-relocator-loader": "1.7.3", "@xterm/xterm": "^5.5.0", + "adm-zip": "^0.5.16", "antd": "^5.26.1", "copy-webpack-plugin": "^13.0.1", "cpy": "^11.1.0", @@ -5755,6 +5758,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/adm-zip": { + "version": "0.5.7", + "resolved": "https://registry.npmmirror.com/@types/adm-zip/-/adm-zip-0.5.7.tgz", + "integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/bittorrent-protocol": { "version": "3.1.7", "resolved": "https://registry.npmmirror.com/@types/bittorrent-protocol/-/bittorrent-protocol-3.1.7.tgz", @@ -6767,6 +6780,16 @@ "node": ">=12.20.0" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmmirror.com/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", diff --git a/package.json b/package.json index b325b13..cb1f159 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@electron/fuses": "^1.8.0", "@stdlib/utils-convert-path": "^0.2.2", "@svgr/webpack": "^8.1.0", + "@types/adm-zip": "^0.5.7", "@types/cross-spawn": "^6.0.6", "@types/dockerode": "^3.3.40", "@types/form-data": "^2.2.1", @@ -44,6 +45,7 @@ "@typescript-eslint/parser": "^5.62.0", "@vercel/webpack-asset-relocator-loader": "1.7.3", "@xterm/xterm": "^5.5.0", + "adm-zip": "^0.5.16", "antd": "^5.26.1", "copy-webpack-plugin": "^13.0.1", "cpy": "^11.1.0", diff --git a/src/main/cmd/index.ts b/src/main/cmd/index.ts index dbe8798..bd84404 100644 --- a/src/main/cmd/index.ts +++ b/src/main/cmd/index.ts @@ -488,6 +488,93 @@ async function checkWSLComponent() { } export async function installObsidian() { + try { + // 首先检查是否有P2P下载的文件 + const { getDLCFromDLCIndex, destroyWebtorrentForInstall } = await import( + '../dlc' + ); + const dlcInfo = getDLCFromDLCIndex('OBSIDIAN_SETUP_EXE'); + + if (dlcInfo) { + const latestVersion = Object.keys(dlcInfo.versions).sort().pop(); + if (latestVersion) { + const versionInfo = dlcInfo.versions[latestVersion]; + + // 完全销毁 torrent 以释放文件句柄,避免 EBUSY 错误 + if (versionInfo.magnet) { + try { + console.debug('开始销毁种子以释放文件句柄...'); + await destroyWebtorrentForInstall(versionInfo.magnet); + // 等待额外的时间确保文件句柄完全释放 + await new Promise((resolve) => setTimeout(resolve, 2000)); + console.debug('种子已销毁,等待完成'); + } catch (e) { + console.warn('销毁torrent失败:', e); + } + } + + const downloadPath = path.join( + appPath, + 'external-resources', + 'dlc', + 'OBSIDIAN_SETUP_EXE', + latestVersion, + ); + + const fs = await import('fs'); + if (fs.existsSync(downloadPath)) { + const files = fs.readdirSync(downloadPath); + const exeFile = files.find((f: string) => f.endsWith('.exe')); + + if (exeFile) { + const exePath = path.join(downloadPath, exeFile); + + // 使用更激进的重试机制处理 EBUSY 错误 + const maxRetries = 8; + for (let retry = 0; retry < maxRetries; retry++) { + try { + // 每次重试前等待更长时间,让文件句柄有充足时间释放 + const waitTime = retry === 0 ? 1000 : 1000 + retry * 1000; + await new Promise((resolve) => setTimeout(resolve, waitTime)); + + console.debug( + `尝试执行安装程序 (${retry + 1}/${maxRetries}),等待了 ${waitTime}ms`, + ); + const result = await commandLine.exec(exePath, ['/s']); + console.debug('安装程序执行成功:', result); + return true; + } catch (e) { + const isEBUSY = e.message && e.message.includes('EBUSY'); + if (isEBUSY && retry < maxRetries - 1) { + console.warn( + `执行安装程序失败 (EBUSY),重试 ${retry + 1}/${maxRetries}...`, + ); + } else if (isEBUSY) { + // 所有重试都失败了,尝试最后一次长等待 + console.warn('所有重试都失败,尝试最后一次长等待(10秒)...'); + await new Promise((resolve) => setTimeout(resolve, 10000)); + try { + const result = await commandLine.exec(exePath, ['/s']); + console.debug('最后一次尝试成功:', result); + return true; + } catch (finalError) { + console.error('最后一次尝试也失败了:', finalError); + throw finalError; + } + } else { + throw e; + } + } + } + } + } + } + } + } catch (error) { + console.error('尝试使用P2P下载包失败,使用默认方式:', error); + } + + // 降级到原有的本地安装包方式 const result = await commandLine.exec( path.join( appPath, @@ -520,6 +607,93 @@ export async function isObsidianInstall() { } export async function installLMStudio() { + try { + // 首先检查是否有P2P下载的文件 + const { getDLCFromDLCIndex, destroyWebtorrentForInstall } = await import( + '../dlc' + ); + const dlcInfo = getDLCFromDLCIndex('LM_STUDIO_SETUP_EXE'); + + if (dlcInfo) { + const latestVersion = Object.keys(dlcInfo.versions).sort().pop(); + if (latestVersion) { + const versionInfo = dlcInfo.versions[latestVersion]; + + // 完全销毁 torrent 以释放文件句柄,避免 EBUSY 错误 + if (versionInfo.magnet) { + try { + console.debug('开始销毁LM Studio种子以释放文件句柄...'); + await destroyWebtorrentForInstall(versionInfo.magnet); + // 等待额外的时间确保文件句柄完全释放 + await new Promise((resolve) => setTimeout(resolve, 2000)); + console.debug('种子已销毁,等待完成'); + } catch (e) { + console.warn('销毁torrent失败:', e); + } + } + + const downloadPath = path.join( + appPath, + 'external-resources', + 'dlc', + 'LM_STUDIO_SETUP_EXE', + latestVersion, + ); + + const fs = await import('fs'); + if (fs.existsSync(downloadPath)) { + const files = fs.readdirSync(downloadPath); + const exeFile = files.find((f: string) => f.endsWith('.exe')); + + if (exeFile) { + const exePath = path.join(downloadPath, exeFile); + + // 使用更激进的重试机制处理 EBUSY 错误 + const maxRetries = 8; + for (let retry = 0; retry < maxRetries; retry++) { + try { + // 每次重试前等待更长时间,让文件句柄有充足时间释放 + const waitTime = retry === 0 ? 1000 : 1000 + retry * 1000; + await new Promise((resolve) => setTimeout(resolve, waitTime)); + + console.debug( + `尝试执行LM Studio安装程序 (${retry + 1}/${maxRetries}),等待了 ${waitTime}ms`, + ); + const result = await commandLine.exec(exePath, []); + console.debug('LM Studio安装程序执行成功:', result); + return true; + } catch (e) { + const isEBUSY = e.message && e.message.includes('EBUSY'); + if (isEBUSY && retry < maxRetries - 1) { + console.warn( + `执行LM Studio安装程序失败 (EBUSY),重试 ${retry + 1}/${maxRetries}...`, + ); + } else if (isEBUSY) { + // 所有重试都失败了,尝试最后一次长等待 + console.warn('所有重试都失败,尝试最后一次长等待(10秒)...'); + await new Promise((resolve) => setTimeout(resolve, 10000)); + try { + const result = await commandLine.exec(exePath, []); + console.debug('最后一次尝试成功:', result); + return true; + } catch (finalError) { + console.error('最后一次尝试也失败了:', finalError); + throw finalError; + } + } else { + throw e; + } + } + } + } + } + } + } + } catch (error) { + console.error('尝试使用P2P下载包失败,使用默认方式:', error); + } + + // 降级到原有的本地安装包方式 const result = await commandLine.exec( path.join( appPath, diff --git a/src/main/dlc/index.ts b/src/main/dlc/index.ts index 869cebe..5225291 100644 --- a/src/main/dlc/index.ts +++ b/src/main/dlc/index.ts @@ -99,9 +99,9 @@ export async function startWebtorrent(magnet: string) { }); console.debug('种子文件已经存在,继续下载'); torrent.resume(); - return torrent; + return { success: true, infoHash: torrent.infoHash }; } else { - return client.add( + const newTorrent = client.add( magnet, { path: filePath, @@ -114,9 +114,11 @@ export async function startWebtorrent(magnet: string) { saveTorrentFile(filePath, torrent); }, ); + return { success: true, infoHash: newTorrent.infoHash }; } } else { console.warn('没找到链接对应的索引信息', magnet); + return { success: false, error: '没找到链接对应的索引信息' }; } } @@ -152,6 +154,34 @@ export async function pauseWebtorrent(magnet: string) { } } +/** + * 完全销毁种子并释放文件句柄 + * 注意:这会关闭所有文件句柄,但不会删除已下载的文件 + */ +export async function destroyWebtorrentForInstall( + magnet: string, +): Promise { + const torrent = await client.get(magnet); + if (torrent) { + return new Promise((resolve) => { + // 先暂停下载 + torrent.pause(); + + // 销毁种子,这会关闭所有文件句柄 + torrent.destroy({ destroyStore: false }, () => { + console.debug('种子已销毁,文件句柄已释放:', torrent.infoHash); + resolve(); + }); + + // 设置一个超时,防止回调永远不执行 + setTimeout(() => { + console.debug('destroy回调超时,强制resolve'); + resolve(); + }, 3000); + }); + } +} + export async function removeWebtorrent(magnet: string) { const torrent = await client.get(magnet); if (torrent) { diff --git a/src/main/dlc/type-info.ts b/src/main/dlc/type-info.ts index 37d0a5d..dfc5a37 100644 --- a/src/main/dlc/type-info.ts +++ b/src/main/dlc/type-info.ts @@ -20,6 +20,9 @@ export const dLCIds = [ 'TRAINING_VOICE_TAR', 'TRAINING_COURSE', 'LMSTUDIO_WINDOWS', + 'AI_LEARNING_ASSISTANT_LAUNCHER', + 'OBSIDIAN_SETUP_EXE', + 'LM_STUDIO_SETUP_EXE', 'TEST_FILE', ] as const; diff --git a/src/main/index.ts b/src/main/index.ts index 3b8f105..623f96a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -14,6 +14,7 @@ import initTrainingService from './training-service'; import initLogService from './backup'; import initExternalUrl from './external-url'; import initDLC from './dlc'; +import initLauncherUpdate from './launcher-update'; import path from 'node:path'; import { appPath, autoAdaptEncodingForWindows } from './exec'; import { logDeviceInfo } from './logger/log-device-info'; @@ -81,6 +82,7 @@ initPdfConvert(ipcMain); initTrainingService(ipcMain); initExternalUrl(ipcMain); initDLC(ipcMain); +initLauncherUpdate(ipcMain); updateTemplate(); const createWindow = async () => { diff --git a/src/main/ipc-data-type.ts b/src/main/ipc-data-type.ts index acd7b57..5e606a6 100644 --- a/src/main/ipc-data-type.ts +++ b/src/main/ipc-data-type.ts @@ -16,7 +16,8 @@ export type Channels = | 'training-service' | 'backup' | 'open-external-url' - | 'webtorrent'; + | 'webtorrent' + | 'launcher-update'; export enum MESSAGE_TYPE { /** 阻断性错误,会把转圈中的按钮变成不转圈 */ @@ -69,12 +70,11 @@ import { ActionName as LogActionName, ServiceName as LogServiceName, } from './backup/type-info'; -import { +import { ActionName as ActionNameUrl, ServiceName as ServiceNameUrl, } from './external-url/type-info'; - export type AllAction = | ActionNamePodman | ActionNameCmd diff --git a/src/main/launcher-update/index.ts b/src/main/launcher-update/index.ts new file mode 100644 index 0000000..80867d2 --- /dev/null +++ b/src/main/launcher-update/index.ts @@ -0,0 +1,342 @@ +import { IpcMain, app, dialog } from 'electron'; +import { ipcHandle } from '../ipc-util'; +import { + checkLauncherUpdateHandle, + downloadLauncherUpdateHandle, + installLauncherUpdateHandle, +} from './type-info'; +import { + getLatestVersion, + isLatestVersion, + startWebtorrent, + waitTorrentDone, +} from '../dlc'; +import path from 'path'; +import { appPath } from '../exec'; +import { + existsSync, + mkdirSync, + readdirSync, + statSync, + rmSync, + writeFileSync, + readFileSync, +} from 'fs'; +import { spawn } from 'child_process'; +import AdmZip from 'adm-zip'; + +import packageJson from '../../../package.json'; +const currentVersion = packageJson.version; + +// 在 Electron 中使用 original-fs 处理 asar 打包后的文件操作 +// original-fs 是 Electron 内置的,需要通过 require 动态导入 +// eslint-disable-next-line @typescript-eslint/no-var-requires +const originalFs = require('original-fs'); + +export default async function init(ipcMain: IpcMain) { + ipcHandle(ipcMain, checkLauncherUpdateHandle, async (_event) => + checkLauncherUpdate(), + ); + ipcHandle(ipcMain, downloadLauncherUpdateHandle, async (_event) => + downloadLauncherUpdate(), + ); + ipcHandle(ipcMain, installLauncherUpdateHandle, async (_event) => + installLauncherUpdate(), + ); +} + +export async function checkLauncherUpdate() { + try { + const latestVersionInfo = getLatestVersion( + 'AI_LEARNING_ASSISTANT_LAUNCHER', + ); + const latestVersion = latestVersionInfo.version; + const haveNew = !isLatestVersion( + 'AI_LEARNING_ASSISTANT_LAUNCHER', + currentVersion, + ); + + console.debug('检查启动器更新:', { + currentVersion, + latestVersion, + haveNew, + }); + + return { + currentVersion, + latestVersion, + haveNew, + }; + } catch (error) { + console.error('检查启动器更新失败:', error); + return { + currentVersion, + latestVersion: currentVersion, + haveNew: false, + }; + } +} + +export async function downloadLauncherUpdate() { + try { + const latestVersionInfo = getLatestVersion( + 'AI_LEARNING_ASSISTANT_LAUNCHER', + ); + + console.debug('开始下载启动器更新:', latestVersionInfo); + + await startWebtorrent(latestVersionInfo.dlcInfo.magnet); + + const torrent = await waitTorrentDone( + 'AI_LEARNING_ASSISTANT_LAUNCHER', + latestVersionInfo.version, + ); + + console.debug('启动器更新下载完成'); + + // 检查是否为本地开发环境 + // const isDev = !app.isPackaged; + // if (isDev) { + // console.warn('当前为本地开发环境,启动器更新功能可能无法正常工作'); + // } + + return { + success: true, + version: latestVersionInfo.version, + filePath: path.join(torrent.path, torrent.files[0].name), + // isDev, + }; + } catch (error) { + console.error('下载启动器更新失败:', error); + throw error; + } +} + +export async function installLauncherUpdate() { + console.debug('[installLauncherUpdate] 开始执行安装更新'); + + // 如果是开发模式,不执行更新 + // const isPackaged = app.isPackaged; + // if (!isPackaged) { + // return { + // success: false, + // message: '开发模式下不支持自动更新,请手动解压', + // }; + // } + + try { + console.debug('[installLauncherUpdate] 获取最新版本信息...'); + const latestVersionInfo = getLatestVersion( + 'AI_LEARNING_ASSISTANT_LAUNCHER', + ); + const version = latestVersionInfo.version; + console.debug('[installLauncherUpdate] 最新版本:', version); + + const downloadPath = path.join( + appPath, + 'external-resources', + 'dlc', + 'AI_LEARNING_ASSISTANT_LAUNCHER', + version, + ); + console.debug('[installLauncherUpdate] 下载路径:', downloadPath); + console.debug( + '[installLauncherUpdate] 下载路径是否存在:', + existsSync(downloadPath), + ); + + const files = readdirSync(downloadPath); + console.debug('[installLauncherUpdate] 下载目录文件列表:', files); + const zipFile = files.find((f) => f.endsWith('.zip')); + + if (!zipFile) { + throw new Error('未找到下载的更新包'); + } + + const zipPath = path.join(downloadPath, zipFile); + console.debug('[installLauncherUpdate] zip文件路径:', zipPath); + console.debug( + '[installLauncherUpdate] zip文件是否存在:', + existsSync(zipPath), + ); + + const tempDir = path.join(appPath, 'update-temp'); + console.debug('[installLauncherUpdate] 临时目录:', tempDir); + if (existsSync(tempDir)) { + console.debug('[installLauncherUpdate] 清理已存在的临时目录...'); + rmSync(tempDir, { recursive: true, force: true }); + console.debug('[installLauncherUpdate] 临时目录清理完成'); + } + mkdirSync(tempDir, { recursive: true }); + console.debug('[installLauncherUpdate] 临时目录已创建'); + + // 使用 adm-zip 解压 zip 文件 + console.log('[installLauncherUpdate] 正在解压文件...'); + console.log('[installLauncherUpdate] ZIP路径:', zipPath); + console.log('[installLauncherUpdate] 目标路径:', tempDir); + + try { + const zip = new AdmZip(zipPath); + const zipEntries = zip.getEntries(); + console.log( + '[installLauncherUpdate] ZIP包含的文件数量:', + zipEntries.length, + ); + console.log( + '[installLauncherUpdate] ZIP包含的文件列表:', + zipEntries.slice(0, 5).map((e) => e.entryName), + ); + + // 手动解压每个文件,使用 original-fs 避免 asar 打包问题和 Windows 下的 chmod 问题 + for (const entry of zipEntries) { + const entryPath = path.join(tempDir, entry.entryName); + if (entry.isDirectory) { + // 创建目录 + if (!originalFs.existsSync(entryPath)) { + originalFs.mkdirSync(entryPath, { recursive: true }); + } + } else { + // 确保父目录存在 + const parentDir = path.dirname(entryPath); + if (!originalFs.existsSync(parentDir)) { + originalFs.mkdirSync(parentDir, { recursive: true }); + } + // 写入文件 + const content = entry.getData(); + originalFs.writeFileSync(entryPath, content); + } + } + console.log('[installLauncherUpdate] 解压完成'); + } catch (extractError) { + console.error('[installLauncherUpdate] 解压失败:', extractError); + console.error( + '[installLauncherUpdate] 解压错误堆栈:', + extractError instanceof Error ? extractError.stack : '无堆栈信息', + ); + throw extractError; + } + + // 查找解压后的目录(通常会有一个子目录) + const extractedItems = readdirSync(tempDir); + console.log( + '[installLauncherUpdate] 解压后的文件数量:', + extractedItems.length, + ); + console.log('[installLauncherUpdate] 解压后的文件列表:', extractedItems); + + let sourceDir = tempDir; + if (extractedItems.length === 1) { + const subPath = path.join(tempDir, extractedItems[0]); + const subStat = statSync(subPath); + console.log('[installLauncherUpdate] 检查子路径:', subPath); + console.log('[installLauncherUpdate] 是否为目录:', subStat.isDirectory()); + if (subStat.isDirectory()) { + sourceDir = subPath; + const subItems = readdirSync(sourceDir); + console.log('[installLauncherUpdate] 子目录文件列表:', subItems); + } + } + + console.log('[installLauncherUpdate] 解压源目录:', sourceDir); + + const findExe = (dir: string): string | null => { + const items = readdirSync(dir); + for (const item of items) { + const fullPath = path.join(dir, item); + const stat = statSync(fullPath); + if (stat.isDirectory()) { + const result = findExe(fullPath); + if (result) return result; + } else if ( + item.endsWith('.exe') && + item.includes('AI-Learning-Assistant-Launcher') + ) { + return fullPath; + } + } + return null; + }; + + const newExePath = findExe(tempDir); + + if (!newExePath) { + throw new Error('未在更新包中找到启动器可执行文件'); + } + + console.debug('找到新版本可执行文件:', newExePath); + + const currentExePath = app.getPath('exe'); + const currentExeDir = path.dirname(currentExePath); + const currentExeName = path.basename(currentExePath); + + const backupPath = path.join(currentExeDir, `${currentExeName}.old`); + + const updateScriptPath = path.join(appPath, 'update.bat'); + const updateScript = `@echo off +chcp 65001 +echo 正在更新启动器... +timeout /t 2 /nobreak >nul + +REM 删除旧备份 +if exist "${backupPath}" del /f /q "${backupPath}" + +REM 备份当前版本 +move /y "${currentExePath}" "${backupPath}" + +REM 复制新版本 +copy /y "${newExePath}" "${currentExePath}" + +REM 清理临时文件 +rmdir /s /q "${tempDir}" + +REM 启动新版本 +start "" "${currentExePath}" + +REM 删除更新脚本自身 +(goto) 2>nul & del "%~f0" +`; + + writeFileSync(updateScriptPath, updateScript, { encoding: 'utf8' }); + + console.debug('更新脚本已创建:', updateScriptPath); + + const result = await dialog.showMessageBox({ + type: 'info', + title: '准备更新', + message: '启动器将在关闭后自动更新', + detail: `当前版本: ${currentVersion}\n新版本: ${version}\n\n点击"确定"后程序将关闭并自动更新,更新完成后会自动重启。`, + buttons: ['确定', '取消'], + defaultId: 0, + cancelId: 1, + }); + + if (result.response === 0) { + spawn('cmd.exe', ['/c', updateScriptPath], { + detached: true, + stdio: 'ignore', + windowsHide: true, + }); + + setTimeout(() => { + app.quit(); + }, 1000); + + return { + success: true, + message: '正在更新启动器...', + }; + } else { + return { + success: false, + message: '用户取消更新', + }; + } + } catch (error) { + console.error('[installLauncherUpdate] 安装启动器更新失败:', error); + console.error( + '[installLauncherUpdate] 错误堆栈:', + error instanceof Error ? error.stack : '无堆栈信息', + ); + throw error; + } +} diff --git a/src/main/launcher-update/type-info.ts b/src/main/launcher-update/type-info.ts new file mode 100644 index 0000000..d43ca0e --- /dev/null +++ b/src/main/launcher-update/type-info.ts @@ -0,0 +1,9 @@ +import type { Channels } from '../ipc-data-type'; + +export const channel: Channels = 'launcher-update'; + +export const checkLauncherUpdateHandle = `${channel}checkUpdate`; + +export const downloadLauncherUpdateHandle = `${channel}downloadUpdate`; + +export const installLauncherUpdateHandle = `${channel}installUpdate`; diff --git a/src/main/preload.ts b/src/main/preload.ts index 7dc73b2..5956736 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -30,6 +30,11 @@ import { startWebtorrentHandle, DLCId, } from './dlc/type-info'; +import { + checkLauncherUpdateHandle, + downloadLauncherUpdateHandle, + installLauncherUpdateHandle, +} from './launcher-update/type-info'; const electronHandler = { ipcRenderer: { @@ -130,7 +135,9 @@ const mainHandle = { ); }, startWebtorrentHandle: async (url: string) => { - return ipcInvoke(startWebtorrentHandle, url); + return ipcInvoke< + { success: true; infoHash: string } | { success: false; error: string } + >(startWebtorrentHandle, url); }, queryWebtorrentHandle: async () => { return ipcInvoke(queryWebtorrentHandle); @@ -144,6 +151,27 @@ const mainHandle = { logsWebtorrentHandle: async (url: string) => { return ipcInvoke(logsWebtorrentHandle, url); }, + checkLauncherUpdateHandle: async () => { + return ipcInvoke<{ + currentVersion: string; + latestVersion: string; + haveNew: boolean; + }>(checkLauncherUpdateHandle); + }, + downloadLauncherUpdateHandle: async () => { + return ipcInvoke<{ + success: boolean; + version: string; + filePath: string; + isDev: boolean; + }>(downloadLauncherUpdateHandle); + }, + installLauncherUpdateHandle: async () => { + return ipcInvoke<{ + success: boolean; + message: string; + }>(installLauncherUpdateHandle); + }, }; export type MainHandle = typeof mainHandle; diff --git a/src/main/training-service/index.ts b/src/main/training-service/index.ts index 562e30c..8f7aace 100644 --- a/src/main/training-service/index.ts +++ b/src/main/training-service/index.ts @@ -79,8 +79,8 @@ const createWindow = (): void => { export async function installTrainingService() { const latestVersion = getLatestVersion('TRAINING_TAR'); - const torrent = await startWebtorrent(latestVersion.dlcInfo.magnet); - await waitTorrentDone('TRAINING_TAR', latestVersion.version); + await startWebtorrent(latestVersion.dlcInfo.magnet); + const torrent = await waitTorrentDone('TRAINING_TAR', latestVersion.version); const tarPath = path.join(torrent.path, torrent.files[0].name); return installService('TRAINING', null, tarPath); } @@ -117,8 +117,11 @@ export async function updateCourseTrainingService() { } if ((await courseHaveNewVersionTrainingService()).haveNew) { const latestVersion = getLatestVersion('TRAINING_TAR'); - const torrent = await startWebtorrent(latestVersion.dlcInfo.magnet); - await waitTorrentDone('TRAINING_TAR', latestVersion.version); + await startWebtorrent(latestVersion.dlcInfo.magnet); + const torrent = await waitTorrentDone( + 'TRAINING_TAR', + latestVersion.version, + ); try { await stopService('TRAINING'); await removeService('TRAINING'); diff --git a/src/renderer/pages/hello/index.scss b/src/renderer/pages/hello/index.scss index e3bc9f6..fec4dba 100644 --- a/src/renderer/pages/hello/index.scss +++ b/src/renderer/pages/hello/index.scss @@ -621,6 +621,15 @@ body{ transform: translateY(-2px); } + // 更新按钮使用蓝色 + &.update-button { + background: #1677ff; + + &:hover { + background: #1677ff; + } + } + .log-text { display: flex; flex-direction: row; diff --git a/src/renderer/pages/hello/index.tsx b/src/renderer/pages/hello/index.tsx index 1497ae8..49e812c 100644 --- a/src/renderer/pages/hello/index.tsx +++ b/src/renderer/pages/hello/index.tsx @@ -152,6 +152,7 @@ export default function Hello() { useEffect(() => { startAutoSlide(); + checkLauncherUpdate(); return () => { clearSlideInterval(); @@ -189,6 +190,12 @@ export default function Hello() { }; const [trainingServiceRemoving, setTrainingServiceRemoving] = useState(false); + const [launcherUpdateInfo, setLauncherUpdateInfo] = useState<{ + currentVersion: string; + latestVersion: string; + haveNew: boolean; + } | null>(null); + const [launcherUpdating, setLauncherUpdating] = useState(false); const removeTrainingService = async () => { setTrainingServiceRemoving(true); @@ -205,6 +212,50 @@ export default function Hello() { setTrainingServiceRemoving(false); }; + const checkLauncherUpdate = async () => { + try { + const info = await window.mainHandle.checkLauncherUpdateHandle(); + setLauncherUpdateInfo(info); + } catch (error) { + console.error('检查启动器更新失败:', error); + } + }; + + const handleLauncherUpdate = async () => { + if (!launcherUpdateInfo || !launcherUpdateInfo.haveNew) { + message.info('已是最新版本'); + return; + } + + setLauncherUpdating(true); + const hideLoading = message.loading('正在下载更新包...', 0); + try { + const downloadResult = + await window.mainHandle.downloadLauncherUpdateHandle(); + hideLoading(); + + // 开发模式下不执行安装,直接提示 + if (downloadResult.isDev) { + message.warning('开发模式下不支持自动更新,请手动解压'); + return; + } + + message.success('下载完成,准备安装...'); + const result = await window.mainHandle.installLauncherUpdateHandle(); + if (result.success) { + message.success(result.message); + } else { + message.warning(result.message); + } + } catch (error) { + hideLoading(); + console.error('更新启动器失败:', error); + message.error('更新失败:' + error.message); + } finally { + setLauncherUpdating(false); + } + }; + const wslStatusText = () => { if (!vTReady) { return '请在BIOS开启虚拟化'; @@ -623,9 +674,18 @@ export default function Hello() { 版本号:{__NPM_PACKAGE_VERSION__} 源码版本:{__COMMIT_HASH__}
- + {/* - + */} +
- + {/* 更新/安装 LM Studio */} + {isInstallLMStudio ? ( + + ) : ( +
+ + {(downloading || isDownloadComplete) && ( +
+ + {isDownloadComplete + ? '下载完成' + : `${downloadProgress}%`} + + +
+ )} +
+ )}
} diff --git a/src/renderer/pages/obsidian-app/index.scss b/src/renderer/pages/obsidian-app/index.scss index 7af0edb..c81c88d 100644 --- a/src/renderer/pages/obsidian-app/index.scss +++ b/src/renderer/pages/obsidian-app/index.scss @@ -13,4 +13,53 @@ display: flex; justify-content: space-between; } + + .download-wrapper { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 6px 12px; + background: #FFFFFF; + border: 1px solid #E5E5E5; + border-radius: 8px; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + + &:hover { + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.15); + } + + &.downloading { + border-color: #1677ff; + background: #f0f5ff; + } + + &.complete { + border-color: #52c41a; + background: #f6ffed; + } + + .download-progress-container { + display: flex; + flex-direction: column; + min-width: 100px; + + .progress-text { + font-family: 'Inter', sans-serif; + font-size: 12px; + font-weight: 500; + color: #737373; + margin-bottom: 2px; + text-align: center; + } + + .ant-progress { + margin: 0; + + .ant-progress-inner { + background-color: #E5E5E5; + } + } + } + } } diff --git a/src/renderer/pages/obsidian-app/index.tsx b/src/renderer/pages/obsidian-app/index.tsx index ead9183..245f6df 100644 --- a/src/renderer/pages/obsidian-app/index.tsx +++ b/src/renderer/pages/obsidian-app/index.tsx @@ -1,5 +1,5 @@ -import { useCallback } from 'react'; -import { Button, List, Skeleton } from 'antd'; +import { useCallback, useEffect, useState } from 'react'; +import { Button, List, Skeleton, message, Progress } from 'antd'; import { Link, NavLink } from 'react-router-dom'; import './index.scss'; import { channel } from '../../../main/cmd/type-info'; @@ -19,6 +19,145 @@ export default function ObsidianApp() { loading: configsLoading, } = useConfigs(); + const [downloading, setDownloading] = useState(false); + const [downloadProgress, setDownloadProgress] = useState(0); + const [isDownloadComplete, setIsDownloadComplete] = useState(false); + + useEffect(() => { + checkObsidianUpdate(); + }, []); + + useEffect(() => { + const intervalId = setInterval(() => { + if (downloading && !isDownloadComplete) { + updateDownloadProgress(); + } + }, 2000); + + return () => clearInterval(intervalId); + }, [downloading, isDownloadComplete]); + + const checkObsidianUpdate = async () => { + // 检查是否已经有下载完成的文件 + try { + const dlcIndex = await window.mainHandle.queryWebtorrentHandle(); + const obsidianDLC = dlcIndex.find( + (item) => item.id === 'OBSIDIAN_SETUP_EXE', + ); + if (obsidianDLC) { + const latestVersion = Object.keys(obsidianDLC.versions).sort().pop(); + if (latestVersion) { + const versionInfo = obsidianDLC.versions[latestVersion]; + if (versionInfo.progress && versionInfo.progress.progress >= 1) { + setIsDownloadComplete(true); + setDownloadProgress(100); + } + } + } + } catch (error) { + console.error('检查下载状态失败:', error); + } + }; + + const updateDownloadProgress = async () => { + try { + const dlcIndex = await window.mainHandle.queryWebtorrentHandle(); + const obsidianDLC = dlcIndex.find( + (item) => item.id === 'OBSIDIAN_SETUP_EXE', + ); + if (obsidianDLC) { + const latestVersion = Object.keys(obsidianDLC.versions).sort().pop(); + if (latestVersion) { + const versionInfo = obsidianDLC.versions[latestVersion]; + if (versionInfo.progress) { + const progress = versionInfo.progress.progress || 0; + setDownloadProgress(Math.floor(progress * 100)); + if (progress >= 1) { + setDownloading(false); + setIsDownloadComplete(true); + message.success('下载完成,可以点击安装按钮进行安装'); + } + } + } + } + } catch (error) { + console.error('获取下载进度失败:', error); + } + }; + + const handleDownloadOrInstall = async () => { + // 如果已经下载完成,直接安装 + if (isDownloadComplete) { + handleInstallObsidian(); + return; + } + + // 否则开始下载 + try { + setDownloading(true); + setDownloadProgress(0); + message.info('正在启动下载...'); + + const dlcIndex = await window.mainHandle.queryWebtorrentHandle(); + const obsidianDLC = dlcIndex.find( + (item) => item.id === 'OBSIDIAN_SETUP_EXE', + ); + + if (!obsidianDLC) { + throw new Error('未找到Obsidian安装包信息'); + } + + const latestVersion = Object.keys(obsidianDLC.versions).sort().pop(); + if (!latestVersion) { + throw new Error('未找到可用版本'); + } + + const versionInfo = obsidianDLC.versions[latestVersion]; + const magnet = versionInfo.magnet; + + const result = await window.mainHandle.startWebtorrentHandle(magnet); + if (result.success) { + message.success(`开始下载Obsidian ${latestVersion}`); + } else { + throw new Error('error' in result ? result.error : '启动下载失败'); + } + } catch (error) { + console.error('下载Obsidian失败:', error); + message.error('下载失败:' + error.message); + setDownloading(false); + } + }; + + const handleInstallObsidian = async () => { + try { + const dlcIndex = await window.mainHandle.queryWebtorrentHandle(); + const obsidianDLC = dlcIndex.find( + (item) => item.id === 'OBSIDIAN_SETUP_EXE', + ); + + if (!obsidianDLC) { + throw new Error('未找到Obsidian安装包'); + } + + const latestVersion = Object.keys(obsidianDLC.versions).sort().pop(); + if (!latestVersion) { + throw new Error('未找到可用版本'); + } + + const versionInfo = obsidianDLC.versions[latestVersion]; + if (!versionInfo.progress || versionInfo.progress.progress < 1) { + message.warning('请先完成下载'); + return; + } + + message.info('正在打开安装程序...'); + cmdAction('install', 'obsidianApp'); + } catch (error) { + console.error('打开安装程序失败:', error); + message.error('失败:' + error.message); + } + }; + return (
cmdAction('install', 'obsidianApp')} +
- 安装阅读器 - + + {(downloading || isDownloadComplete) && ( +
+ + {isDownloadComplete ? '下载完成' : `${downloadProgress}%`} + + +
+ )} +
), , isInstallObsidian && ( + ), + ].filter((item) => item)} + > + + + {/* 老版本兼容 - 直接安装 */} + {/* 老版本兼容 - 直接安装 */} + cmdAction('install', 'obsidianApp')} + > + 本地安装 ), ].filter((item) => item)} >
From 17472b568ffa22a2762a7bf2d520f3c95dd970c4 Mon Sep 17 00:00:00 2001 From: xiaoshen Date: Fri, 23 Jan 2026 15:16:28 +0800 Subject: [PATCH 09/22] fix: prevent duplicate resolve and file handle leaks in WebTorrent operations --- src/main/cmd/index.ts | 420 ++++++++++++++++-------------- src/main/dlc/index.ts | 22 +- src/main/launcher-update/index.ts | 20 +- 3 files changed, 245 insertions(+), 217 deletions(-) diff --git a/src/main/cmd/index.ts b/src/main/cmd/index.ts index bd84404..2a429c6 100644 --- a/src/main/cmd/index.ts +++ b/src/main/cmd/index.ts @@ -24,6 +24,211 @@ import { podMachineName } from '../podman-desktop/type-info'; import { isVTReady, isWSLInstall, wslVersion } from './is-wsl-install'; import { loggerFactory } from '../terminal-log'; +/** + * 安装程序相关的时间配置(单位:毫秒) + */ +const INSTALL_TIMING_CONFIG = { + /** 销毁 torrent 后等待文件句柄释放的时间 */ + TORRENT_DESTROY_WAIT: 2000, + /** 重试的基础等待时间 */ + RETRY_BASE_WAIT: 1000, + /** 每次重试增加的等待时间 */ + RETRY_INCREMENT: 1000, + /** 所有重试失败后的最终等待时间 */ + FINAL_RETRY_WAIT: 10000, + /** 最大重试次数 */ + MAX_RETRIES: 8, + /** LM Studio 安装检测命令超时时间 */ + LM_STUDIO_CHECK_TIMEOUT: 15000, +} as const; + +/** + * 判断错误是否为 EBUSY 错误(文件被占用) + */ +function isEBUSYError(error: unknown): boolean { + if (error instanceof Error) { + return error.message.includes('EBUSY'); + } + if (typeof error === 'object' && error !== null && 'message' in error) { + return String((error as { message: unknown }).message).includes('EBUSY'); + } + return false; +} + +/** + * 获取错误信息 + */ +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + if (typeof error === 'string') { + return error; + } + return String(error); +} + +interface RetryExecuteOptions { + /** 执行的程序路径 */ + exePath: string; + /** 执行参数 */ + args: string[]; + /** 日志前缀,用于区分不同的安装程序 */ + logPrefix: string; +} + +/** + * 带重试机制执行安装程序 + * 处理 EBUSY 错误(文件被占用)的情况 + */ +async function executeWithRetry( + options: RetryExecuteOptions, +): Promise { + const { exePath, args, logPrefix } = options; + const maxRetries = INSTALL_TIMING_CONFIG.MAX_RETRIES; + + for (let retry = 0; retry < maxRetries; retry++) { + try { + // 每次重试前等待更长时间,让文件句柄有充足时间释放 + const waitTime = + retry === 0 + ? INSTALL_TIMING_CONFIG.RETRY_BASE_WAIT + : INSTALL_TIMING_CONFIG.RETRY_BASE_WAIT + + retry * INSTALL_TIMING_CONFIG.RETRY_INCREMENT; + await new Promise((resolve) => setTimeout(resolve, waitTime)); + + console.debug( + `[${logPrefix}] 尝试执行安装程序 (${retry + 1}/${maxRetries}),等待了 ${waitTime}ms`, + ); + const result = await commandLine.exec(exePath, args); + console.debug(`[${logPrefix}] 安装程序执行成功:`, result); + return true; + } catch (e) { + if (isEBUSYError(e) && retry < maxRetries - 1) { + console.warn( + `[${logPrefix}] 执行安装程序失败 (EBUSY),重试 ${retry + 1}/${maxRetries}...`, + ); + } else if (isEBUSYError(e)) { + // 所有重试都失败了,尝试最后一次长等待 + console.warn(`[${logPrefix}] 所有重试都失败,尝试最后一次长等待...`); + await new Promise((resolve) => + setTimeout(resolve, INSTALL_TIMING_CONFIG.FINAL_RETRY_WAIT), + ); + try { + const result = await commandLine.exec(exePath, args); + console.debug(`[${logPrefix}] 最后一次尝试成功:`, result); + return true; + } catch (finalError) { + console.error(`[${logPrefix}] 最后一次尝试也失败了:`, finalError); + throw finalError; + } + } else { + throw e; + } + } + } + return false; +} + +interface P2PInstallOptions { + /** DLC 索引中的 key */ + dlcKey: + | 'OBSIDIAN_SETUP_EXE' + | 'LM_STUDIO_SETUP_EXE' + | 'PDF_TAR' + | 'VOICE_TAR' + | 'TRAINING_TAR' + | 'TRAINING_VOICE_TAR' + | 'TRAINING_COURSE' + | 'LMSTUDIO_WINDOWS' + | 'AI_LEARNING_ASSISTANT_LAUNCHER' + | 'TEST_FILE'; + /** 安装程序参数 */ + installerArgs: string[]; + /** 日志前缀 */ + logPrefix: string; + /** 降级时使用的本地安装包相对路径 */ + fallbackInstallerPath: string; +} + +/** + * 通用的 P2P 下载安装逻辑 + * 优先使用 P2P 下载的安装包,失败时降级到本地安装包 + */ +async function installFromP2POrFallback( + options: P2PInstallOptions, +): Promise { + const { dlcKey, installerArgs, logPrefix, fallbackInstallerPath } = options; + + try { + // 首先检查是否有P2P下载的文件 + const { getDLCFromDLCIndex, destroyWebtorrentForInstall } = await import( + '../dlc' + ); + const dlcInfo = getDLCFromDLCIndex(dlcKey); + + if (dlcInfo) { + const latestVersion = Object.keys(dlcInfo.versions).sort().pop(); + if (latestVersion) { + const versionInfo = dlcInfo.versions[latestVersion]; + + // 完全销毁 torrent 以释放文件句柄,避免 EBUSY 错误 + if (versionInfo.magnet) { + try { + console.debug(`[${logPrefix}] 开始销毁种子以释放文件句柄...`); + await destroyWebtorrentForInstall(versionInfo.magnet); + // 等待额外的时间确保文件句柄完全释放 + await new Promise((resolve) => + setTimeout(resolve, INSTALL_TIMING_CONFIG.TORRENT_DESTROY_WAIT), + ); + console.debug(`[${logPrefix}] 种子已销毁,等待完成`); + } catch (e) { + console.warn(`[${logPrefix}] 销毁torrent失败:`, e); + } + } + + const downloadPath = path.join( + appPath, + 'external-resources', + 'dlc', + dlcKey, + latestVersion, + ); + + const fs = await import('fs'); + if (fs.existsSync(downloadPath)) { + const files = fs.readdirSync(downloadPath); + const exeFile = files.find((f: string) => f.endsWith('.exe')); + + if (exeFile) { + const exePath = path.join(downloadPath, exeFile); + return await executeWithRetry({ + exePath, + args: installerArgs, + logPrefix, + }); + } + } + } + } + } catch (error) { + console.error(`[${logPrefix}] 尝试使用P2P下载包失败,使用默认方式:`, error); + } + + // 降级到原有的本地安装包方式 + const result = await commandLine.exec( + path.join( + appPath, + 'external-resources', + 'ai-assistant-backend', + fallbackInstallerPath, + ), + installerArgs, + ); + console.debug(`[${logPrefix}] 本地安装包执行结果:`, result); + return true; +} + const commandLine = new Exec(); export default async function init(ipcMain: IpcMain) { @@ -488,104 +693,12 @@ async function checkWSLComponent() { } export async function installObsidian() { - try { - // 首先检查是否有P2P下载的文件 - const { getDLCFromDLCIndex, destroyWebtorrentForInstall } = await import( - '../dlc' - ); - const dlcInfo = getDLCFromDLCIndex('OBSIDIAN_SETUP_EXE'); - - if (dlcInfo) { - const latestVersion = Object.keys(dlcInfo.versions).sort().pop(); - if (latestVersion) { - const versionInfo = dlcInfo.versions[latestVersion]; - - // 完全销毁 torrent 以释放文件句柄,避免 EBUSY 错误 - if (versionInfo.magnet) { - try { - console.debug('开始销毁种子以释放文件句柄...'); - await destroyWebtorrentForInstall(versionInfo.magnet); - // 等待额外的时间确保文件句柄完全释放 - await new Promise((resolve) => setTimeout(resolve, 2000)); - console.debug('种子已销毁,等待完成'); - } catch (e) { - console.warn('销毁torrent失败:', e); - } - } - - const downloadPath = path.join( - appPath, - 'external-resources', - 'dlc', - 'OBSIDIAN_SETUP_EXE', - latestVersion, - ); - - const fs = await import('fs'); - if (fs.existsSync(downloadPath)) { - const files = fs.readdirSync(downloadPath); - const exeFile = files.find((f: string) => f.endsWith('.exe')); - - if (exeFile) { - const exePath = path.join(downloadPath, exeFile); - - // 使用更激进的重试机制处理 EBUSY 错误 - const maxRetries = 8; - for (let retry = 0; retry < maxRetries; retry++) { - try { - // 每次重试前等待更长时间,让文件句柄有充足时间释放 - const waitTime = retry === 0 ? 1000 : 1000 + retry * 1000; - await new Promise((resolve) => setTimeout(resolve, waitTime)); - - console.debug( - `尝试执行安装程序 (${retry + 1}/${maxRetries}),等待了 ${waitTime}ms`, - ); - const result = await commandLine.exec(exePath, ['/s']); - console.debug('安装程序执行成功:', result); - return true; - } catch (e) { - const isEBUSY = e.message && e.message.includes('EBUSY'); - if (isEBUSY && retry < maxRetries - 1) { - console.warn( - `执行安装程序失败 (EBUSY),重试 ${retry + 1}/${maxRetries}...`, - ); - } else if (isEBUSY) { - // 所有重试都失败了,尝试最后一次长等待 - console.warn('所有重试都失败,尝试最后一次长等待(10秒)...'); - await new Promise((resolve) => setTimeout(resolve, 10000)); - try { - const result = await commandLine.exec(exePath, ['/s']); - console.debug('最后一次尝试成功:', result); - return true; - } catch (finalError) { - console.error('最后一次尝试也失败了:', finalError); - throw finalError; - } - } else { - throw e; - } - } - } - } - } - } - } - } catch (error) { - console.error('尝试使用P2P下载包失败,使用默认方式:', error); - } - - // 降级到原有的本地安装包方式 - const result = await commandLine.exec( - path.join( - appPath, - 'external-resources', - 'ai-assistant-backend', - 'install_obsidian.exe', - ), - ['/s'], - ); - console.debug(result); - return true; + return installFromP2POrFallback({ + dlcKey: 'OBSIDIAN_SETUP_EXE', + installerArgs: ['/s'], + logPrefix: 'Obsidian', + fallbackInstallerPath: 'install_obsidian.exe', + }); } export async function isObsidianInstall() { @@ -607,113 +720,24 @@ export async function isObsidianInstall() { } export async function installLMStudio() { - try { - // 首先检查是否有P2P下载的文件 - const { getDLCFromDLCIndex, destroyWebtorrentForInstall } = await import( - '../dlc' - ); - const dlcInfo = getDLCFromDLCIndex('LM_STUDIO_SETUP_EXE'); - - if (dlcInfo) { - const latestVersion = Object.keys(dlcInfo.versions).sort().pop(); - if (latestVersion) { - const versionInfo = dlcInfo.versions[latestVersion]; - - // 完全销毁 torrent 以释放文件句柄,避免 EBUSY 错误 - if (versionInfo.magnet) { - try { - console.debug('开始销毁LM Studio种子以释放文件句柄...'); - await destroyWebtorrentForInstall(versionInfo.magnet); - // 等待额外的时间确保文件句柄完全释放 - await new Promise((resolve) => setTimeout(resolve, 2000)); - console.debug('种子已销毁,等待完成'); - } catch (e) { - console.warn('销毁torrent失败:', e); - } - } - - const downloadPath = path.join( - appPath, - 'external-resources', - 'dlc', - 'LM_STUDIO_SETUP_EXE', - latestVersion, - ); - - const fs = await import('fs'); - if (fs.existsSync(downloadPath)) { - const files = fs.readdirSync(downloadPath); - const exeFile = files.find((f: string) => f.endsWith('.exe')); - - if (exeFile) { - const exePath = path.join(downloadPath, exeFile); - - // 使用更激进的重试机制处理 EBUSY 错误 - const maxRetries = 8; - for (let retry = 0; retry < maxRetries; retry++) { - try { - // 每次重试前等待更长时间,让文件句柄有充足时间释放 - const waitTime = retry === 0 ? 1000 : 1000 + retry * 1000; - await new Promise((resolve) => setTimeout(resolve, waitTime)); - - console.debug( - `尝试执行LM Studio安装程序 (${retry + 1}/${maxRetries}),等待了 ${waitTime}ms`, - ); - const result = await commandLine.exec(exePath, []); - console.debug('LM Studio安装程序执行成功:', result); - return true; - } catch (e) { - const isEBUSY = e.message && e.message.includes('EBUSY'); - if (isEBUSY && retry < maxRetries - 1) { - console.warn( - `执行LM Studio安装程序失败 (EBUSY),重试 ${retry + 1}/${maxRetries}...`, - ); - } else if (isEBUSY) { - // 所有重试都失败了,尝试最后一次长等待 - console.warn('所有重试都失败,尝试最后一次长等待(10秒)...'); - await new Promise((resolve) => setTimeout(resolve, 10000)); - try { - const result = await commandLine.exec(exePath, []); - console.debug('最后一次尝试成功:', result); - return true; - } catch (finalError) { - console.error('最后一次尝试也失败了:', finalError); - throw finalError; - } - } else { - throw e; - } - } - } - } - } - } - } - } catch (error) { - console.error('尝试使用P2P下载包失败,使用默认方式:', error); - } - - // 降级到原有的本地安装包方式 - const result = await commandLine.exec( - path.join( - appPath, - 'external-resources', - 'ai-assistant-backend', - 'install_lm_studio.exe', - ), - ['/s'], - ); - console.debug(result); - return true; + return installFromP2POrFallback({ + dlcKey: 'LM_STUDIO_SETUP_EXE', + installerArgs: [], + logPrefix: 'LM Studio', + fallbackInstallerPath: 'install_lm_studio.exe', + }); } export async function isLMStudioInstall() { try { const result = await Promise.race([ new Promise((resolve, reject) => - setTimeout(() => reject('isLMStudioInstall命令超时'), 15000), + setTimeout( + () => reject('isLMStudioInstall命令超时'), + INSTALL_TIMING_CONFIG.LM_STUDIO_CHECK_TIMEOUT, + ), ), - // 如果用户安装lmstudio然后又卸载了lmstudio,那么这个命令会一直卡着,也不报错,所以要用一个10000ms的报错promise与它竞赛 + // 如果用户安装lmstudio然后又卸载了lmstudio,那么这个命令会一直卡着,也不报错,所以要用一个超时promise与它竞赛 commandLine.exec('lms', ['ls']), ]); console.debug('isLMStudioInstall', result); diff --git a/src/main/dlc/index.ts b/src/main/dlc/index.ts index 5225291..ff5c825 100644 --- a/src/main/dlc/index.ts +++ b/src/main/dlc/index.ts @@ -164,20 +164,18 @@ export async function destroyWebtorrentForInstall( const torrent = await client.get(magnet); if (torrent) { return new Promise((resolve) => { - // 先暂停下载 - torrent.pause(); + let resolved = false; + const safeResolve = () => { + if (!resolved) { + resolved = true; + resolve(); + } + }; - // 销毁种子,这会关闭所有文件句柄 - torrent.destroy({ destroyStore: false }, () => { - console.debug('种子已销毁,文件句柄已释放:', torrent.infoHash); - resolve(); - }); + torrent.pause(); + torrent.destroy({ destroyStore: false }, safeResolve); - // 设置一个超时,防止回调永远不执行 - setTimeout(() => { - console.debug('destroy回调超时,强制resolve'); - resolve(); - }, 3000); + setTimeout(safeResolve, 3000); }); } } diff --git a/src/main/launcher-update/index.ts b/src/main/launcher-update/index.ts index 80867d2..4563f23 100644 --- a/src/main/launcher-update/index.ts +++ b/src/main/launcher-update/index.ts @@ -7,10 +7,11 @@ import { } from './type-info'; import { getLatestVersion, - isLatestVersion, startWebtorrent, waitTorrentDone, + destroyWebtorrentForInstall, } from '../dlc'; +import semver from 'semver'; import path from 'path'; import { appPath } from '../exec'; import { @@ -29,9 +30,10 @@ import packageJson from '../../../package.json'; const currentVersion = packageJson.version; // 在 Electron 中使用 original-fs 处理 asar 打包后的文件操作 -// original-fs 是 Electron 内置的,需要通过 require 动态导入 +// original-fs 是 Electron 内置模块,API 与 Node.js fs 模块相同 +// 使用类型注解确保类型安全 // eslint-disable-next-line @typescript-eslint/no-var-requires -const originalFs = require('original-fs'); +const originalFs: typeof import('fs') = require('original-fs'); export default async function init(ipcMain: IpcMain) { ipcHandle(ipcMain, checkLauncherUpdateHandle, async (_event) => @@ -51,10 +53,7 @@ export async function checkLauncherUpdate() { 'AI_LEARNING_ASSISTANT_LAUNCHER', ); const latestVersion = latestVersionInfo.version; - const haveNew = !isLatestVersion( - 'AI_LEARNING_ASSISTANT_LAUNCHER', - currentVersion, - ); + const haveNew = semver.lt(currentVersion, latestVersion); console.debug('检查启动器更新:', { currentVersion, @@ -153,6 +152,13 @@ export async function installLauncherUpdate() { throw new Error('未找到下载的更新包'); } + // 销毁 WebTorrent 以释放文件句柄,避免后续读取 zip 文件时出现 EBUSY 错误 + console.debug('[installLauncherUpdate] 销毁 WebTorrent 释放文件句柄...'); + await destroyWebtorrentForInstall(latestVersionInfo.dlcInfo.magnet); + // 等待文件句柄完全释放 + await new Promise((resolve) => setTimeout(resolve, 500)); + console.debug('[installLauncherUpdate] 文件句柄已释放'); + const zipPath = path.join(downloadPath, zipFile); console.debug('[installLauncherUpdate] zip文件路径:', zipPath); console.debug( From 726b3b5659c7de9ed43e0494156787e30b25ce29 Mon Sep 17 00:00:00 2001 From: xiaoshen Date: Fri, 23 Jan 2026 16:57:46 +0800 Subject: [PATCH 10/22] feat(webtorrent): add upload control capability --- src/main/dlc/index.ts | 124 ++++++++++++++++++++++++++++++++++++++ src/main/dlc/type-info.ts | 6 ++ src/main/preload.ts | 20 ++++++ 3 files changed, 150 insertions(+) diff --git a/src/main/dlc/index.ts b/src/main/dlc/index.ts index ff5c825..fcc4b4d 100644 --- a/src/main/dlc/index.ts +++ b/src/main/dlc/index.ts @@ -8,6 +8,9 @@ import { DLCIndex, DLCId, OneDLCInfo, + setUploadEnabledHandle, + getUploadEnabledHandle, + getUploadStatsHandle, } from './type-info'; import { ipcHandle } from '../ipc-util'; import WebTorrent, * as allExports from 'webtorrent'; @@ -17,6 +20,45 @@ import fs, { existsSync, mkdirSync, readFileSync, unlinkSync } from 'fs'; import { compare } from 'semver'; let client: WebTorrent.Instance | null = null; +let uploadEnabled = true; // 默认开启上传 + +// 上传配置文件路径 +const uploadConfigPath = path.join( + appPath, + 'external-resources', + 'dlc', + 'upload-config.json', +); + +// 读取上传配置 +function loadUploadConfig(): boolean { + try { + if (existsSync(uploadConfigPath)) { + const config = JSON.parse(readFileSync(uploadConfigPath, 'utf8')); + return config.uploadEnabled ?? true; + } + } catch (error) { + console.error('读取上传配置失败:', error); + } + return true; +} + +// 保存上传配置 +function saveUploadConfig(enabled: boolean): void { + try { + const dir = path.dirname(uploadConfigPath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync( + uploadConfigPath, + JSON.stringify({ uploadEnabled: enabled }, null, 2), + ); + } catch (error) { + console.error('保存上传配置失败:', error); + } +} + const getWebTorrent = async () => { const WebTorrentClass: WebTorrent.WebTorrent = // @ts-ignore @@ -59,7 +101,14 @@ const getWebTorrent = async () => { // }; export default async function init(ipcMain: IpcMain) { + // 加载上传配置 + uploadEnabled = loadUploadConfig(); + client = await getWebTorrent(); + + // 根据配置应用上传限制 + applyUploadThrottle(); + ipcHandle(ipcMain, startWebtorrentHandle, async (_event, magnet: string) => startWebtorrent(magnet), ); @@ -75,6 +124,13 @@ export default async function init(ipcMain: IpcMain) { ipcHandle(ipcMain, logsWebtorrentHandle, async (_event, magnet: string) => logsWebtorrent(magnet), ); + ipcHandle(ipcMain, setUploadEnabledHandle, async (_event, enabled: boolean) => + setUploadEnabled(enabled), + ); + ipcHandle(ipcMain, getUploadEnabledHandle, async (_event) => + getUploadEnabled(), + ); + ipcHandle(ipcMain, getUploadStatsHandle, async (_event) => getUploadStats()); // 检查种子文件和对应的实际文件,如果有文件,就添加到webtorrent中 await restoreTorrentsFromFiles(); } @@ -578,3 +634,71 @@ async function restoreTorrentsFromFiles() { console.error('恢复种子过程中发生错误:', error); } } + +/** 应用上传限速设置 */ +function applyUploadThrottle(): void { + if (!client) return; + + if (uploadEnabled) { + // 不限制上传速度 (-1 表示无限制) + client.throttleUpload(-1); + console.debug('上传已启用,不限速'); + } else { + // 限制上传速度为 0(禁用上传) + client.throttleUpload(0); + console.debug('上传已禁用,限速为 0'); + } +} + +/** 设置上传开关 */ +export function setUploadEnabled(enabled: boolean): { + success: boolean; + enabled: boolean; +} { + uploadEnabled = enabled; + saveUploadConfig(enabled); + applyUploadThrottle(); + console.debug(`上传设置已更新: ${enabled ? '启用' : '禁用'}`); + return { success: true, enabled }; +} + +/** 获取上传开关状态 */ +export function getUploadEnabled(): { enabled: boolean } { + return { enabled: uploadEnabled }; +} + +/** 获取上传统计信息 */ +export function getUploadStats(): { + enabled: boolean; + totalUploaded: number; + uploadSpeed: number; + activeTorrents: number; +} { + if (!client) { + return { + enabled: uploadEnabled, + totalUploaded: 0, + uploadSpeed: 0, + activeTorrents: 0, + }; + } + + let totalUploaded = 0; + let uploadSpeed = 0; + let activeTorrents = 0; + + for (const torrent of client.torrents) { + totalUploaded += torrent.uploaded || 0; + uploadSpeed += torrent.uploadSpeed || 0; + if (torrent.progress === 1 && !torrent.paused) { + activeTorrents++; + } + } + + return { + enabled: uploadEnabled, + totalUploaded, + uploadSpeed, + activeTorrents, + }; +} diff --git a/src/main/dlc/type-info.ts b/src/main/dlc/type-info.ts index dfc5a37..7770c7f 100644 --- a/src/main/dlc/type-info.ts +++ b/src/main/dlc/type-info.ts @@ -13,6 +13,12 @@ export const queryWebtorrentHandle = `${channel}query`; export const logsWebtorrentHandle = `${channel}logs`; +export const setUploadEnabledHandle = `${channel}setUploadEnabled`; + +export const getUploadEnabledHandle = `${channel}getUploadEnabled`; + +export const getUploadStatsHandle = `${channel}getUploadStats`; + export const dLCIds = [ 'PDF_TAR', 'VOICE_TAR', diff --git a/src/main/preload.ts b/src/main/preload.ts index 5956736..fe191f3 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -29,6 +29,9 @@ import { removeWebtorrentHandle, startWebtorrentHandle, DLCId, + setUploadEnabledHandle, + getUploadEnabledHandle, + getUploadStatsHandle, } from './dlc/type-info'; import { checkLauncherUpdateHandle, @@ -172,6 +175,23 @@ const mainHandle = { message: string; }>(installLauncherUpdateHandle); }, + setUploadEnabledHandle: async (enabled: boolean) => { + return ipcInvoke<{ success: boolean; enabled: boolean }>( + setUploadEnabledHandle, + enabled, + ); + }, + getUploadEnabledHandle: async () => { + return ipcInvoke<{ enabled: boolean }>(getUploadEnabledHandle); + }, + getUploadStatsHandle: async () => { + return ipcInvoke<{ + enabled: boolean; + totalUploaded: number; + uploadSpeed: number; + activeTorrents: number; + }>(getUploadStatsHandle); + }, }; export type MainHandle = typeof mainHandle; From 902a45fcd11d86e257219aa27fb3ede655d889cb Mon Sep 17 00:00:00 2001 From: forceOfPx <3239735744@qq.com> Date: Fri, 23 Jan 2026 18:45:35 +0800 Subject: [PATCH 11/22] merge fix --- src/main/preload.ts | 2 ++ src/renderer/pages/hello/index.tsx | 1 + 2 files changed, 3 insertions(+) diff --git a/src/main/preload.ts b/src/main/preload.ts index 573f4a5..e456e93 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -27,6 +27,7 @@ import { setTrayEnabledHandle, DiskInfo, } from './joint-build/type-info'; +import { DLCIndex, logsWebtorrentHandle, pauseWebtorrentHandle, @@ -154,6 +155,7 @@ const mainHandle = { const handler = (_event: IpcRendererEvent, enabled: boolean) => callback(enabled); ipcRenderer.on('joint-build-status-changed', handler); return () => ipcRenderer.removeListener('joint-build-status-changed', handler); + }, startWebtorrentHandle: async (url: string) => { return ipcInvoke(startWebtorrentHandle, url); }, diff --git a/src/renderer/pages/hello/index.tsx b/src/renderer/pages/hello/index.tsx index 720bc4d..6730778 100644 --- a/src/renderer/pages/hello/index.tsx +++ b/src/renderer/pages/hello/index.tsx @@ -652,6 +652,7 @@ export default function Hello() { 共建计划 共建计划 + From 9f22a843744ba088f47084053f2fc8b1e2e9b922 Mon Sep 17 00:00:00 2001 From: xiaoshen Date: Fri, 23 Jan 2026 18:56:06 +0800 Subject: [PATCH 12/22] feat: replace linear progress bars with circular progress --- src/main/launcher-update/index.ts | 25 ++-- src/renderer/pages/hello/index.scss | 41 ++++++- src/renderer/pages/hello/index.tsx | 132 +++++++++++++++++---- src/renderer/pages/lm-service/index.scss | 60 +++++----- src/renderer/pages/lm-service/index.tsx | 37 ++---- src/renderer/pages/obsidian-app/index.scss | 60 +++++----- src/renderer/pages/obsidian-app/index.tsx | 35 ++---- 7 files changed, 248 insertions(+), 142 deletions(-) diff --git a/src/main/launcher-update/index.ts b/src/main/launcher-update/index.ts index 4563f23..9567da0 100644 --- a/src/main/launcher-update/index.ts +++ b/src/main/launcher-update/index.ts @@ -94,16 +94,16 @@ export async function downloadLauncherUpdate() { console.debug('启动器更新下载完成'); // 检查是否为本地开发环境 - // const isDev = !app.isPackaged; - // if (isDev) { - // console.warn('当前为本地开发环境,启动器更新功能可能无法正常工作'); - // } + const isDev = !app.isPackaged; + if (isDev) { + console.warn('当前为本地开发环境,启动器更新功能可能无法正常工作'); + } return { success: true, version: latestVersionInfo.version, filePath: path.join(torrent.path, torrent.files[0].name), - // isDev, + isDev, }; } catch (error) { console.error('下载启动器更新失败:', error); @@ -115,13 +115,14 @@ export async function installLauncherUpdate() { console.debug('[installLauncherUpdate] 开始执行安装更新'); // 如果是开发模式,不执行更新 - // const isPackaged = app.isPackaged; - // if (!isPackaged) { - // return { - // success: false, - // message: '开发模式下不支持自动更新,请手动解压', - // }; - // } + const isPackaged = app.isPackaged; + if (!isPackaged) { + console.warn('[installLauncherUpdate] 开发模式下不支持自动更新'); + return { + success: false, + message: '开发模式下不支持自动更新,请手动解压', + }; + } try { console.debug('[installLauncherUpdate] 获取最新版本信息...'); diff --git a/src/renderer/pages/hello/index.scss b/src/renderer/pages/hello/index.scss index fec4dba..4054172 100644 --- a/src/renderer/pages/hello/index.scss +++ b/src/renderer/pages/hello/index.scss @@ -596,11 +596,27 @@ body{ padding: 8px 0; gap: 10px; width: auto; - max-width: 300px; + max-width: 400px; min-height: 53px; flex: none; order: 1; + .launcher-update-wrapper { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 4px 8px; + background: linear-gradient(135deg, rgba(22, 119, 255, 0.1) 0%, rgba(22, 119, 255, 0.05) 100%); + border-radius: 8px; + box-shadow: 0 2px 6px rgba(22, 119, 255, 0.15); + + .ant-progress { + .ant-progress-inner { + background-color: rgba(22, 119, 255, 0.15); + } + } + } + .status-indicator { box-sizing: border-box; display: flex; @@ -626,7 +642,16 @@ body{ background: #1677ff; &:hover { - background: #1677ff; + background: #4096ff; + } + + &.download-complete { + background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); + animation: pulse-green 2s infinite; + + &:hover { + background: linear-gradient(135deg, #73d13d 0%, #95de64 100%); + } } } @@ -1118,6 +1143,18 @@ body{ } } +@keyframes pulse-green { + 0% { + box-shadow: 0 0 0 0 rgba(82, 196, 26, 0.4); + } + 70% { + box-shadow: 0 0 0 8px rgba(82, 196, 26, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(82, 196, 26, 0); + } +} + @media screen and (max-width: 1200px) { .hello-root .hello-container .hello-content .features-section .features-container { flex-wrap: wrap; diff --git a/src/renderer/pages/hello/index.tsx b/src/renderer/pages/hello/index.tsx index 49e812c..2f9d4d3 100644 --- a/src/renderer/pages/hello/index.tsx +++ b/src/renderer/pages/hello/index.tsx @@ -1,4 +1,12 @@ -import { Button, message, Space, Modal, notification, Popconfirm } from 'antd'; +import { + Button, + message, + Space, + Modal, + notification, + Popconfirm, + Progress, +} from 'antd'; import { NavLink } from 'react-router-dom'; import { useEffect, useState, useRef } from 'react'; // 添加 useRef 导入 import obsidianLogo from './2023_Obsidian_logo.png'; @@ -196,6 +204,43 @@ export default function Hello() { haveNew: boolean; } | null>(null); const [launcherUpdating, setLauncherUpdating] = useState(false); + const [launcherDownloadProgress, setLauncherDownloadProgress] = useState(0); + const [launcherDownloadComplete, setLauncherDownloadComplete] = + useState(false); + + // 轮询下载进度 + useEffect(() => { + if (!launcherUpdating || !launcherUpdateInfo?.haveNew) return; + + let mounted = true; + const fetchProgress = async () => { + try { + const dlcIndex = await window.mainHandle.queryWebtorrentHandle(); + const dlc = dlcIndex.find( + (item) => item.id === 'AI_LEARNING_ASSISTANT_LAUNCHER', + ); + if (!dlc || !mounted) return; + + const versionInfo = dlc.versions[launcherUpdateInfo.latestVersion]; + if (versionInfo?.progress) { + const progress = (versionInfo.progress.progress || 0) * 100; + if (mounted) { + setLauncherDownloadProgress(progress); + } + } + } catch (error) { + console.error('获取下载进度失败:', error); + } + }; + + fetchProgress(); + const intervalId = setInterval(fetchProgress, 1000); + + return () => { + mounted = false; + clearInterval(intervalId); + }; + }, [launcherUpdating, launcherUpdateInfo]); const removeTrainingService = async () => { setTrainingServiceRemoving(true); @@ -216,6 +261,21 @@ export default function Hello() { try { const info = await window.mainHandle.checkLauncherUpdateHandle(); setLauncherUpdateInfo(info); + + // 检查是否已有下载完成的更新包 + if (info?.haveNew) { + const dlcIndex = await window.mainHandle.queryWebtorrentHandle(); + const dlc = dlcIndex.find( + (item) => item.id === 'AI_LEARNING_ASSISTANT_LAUNCHER', + ); + if (dlc) { + const versionInfo = dlc.versions[info.latestVersion]; + if (versionInfo?.progress?.progress >= 1) { + setLauncherDownloadComplete(true); + setLauncherDownloadProgress(100); + } + } + } } catch (error) { console.error('检查启动器更新失败:', error); } @@ -227,32 +287,49 @@ export default function Hello() { return; } + // 如果下载已完成,直接执行安装 + if (launcherDownloadComplete) { + setLauncherUpdating(true); + try { + const result = await window.mainHandle.installLauncherUpdateHandle(); + if (result.success) { + message.success(result.message); + } else { + message.warning(result.message); + } + } catch (error) { + console.error('安装启动器更新失败:', error); + message.error('安装失败:' + error.message); + } finally { + setLauncherUpdating(false); + } + return; + } + setLauncherUpdating(true); - const hideLoading = message.loading('正在下载更新包...', 0); + setLauncherDownloadProgress(0); + setLauncherDownloadComplete(false); + try { const downloadResult = await window.mainHandle.downloadLauncherUpdateHandle(); - hideLoading(); // 开发模式下不执行安装,直接提示 if (downloadResult.isDev) { message.warning('开发模式下不支持自动更新,请手动解压'); + setLauncherUpdating(false); return; } - message.success('下载完成,准备安装...'); - const result = await window.mainHandle.installLauncherUpdateHandle(); - if (result.success) { - message.success(result.message); - } else { - message.warning(result.message); - } + setLauncherDownloadProgress(100); + setLauncherDownloadComplete(true); + setLauncherUpdating(false); + message.success('下载完成,点击按钮重启并更新'); } catch (error) { - hideLoading(); console.error('更新启动器失败:', error); message.error('更新失败:' + error.message); - } finally { setLauncherUpdating(false); + setLauncherDownloadProgress(0); } }; @@ -677,15 +754,28 @@ export default function Hello() { {/* */} - + {launcherUpdateInfo?.haveNew && ( +
+ {launcherUpdating && !launcherDownloadComplete && ( + + )} + +
+ )}
)} diff --git a/src/renderer/pages/obsidian-app/index.scss b/src/renderer/pages/obsidian-app/index.scss index c81c88d..b2095b8 100644 --- a/src/renderer/pages/obsidian-app/index.scss +++ b/src/renderer/pages/obsidian-app/index.scss @@ -17,49 +17,49 @@ .download-wrapper { display: inline-flex; align-items: center; - gap: 10px; - padding: 6px 12px; - background: #FFFFFF; - border: 1px solid #E5E5E5; + gap: 8px; + padding: 4px 8px; + background: linear-gradient(135deg, rgba(22, 119, 255, 0.1) 0%, rgba(22, 119, 255, 0.05) 100%); border-radius: 8px; - box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 6px rgba(22, 119, 255, 0.15); transition: all 0.3s ease; - &:hover { - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.15); + .ant-progress { + .ant-progress-inner { + background-color: rgba(22, 119, 255, 0.15); + } } &.downloading { border-color: #1677ff; - background: #f0f5ff; + background: linear-gradient(135deg, rgba(22, 119, 255, 0.15) 0%, rgba(22, 119, 255, 0.08) 100%); } - &.complete { - border-color: #52c41a; - background: #f6ffed; - } + &.download-complete { + background: linear-gradient(135deg, rgba(82, 196, 26, 0.15) 0%, rgba(82, 196, 26, 0.08) 100%); + box-shadow: 0 2px 6px rgba(82, 196, 26, 0.2); - .download-progress-container { - display: flex; - flex-direction: column; - min-width: 100px; + .download-complete-btn { + background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); + border-color: #52c41a; + animation: pulse-green 2s infinite; - .progress-text { - font-family: 'Inter', sans-serif; - font-size: 12px; - font-weight: 500; - color: #737373; - margin-bottom: 2px; - text-align: center; - } - - .ant-progress { - margin: 0; - - .ant-progress-inner { - background-color: #E5E5E5; + &:hover { + background: linear-gradient(135deg, #73d13d 0%, #95de64 100%); } } } } } + +@keyframes pulse-green { + 0% { + box-shadow: 0 0 0 0 rgba(82, 196, 26, 0.4); + } + 70% { + box-shadow: 0 0 0 8px rgba(82, 196, 26, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(82, 196, 26, 0); + } +} diff --git a/src/renderer/pages/obsidian-app/index.tsx b/src/renderer/pages/obsidian-app/index.tsx index 245f6df..a78ea23 100644 --- a/src/renderer/pages/obsidian-app/index.tsx +++ b/src/renderer/pages/obsidian-app/index.tsx @@ -204,12 +204,22 @@ export default function ObsidianApp() { key="download" className={`download-wrapper ${ downloading ? 'downloading' : '' - } ${isDownloadComplete ? 'complete' : ''}`} + } ${isDownloadComplete ? 'download-complete' : ''}`} > + {downloading && !isDownloadComplete && ( + + )} - {(downloading || isDownloadComplete) && ( -
- - {isDownloadComplete ? '下载完成' : `${downloadProgress}%`} - - -
- )} ), + {launcherUpdating && !launcherDownloadComplete ? ( + + ) : ( + + )} )} + ) : ( + + + )} )} diff --git a/src/renderer/pages/obsidian-app/index.tsx b/src/renderer/pages/obsidian-app/index.tsx index a78ea23..f4c0091 100644 --- a/src/renderer/pages/obsidian-app/index.tsx +++ b/src/renderer/pages/obsidian-app/index.tsx @@ -22,6 +22,7 @@ export default function ObsidianApp() { const [downloading, setDownloading] = useState(false); const [downloadProgress, setDownloadProgress] = useState(0); const [isDownloadComplete, setIsDownloadComplete] = useState(false); + const [currentMagnet, setCurrentMagnet] = useState(null); useEffect(() => { checkObsidianUpdate(); @@ -114,6 +115,7 @@ export default function ObsidianApp() { const versionInfo = obsidianDLC.versions[latestVersion]; const magnet = versionInfo.magnet; + setCurrentMagnet(magnet); const result = await window.mainHandle.startWebtorrentHandle(magnet); if (result.success) { @@ -125,6 +127,24 @@ export default function ObsidianApp() { console.error('下载Obsidian失败:', error); message.error('下载失败:' + error.message); setDownloading(false); + setCurrentMagnet(null); + } + }; + + const handleCancelDownload = async () => { + if (!currentMagnet) { + message.warning('没有正在进行的下载'); + return; + } + try { + await window.mainHandle.pauseWebtorrentHandle(currentMagnet); + setDownloading(false); + setDownloadProgress(0); + setCurrentMagnet(null); + message.info('已取消下载'); + } catch (error) { + console.error('取消下载失败:', error); + message.error('取消下载失败'); } }; @@ -215,19 +235,23 @@ export default function ObsidianApp() { strokeColor="#1677ff" /> )} - + {downloading && !isDownloadComplete ? ( + + ) : ( + + )} ), )} From a5bb6b7d9083ac42eb730f9044c1a3e8788a5471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=9A=E5=8B=83=E9=B9=8F?= <2032806273@qq.com> Date: Fri, 30 Jan 2026 18:01:58 +0800 Subject: [PATCH 17/22] feat: add persistent logging to launcher update module --- src/main/launcher-update/index.ts | 376 ++++++++++++++++++++++++------ 1 file changed, 303 insertions(+), 73 deletions(-) diff --git a/src/main/launcher-update/index.ts b/src/main/launcher-update/index.ts index 9567da0..63b70ea 100644 --- a/src/main/launcher-update/index.ts +++ b/src/main/launcher-update/index.ts @@ -21,7 +21,7 @@ import { statSync, rmSync, writeFileSync, - readFileSync, + appendFileSync, } from 'fs'; import { spawn } from 'child_process'; import AdmZip from 'adm-zip'; @@ -35,6 +35,70 @@ const currentVersion = packageJson.version; // eslint-disable-next-line @typescript-eslint/no-var-requires const originalFs: typeof import('fs') = require('original-fs'); +// 更新日志文件路径 +const updateLogPath = path.join(appPath, 'launcher-update.log'); + +/** + * 写入更新日志到文件 + * @param level 日志级别 + * @param message 日志消息 + * @param data 附加数据 + */ +function writeUpdateLog( + level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR', + message: string, + data?: unknown, +) { + const timestamp = new Date().toISOString(); + let logLine = `[${timestamp}] [${level}] ${message}`; + if (data !== undefined) { + try { + logLine += ` ${JSON.stringify(data)}`; + } catch { + logLine += ` [无法序列化的数据]`; + } + } + logLine += '\n'; + + try { + appendFileSync(updateLogPath, logLine, { encoding: 'utf8' }); + } catch (err) { + console.error('写入更新日志失败:', err); + } + + // 同时输出到控制台 + switch (level) { + case 'DEBUG': + console.debug(message, data ?? ''); + break; + case 'INFO': + console.log(message, data ?? ''); + break; + case 'WARN': + console.warn(message, data ?? ''); + break; + case 'ERROR': + console.error(message, data ?? ''); + break; + } +} + +/** + * 初始化日志文件(清空旧日志或添加分隔符) + */ +function initUpdateLog() { + const separator = ` +${'='.repeat(60)} +[${new Date().toISOString()}] 启动器更新日志开始 +${'='.repeat(60)} +`; + try { + appendFileSync(updateLogPath, separator, { encoding: 'utf8' }); + } catch { + // 忽略错误 + } +} + export default async function init(ipcMain: IpcMain) { ipcHandle(ipcMain, checkLauncherUpdateHandle, async (_event) => checkLauncherUpdate(), @@ -48,6 +112,9 @@ export default async function init(ipcMain: IpcMain) { } export async function checkLauncherUpdate() { + initUpdateLog(); + writeUpdateLog('INFO', '[checkLauncherUpdate] 开始检查启动器更新'); + try { const latestVersionInfo = getLatestVersion( 'AI_LEARNING_ASSISTANT_LAUNCHER', @@ -55,7 +122,7 @@ export async function checkLauncherUpdate() { const latestVersion = latestVersionInfo.version; const haveNew = semver.lt(currentVersion, latestVersion); - console.debug('检查启动器更新:', { + writeUpdateLog('INFO', '[checkLauncherUpdate] 检查启动器更新完成', { currentVersion, latestVersion, haveNew, @@ -67,7 +134,13 @@ export async function checkLauncherUpdate() { haveNew, }; } catch (error) { - console.error('检查启动器更新失败:', error); + writeUpdateLog( + 'ERROR', + '[checkLauncherUpdate] 检查启动器更新失败', + error instanceof Error + ? { message: error.message, stack: error.stack } + : error, + ); return { currentVersion, latestVersion: currentVersion, @@ -77,47 +150,66 @@ export async function checkLauncherUpdate() { } export async function downloadLauncherUpdate() { + writeUpdateLog('INFO', '[downloadLauncherUpdate] 开始下载启动器更新'); + try { const latestVersionInfo = getLatestVersion( 'AI_LEARNING_ASSISTANT_LAUNCHER', ); - console.debug('开始下载启动器更新:', latestVersionInfo); + writeUpdateLog('DEBUG', '[downloadLauncherUpdate] 获取到最新版本信息', { + version: latestVersionInfo.version, + magnet: latestVersionInfo.dlcInfo?.magnet?.substring(0, 50) + '...', + }); await startWebtorrent(latestVersionInfo.dlcInfo.magnet); + writeUpdateLog('DEBUG', '[downloadLauncherUpdate] WebTorrent 已启动'); const torrent = await waitTorrentDone( 'AI_LEARNING_ASSISTANT_LAUNCHER', latestVersionInfo.version, ); - console.debug('启动器更新下载完成'); + const filePath = path.join(torrent.path, torrent.files[0].name); + writeUpdateLog('INFO', '[downloadLauncherUpdate] 启动器更新下载完成', { + path: torrent.path, + fileName: torrent.files[0].name, + }); // 检查是否为本地开发环境 const isDev = !app.isPackaged; if (isDev) { - console.warn('当前为本地开发环境,启动器更新功能可能无法正常工作'); + writeUpdateLog( + 'WARN', + '[downloadLauncherUpdate] 当前为本地开发环境,启动器更新功能可能无法正常工作', + ); } return { success: true, version: latestVersionInfo.version, - filePath: path.join(torrent.path, torrent.files[0].name), + filePath, isDev, }; } catch (error) { - console.error('下载启动器更新失败:', error); + writeUpdateLog( + 'ERROR', + '[downloadLauncherUpdate] 下载启动器更新失败', + error instanceof Error + ? { message: error.message, stack: error.stack } + : error, + ); throw error; } } export async function installLauncherUpdate() { - console.debug('[installLauncherUpdate] 开始执行安装更新'); + writeUpdateLog('INFO', '[installLauncherUpdate] 开始执行安装更新'); // 如果是开发模式,不执行更新 const isPackaged = app.isPackaged; if (!isPackaged) { - console.warn('[installLauncherUpdate] 开发模式下不支持自动更新'); + writeUpdateLog('WARN', '[installLauncherUpdate] 开发模式下不支持自动更新'); return { success: false, message: '开发模式下不支持自动更新,请手动解压', @@ -125,12 +217,12 @@ export async function installLauncherUpdate() { } try { - console.debug('[installLauncherUpdate] 获取最新版本信息...'); + writeUpdateLog('DEBUG', '[installLauncherUpdate] 获取最新版本信息...'); const latestVersionInfo = getLatestVersion( 'AI_LEARNING_ASSISTANT_LAUNCHER', ); const version = latestVersionInfo.version; - console.debug('[installLauncherUpdate] 最新版本:', version); + writeUpdateLog('DEBUG', '[installLauncherUpdate] 最新版本:', version); const downloadPath = path.join( appPath, @@ -139,57 +231,68 @@ export async function installLauncherUpdate() { 'AI_LEARNING_ASSISTANT_LAUNCHER', version, ); - console.debug('[installLauncherUpdate] 下载路径:', downloadPath); - console.debug( + writeUpdateLog('DEBUG', '[installLauncherUpdate] 下载路径:', downloadPath); + writeUpdateLog( + 'DEBUG', '[installLauncherUpdate] 下载路径是否存在:', existsSync(downloadPath), ); const files = readdirSync(downloadPath); - console.debug('[installLauncherUpdate] 下载目录文件列表:', files); + writeUpdateLog('DEBUG', '[installLauncherUpdate] 下载目录文件列表:', files); const zipFile = files.find((f) => f.endsWith('.zip')); if (!zipFile) { + writeUpdateLog('ERROR', '[installLauncherUpdate] 未找到下载的更新包'); throw new Error('未找到下载的更新包'); } // 销毁 WebTorrent 以释放文件句柄,避免后续读取 zip 文件时出现 EBUSY 错误 - console.debug('[installLauncherUpdate] 销毁 WebTorrent 释放文件句柄...'); + writeUpdateLog( + 'DEBUG', + '[installLauncherUpdate] 销毁 WebTorrent 释放文件句柄...', + ); await destroyWebtorrentForInstall(latestVersionInfo.dlcInfo.magnet); // 等待文件句柄完全释放 - await new Promise((resolve) => setTimeout(resolve, 500)); - console.debug('[installLauncherUpdate] 文件句柄已释放'); + await new Promise((resolve) => setTimeout(resolve, 1000)); + writeUpdateLog('DEBUG', '[installLauncherUpdate] 文件句柄已释放'); const zipPath = path.join(downloadPath, zipFile); - console.debug('[installLauncherUpdate] zip文件路径:', zipPath); - console.debug( + writeUpdateLog('DEBUG', '[installLauncherUpdate] zip文件路径:', zipPath); + writeUpdateLog( + 'DEBUG', '[installLauncherUpdate] zip文件是否存在:', existsSync(zipPath), ); const tempDir = path.join(appPath, 'update-temp'); - console.debug('[installLauncherUpdate] 临时目录:', tempDir); + writeUpdateLog('DEBUG', '[installLauncherUpdate] 临时目录:', tempDir); if (existsSync(tempDir)) { - console.debug('[installLauncherUpdate] 清理已存在的临时目录...'); + writeUpdateLog( + 'DEBUG', + '[installLauncherUpdate] 清理已存在的临时目录...', + ); rmSync(tempDir, { recursive: true, force: true }); - console.debug('[installLauncherUpdate] 临时目录清理完成'); + writeUpdateLog('DEBUG', '[installLauncherUpdate] 临时目录清理完成'); } mkdirSync(tempDir, { recursive: true }); - console.debug('[installLauncherUpdate] 临时目录已创建'); + writeUpdateLog('DEBUG', '[installLauncherUpdate] 临时目录已创建'); // 使用 adm-zip 解压 zip 文件 - console.log('[installLauncherUpdate] 正在解压文件...'); - console.log('[installLauncherUpdate] ZIP路径:', zipPath); - console.log('[installLauncherUpdate] 目标路径:', tempDir); + writeUpdateLog('INFO', '[installLauncherUpdate] 正在解压文件...'); + writeUpdateLog('DEBUG', '[installLauncherUpdate] ZIP路径:', zipPath); + writeUpdateLog('DEBUG', '[installLauncherUpdate] 目标路径:', tempDir); try { const zip = new AdmZip(zipPath); const zipEntries = zip.getEntries(); - console.log( + writeUpdateLog( + 'DEBUG', '[installLauncherUpdate] ZIP包含的文件数量:', zipEntries.length, ); - console.log( + writeUpdateLog( + 'DEBUG', '[installLauncherUpdate] ZIP包含的文件列表:', zipEntries.slice(0, 5).map((e) => e.entryName), ); @@ -213,38 +316,53 @@ export async function installLauncherUpdate() { originalFs.writeFileSync(entryPath, content); } } - console.log('[installLauncherUpdate] 解压完成'); + writeUpdateLog('INFO', '[installLauncherUpdate] 解压完成'); } catch (extractError) { - console.error('[installLauncherUpdate] 解压失败:', extractError); - console.error( - '[installLauncherUpdate] 解压错误堆栈:', - extractError instanceof Error ? extractError.stack : '无堆栈信息', + writeUpdateLog( + 'ERROR', + '[installLauncherUpdate] 解压失败', + extractError instanceof Error + ? { message: extractError.message, stack: extractError.stack } + : extractError, ); throw extractError; } // 查找解压后的目录(通常会有一个子目录) const extractedItems = readdirSync(tempDir); - console.log( + writeUpdateLog( + 'DEBUG', '[installLauncherUpdate] 解压后的文件数量:', extractedItems.length, ); - console.log('[installLauncherUpdate] 解压后的文件列表:', extractedItems); + writeUpdateLog( + 'DEBUG', + '[installLauncherUpdate] 解压后的文件列表:', + extractedItems, + ); let sourceDir = tempDir; if (extractedItems.length === 1) { const subPath = path.join(tempDir, extractedItems[0]); const subStat = statSync(subPath); - console.log('[installLauncherUpdate] 检查子路径:', subPath); - console.log('[installLauncherUpdate] 是否为目录:', subStat.isDirectory()); + writeUpdateLog('DEBUG', '[installLauncherUpdate] 检查子路径:', subPath); + writeUpdateLog( + 'DEBUG', + '[installLauncherUpdate] 是否为目录:', + subStat.isDirectory(), + ); if (subStat.isDirectory()) { sourceDir = subPath; const subItems = readdirSync(sourceDir); - console.log('[installLauncherUpdate] 子目录文件列表:', subItems); + writeUpdateLog( + 'DEBUG', + '[installLauncherUpdate] 子目录文件列表:', + subItems, + ); } } - console.log('[installLauncherUpdate] 解压源目录:', sourceDir); + writeUpdateLog('DEBUG', '[installLauncherUpdate] 解压源目录:', sourceDir); const findExe = (dir: string): string | null => { const items = readdirSync(dir); @@ -264,13 +382,21 @@ export async function installLauncherUpdate() { return null; }; - const newExePath = findExe(tempDir); + const newExePath = findExe(sourceDir); if (!newExePath) { + writeUpdateLog( + 'ERROR', + '[installLauncherUpdate] 未在更新包中找到启动器可执行文件', + ); throw new Error('未在更新包中找到启动器可执行文件'); } - console.debug('找到新版本可执行文件:', newExePath); + writeUpdateLog( + 'DEBUG', + '[installLauncherUpdate] 找到新版本可执行文件:', + newExePath, + ); const currentExePath = app.getPath('exe'); const currentExeDir = path.dirname(currentExePath); @@ -278,54 +404,155 @@ export async function installLauncherUpdate() { const backupPath = path.join(currentExeDir, `${currentExeName}.old`); - const updateScriptPath = path.join(appPath, 'update.bat'); - const updateScript = `@echo off + // 找到新版本可执行文件所在目录的所有文件 + const newExeDir = path.dirname(newExePath); + writeUpdateLog( + 'DEBUG', + '[installLauncherUpdate] 新版本文件目录:', + newExeDir, + ); + writeUpdateLog('INFO', '[installLauncherUpdate] 更新路径信息', { + currentExePath, + currentExeDir, + backupPath, + newExeDir, + }); + + const result = await dialog.showMessageBox({ + type: 'info', + title: '准备更新', + message: '启动器将在关闭后自动更新', + detail: `当前版本: ${currentVersion}\n新版本: ${version}\n\n点击"确定"后程序将关闭并自动更新,更新完成后会自动重启。`, + buttons: ['确定', '取消'], + defaultId: 0, + cancelId: 1, + }); + + if (result.response === 0) { + writeUpdateLog( + 'INFO', + '[installLauncherUpdate] 用户确认更新,准备执行更新', + ); + + // 创建更新脚本,在应用退出后执行 + const updateScriptPath = path.join(appPath, 'update.bat'); + // bat 脚本也写入日志文件,方便追踪更新过程(Windows BAT 脚本路径不需要转义) + const batLogPath = updateLogPath; + const updateScript = `@echo off chcp 65001 +echo =================================== echo 正在更新启动器... -timeout /t 2 /nobreak >nul +echo =================================== -REM 删除旧备份 -if exist "${backupPath}" del /f /q "${backupPath}" +REM 写入日志函数 +set "LOG_FILE=${batLogPath}" +echo [%date% %time%] [BAT] 更新脚本开始执行 >> "%LOG_FILE%" -REM 备份当前版本 -move /y "${currentExePath}" "${backupPath}" +REM 等待应用完全退出并释放文件句柄 +echo 等待应用退出... +echo [%date% %time%] [BAT] 等待应用退出... >> "%LOG_FILE%" +timeout /t 3 /nobreak >nul -REM 复制新版本 -copy /y "${newExePath}" "${currentExePath}" +REM 删除旧备份 +if exist "${backupPath}" ( + echo 删除旧备份... + echo [%date% %time%] [BAT] 删除旧备份: ${backupPath} >> "%LOG_FILE%" + del /f /q "${backupPath}" +) + +REM 备份当前版本的可执行文件 +if exist "${currentExePath}" ( + echo 备份当前版本... + echo [%date% %time%] [BAT] 备份当前版本: ${currentExePath} -^> ${backupPath} >> "%LOG_FILE%" + move /y "${currentExePath}" "${backupPath}" + if errorlevel 1 ( + echo 备份失败! + echo [%date% %time%] [BAT] [ERROR] 备份失败! >> "%LOG_FILE%" + pause + exit /b 1 + ) + echo [%date% %time%] [BAT] 备份成功 >> "%LOG_FILE%" +) + +REM 使用 robocopy 复制新版本的所有文件到当前目录 +echo 正在复制文件... +echo [%date% %time%] [BAT] 开始复制文件: ${newExeDir} -^> ${currentExeDir} >> "%LOG_FILE%" +robocopy "${newExeDir}" "${currentExeDir}" /E /IS /IT /NFL /NDL /NP + +REM robocopy 返回值说明: +REM 0 = 没有文件被复制 +REM 1 = 所有文件复制成功 +REM 2 = 有额外的文件或目录 +REM 3 = 有文件被复制,也有不匹配的文件 +REM 4 = 有不匹配的文件或目录 +REM 5 = 有文件被复制,也有不匹配的文件或目录 +REM 6 = 有额外的文件/目录和不匹配的文件 +REM 7 = 文件被复制,有额外的文件和不匹配的文件 +REM 8+ = 有错误 + +if %ERRORLEVEL% GEQ 8 ( + echo =================================== + echo 复制文件失败!错误码: %ERRORLEVEL% + echo 正在恢复原版本... + echo =================================== + echo [%date% %time%] [BAT] [ERROR] 复制文件失败!错误码: %ERRORLEVEL% >> "%LOG_FILE%" + if exist "${backupPath}" ( + move /y "${backupPath}" "${currentExePath}" + echo [%date% %time%] [BAT] 已恢复原版本 >> "%LOG_FILE%" + ) + echo 更新失败,已恢复原版本 + pause + exit /b 1 +) + +echo 文件复制完成,错误码: %ERRORLEVEL% +echo [%date% %time%] [BAT] 文件复制完成,错误码: %ERRORLEVEL% >> "%LOG_FILE%" REM 清理临时文件 -rmdir /s /q "${tempDir}" +echo 清理临时文件... +echo [%date% %time%] [BAT] 清理临时文件: ${tempDir} >> "%LOG_FILE%" +if exist "${tempDir}" ( + rmdir /s /q "${tempDir}" 2>nul +) +echo [%date% %time%] [BAT] 临时文件清理完成 >> "%LOG_FILE%" REM 启动新版本 +echo =================================== +echo 启动新版本... +echo =================================== +echo [%date% %time%] [BAT] 启动新版本: ${currentExePath} >> "%LOG_FILE%" start "" "${currentExePath}" +REM 等待一下确保应用启动 +timeout /t 2 /nobreak >nul +echo [%date% %time%] [BAT] 更新脚本执行完成 >> "%LOG_FILE%" + REM 删除更新脚本自身 (goto) 2>nul & del "%~f0" `; - writeFileSync(updateScriptPath, updateScript, { encoding: 'utf8' }); - - console.debug('更新脚本已创建:', updateScriptPath); - - const result = await dialog.showMessageBox({ - type: 'info', - title: '准备更新', - message: '启动器将在关闭后自动更新', - detail: `当前版本: ${currentVersion}\n新版本: ${version}\n\n点击"确定"后程序将关闭并自动更新,更新完成后会自动重启。`, - buttons: ['确定', '取消'], - defaultId: 0, - cancelId: 1, - }); + writeFileSync(updateScriptPath, updateScript, { encoding: 'utf8' }); + writeUpdateLog( + 'INFO', + '[installLauncherUpdate] 更新脚本已创建:', + updateScriptPath, + ); - if (result.response === 0) { - spawn('cmd.exe', ['/c', updateScriptPath], { + // 使用 detached 模式启动更新脚本 + const updateProcess = spawn('cmd.exe', ['/c', updateScriptPath], { detached: true, stdio: 'ignore', - windowsHide: true, + windowsHide: false, }); + updateProcess.unref(); + writeUpdateLog('INFO', '[installLauncherUpdate] 更新脚本已启动'); + + // 延迟退出,确保更新脚本已经启动 + // 注意:这里不使用 app.relaunch(),而是让更新脚本完成后手动启动新版本 setTimeout(() => { - app.quit(); + writeUpdateLog('INFO', '[installLauncherUpdate] 准备退出应用'); + app.exit(0); }, 1000); return { @@ -333,16 +560,19 @@ REM 删除更新脚本自身 message: '正在更新启动器...', }; } else { + writeUpdateLog('INFO', '[installLauncherUpdate] 用户取消更新'); return { success: false, message: '用户取消更新', }; } } catch (error) { - console.error('[installLauncherUpdate] 安装启动器更新失败:', error); - console.error( - '[installLauncherUpdate] 错误堆栈:', - error instanceof Error ? error.stack : '无堆栈信息', + writeUpdateLog( + 'ERROR', + '[installLauncherUpdate] 安装启动器更新失败', + error instanceof Error + ? { message: error.message, stack: error.stack } + : error, ); throw error; } From d2442a7afb6f85c9ca3db630e927f6746356650a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=9A=E5=8B=83=E9=B9=8F?= <2032806273@qq.com> Date: Sat, 31 Jan 2026 09:23:47 +0800 Subject: [PATCH 18/22] feat: configure Squirrel builder for auto-updates --- forge.config.ts | 8 +- package-lock.json | 1892 ++++++++++++++++++++------------------------- 2 files changed, 848 insertions(+), 1052 deletions(-) diff --git a/forge.config.ts b/forge.config.ts index 8105eaa..e64f757 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -20,7 +20,13 @@ const config: ForgeConfig = { }, rebuildConfig: {}, makers: [ - // new MakerSquirrel({}), + new MakerSquirrel({ + name: 'AILearningAssistantLauncher', + authors: 'James Zhang', + description: 'AI Learning Assistant Launcher', + // 设置图标路径 + setupIcon: path.join(__dirname, 'icons', 'icon.ico'), + }), new MakerZIP({}, ['darwin', 'win32']), // new MakerRpm({}), // new MakerDeb({}) diff --git a/package-lock.json b/package-lock.json index 1b17429..c799fac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@langchain/ollama": "^0.2.4", "@langchain/openai": "^0.6.14", "@types/webtorrent": "^0.110.1", + "adm-zip": "^0.5.16", "electron-squirrel-startup": "^1.0.1", "langchain": "^0.3.35", "pdf-lib": "^1.17.1", @@ -48,7 +49,6 @@ "@typescript-eslint/parser": "^5.62.0", "@vercel/webpack-asset-relocator-loader": "1.7.3", "@xterm/xterm": "^5.5.0", - "adm-zip": "^0.5.16", "antd": "^5.26.1", "copy-webpack-plugin": "^13.0.1", "cpy": "^11.1.0", @@ -227,13 +227,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -242,9 +242,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", "dev": true, "license": "MIT", "engines": { @@ -252,21 +252,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -293,14 +293,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -323,13 +323,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -350,18 +350,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", - "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.5", + "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "engines": { @@ -410,17 +410,17 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.5", - "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "version": "0.6.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", + "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" + "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -451,29 +451,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -496,9 +496,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -524,15 +524,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -586,42 +586,42 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.28.3", - "resolved": "https://registry.npmmirror.com/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", - "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2" + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -698,14 +698,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.3", - "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", - "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -728,13 +728,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -744,13 +744,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -760,13 +760,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -776,13 +776,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -825,15 +825,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.6.tgz", + "integrity": "sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -843,14 +843,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { @@ -877,13 +877,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", - "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -893,14 +893,14 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", - "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -910,14 +910,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.3", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", - "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -927,18 +927,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.4", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", - "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.4" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -948,14 +948,14 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/template": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -982,14 +982,14 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", - "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1015,14 +1015,14 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz", + "integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1048,14 +1048,14 @@ } }, "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.0", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", - "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1065,13 +1065,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", - "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1132,13 +1132,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", - "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1164,13 +1164,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", - "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1213,14 +1213,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1299,13 +1299,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", - "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1315,13 +1315,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", - "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1331,17 +1331,17 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.4", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", - "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.4" + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1368,13 +1368,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", - "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1384,13 +1384,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", - "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { @@ -1417,14 +1417,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", - "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1434,15 +1434,15 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", - "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1500,17 +1500,17 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", - "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1553,13 +1553,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.4", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", - "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.6.tgz", + "integrity": "sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1569,14 +1569,14 @@ } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", - "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1618,13 +1618,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { @@ -1683,17 +1683,17 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", - "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1" + "@babel/plugin-syntax-typescript": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1719,14 +1719,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", - "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1753,14 +1753,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", - "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1770,76 +1770,76 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/preset-env/-/preset-env-7.28.5.tgz", - "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/preset-env/-/preset-env-7.28.6.tgz", + "integrity": "sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/compat-data": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.0", - "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.6", + "@babel/plugin-transform-async-to-generator": "^7.28.6", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.5", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.4", - "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-dotall-regex": "^7.28.6", "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.28.6", "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.28.5", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", - "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.4", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.28.5", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.27.1", - "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.4", - "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.6", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", @@ -1921,42 +1921,42 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", "debug": "^4.3.1" }, "engines": { @@ -1964,9 +1964,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "dev": true, "license": "MIT", "dependencies": { @@ -2016,9 +2016,9 @@ } }, "node_modules/@electron-forge/cli": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/cli/-/cli-7.10.2.tgz", - "integrity": "sha512-X1RtS5IqNgzGDS2rr1q0Y74wU/m3DbU4vSgllNun1ZQv1BfMpDcKLhnKi3aeetoA0huLTpMVU9eWJ7bziI9fxA==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/cli/-/cli-7.11.1.tgz", + "integrity": "sha512-pk8AoLsr7t7LBAt0cFD06XFA6uxtPdvtLx06xeal7O9o7GHGCbj29WGwFoJ8Br/ENM0Ho868S3PrAn1PtBXt5g==", "dev": true, "funding": [ { @@ -2032,9 +2032,9 @@ ], "license": "MIT", "dependencies": { - "@electron-forge/core": "7.10.2", - "@electron-forge/core-utils": "7.10.2", - "@electron-forge/shared-types": "7.10.2", + "@electron-forge/core": "7.11.1", + "@electron-forge/core-utils": "7.11.1", + "@electron-forge/shared-types": "7.11.1", "@electron/get": "^3.0.0", "@inquirer/prompts": "^6.0.1", "@listr2/prompt-adapter-inquirer": "^2.0.22", @@ -2056,9 +2056,9 @@ } }, "node_modules/@electron-forge/core": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/core/-/core-7.10.2.tgz", - "integrity": "sha512-HAIuOtpOfGjA0cd55tbEV2gAv+A7tSZg9bonmVDYFEe6dBgbLk8a3+/1fJUdWW8fyFkg1wa8zK7pjP751bAXsA==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/core/-/core-7.11.1.tgz", + "integrity": "sha512-YtuPLzggPKPabFAD2rOZFE0s7f4KaUTpGRduhSMbZUqpqD1TIPyfoDBpYiZvao3Ht8pyZeOJjbzcC0LpFs9gIQ==", "dev": true, "funding": [ { @@ -2072,17 +2072,17 @@ ], "license": "MIT", "dependencies": { - "@electron-forge/core-utils": "7.10.2", - "@electron-forge/maker-base": "7.10.2", - "@electron-forge/plugin-base": "7.10.2", - "@electron-forge/publisher-base": "7.10.2", - "@electron-forge/shared-types": "7.10.2", - "@electron-forge/template-base": "7.10.2", - "@electron-forge/template-vite": "7.10.2", - "@electron-forge/template-vite-typescript": "7.10.2", - "@electron-forge/template-webpack": "7.10.2", - "@electron-forge/template-webpack-typescript": "7.10.2", - "@electron-forge/tracer": "7.10.2", + "@electron-forge/core-utils": "7.11.1", + "@electron-forge/maker-base": "7.11.1", + "@electron-forge/plugin-base": "7.11.1", + "@electron-forge/publisher-base": "7.11.1", + "@electron-forge/shared-types": "7.11.1", + "@electron-forge/template-base": "7.11.1", + "@electron-forge/template-vite": "7.11.1", + "@electron-forge/template-vite-typescript": "7.11.1", + "@electron-forge/template-webpack": "7.11.1", + "@electron-forge/template-webpack-typescript": "7.11.1", + "@electron-forge/tracer": "7.11.1", "@electron/get": "^3.0.0", "@electron/packager": "^18.3.5", "@electron/rebuild": "^3.7.0", @@ -2112,13 +2112,13 @@ } }, "node_modules/@electron-forge/core-utils": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/core-utils/-/core-utils-7.10.2.tgz", - "integrity": "sha512-JXrk2hWR4q8KgZFABpojjuqql3tYeVIH6qmtbkNEkZEQq7YIxajJBCct7J7bWfNQTmHotsQ3k5KLknhyhTaBMw==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/core-utils/-/core-utils-7.11.1.tgz", + "integrity": "sha512-9UxRWVsfcziBsbAA2MS0Oz4yYovQCO2BhnGIfsbKNTBtMc/RcVSxAS0NMyymce44i43p1ZC/FqWhnt1XqYw3bQ==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.10.2", + "@electron-forge/shared-types": "7.11.1", "@electron/rebuild": "^3.7.0", "@malept/cross-spawn-promise": "^2.0.0", "chalk": "^4.0.0", @@ -2134,13 +2134,13 @@ } }, "node_modules/@electron-forge/maker-base": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/maker-base/-/maker-base-7.10.2.tgz", - "integrity": "sha512-1QN4qnPVTjo+qWYG+s0kYv7XcuIowsPVvbl718FgJUcvkxyRjUA6kWHjFxRvdV6g7Sa2PzZBF+/Mrjpws1lehQ==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/maker-base/-/maker-base-7.11.1.tgz", + "integrity": "sha512-yhZrCGoN6bDeiB5DHFaueZ1h84AReElEj+f0hl2Ph4UbZnO0cnLpbx+Bs+XfMLAiA+beC8muB5UDK5ysfuT9BQ==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.10.2", + "@electron-forge/shared-types": "7.11.1", "fs-extra": "^10.0.0", "which": "^2.0.2" }, @@ -2149,14 +2149,14 @@ } }, "node_modules/@electron-forge/maker-deb": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/maker-deb/-/maker-deb-7.10.2.tgz", - "integrity": "sha512-4MPr9NW5UbEUbf9geZn5R/0O/QVIiy2EgUXOYOeKkA7oR8U6I1I3+BytYFHYcxbY6+PGhi1H1VTLJLITbHGVWw==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/maker-deb/-/maker-deb-7.11.1.tgz", + "integrity": "sha512-QTYiryQLYPDkq6pIfBmx0GQ6D8QatUkowH7rTlW5MnCUa0uumX0Xu7yGIjesuwW37fxT3Lv4xi+FSXMCm2eC1w==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/maker-base": "7.10.2", - "@electron-forge/shared-types": "7.10.2" + "@electron-forge/maker-base": "7.11.1", + "@electron-forge/shared-types": "7.11.1" }, "engines": { "node": ">= 16.4.0" @@ -2166,14 +2166,14 @@ } }, "node_modules/@electron-forge/maker-rpm": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/maker-rpm/-/maker-rpm-7.10.2.tgz", - "integrity": "sha512-LQoeYzbY/z1yuBBA+bNutCJmhCA4NcXUbFO4OTqsIX8B6y1zNTYZT4JEuhoK7eBsP4/Rz6u/JnNp0XOyjftOUQ==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/maker-rpm/-/maker-rpm-7.11.1.tgz", + "integrity": "sha512-iEfJPRQQyaTqk2EbUfZgulChNWvxGXeYUH0xBX/r5cj1pL4vcJXt3jLMQBVn3mk/0Ytv9UWRs8R/XuNWX6sf2w==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/maker-base": "7.10.2", - "@electron-forge/shared-types": "7.10.2" + "@electron-forge/maker-base": "7.11.1", + "@electron-forge/shared-types": "7.11.1" }, "engines": { "node": ">= 16.4.0" @@ -2183,14 +2183,14 @@ } }, "node_modules/@electron-forge/maker-squirrel": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/maker-squirrel/-/maker-squirrel-7.10.2.tgz", - "integrity": "sha512-Y5EhNSBXf4a7qcq+BK/x5qVDlQ1Gez5V+arUpDvVxf1zwvsB1aSyAjmoBrOKGYD9A5pJzjkMWMDw95MStl1W4A==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/maker-squirrel/-/maker-squirrel-7.11.1.tgz", + "integrity": "sha512-oSg7fgad6l+X0DjtRkSpMzB0AjzyDO4mb2gzM4kTodkP1ADeiMi08bxy0ZeCESqLm5+fG72cAPmEr3BAPvI1yw==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/maker-base": "7.10.2", - "@electron-forge/shared-types": "7.10.2", + "@electron-forge/maker-base": "7.11.1", + "@electron-forge/shared-types": "7.11.1", "fs-extra": "^10.0.0" }, "engines": { @@ -2201,14 +2201,14 @@ } }, "node_modules/@electron-forge/maker-zip": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/maker-zip/-/maker-zip-7.10.2.tgz", - "integrity": "sha512-APRqVPM+O1rj4O7sk5f8tqJpS5UgxcUJEsCnXN4JRpdRvsOlMopzYZdazlCLH9l7S+r4ZKirjtMluIGeYq8YOg==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/maker-zip/-/maker-zip-7.11.1.tgz", + "integrity": "sha512-30rcp0AbJLfkFBX2hmO14LKXx7z9V61LffTVbTCFMh5vUB2kZvcA5xAhsBk2oUJWfGVxe1DuSEU0rDR9bUMHUg==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/maker-base": "7.10.2", - "@electron-forge/shared-types": "7.10.2", + "@electron-forge/maker-base": "7.11.1", + "@electron-forge/shared-types": "7.11.1", "cross-zip": "^4.0.0", "fs-extra": "^10.0.0", "got": "^11.8.5" @@ -2218,41 +2218,41 @@ } }, "node_modules/@electron-forge/plugin-auto-unpack-natives": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/plugin-auto-unpack-natives/-/plugin-auto-unpack-natives-7.10.2.tgz", - "integrity": "sha512-uQnahm1DECwqI8hBC7PKccyfovY/YqHNz8de3OxyjQDmwsqQfCA8Ucyh1E9n4NMEpw6Co8KLn+qF2BuIOsftLA==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/plugin-auto-unpack-natives/-/plugin-auto-unpack-natives-7.11.1.tgz", + "integrity": "sha512-5uRM3WNv7jIeDt8pLP3V4U2puWHPGJ/3qRuSE47RKgTp5qxpZidWHSYcEJJxjoqOL/7KFwSqKSQ/a36GoZV4Fg==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/plugin-base": "7.10.2", - "@electron-forge/shared-types": "7.10.2" + "@electron-forge/plugin-base": "7.11.1", + "@electron-forge/shared-types": "7.11.1" }, "engines": { "node": ">= 16.4.0" } }, "node_modules/@electron-forge/plugin-base": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/plugin-base/-/plugin-base-7.10.2.tgz", - "integrity": "sha512-+4YLmkLZxvS6JFXYNI4dHt8Il8iIvwk2o6lCJGwNysOUq2KOZ3Wu1He4Ko8HhKcO1VWbFvslbh57oQn963Aryw==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/plugin-base/-/plugin-base-7.11.1.tgz", + "integrity": "sha512-lKpSOV1GA3FoYiD9k05i6v4KaQVmojnRgCr7d6VL1bFp13QOtXSaAWhFI9mtSY7rGElOacX6Zt7P7rPoB8T9eQ==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.10.2" + "@electron-forge/shared-types": "7.11.1" }, "engines": { "node": ">= 16.4.0" } }, "node_modules/@electron-forge/plugin-fuses": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/plugin-fuses/-/plugin-fuses-7.10.2.tgz", - "integrity": "sha512-X8FaBL5pVvKCTBNaa9EjbH6vuaeIU7UcPSmP9501XF4zcKPCfTbQKz49LTMl7gd5YzUm82IlqRjte12LLpcSDQ==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/plugin-fuses/-/plugin-fuses-7.11.1.tgz", + "integrity": "sha512-Td517mHf+RjQAayFDM2kKb7NaGdRXrZfPbc7KOHlGbXthp5YTkFu2cCZGWokiqt1y1wsFaAodULhqBIg7vbbbw==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/plugin-base": "7.10.2", - "@electron-forge/shared-types": "7.10.2" + "@electron-forge/plugin-base": "7.11.1", + "@electron-forge/shared-types": "7.11.1" }, "engines": { "node": ">= 16.4.0" @@ -2262,16 +2262,16 @@ } }, "node_modules/@electron-forge/plugin-webpack": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/plugin-webpack/-/plugin-webpack-7.10.2.tgz", - "integrity": "sha512-DUv82kOWsZQ8DQmwZ7SlM2BdDQDwAzTYcplEu2dMuk/kEjEyykzvULS+LPHW5pR8qeq3usJlZdPGwf5/oJ4y4w==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/plugin-webpack/-/plugin-webpack-7.11.1.tgz", + "integrity": "sha512-WeP65H68/81FLiEENjq7XIwCc+ZUTlj1+Xz8Ue/uqKZeBnH3B4bjE6SaMWFgkbl/BgIGs2m61huKfl84z0w6fA==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/core-utils": "7.10.2", - "@electron-forge/plugin-base": "7.10.2", - "@electron-forge/shared-types": "7.10.2", - "@electron-forge/web-multi-logger": "7.10.2", + "@electron-forge/core-utils": "7.11.1", + "@electron-forge/plugin-base": "7.11.1", + "@electron-forge/shared-types": "7.11.1", + "@electron-forge/web-multi-logger": "7.11.1", "chalk": "^4.0.0", "debug": "^4.3.1", "fast-glob": "^3.2.7", @@ -2287,26 +2287,26 @@ } }, "node_modules/@electron-forge/publisher-base": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/publisher-base/-/publisher-base-7.10.2.tgz", - "integrity": "sha512-2k2VOY0wOoAgQoQXn/u3EJ2Ka2v363+wC/+zUMTWGeRHW8pRwX84WX2SpsTttRzbsqAEMJYw5FAzgMBEQUTfpg==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/publisher-base/-/publisher-base-7.11.1.tgz", + "integrity": "sha512-rXE9oMFGMtdQrixnumWYH5TTGsp99iPHZb3jI74YWq518ctCh6DlIgWlhf6ok2X0+lhWovcIb45KJucUFAQ13w==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.10.2" + "@electron-forge/shared-types": "7.11.1" }, "engines": { "node": ">= 16.4.0" } }, "node_modules/@electron-forge/shared-types": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/shared-types/-/shared-types-7.10.2.tgz", - "integrity": "sha512-e2pd9RsdbKwsNf6UtKoolmJGy92Nc0/XO4SI91doV8cM954hM2XSYz3VHoqXebMFAF1JDfXoEUt6UCRbEDgMgw==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/shared-types/-/shared-types-7.11.1.tgz", + "integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/tracer": "7.10.2", + "@electron-forge/tracer": "7.11.1", "@electron/packager": "^18.3.5", "@electron/rebuild": "^3.7.0", "listr2": "^7.0.2" @@ -2316,14 +2316,14 @@ } }, "node_modules/@electron-forge/template-base": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/template-base/-/template-base-7.10.2.tgz", - "integrity": "sha512-D9DbEx3rtikIhUyn4tcz2pJqHNU/+FXKNnzSvmrJoJ9LusR3C42OU9GtbU8oT3nawpnCGgPFIOGXrzexFPp6DA==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/template-base/-/template-base-7.11.1.tgz", + "integrity": "sha512-XpTaEf+EfQw+0BlSAtSpZKYIKYvKu4raNzSGHZZoSYHp+HDC7R+MlpFQmSJiGdYQzQ14C+uxO42tVjgM0DMbpw==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/core-utils": "7.10.2", - "@electron-forge/shared-types": "7.10.2", + "@electron-forge/core-utils": "7.11.1", + "@electron-forge/shared-types": "7.11.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "fs-extra": "^10.0.0", @@ -2335,14 +2335,14 @@ } }, "node_modules/@electron-forge/template-vite": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/template-vite/-/template-vite-7.10.2.tgz", - "integrity": "sha512-hR9HBOM902yq7zhFl8bO3w5ufMgitdd5ZwDzAdKITFh2ttZemHy9ha5S0K+R+4GoXHz8t7hUTHk8+iPy09qrpA==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/template-vite/-/template-vite-7.11.1.tgz", + "integrity": "sha512-Or8Lxf4awoeUZoMTKJEw5KQDIhqOFs24WhVka3yZXxc6VgVWN79KmYKYM6uM/YMQttmafhsBhY2t1Lxo1WR/ug==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.10.2", - "@electron-forge/template-base": "7.10.2", + "@electron-forge/shared-types": "7.11.1", + "@electron-forge/template-base": "7.11.1", "fs-extra": "^10.0.0" }, "engines": { @@ -2350,14 +2350,14 @@ } }, "node_modules/@electron-forge/template-vite-typescript": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.10.2.tgz", - "integrity": "sha512-df7rpxxIOIyZn0RfQ1GIlLW7dXhxkerc9uZ3ozO4C7zfvip3z0Mg+wS1synktPfr4WISaPktIdnj3mVu6Uu7Mw==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.11.1.tgz", + "integrity": "sha512-Us4AHXFb+4z+gXgZImSqMBS63oKnsQWLOhqRg321xiDzu2UcQPlwgWNb4rAEKNVC1e7LXrUNDHuBiTrQkvWXbg==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.10.2", - "@electron-forge/template-base": "7.10.2", + "@electron-forge/shared-types": "7.11.1", + "@electron-forge/template-base": "7.11.1", "fs-extra": "^10.0.0" }, "engines": { @@ -2365,14 +2365,14 @@ } }, "node_modules/@electron-forge/template-webpack": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/template-webpack/-/template-webpack-7.10.2.tgz", - "integrity": "sha512-VIUXA+XHM5SLjg7fIpOOmBsgi0LstkjrEz4gUzVL0AaITM7e+BCziIHld1ceXLbQ1FnKtrUGnQ9X/cHYxYvhHg==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/template-webpack/-/template-webpack-7.11.1.tgz", + "integrity": "sha512-15lbXxi+er461MPk6sbwAOyjofAHwmQjTvxNCiNpaU2naEwbj3t0SlLq/BMr5HxnVOaMmA7+lKV9afkIom+d4Q==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.10.2", - "@electron-forge/template-base": "7.10.2", + "@electron-forge/shared-types": "7.11.1", + "@electron-forge/template-base": "7.11.1", "fs-extra": "^10.0.0" }, "engines": { @@ -2380,14 +2380,14 @@ } }, "node_modules/@electron-forge/template-webpack-typescript": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.10.2.tgz", - "integrity": "sha512-JtrLUAFbxxWJ1kU7b8MNyL5SO9/rY5UeNz1b9hvMvilW8GxyMWUen58dafgdnx3OpKLNZnhOOhgRagNppEzJOA==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.11.1.tgz", + "integrity": "sha512-6ExfFnFkHBz8rvRFTFg5HVGTC12uJpbVk4q8DVg0R8rhhxhqiVNh8lF2UPtZ2yT2UtGWjXNVlyP3Y3T6q6E3GQ==", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.10.2", - "@electron-forge/template-base": "7.10.2", + "@electron-forge/shared-types": "7.11.1", + "@electron-forge/template-base": "7.11.1", "fs-extra": "^10.0.0", "typescript": "~5.4.5", "webpack": "^5.69.1" @@ -2411,9 +2411,9 @@ } }, "node_modules/@electron-forge/tracer": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/tracer/-/tracer-7.10.2.tgz", - "integrity": "sha512-jhLLQbttfZViSOYn/3SJc8HML+jNZAytPVJwgGGd3coUiFysWJ2Xald99iqOiouPAhIigBfNPxQb/q/EbcDu4g==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/tracer/-/tracer-7.11.1.tgz", + "integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==", "dev": true, "license": "MIT", "dependencies": { @@ -2424,9 +2424,9 @@ } }, "node_modules/@electron-forge/web-multi-logger": { - "version": "7.10.2", - "resolved": "https://registry.npmmirror.com/@electron-forge/web-multi-logger/-/web-multi-logger-7.10.2.tgz", - "integrity": "sha512-6ltVtkpGSleEGOPd4C+Z+GUKoMsKjTGD1YwcedZM1L07dlpQAgvRU2e/6lRQuOxVPmylpzbotow/h1mRgDBVWQ==", + "version": "7.11.1", + "resolved": "https://registry.npmmirror.com/@electron-forge/web-multi-logger/-/web-multi-logger-7.11.1.tgz", + "integrity": "sha512-NZQyAxaDPHK/9G24sMKB3RknYxz79C+qO+SkIKoGjN0vT44kxAYc0u4rpem9FYPNN3hpSLFVOgn1EWoaKXnl7A==", "dev": true, "license": "MIT", "dependencies": { @@ -2881,9 +2881,9 @@ "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3105,9 +3105,9 @@ } }, "node_modules/@inquirer/core/node_modules/@types/node": { - "version": "22.19.3", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.3.tgz", - "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "version": "22.19.7", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", "dev": true, "license": "MIT", "dependencies": { @@ -3632,18 +3632,18 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">= 10.0.0" @@ -3653,25 +3653,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], @@ -3690,9 +3690,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -3711,9 +3711,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], @@ -3732,9 +3732,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], @@ -3753,9 +3753,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], @@ -3774,9 +3774,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ "arm" ], @@ -3795,9 +3795,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], @@ -3816,9 +3816,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ "arm64" ], @@ -3837,9 +3837,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], @@ -3858,9 +3858,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], @@ -3879,9 +3879,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -3900,9 +3900,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ "ia32" ], @@ -3921,9 +3921,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -3941,18 +3941,18 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@pdf-lib/standard-fonts": { @@ -4061,9 +4061,9 @@ "license": "BSD-3-Clause" }, "node_modules/@rc-component/async-validator": { - "version": "5.0.4", - "resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.0.4.tgz", - "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==", + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.1.0.tgz", + "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==", "dev": true, "license": "MIT", "dependencies": { @@ -4195,9 +4195,9 @@ } }, "node_modules/@rc-component/trigger": { - "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-2.3.0.tgz", - "integrity": "sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg==", + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-2.3.1.tgz", + "integrity": "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==", "dev": true, "license": "MIT", "dependencies": { @@ -4291,16 +4291,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@silentbot1/nat-api/node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=14.18.0" - } - }, "node_modules/@silentbot1/nat-api/node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-3.0.0.tgz", @@ -4379,19 +4369,6 @@ "dev": true, "license": "ISC" }, - "node_modules/@silentbot1/nat-api/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmmirror.com/@sindresorhus/is/-/is-4.6.0.tgz", @@ -4851,9 +4828,9 @@ } }, "node_modules/@stdlib/string-base-format-interpolate": { - "version": "0.2.2", - "resolved": "https://registry.npmmirror.com/@stdlib/string-base-format-interpolate/-/string-base-format-interpolate-0.2.2.tgz", - "integrity": "sha512-i9nU9rAB2+o/RR66TS9iQ8x+YzeUDL1SGiAo6GY3hP6Umz5Dx9Qp/v8T69gWVsb4a1YSclz5+YeCWaFgwvPjKA==", + "version": "0.2.3", + "resolved": "https://registry.npmmirror.com/@stdlib/string-base-format-interpolate/-/string-base-format-interpolate-0.2.3.tgz", + "integrity": "sha512-6r8SxRqSuzFaU1Qhvz5XORtYSo/BZ0E0/V0RS1qAX6Z3/Dn+g/qlfYEwQXpSwsch1yIURuNfBc77ychnP7d8XA==", "dev": true, "license": "Apache-2.0", "os": [ @@ -4877,9 +4854,9 @@ } }, "node_modules/@stdlib/string-base-format-tokenize": { - "version": "0.2.2", - "resolved": "https://registry.npmmirror.com/@stdlib/string-base-format-tokenize/-/string-base-format-tokenize-0.2.2.tgz", - "integrity": "sha512-kXq2015i+LJjqth5dN+hYnvJXBSzRm8w0ABWB5tYAsIuQTpQK+mSo2muM8JBEFEnqUHAwpUsu2qNTK/9o8lsJg==", + "version": "0.2.3", + "resolved": "https://registry.npmmirror.com/@stdlib/string-base-format-tokenize/-/string-base-format-tokenize-0.2.3.tgz", + "integrity": "sha512-Ha8ADT8sRlRClu3NSKbmKZY9V9I8TK80pU/pwTbr1F8j+KiIZx2Fs7M5SMk8n1rm1hwxC3Af5qU5t2EZ0fJISg==", "dev": true, "license": "Apache-2.0", "os": [ @@ -5507,10 +5484,8 @@ "version": "5.9.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "extraneous": true, "license": "Apache-2.0", - "optional": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5613,10 +5588,8 @@ "version": "5.9.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "extraneous": true, "license": "Apache-2.0", - "optional": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5663,17 +5636,17 @@ } }, "node_modules/@thaunknown/simple-peer": { - "version": "10.0.12", - "resolved": "https://registry.npmmirror.com/@thaunknown/simple-peer/-/simple-peer-10.0.12.tgz", - "integrity": "sha512-sDrkkOdzlJL8+FXQqYcBb2THHQU+Yrar92SjfW4ZLs877/4QA2kFejuA6DVepsoMpoIbXShc7OCXCwYt4AtGdQ==", + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/@thaunknown/simple-peer/-/simple-peer-10.1.0.tgz", + "integrity": "sha512-xNM49v0rBbjIKrS9XNwXW3FFuGvsPGadFRWbBdLAY87pEJeo7V0dxyX6GBHP8UVlefffRedCLsjYXb6i8W9Ofg==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.7", + "debug": "^4.4.3", "err-code": "^3.0.1", - "streamx": "^2.20.1", - "uint8-util": "^2.2.5", - "webrtc-polyfill": "^1.1.10" + "streamx": "^2.23.0", + "uint8-util": "^2.2.6", + "webrtc-polyfill": "^1.2.0" } }, "node_modules/@thaunknown/simple-peer/node_modules/err-code": { @@ -5683,6 +5656,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@thaunknown/simple-peer/node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmmirror.com/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/@thaunknown/simple-websocket": { "version": "9.1.3", "resolved": "https://registry.npmmirror.com/@thaunknown/simple-websocket/-/simple-websocket-9.1.3.tgz", @@ -5908,9 +5893,22 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.7", - "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", - "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "dev": true, "license": "MIT", "dependencies": { @@ -5949,9 +5947,9 @@ "license": "MIT" }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmmirror.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", "dev": true, "license": "MIT" }, @@ -5997,9 +5995,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==", + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", "dev": true, "license": "MIT" }, @@ -6030,9 +6028,9 @@ } }, "node_modules/@types/node": { - "version": "24.10.4", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.10.4.tgz", - "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", + "version": "24.10.9", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -6090,9 +6088,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.7", - "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.7.tgz", - "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "version": "19.2.10", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.10.tgz", + "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", "dev": true, "license": "MIT", "dependencies": { @@ -6133,13 +6131,12 @@ "license": "MIT" }, "node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmmirror.com/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, @@ -6165,6 +6162,17 @@ "@types/send": "<1" } }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@types/simple-peer": { "version": "9.11.9", "resolved": "https://registry.npmmirror.com/@types/simple-peer/-/simple-peer-9.11.9.tgz", @@ -6471,9 +6479,9 @@ } }, "node_modules/@vscode/sudo-prompt": { - "version": "9.3.1", - "resolved": "https://registry.npmmirror.com/@vscode/sudo-prompt/-/sudo-prompt-9.3.1.tgz", - "integrity": "sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA==", + "version": "9.3.2", + "resolved": "https://registry.npmmirror.com/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz", + "integrity": "sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw==", "dev": true, "license": "MIT" }, @@ -6784,7 +6792,6 @@ "version": "0.5.16", "resolved": "https://registry.npmmirror.com/adm-zip/-/adm-zip-0.5.16.tgz", "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0" @@ -7340,14 +7347,14 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.14", - "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "version": "0.4.15", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", + "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.6", "semver": "^6.3.1" }, "peerDependencies": { @@ -7379,13 +7386,13 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.5", - "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "version": "0.6.6", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", + "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5" + "@babel/helper-define-polyfill-provider": "^0.6.6" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -7434,9 +7441,9 @@ } }, "node_modules/bare-fs": { - "version": "4.5.2", - "resolved": "https://registry.npmmirror.com/bare-fs/-/bare-fs-4.5.2.tgz", - "integrity": "sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==", + "version": "4.5.3", + "resolved": "https://registry.npmmirror.com/bare-fs/-/bare-fs-4.5.3.tgz", + "integrity": "sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -7459,9 +7466,9 @@ } }, "node_modules/bare-module-resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmmirror.com/bare-module-resolve/-/bare-module-resolve-1.12.0.tgz", - "integrity": "sha512-JrzrqlC3Tds0iKRwQs8xIIJ+FRieKA9ll0jaqpotDLZtjJPVevzRoeuUYZ5GIo1t1z7/pIRdk85Q3i/2xQLfEQ==", + "version": "1.12.1", + "resolved": "https://registry.npmmirror.com/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz", + "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -7568,9 +7575,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "version": "2.9.19", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -7837,9 +7844,9 @@ } }, "node_modules/block-iterator": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/block-iterator/-/block-iterator-1.1.1.tgz", - "integrity": "sha512-DrjdVWZemVO4iBf4tiOXjUrY5cNesjzy0t7sIiu2rdl8cOCHRxAgKjSJFc3vBZYYMMmshUAxajl8QQh/uxXTKQ==", + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/block-iterator/-/block-iterator-1.1.2.tgz", + "integrity": "sha512-yAHUP44v2K25xLPdrgVTgwtuQctlullzjczu9CoUZom5AP3g4p1R1+aWHjS1GHG9JtcSUVUnbEPiuXiW5YZ24w==", "dev": true, "license": "MIT" }, @@ -8200,7 +8207,6 @@ "version": "4.1.0", "resolved": "https://registry.npmmirror.com/bufferutil/-/bufferutil-4.1.0.tgz", "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -8476,9 +8482,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001761", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", - "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "version": "1.0.30001766", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", "dev": true, "funding": [ { @@ -9159,96 +9165,39 @@ "webpack": "^5.1.0" } }, - "node_modules/copy-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "node_modules/core-js-compat": { + "version": "3.48.0", + "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.48.0.tgz", + "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "browserslist": "^4.28.1" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, - "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true, "license": "MIT" }, - "node_modules/copy-webpack-plugin/node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "dev": true, "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/core-js-compat": { - "version": "3.47.0", - "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.47.0.tgz", - "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" }, "engines": { "node": ">=10" @@ -9965,6 +9914,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/default-gateway/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/default-gateway/node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", @@ -9998,6 +9957,16 @@ "dev": true, "license": "ISC" }, + "node_modules/default-gateway/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.4.tgz", @@ -10127,9 +10096,9 @@ "license": "MIT" }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmmirror.com/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -10883,9 +10852,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.283", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", + "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==", "dev": true, "license": "ISC" }, @@ -10972,9 +10941,9 @@ } }, "node_modules/electron/node_modules/@types/node": { - "version": "22.19.3", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.3.tgz", - "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "version": "22.19.7", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", "dev": true, "license": "MIT", "dependencies": { @@ -11571,14 +11540,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.5.4", - "resolved": "https://registry.npmmirror.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", - "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "version": "5.5.5", + "resolved": "https://registry.npmmirror.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", "dev": true, "license": "MIT", "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.7" + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -11748,9 +11717,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -11814,9 +11783,9 @@ } }, "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true, "license": "MIT" }, @@ -11830,6 +11799,16 @@ "node": ">=0.8.x" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmmirror.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -12037,11 +12016,9 @@ "version": "5.0.10", "resolved": "https://registry.npmmirror.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz", "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "dev": true, + "extraneous": true, "hasInstallScript": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -12335,6 +12312,25 @@ "webpack": "^4.0.0 || ^5.0.0" } }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/filename-reserved-regex": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", @@ -12547,6 +12543,25 @@ } } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", @@ -13438,9 +13453,9 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.6.5", - "resolved": "https://registry.npmmirror.com/html-webpack-plugin/-/html-webpack-plugin-5.6.5.tgz", - "integrity": "sha512-4xynFbKNNk+WlzXeQQ+6YYsH2g7mpfPszQZUi3ovKlj+pDmngQ7vRXjrrmGROabmKwyQkcgcX5hqfOwHbFmK5g==", + "version": "5.6.6", + "resolved": "https://registry.npmmirror.com/html-webpack-plugin/-/html-webpack-plugin-5.6.6.tgz", + "integrity": "sha512-bLjW01UTrvoWTJQL5LsMRo1SypHW80FTm12OJRSnr3v6YHNhfe+1r0MYUZJMACxnCHURVnBWRwAsWs2yPU9Ezw==", "dev": true, "license": "MIT", "dependencies": { @@ -13640,13 +13655,13 @@ } }, "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=10.17.0" + "node": ">=14.18.0" } }, "node_modules/humanize-ms": { @@ -14491,9 +14506,9 @@ } }, "node_modules/isomorphic-git": { - "version": "1.36.1", - "resolved": "https://registry.npmmirror.com/isomorphic-git/-/isomorphic-git-1.36.1.tgz", - "integrity": "sha512-fC8SRT8MwoaXDK8G4z5biPEbqf2WyEJUb2MJ2ftSd39/UIlsnoZxLGux+lae0poLZO4AEcx6aUVOh5bV+P8zFA==", + "version": "1.36.3", + "resolved": "https://registry.npmmirror.com/isomorphic-git/-/isomorphic-git-1.36.3.tgz", + "integrity": "sha512-bHF1nQTjL0IfSo13BHDO8oQ6SvYNQduTAdPJdSmrJ5JwZY2fsyjLujEXav5hqPCegSCAnc75ZsBUHqT/NqR7QA==", "dev": true, "license": "MIT", "dependencies": { @@ -15267,9 +15282,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, @@ -16045,9 +16060,9 @@ } }, "node_modules/nan": { - "version": "2.24.0", - "resolved": "https://registry.npmmirror.com/nan/-/nan-2.24.0.tgz", - "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", + "version": "2.25.0", + "resolved": "https://registry.npmmirror.com/nan/-/nan-2.25.0.tgz", + "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", "dev": true, "license": "MIT", "optional": true @@ -16146,9 +16161,9 @@ } }, "node_modules/node-abi": { - "version": "3.85.0", - "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.85.0.tgz", - "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "version": "3.87.0", + "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16184,39 +16199,17 @@ } }, "node_modules/node-datachannel": { - "version": "0.12.0", - "resolved": "https://registry.npmmirror.com/node-datachannel/-/node-datachannel-0.12.0.tgz", - "integrity": "sha512-pZ9FsVZpHdUKqyWynuCc9IBLkZPJMpDzpNk4YNPCizbIXHYifpYeWqSF35REHGIWi9JMCf11QzapsyQGo/Y4Ig==", + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/node-datachannel/-/node-datachannel-0.32.0.tgz", + "integrity": "sha512-kohrPL3rOWfiD+oJgc9R7Ibx10U3/yJ+oQ/uB2W2d2B+y4B1bkAMPoONRVS3PTz7AHFqdvoEe5VoRDZ+I6xFEw==", "dev": true, "hasInstallScript": true, "license": "MPL 2.0", "dependencies": { - "node-domexception": "^2.0.1", - "prebuild-install": "^7.0.1" + "prebuild-install": "^7.1.3" }, "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/node-datachannel/node_modules/node-domexception": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-2.0.2.tgz", - "integrity": "sha512-Qf9vHK9c5MGgUXj8SnucCIS4oEPuUstjRaMplLGeZpbWMfNV1rvEcXuwoXfN51dUfD1b4muPHPQtCx/5Dj/QAA==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=16" + "node": ">=18.20.0" } }, "node_modules/node-domexception": { @@ -16275,7 +16268,6 @@ "version": "4.8.4", "resolved": "https://registry.npmmirror.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz", "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "dev": true, "license": "MIT", "optional": true, "bin": { @@ -16380,13 +16372,6 @@ "node": ">=10" } }, - "node_modules/node-stdlib-browser/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmmirror.com/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT" - }, "node_modules/nopt": { "version": "6.0.0", "resolved": "https://registry.npmmirror.com/nopt/-/nopt-6.0.0.tgz", @@ -17554,9 +17539,9 @@ } }, "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", "dev": true, "license": "MIT", "dependencies": { @@ -17730,19 +17715,16 @@ } }, "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } + "license": "MIT" }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -18589,24 +18571,24 @@ } }, "node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmmirror.com/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "version": "19.2.4", + "resolved": "https://registry.npmmirror.com/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "version": "19.2.4", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.3" + "react": "^19.2.4" } }, "node_modules/react-is": { @@ -18617,9 +18599,9 @@ "license": "MIT" }, "node_modules/react-router": { - "version": "7.11.0", - "resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.11.0.tgz", - "integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==", + "version": "7.13.0", + "resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", "dev": true, "license": "MIT", "dependencies": { @@ -18640,13 +18622,13 @@ } }, "node_modules/react-router-dom": { - "version": "7.11.0", - "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.11.0.tgz", - "integrity": "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==", + "version": "7.13.0", + "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", "dev": true, "license": "MIT", "dependencies": { - "react-router": "7.11.0" + "react-router": "7.13.0" }, "engines": { "node": ">=20.0.0" @@ -19409,9 +19391,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.97.1", - "resolved": "https://registry.npmmirror.com/sass/-/sass-1.97.1.tgz", - "integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==", + "version": "1.97.3", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", "dev": true, "license": "MIT", "dependencies": { @@ -19501,11 +19483,14 @@ } }, "node_modules/sax": { - "version": "1.4.3", - "resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.3.tgz", - "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", + "version": "1.4.4", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", "dev": true, - "license": "BlueOak-1.0.0" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } }, "node_modules/scheduler": { "version": "0.27.0", @@ -19514,15 +19499,16 @@ "license": "MIT" }, "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "version": "4.3.3", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 10.13.0" @@ -19532,6 +19518,43 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/scroll-into-view-if-needed": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", @@ -19667,22 +19690,26 @@ } }, "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmmirror.com/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", "dev": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.4", + "accepts": "~1.3.8", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-index/node_modules/debug": { @@ -19706,28 +19733,22 @@ } }, "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, "license": "MIT", "dependencies": { "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.6" } }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true, - "license": "ISC" - }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", @@ -19735,13 +19756,6 @@ "dev": true, "license": "MIT" }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true, - "license": "ISC" - }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz", @@ -20615,13 +20629,16 @@ } }, "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strip-json-comments": { @@ -20849,9 +20866,9 @@ } }, "node_modules/synckit": { - "version": "0.11.11", - "resolved": "https://registry.npmmirror.com/synckit/-/synckit-0.11.11.tgz", - "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "version": "0.11.12", + "resolved": "https://registry.npmmirror.com/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -20995,9 +21012,9 @@ } }, "node_modules/terser": { - "version": "5.44.1", - "resolved": "https://registry.npmmirror.com/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "version": "5.46.0", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -21048,63 +21065,6 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", @@ -21702,9 +21662,9 @@ } }, "node_modules/uint8-util": { - "version": "2.2.5", - "resolved": "https://registry.npmmirror.com/uint8-util/-/uint8-util-2.2.5.tgz", - "integrity": "sha512-/QxVQD7CttWpVUKVPz9znO+3Dd4BdTSnFQ7pv/4drVhC9m4BaL2LFHTkJn6EsYoxT79VDq/2Gg8L0H22PrzyMw==", + "version": "2.2.6", + "resolved": "https://registry.npmmirror.com/uint8-util/-/uint8-util-2.2.6.tgz", + "integrity": "sha512-r+ZjS8CzPhtPF771ROOadUoqC40OVdiMKBI8lTfJQWb4W7+73sMBwMYmai/uvNcmZ7tBJJyZSad03yMWIt3RQg==", "dev": true, "license": "MIT", "dependencies": { @@ -21895,6 +21855,16 @@ "punycode": "^2.1.0" } }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/url": { "version": "0.11.4", "resolved": "https://registry.npmmirror.com/url/-/url-0.11.4.tgz", @@ -21909,13 +21879,6 @@ "node": ">= 0.4" } }, - "node_modules/url/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmmirror.com/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT" - }, "node_modules/username": { "version": "5.1.0", "resolved": "https://registry.npmmirror.com/username/-/username-5.1.0.tgz", @@ -21993,7 +21956,6 @@ "version": "6.0.6", "resolved": "https://registry.npmmirror.com/utf-8-validate/-/utf-8-validate-6.0.6.tgz", "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -22113,9 +22075,9 @@ "license": "MIT" }, "node_modules/watchpack": { - "version": "2.5.0", - "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.5.0.tgz", - "integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==", + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "license": "MIT", "dependencies": { @@ -22236,63 +22198,6 @@ "webpack": "^4.0.0 || ^5.0.0" } }, - "node_modules/webpack-dev-middleware/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack-dev-middleware/node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/webpack-dev-server": { "version": "4.15.2", "resolved": "https://registry.npmmirror.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", @@ -22353,36 +22258,6 @@ } } }, - "node_modules/webpack-dev-server/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack-dev-server/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.3.0.tgz", @@ -22393,33 +22268,6 @@ "node": ">= 10" } }, - "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack-dev-server/node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/webpack-merge": { "version": "5.10.0", "resolved": "https://registry.npmmirror.com/webpack-merge/-/webpack-merge-5.10.0.tgz", @@ -22445,72 +22293,14 @@ "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/webrtc-polyfill": { - "version": "1.1.10", - "resolved": "https://registry.npmmirror.com/webrtc-polyfill/-/webrtc-polyfill-1.1.10.tgz", - "integrity": "sha512-sOn0bj3/noUdzQX7rvk0jFbBurqWDGGo2ipl+WfgoOe/x3cxbGLk/ZUY+WHCISSlLaIeBumi1X3wxQZnUESExQ==", + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/webrtc-polyfill/-/webrtc-polyfill-1.2.0.tgz", + "integrity": "sha512-epaVJbKzWOY5Wf3k7DoZLNgHP/5IoALBvjvlZQgX+9vFnf9UfCHv+rc+r/vJ7jxQUwH3cIYx9blHfyWWxGbw1g==", "dev": true, "license": "MIT", "dependencies": { - "node-datachannel": "^v0.12.0", - "node-domexception": "^1.0.0" + "node-datachannel": "^0.32.0" }, "engines": { "node": ">=16.0.0" @@ -22731,9 +22521,9 @@ "optional": true }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.20", + "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -22840,9 +22630,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "devOptional": true, "license": "MIT", "engines": { From 7e5569f201d4715982bac8b41230968fc56a835f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=9A=E5=8B=83=E9=B9=8F?= <2032806273@qq.com> Date: Sat, 31 Jan 2026 17:51:44 +0800 Subject: [PATCH 19/22] refactor(launcher-update): use Squirrel for Windows updates instead of manual ZIP extraction --- external-resources/dlc/dlc-index.json | 6 +- src/main/launcher-update/index.ts | 577 ++++++++++++-------------- 2 files changed, 259 insertions(+), 324 deletions(-) diff --git a/external-resources/dlc/dlc-index.json b/external-resources/dlc/dlc-index.json index 1c4d1d0..463a477 100644 --- a/external-resources/dlc/dlc-index.json +++ b/external-resources/dlc/dlc-index.json @@ -32,9 +32,9 @@ "id": "AI_LEARNING_ASSISTANT_LAUNCHER", "name": "AI学习助手启动器", "versions": { - "2.0.3": { - "magnet": "magnet:?xt=urn:btih:5e685a7cbe4045efc40f7a593dddfdf56ae7c59d&dn=AI-Learning-Assistant-Launcher-win32-x64-2.0.3.zip&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.btorrent.xyz", - "comment": "第一个版本", + "2.0.4": { + "magnet": "magnet:?xt=urn:btih:ea0ebea633f25e936c55a095f66f15d65089dcec&dn=AILearningAssistantLauncher-2.0.4-full.nupkg&tr=wss%3A%2F%2Ftracker.openwebtorrent.com", + "comment": "测试更新使用版本", "require": {} } } diff --git a/src/main/launcher-update/index.ts b/src/main/launcher-update/index.ts index 63b70ea..b6fb248 100644 --- a/src/main/launcher-update/index.ts +++ b/src/main/launcher-update/index.ts @@ -14,27 +14,23 @@ import { import semver from 'semver'; import path from 'path'; import { appPath } from '../exec'; +import { existsSync } from 'fs'; import { - existsSync, - mkdirSync, - readdirSync, - statSync, - rmSync, - writeFileSync, - appendFileSync, -} from 'fs'; + readdir, + writeFile, + appendFile, + readFile, + stat, + access, + rename, + constants, +} from 'fs/promises'; import { spawn } from 'child_process'; -import AdmZip from 'adm-zip'; +import { createHash } from 'crypto'; import packageJson from '../../../package.json'; const currentVersion = packageJson.version; -// 在 Electron 中使用 original-fs 处理 asar 打包后的文件操作 -// original-fs 是 Electron 内置模块,API 与 Node.js fs 模块相同 -// 使用类型注解确保类型安全 -// eslint-disable-next-line @typescript-eslint/no-var-requires -const originalFs: typeof import('fs') = require('original-fs'); - // 更新日志文件路径 const updateLogPath = path.join(appPath, 'launcher-update.log'); @@ -44,7 +40,7 @@ const updateLogPath = path.join(appPath, 'launcher-update.log'); * @param message 日志消息 * @param data 附加数据 */ -function writeUpdateLog( +async function writeUpdateLog( level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR', message: string, data?: unknown, @@ -61,7 +57,7 @@ function writeUpdateLog( logLine += '\n'; try { - appendFileSync(updateLogPath, logLine, { encoding: 'utf8' }); + await appendFile(updateLogPath, logLine, { encoding: 'utf8' }); } catch (err) { console.error('写入更新日志失败:', err); } @@ -86,14 +82,14 @@ function writeUpdateLog( /** * 初始化日志文件(清空旧日志或添加分隔符) */ -function initUpdateLog() { +async function initUpdateLog() { const separator = ` ${'='.repeat(60)} [${new Date().toISOString()}] 启动器更新日志开始 ${'='.repeat(60)} `; try { - appendFileSync(updateLogPath, separator, { encoding: 'utf8' }); + await appendFile(updateLogPath, separator, { encoding: 'utf8' }); } catch { // 忽略错误 } @@ -112,8 +108,8 @@ export default async function init(ipcMain: IpcMain) { } export async function checkLauncherUpdate() { - initUpdateLog(); - writeUpdateLog('INFO', '[checkLauncherUpdate] 开始检查启动器更新'); + await initUpdateLog(); + await writeUpdateLog('INFO', '[checkLauncherUpdate] 开始检查启动器更新'); try { const latestVersionInfo = getLatestVersion( @@ -122,7 +118,7 @@ export async function checkLauncherUpdate() { const latestVersion = latestVersionInfo.version; const haveNew = semver.lt(currentVersion, latestVersion); - writeUpdateLog('INFO', '[checkLauncherUpdate] 检查启动器更新完成', { + await writeUpdateLog('INFO', '[checkLauncherUpdate] 检查启动器更新完成', { currentVersion, latestVersion, haveNew, @@ -134,7 +130,7 @@ export async function checkLauncherUpdate() { haveNew, }; } catch (error) { - writeUpdateLog( + await writeUpdateLog( 'ERROR', '[checkLauncherUpdate] 检查启动器更新失败', error instanceof Error @@ -150,20 +146,24 @@ export async function checkLauncherUpdate() { } export async function downloadLauncherUpdate() { - writeUpdateLog('INFO', '[downloadLauncherUpdate] 开始下载启动器更新'); + await writeUpdateLog('INFO', '[downloadLauncherUpdate] 开始下载启动器更新'); try { const latestVersionInfo = getLatestVersion( 'AI_LEARNING_ASSISTANT_LAUNCHER', ); - writeUpdateLog('DEBUG', '[downloadLauncherUpdate] 获取到最新版本信息', { - version: latestVersionInfo.version, - magnet: latestVersionInfo.dlcInfo?.magnet?.substring(0, 50) + '...', - }); + await writeUpdateLog( + 'DEBUG', + '[downloadLauncherUpdate] 获取到最新版本信息', + { + version: latestVersionInfo.version, + magnet: latestVersionInfo.dlcInfo?.magnet?.substring(0, 50) + '...', + }, + ); await startWebtorrent(latestVersionInfo.dlcInfo.magnet); - writeUpdateLog('DEBUG', '[downloadLauncherUpdate] WebTorrent 已启动'); + await writeUpdateLog('DEBUG', '[downloadLauncherUpdate] WebTorrent 已启动'); const torrent = await waitTorrentDone( 'AI_LEARNING_ASSISTANT_LAUNCHER', @@ -171,15 +171,19 @@ export async function downloadLauncherUpdate() { ); const filePath = path.join(torrent.path, torrent.files[0].name); - writeUpdateLog('INFO', '[downloadLauncherUpdate] 启动器更新下载完成', { - path: torrent.path, - fileName: torrent.files[0].name, - }); + await writeUpdateLog( + 'INFO', + '[downloadLauncherUpdate] 启动器更新下载完成', + { + path: torrent.path, + fileName: torrent.files[0].name, + }, + ); // 检查是否为本地开发环境 const isDev = !app.isPackaged; if (isDev) { - writeUpdateLog( + await writeUpdateLog( 'WARN', '[downloadLauncherUpdate] 当前为本地开发环境,启动器更新功能可能无法正常工作', ); @@ -192,7 +196,7 @@ export async function downloadLauncherUpdate() { isDev, }; } catch (error) { - writeUpdateLog( + await writeUpdateLog( 'ERROR', '[downloadLauncherUpdate] 下载启动器更新失败', error instanceof Error @@ -203,26 +207,177 @@ export async function downloadLauncherUpdate() { } } +/** + * 获取 Squirrel Update.exe 的路径 + * Squirrel 安装后,Update.exe 位于应用安装目录的上一级 + */ +function getSquirrelUpdateExePath(): string | null { + const exePath = app.getPath('exe'); + const exeDir = path.dirname(exePath); + // Squirrel 安装结构: /Update.exe, /app-x.x.x/app.exe + const updateExePath = path.join(exeDir, '..', 'Update.exe'); + + if (existsSync(updateExePath)) { + return path.resolve(updateExePath); + } + return null; +} + +/** + * 计算文件的 SHA1 哈希值 + */ +async function calculateSha1(filePath: string): Promise { + const fileBuffer = await readFile(filePath); + const hash = createHash('sha1'); + hash.update(fileBuffer); + return hash.digest('hex').toUpperCase(); +} + +/** + * 生成 Squirrel RELEASES 文件 + * RELEASES 文件格式: SHA1 filename size + */ +async function generateReleasesFile( + nupkgPath: string, + outputDir: string, +): Promise { + const fileName = path.basename(nupkgPath); + const fileStat = await stat(nupkgPath); + const fileSize = fileStat.size; + const sha1Hash = await calculateSha1(nupkgPath); + + const releasesContent = `${sha1Hash} ${fileName} ${fileSize}`; + const releasesPath = path.join(outputDir, 'RELEASES'); + + await writeFile(releasesPath, releasesContent, { encoding: 'utf8' }); + await writeUpdateLog('INFO', '[generateReleasesFile] RELEASES 文件已生成', { + path: releasesPath, + content: releasesContent, + }); + + return releasesPath; +} + +/** + * 等待文件句柄释放的重试配置 + */ +const FILE_RELEASE_CONFIG = { + MAX_RETRIES: 10, + INITIAL_DELAY_MS: 200, + MAX_DELAY_MS: 2000, +}; + +/** + * 检测文件是否可访问(句柄已释放) + * 通过尝试 rename 操作来验证文件是否被占用 + * @param filePath 文件路径 + * @returns 是否可访问 + */ +async function isFileAccessible(filePath: string): Promise { + try { + // 检查文件是否可读 + await access(filePath, constants.R_OK); + + // 尝试 rename 到自身(Windows 下被占用的文件无法 rename) + // 这是检测文件句柄是否释放的可靠方法 + const tempPath = filePath + '.tmp_check'; + await rename(filePath, tempPath); + await rename(tempPath, filePath); + + return true; + } catch { + return false; + } +} + +/** + * 等待文件句柄释放,使用指数退避重试机制 + * @param filePath 文件路径 + * @returns 文件是否可访问 + */ +async function waitForFileRelease(filePath: string): Promise { + let delay = FILE_RELEASE_CONFIG.INITIAL_DELAY_MS; + + for (let attempt = 1; attempt <= FILE_RELEASE_CONFIG.MAX_RETRIES; attempt++) { + const accessible = await isFileAccessible(filePath); + + if (accessible) { + await writeUpdateLog('DEBUG', '[waitForFileRelease] 文件句柄已释放', { + filePath, + attempt, + }); + return; + } + + await writeUpdateLog( + 'DEBUG', + '[waitForFileRelease] 文件仍被占用,等待重试', + { + filePath, + attempt, + nextDelayMs: delay, + }, + ); + + await new Promise((resolve) => setTimeout(resolve, delay)); + + // 指数退避,但不超过最大延迟 + delay = Math.min(delay * 2, FILE_RELEASE_CONFIG.MAX_DELAY_MS); + } + + throw new Error( + `文件句柄释放超时: ${filePath},已重试 ${FILE_RELEASE_CONFIG.MAX_RETRIES} 次`, + ); +} + export async function installLauncherUpdate() { - writeUpdateLog('INFO', '[installLauncherUpdate] 开始执行安装更新'); + await writeUpdateLog( + 'INFO', + '[installLauncherUpdate] 开始执行 Squirrel 更新', + ); // 如果是开发模式,不执行更新 const isPackaged = app.isPackaged; if (!isPackaged) { - writeUpdateLog('WARN', '[installLauncherUpdate] 开发模式下不支持自动更新'); + await writeUpdateLog( + 'WARN', + '[installLauncherUpdate] 开发模式下不支持自动更新', + ); + return { + success: false, + message: '开发模式下不支持自动更新', + }; + } + + // 检查是否为 Squirrel 安装 + const updateExePath = getSquirrelUpdateExePath(); + if (!updateExePath) { + await writeUpdateLog( + 'WARN', + '[installLauncherUpdate] 未检测到 Squirrel 安装,无法使用 Squirrel 更新', + ); return { success: false, - message: '开发模式下不支持自动更新,请手动解压', + message: '当前安装方式不支持 Squirrel 自动更新,请手动下载安装包更新', }; } + await writeUpdateLog( + 'DEBUG', + '[installLauncherUpdate] 检测到 Update.exe:', + updateExePath, + ); + try { - writeUpdateLog('DEBUG', '[installLauncherUpdate] 获取最新版本信息...'); + await writeUpdateLog( + 'DEBUG', + '[installLauncherUpdate] 获取最新版本信息...', + ); const latestVersionInfo = getLatestVersion( 'AI_LEARNING_ASSISTANT_LAUNCHER', ); const version = latestVersionInfo.version; - writeUpdateLog('DEBUG', '[installLauncherUpdate] 最新版本:', version); + await writeUpdateLog('DEBUG', '[installLauncherUpdate] 最新版本:', version); const downloadPath = path.join( appPath, @@ -231,193 +386,65 @@ export async function installLauncherUpdate() { 'AI_LEARNING_ASSISTANT_LAUNCHER', version, ); - writeUpdateLog('DEBUG', '[installLauncherUpdate] 下载路径:', downloadPath); - writeUpdateLog( + await writeUpdateLog( + 'DEBUG', + '[installLauncherUpdate] 下载路径:', + downloadPath, + ); + await writeUpdateLog( 'DEBUG', '[installLauncherUpdate] 下载路径是否存在:', existsSync(downloadPath), ); - const files = readdirSync(downloadPath); - writeUpdateLog('DEBUG', '[installLauncherUpdate] 下载目录文件列表:', files); - const zipFile = files.find((f) => f.endsWith('.zip')); - - if (!zipFile) { - writeUpdateLog('ERROR', '[installLauncherUpdate] 未找到下载的更新包'); - throw new Error('未找到下载的更新包'); + if (!existsSync(downloadPath)) { + await writeUpdateLog('ERROR', '[installLauncherUpdate] 下载目录不存在'); + throw new Error('更新包未下载,请先下载更新包'); } - // 销毁 WebTorrent 以释放文件句柄,避免后续读取 zip 文件时出现 EBUSY 错误 - writeUpdateLog( - 'DEBUG', - '[installLauncherUpdate] 销毁 WebTorrent 释放文件句柄...', - ); - await destroyWebtorrentForInstall(latestVersionInfo.dlcInfo.magnet); - // 等待文件句柄完全释放 - await new Promise((resolve) => setTimeout(resolve, 1000)); - writeUpdateLog('DEBUG', '[installLauncherUpdate] 文件句柄已释放'); - - const zipPath = path.join(downloadPath, zipFile); - writeUpdateLog('DEBUG', '[installLauncherUpdate] zip文件路径:', zipPath); - writeUpdateLog( + const files = await readdir(downloadPath); + await writeUpdateLog( 'DEBUG', - '[installLauncherUpdate] zip文件是否存在:', - existsSync(zipPath), + '[installLauncherUpdate] 下载目录文件列表:', + files, ); - const tempDir = path.join(appPath, 'update-temp'); - writeUpdateLog('DEBUG', '[installLauncherUpdate] 临时目录:', tempDir); - if (existsSync(tempDir)) { - writeUpdateLog( - 'DEBUG', - '[installLauncherUpdate] 清理已存在的临时目录...', - ); - rmSync(tempDir, { recursive: true, force: true }); - writeUpdateLog('DEBUG', '[installLauncherUpdate] 临时目录清理完成'); - } - mkdirSync(tempDir, { recursive: true }); - writeUpdateLog('DEBUG', '[installLauncherUpdate] 临时目录已创建'); - - // 使用 adm-zip 解压 zip 文件 - writeUpdateLog('INFO', '[installLauncherUpdate] 正在解压文件...'); - writeUpdateLog('DEBUG', '[installLauncherUpdate] ZIP路径:', zipPath); - writeUpdateLog('DEBUG', '[installLauncherUpdate] 目标路径:', tempDir); - - try { - const zip = new AdmZip(zipPath); - const zipEntries = zip.getEntries(); - writeUpdateLog( - 'DEBUG', - '[installLauncherUpdate] ZIP包含的文件数量:', - zipEntries.length, - ); - writeUpdateLog( - 'DEBUG', - '[installLauncherUpdate] ZIP包含的文件列表:', - zipEntries.slice(0, 5).map((e) => e.entryName), - ); - - // 手动解压每个文件,使用 original-fs 避免 asar 打包问题和 Windows 下的 chmod 问题 - for (const entry of zipEntries) { - const entryPath = path.join(tempDir, entry.entryName); - if (entry.isDirectory) { - // 创建目录 - if (!originalFs.existsSync(entryPath)) { - originalFs.mkdirSync(entryPath, { recursive: true }); - } - } else { - // 确保父目录存在 - const parentDir = path.dirname(entryPath); - if (!originalFs.existsSync(parentDir)) { - originalFs.mkdirSync(parentDir, { recursive: true }); - } - // 写入文件 - const content = entry.getData(); - originalFs.writeFileSync(entryPath, content); - } - } - writeUpdateLog('INFO', '[installLauncherUpdate] 解压完成'); - } catch (extractError) { - writeUpdateLog( + // 查找 .nupkg 文件 + const nupkgFile = files.find((f) => f.endsWith('.nupkg')); + if (!nupkgFile) { + await writeUpdateLog( 'ERROR', - '[installLauncherUpdate] 解压失败', - extractError instanceof Error - ? { message: extractError.message, stack: extractError.stack } - : extractError, + '[installLauncherUpdate] 未找到 .nupkg 更新包', ); - throw extractError; + throw new Error('未找到 .nupkg 更新包'); } - // 查找解压后的目录(通常会有一个子目录) - const extractedItems = readdirSync(tempDir); - writeUpdateLog( + const nupkgPath = path.join(downloadPath, nupkgFile); + await writeUpdateLog( 'DEBUG', - '[installLauncherUpdate] 解压后的文件数量:', - extractedItems.length, + '[installLauncherUpdate] nupkg 文件路径:', + nupkgPath, ); - writeUpdateLog( - 'DEBUG', - '[installLauncherUpdate] 解压后的文件列表:', - extractedItems, - ); - - let sourceDir = tempDir; - if (extractedItems.length === 1) { - const subPath = path.join(tempDir, extractedItems[0]); - const subStat = statSync(subPath); - writeUpdateLog('DEBUG', '[installLauncherUpdate] 检查子路径:', subPath); - writeUpdateLog( - 'DEBUG', - '[installLauncherUpdate] 是否为目录:', - subStat.isDirectory(), - ); - if (subStat.isDirectory()) { - sourceDir = subPath; - const subItems = readdirSync(sourceDir); - writeUpdateLog( - 'DEBUG', - '[installLauncherUpdate] 子目录文件列表:', - subItems, - ); - } - } - - writeUpdateLog('DEBUG', '[installLauncherUpdate] 解压源目录:', sourceDir); - - const findExe = (dir: string): string | null => { - const items = readdirSync(dir); - for (const item of items) { - const fullPath = path.join(dir, item); - const stat = statSync(fullPath); - if (stat.isDirectory()) { - const result = findExe(fullPath); - if (result) return result; - } else if ( - item.endsWith('.exe') && - item.includes('AI-Learning-Assistant-Launcher') - ) { - return fullPath; - } - } - return null; - }; - - const newExePath = findExe(sourceDir); - - if (!newExePath) { - writeUpdateLog( - 'ERROR', - '[installLauncherUpdate] 未在更新包中找到启动器可执行文件', - ); - throw new Error('未在更新包中找到启动器可执行文件'); - } - writeUpdateLog( + // 销毁 WebTorrent 以释放文件句柄 + await writeUpdateLog( 'DEBUG', - '[installLauncherUpdate] 找到新版本可执行文件:', - newExePath, + '[installLauncherUpdate] 销毁 WebTorrent 释放文件句柄...', ); + await destroyWebtorrentForInstall(latestVersionInfo.dlcInfo.magnet); - const currentExePath = app.getPath('exe'); - const currentExeDir = path.dirname(currentExePath); - const currentExeName = path.basename(currentExePath); - - const backupPath = path.join(currentExeDir, `${currentExeName}.old`); + // 使用重试机制等待文件句柄释放 + await waitForFileRelease(nupkgPath); + await writeUpdateLog('DEBUG', '[installLauncherUpdate] 文件句柄已释放'); - // 找到新版本可执行文件所在目录的所有文件 - const newExeDir = path.dirname(newExePath); - writeUpdateLog( - 'DEBUG', - '[installLauncherUpdate] 新版本文件目录:', - newExeDir, + // 生成 RELEASES 文件(Squirrel 需要此文件来确定更新信息) + await writeUpdateLog( + 'INFO', + '[installLauncherUpdate] 生成 RELEASES 文件...', ); - writeUpdateLog('INFO', '[installLauncherUpdate] 更新路径信息', { - currentExePath, - currentExeDir, - backupPath, - newExeDir, - }); + await generateReleasesFile(nupkgPath, downloadPath); + // 显示更新确认对话框 const result = await dialog.showMessageBox({ type: 'info', title: '准备更新', @@ -429,147 +456,55 @@ export async function installLauncherUpdate() { }); if (result.response === 0) { - writeUpdateLog( + await writeUpdateLog( 'INFO', - '[installLauncherUpdate] 用户确认更新,准备执行更新', + '[installLauncherUpdate] 用户确认更新,准备执行 Squirrel 更新', ); - // 创建更新脚本,在应用退出后执行 - const updateScriptPath = path.join(appPath, 'update.bat'); - // bat 脚本也写入日志文件,方便追踪更新过程(Windows BAT 脚本路径不需要转义) - const batLogPath = updateLogPath; - const updateScript = `@echo off -chcp 65001 -echo =================================== -echo 正在更新启动器... -echo =================================== - -REM 写入日志函数 -set "LOG_FILE=${batLogPath}" -echo [%date% %time%] [BAT] 更新脚本开始执行 >> "%LOG_FILE%" - -REM 等待应用完全退出并释放文件句柄 -echo 等待应用退出... -echo [%date% %time%] [BAT] 等待应用退出... >> "%LOG_FILE%" -timeout /t 3 /nobreak >nul - -REM 删除旧备份 -if exist "${backupPath}" ( - echo 删除旧备份... - echo [%date% %time%] [BAT] 删除旧备份: ${backupPath} >> "%LOG_FILE%" - del /f /q "${backupPath}" -) - -REM 备份当前版本的可执行文件 -if exist "${currentExePath}" ( - echo 备份当前版本... - echo [%date% %time%] [BAT] 备份当前版本: ${currentExePath} -^> ${backupPath} >> "%LOG_FILE%" - move /y "${currentExePath}" "${backupPath}" - if errorlevel 1 ( - echo 备份失败! - echo [%date% %time%] [BAT] [ERROR] 备份失败! >> "%LOG_FILE%" - pause - exit /b 1 - ) - echo [%date% %time%] [BAT] 备份成功 >> "%LOG_FILE%" -) - -REM 使用 robocopy 复制新版本的所有文件到当前目录 -echo 正在复制文件... -echo [%date% %time%] [BAT] 开始复制文件: ${newExeDir} -^> ${currentExeDir} >> "%LOG_FILE%" -robocopy "${newExeDir}" "${currentExeDir}" /E /IS /IT /NFL /NDL /NP - -REM robocopy 返回值说明: -REM 0 = 没有文件被复制 -REM 1 = 所有文件复制成功 -REM 2 = 有额外的文件或目录 -REM 3 = 有文件被复制,也有不匹配的文件 -REM 4 = 有不匹配的文件或目录 -REM 5 = 有文件被复制,也有不匹配的文件或目录 -REM 6 = 有额外的文件/目录和不匹配的文件 -REM 7 = 文件被复制,有额外的文件和不匹配的文件 -REM 8+ = 有错误 - -if %ERRORLEVEL% GEQ 8 ( - echo =================================== - echo 复制文件失败!错误码: %ERRORLEVEL% - echo 正在恢复原版本... - echo =================================== - echo [%date% %time%] [BAT] [ERROR] 复制文件失败!错误码: %ERRORLEVEL% >> "%LOG_FILE%" - if exist "${backupPath}" ( - move /y "${backupPath}" "${currentExePath}" - echo [%date% %time%] [BAT] 已恢复原版本 >> "%LOG_FILE%" - ) - echo 更新失败,已恢复原版本 - pause - exit /b 1 -) - -echo 文件复制完成,错误码: %ERRORLEVEL% -echo [%date% %time%] [BAT] 文件复制完成,错误码: %ERRORLEVEL% >> "%LOG_FILE%" - -REM 清理临时文件 -echo 清理临时文件... -echo [%date% %time%] [BAT] 清理临时文件: ${tempDir} >> "%LOG_FILE%" -if exist "${tempDir}" ( - rmdir /s /q "${tempDir}" 2>nul -) -echo [%date% %time%] [BAT] 临时文件清理完成 >> "%LOG_FILE%" - -REM 启动新版本 -echo =================================== -echo 启动新版本... -echo =================================== -echo [%date% %time%] [BAT] 启动新版本: ${currentExePath} >> "%LOG_FILE%" -start "" "${currentExePath}" - -REM 等待一下确保应用启动 -timeout /t 2 /nobreak >nul -echo [%date% %time%] [BAT] 更新脚本执行完成 >> "%LOG_FILE%" - -REM 删除更新脚本自身 -(goto) 2>nul & del "%~f0" -`; - - writeFileSync(updateScriptPath, updateScript, { encoding: 'utf8' }); - writeUpdateLog( + // 使用 Squirrel 的 Update.exe 执行更新 + // 参数 --update= 指定包含 RELEASES 和 .nupkg 的目录 + await writeUpdateLog( 'INFO', - '[installLauncherUpdate] 更新脚本已创建:', - updateScriptPath, + '[installLauncherUpdate] 调用 Squirrel Update.exe', + { + updateExePath, + updatePath: downloadPath, + }, ); - // 使用 detached 模式启动更新脚本 - const updateProcess = spawn('cmd.exe', ['/c', updateScriptPath], { + const updateProcess = spawn(updateExePath, ['--update', downloadPath], { detached: true, stdio: 'ignore', - windowsHide: false, }); updateProcess.unref(); - writeUpdateLog('INFO', '[installLauncherUpdate] 更新脚本已启动'); + await writeUpdateLog( + 'INFO', + '[installLauncherUpdate] Squirrel 更新进程已启动', + ); - // 延迟退出,确保更新脚本已经启动 - // 注意:这里不使用 app.relaunch(),而是让更新脚本完成后手动启动新版本 - setTimeout(() => { - writeUpdateLog('INFO', '[installLauncherUpdate] 准备退出应用'); - app.exit(0); - }, 1000); + // 延迟退出应用,让 Squirrel 接管更新流程 + setTimeout(async () => { + await writeUpdateLog('INFO', '[installLauncherUpdate] 准备退出应用'); + // Squirrel 会在更新完成后自动重启应用 + app.quit(); + }, 1500); return { success: true, - message: '正在更新启动器...', + message: '正在通过 Squirrel 更新启动器...', }; } else { - writeUpdateLog('INFO', '[installLauncherUpdate] 用户取消更新'); + await writeUpdateLog('INFO', '[installLauncherUpdate] 用户取消更新'); return { success: false, message: '用户取消更新', }; } } catch (error) { - writeUpdateLog( + await writeUpdateLog( 'ERROR', - '[installLauncherUpdate] 安装启动器更新失败', + '[installLauncherUpdate] Squirrel 更新失败', error instanceof Error ? { message: error.message, stack: error.stack } : error, From b79f311777bd7bec604efd0aff5ba007073fcf80 Mon Sep 17 00:00:00 2001 From: xiaoshen Date: Mon, 2 Feb 2026 08:46:35 +0800 Subject: [PATCH 20/22] refactor(update): replace Squirrel with custom ZIP-based updater --- external-resources/dlc/dlc-index.json | 4 +- package-lock.json | 15 +- src/main/launcher-update/index.ts | 284 ++++++++++++++------------ 3 files changed, 167 insertions(+), 136 deletions(-) diff --git a/external-resources/dlc/dlc-index.json b/external-resources/dlc/dlc-index.json index 463a477..ecc0e7e 100644 --- a/external-resources/dlc/dlc-index.json +++ b/external-resources/dlc/dlc-index.json @@ -32,8 +32,8 @@ "id": "AI_LEARNING_ASSISTANT_LAUNCHER", "name": "AI学习助手启动器", "versions": { - "2.0.4": { - "magnet": "magnet:?xt=urn:btih:ea0ebea633f25e936c55a095f66f15d65089dcec&dn=AILearningAssistantLauncher-2.0.4-full.nupkg&tr=wss%3A%2F%2Ftracker.openwebtorrent.com", + "2.0.3": { + "magnet": "magnet:?xt=urn:btih:5e685a7cbe4045efc40f7a593dddfdf56ae7c59d&dn=AI-Learning-Assistant-Launcher-win32-x64-2.0.3.zip&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.btorrent.xyz", "comment": "测试更新使用版本", "require": {} } diff --git a/package-lock.json b/package-lock.json index c799fac..e3674cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5484,8 +5484,10 @@ "version": "5.9.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "extraneous": true, + "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5588,8 +5590,10 @@ "version": "5.9.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "extraneous": true, + "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8207,6 +8211,7 @@ "version": "4.1.0", "resolved": "https://registry.npmmirror.com/bufferutil/-/bufferutil-4.1.0.tgz", "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -12016,9 +12021,11 @@ "version": "5.0.10", "resolved": "https://registry.npmmirror.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz", "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "extraneous": true, + "dev": true, "hasInstallScript": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -16268,6 +16275,7 @@ "version": "4.8.4", "resolved": "https://registry.npmmirror.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz", "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, "license": "MIT", "optional": true, "bin": { @@ -21956,6 +21964,7 @@ "version": "6.0.6", "resolved": "https://registry.npmmirror.com/utf-8-validate/-/utf-8-validate-6.0.6.tgz", "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, diff --git a/src/main/launcher-update/index.ts b/src/main/launcher-update/index.ts index b6fb248..274d36a 100644 --- a/src/main/launcher-update/index.ts +++ b/src/main/launcher-update/index.ts @@ -14,19 +14,16 @@ import { import semver from 'semver'; import path from 'path'; import { appPath } from '../exec'; -import { existsSync } from 'fs'; +import { existsSync, mkdirSync } from 'fs'; import { readdir, writeFile, appendFile, - readFile, - stat, access, rename, constants, } from 'fs/promises'; import { spawn } from 'child_process'; -import { createHash } from 'crypto'; import packageJson from '../../../package.json'; const currentVersion = packageJson.version; @@ -180,7 +177,7 @@ export async function downloadLauncherUpdate() { }, ); - // 检查是否为本地开发环境 + // 检查是否为本地开发环境,请打包后测试 const isDev = !app.isPackaged; if (isDev) { await writeUpdateLog( @@ -208,54 +205,100 @@ export async function downloadLauncherUpdate() { } /** - * 获取 Squirrel Update.exe 的路径 - * Squirrel 安装后,Update.exe 位于应用安装目录的上一级 + * 生成自定义更新批处理脚本 + * 脚本功能:等待应用退出、备份用户数据、解压更新包、替换文件、恢复数据、启动新版本 */ -function getSquirrelUpdateExePath(): string | null { - const exePath = app.getPath('exe'); - const exeDir = path.dirname(exePath); - // Squirrel 安装结构: /Update.exe, /app-x.x.x/app.exe - const updateExePath = path.join(exeDir, '..', 'Update.exe'); - - if (existsSync(updateExePath)) { - return path.resolve(updateExePath); +async function generateUpdateScript( + zipPath: string, + appDir: string, + tempDir: string, + newVersion: string, +): Promise { + const scriptContent = `@echo off +chcp 65001 >nul +setlocal enabledelayedexpansion + +set "ZIP_PATH=${zipPath.replace(/\//g, '\\')}" +set "APP_DIR=${appDir.replace(/\//g, '\\')}" +set "TEMP_DIR=${tempDir.replace(/\//g, '\\')}" +set "BACKUP_DIR=${tempDir.replace(/\//g, '\\')}\\external-resources-backup" +set "EXTRACT_DIR=${tempDir.replace(/\//g, '\\')}\\extracted" + +echo [更新] 等待应用退出... +:waitloop +tasklist /FI "IMAGENAME eq AI Learning Assistant Launcher.exe" 2>NUL | find /I "AI Learning Assistant Launcher.exe" >NUL +if not errorlevel 1 ( + timeout /t 1 /nobreak >nul + goto waitloop +) + +echo [更新] 应用已退出,开始更新... + +:: 创建临时目录 +if not exist "%TEMP_DIR%" mkdir "%TEMP_DIR%" +if not exist "%EXTRACT_DIR%" mkdir "%EXTRACT_DIR%" + +:: 备份 external-resources +echo [更新] 备份用户数据... +if exist "%APP_DIR%\\external-resources" ( + xcopy "%APP_DIR%\\external-resources" "%BACKUP_DIR%\\" /E /I /H /Y /Q +) + +:: 解压 ZIP 文件 +echo [更新] 解压更新包... +powershell -Command "Expand-Archive -Path '%ZIP_PATH%' -DestinationPath '%EXTRACT_DIR%' -Force" + +:: 查找解压后的目录(ZIP 内可能有一层目录) +for /d %%D in ("%EXTRACT_DIR%\\*") do set "SOURCE_DIR=%%D" +if not defined SOURCE_DIR set "SOURCE_DIR=%EXTRACT_DIR%" + +:: 删除旧文件(保留 external-resources) +echo [更新] 清理旧版本... +for /d %%D in ("%APP_DIR%\\*") do ( + if /I not "%%~nxD"=="external-resources" rd /s /q "%%D" 2>nul +) +for %%F in ("%APP_DIR%\\*") do ( + del /f /q "%%F" 2>nul +) + +:: 复制新文件 +echo [更新] 安装新版本... +xcopy "%SOURCE_DIR%\\*" "%APP_DIR%\\" /E /I /H /Y /Q + +:: 恢复 external-resources(合并) +echo [更新] 恢复用户数据... +if exist "%BACKUP_DIR%" ( + xcopy "%BACKUP_DIR%\\*" "%APP_DIR%\\external-resources\\" /E /I /H /Y /Q +) + +:: 清理临时文件 +echo [更新] 清理临时文件... +rd /s /q "%TEMP_DIR%" 2>nul + +:: 启动新版本 +echo [更新] 启动新版本... +start "" "%APP_DIR%\\AI Learning Assistant Launcher.exe" + +exit +`; + + // 确保临时目录存在 + if (!existsSync(tempDir)) { + mkdirSync(tempDir, { recursive: true }); } - return null; -} -/** - * 计算文件的 SHA1 哈希值 - */ -async function calculateSha1(filePath: string): Promise { - const fileBuffer = await readFile(filePath); - const hash = createHash('sha1'); - hash.update(fileBuffer); - return hash.digest('hex').toUpperCase(); -} + const scriptPath = path.join(tempDir, 'update.bat'); + await writeFile(scriptPath, scriptContent, { encoding: 'utf8' }); -/** - * 生成 Squirrel RELEASES 文件 - * RELEASES 文件格式: SHA1 filename size - */ -async function generateReleasesFile( - nupkgPath: string, - outputDir: string, -): Promise { - const fileName = path.basename(nupkgPath); - const fileStat = await stat(nupkgPath); - const fileSize = fileStat.size; - const sha1Hash = await calculateSha1(nupkgPath); - - const releasesContent = `${sha1Hash} ${fileName} ${fileSize}`; - const releasesPath = path.join(outputDir, 'RELEASES'); - - await writeFile(releasesPath, releasesContent, { encoding: 'utf8' }); - await writeUpdateLog('INFO', '[generateReleasesFile] RELEASES 文件已生成', { - path: releasesPath, - content: releasesContent, + await writeUpdateLog('INFO', '[generateUpdateScript] 更新脚本已生成', { + scriptPath, + zipPath, + appDir, + tempDir, + newVersion, }); - return releasesPath; + return scriptPath; } /** @@ -331,10 +374,7 @@ async function waitForFileRelease(filePath: string): Promise { } export async function installLauncherUpdate() { - await writeUpdateLog( - 'INFO', - '[installLauncherUpdate] 开始执行 Squirrel 更新', - ); + await writeUpdateLog('INFO', '[installLauncherUpdate] 开始执行自定义更新'); // 如果是开发模式,不执行更新 const isPackaged = app.isPackaged; @@ -349,26 +389,8 @@ export async function installLauncherUpdate() { }; } - // 检查是否为 Squirrel 安装 - const updateExePath = getSquirrelUpdateExePath(); - if (!updateExePath) { - await writeUpdateLog( - 'WARN', - '[installLauncherUpdate] 未检测到 Squirrel 安装,无法使用 Squirrel 更新', - ); - return { - success: false, - message: '当前安装方式不支持 Squirrel 自动更新,请手动下载安装包更新', - }; - } - - await writeUpdateLog( - 'DEBUG', - '[installLauncherUpdate] 检测到 Update.exe:', - updateExePath, - ); - try { + // 获取最新版本信息 await writeUpdateLog( 'DEBUG', '[installLauncherUpdate] 获取最新版本信息...', @@ -379,6 +401,7 @@ export async function installLauncherUpdate() { const version = latestVersionInfo.version; await writeUpdateLog('DEBUG', '[installLauncherUpdate] 最新版本:', version); + // 下载目录 const downloadPath = path.join( appPath, 'external-resources', @@ -391,17 +414,13 @@ export async function installLauncherUpdate() { '[installLauncherUpdate] 下载路径:', downloadPath, ); - await writeUpdateLog( - 'DEBUG', - '[installLauncherUpdate] 下载路径是否存在:', - existsSync(downloadPath), - ); if (!existsSync(downloadPath)) { await writeUpdateLog('ERROR', '[installLauncherUpdate] 下载目录不存在'); throw new Error('更新包未下载,请先下载更新包'); } + // 查找 ZIP 文件 const files = await readdir(downloadPath); await writeUpdateLog( 'DEBUG', @@ -409,21 +428,20 @@ export async function installLauncherUpdate() { files, ); - // 查找 .nupkg 文件 - const nupkgFile = files.find((f) => f.endsWith('.nupkg')); - if (!nupkgFile) { + const zipFile = files.find((f) => f.endsWith('.zip')); + if (!zipFile) { await writeUpdateLog( 'ERROR', - '[installLauncherUpdate] 未找到 .nupkg 更新包', + '[installLauncherUpdate] 未找到 .zip 更新包', ); - throw new Error('未找到 .nupkg 更新包'); + throw new Error('未找到 .zip 更新包'); } - const nupkgPath = path.join(downloadPath, nupkgFile); + const zipPath = path.join(downloadPath, zipFile); await writeUpdateLog( 'DEBUG', - '[installLauncherUpdate] nupkg 文件路径:', - nupkgPath, + '[installLauncherUpdate] ZIP 文件路径:', + zipPath, ); // 销毁 WebTorrent 以释放文件句柄 @@ -434,15 +452,16 @@ export async function installLauncherUpdate() { await destroyWebtorrentForInstall(latestVersionInfo.dlcInfo.magnet); // 使用重试机制等待文件句柄释放 - await waitForFileRelease(nupkgPath); + await waitForFileRelease(zipPath); await writeUpdateLog('DEBUG', '[installLauncherUpdate] 文件句柄已释放'); - // 生成 RELEASES 文件(Squirrel 需要此文件来确定更新信息) - await writeUpdateLog( - 'INFO', - '[installLauncherUpdate] 生成 RELEASES 文件...', - ); - await generateReleasesFile(nupkgPath, downloadPath); + // 获取应用目录和临时目录 + const exePath = app.getPath('exe'); + const appDir = path.dirname(exePath); + const tempDir = path.join(app.getPath('temp'), 'launcher-update'); + + await writeUpdateLog('DEBUG', '[installLauncherUpdate] 应用目录:', appDir); + await writeUpdateLog('DEBUG', '[installLauncherUpdate] 临时目录:', tempDir); // 显示更新确认对话框 const result = await dialog.showMessageBox({ @@ -455,56 +474,59 @@ export async function installLauncherUpdate() { cancelId: 1, }); - if (result.response === 0) { - await writeUpdateLog( - 'INFO', - '[installLauncherUpdate] 用户确认更新,准备执行 Squirrel 更新', - ); - - // 使用 Squirrel 的 Update.exe 执行更新 - // 参数 --update= 指定包含 RELEASES 和 .nupkg 的目录 - await writeUpdateLog( - 'INFO', - '[installLauncherUpdate] 调用 Squirrel Update.exe', - { - updateExePath, - updatePath: downloadPath, - }, - ); - - const updateProcess = spawn(updateExePath, ['--update', downloadPath], { - detached: true, - stdio: 'ignore', - }); - - updateProcess.unref(); - await writeUpdateLog( - 'INFO', - '[installLauncherUpdate] Squirrel 更新进程已启动', - ); - - // 延迟退出应用,让 Squirrel 接管更新流程 - setTimeout(async () => { - await writeUpdateLog('INFO', '[installLauncherUpdate] 准备退出应用'); - // Squirrel 会在更新完成后自动重启应用 - app.quit(); - }, 1500); - - return { - success: true, - message: '正在通过 Squirrel 更新启动器...', - }; - } else { + if (result.response !== 0) { await writeUpdateLog('INFO', '[installLauncherUpdate] 用户取消更新'); return { success: false, message: '用户取消更新', }; } + + await writeUpdateLog( + 'INFO', + '[installLauncherUpdate] 用户确认更新,准备执行自定义更新', + ); + + // 生成更新脚本 + const scriptPath = await generateUpdateScript( + zipPath, + appDir, + tempDir, + version, + ); + + await writeUpdateLog( + 'INFO', + '[installLauncherUpdate] 启动更新脚本:', + scriptPath, + ); + + // 启动更新脚本 + const updateProcess = spawn('cmd.exe', ['/c', scriptPath], { + detached: true, + stdio: 'ignore', + windowsHide: true, + }); + updateProcess.unref(); + + await writeUpdateLog( + 'INFO', + '[installLauncherUpdate] 更新脚本已启动,准备退出应用', + ); + + // 延迟退出应用 + setTimeout(() => { + app.quit(); + }, 1000); + + return { + success: true, + message: '正在更新启动器...', + }; } catch (error) { await writeUpdateLog( 'ERROR', - '[installLauncherUpdate] Squirrel 更新失败', + '[installLauncherUpdate] 自定义更新失败', error instanceof Error ? { message: error.message, stack: error.stack } : error, From 17c26015d074e160febf8d5230772ee5fd4aba0c Mon Sep 17 00:00:00 2001 From: xiaoshen Date: Mon, 2 Feb 2026 08:47:24 +0800 Subject: [PATCH 21/22] refactor(update): replace Squirrel with custom ZIP-based updater --- forge.config.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/forge.config.ts b/forge.config.ts index e64f757..8105eaa 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -20,13 +20,7 @@ const config: ForgeConfig = { }, rebuildConfig: {}, makers: [ - new MakerSquirrel({ - name: 'AILearningAssistantLauncher', - authors: 'James Zhang', - description: 'AI Learning Assistant Launcher', - // 设置图标路径 - setupIcon: path.join(__dirname, 'icons', 'icon.ico'), - }), + // new MakerSquirrel({}), new MakerZIP({}, ['darwin', 'win32']), // new MakerRpm({}), // new MakerDeb({}) From e6f43fc4c47093a7b67e68d01e817bf3c4aa6e11 Mon Sep 17 00:00:00 2001 From: xiaoshen Date: Mon, 2 Feb 2026 09:26:42 +0800 Subject: [PATCH 22/22] feat(update): implement atomic update with rollback and safety checks --- src/main/launcher-update/index.ts | 264 ++++++++++++++++++++++++++---- 1 file changed, 231 insertions(+), 33 deletions(-) diff --git a/src/main/launcher-update/index.ts b/src/main/launcher-update/index.ts index 274d36a..3423050 100644 --- a/src/main/launcher-update/index.ts +++ b/src/main/launcher-update/index.ts @@ -14,13 +14,15 @@ import { import semver from 'semver'; import path from 'path'; import { appPath } from '../exec'; -import { existsSync, mkdirSync } from 'fs'; +import { existsSync, mkdirSync, rmSync } from 'fs'; import { readdir, writeFile, appendFile, access, rename, + stat, + unlink, constants, } from 'fs/promises'; import { spawn } from 'child_process'; @@ -30,6 +32,12 @@ const currentVersion = packageJson.version; // 更新日志文件路径 const updateLogPath = path.join(appPath, 'launcher-update.log'); +const updateLogBackupPath = path.join(appPath, 'launcher-update.log.old'); + +// 日志滚动配置 +const LOG_ROTATION_CONFIG = { + MAX_SIZE_BYTES: 1 * 1024 * 1024, // 1MB +}; /** * 写入更新日志到文件 @@ -77,9 +85,36 @@ async function writeUpdateLog( } /** - * 初始化日志文件(清空旧日志或添加分隔符) + * 初始化日志文件(检查大小并执行滚动) + * 当日志文件超过 MAX_SIZE_BYTES 时,将旧日志重命名为 .old 文件 */ async function initUpdateLog() { + try { + // 检查日志文件是否存在及其大小 + if (existsSync(updateLogPath)) { + const stats = await stat(updateLogPath); + + if (stats.size >= LOG_ROTATION_CONFIG.MAX_SIZE_BYTES) { + // 删除旧的备份文件(如果存在) + try { + await unlink(updateLogBackupPath); + } catch { + // 备份文件不存在,忽略 + } + + // 将当前日志重命名为备份 + await rename(updateLogPath, updateLogBackupPath); + + console.log( + `[initUpdateLog] 日志文件已滚动,旧日志大小: ${(stats.size / 1024).toFixed(2)} KB`, + ); + } + } + } catch (err) { + console.error('[initUpdateLog] 日志滚动检查失败:', err); + } + + // 添加分隔符 const separator = ` ${'='.repeat(60)} [${new Date().toISOString()}] 启动器更新日志开始 @@ -104,6 +139,31 @@ export default async function init(ipcMain: IpcMain) { ); } +/** + * 清理更新临时目录 + * 当已是最新版本时调用,清理之前更新留下的临时文件 + */ +async function cleanupUpdateTempDir() { + const tempDir = path.join(app.getPath('temp'), 'launcher-update'); + + try { + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true, force: true }); + await writeUpdateLog( + 'INFO', + '[cleanupUpdateTempDir] 已清理更新临时目录', + { tempDir }, + ); + } + } catch (err) { + await writeUpdateLog( + 'WARN', + '[cleanupUpdateTempDir] 清理更新临时目录失败', + err instanceof Error ? { message: err.message } : err, + ); + } +} + export async function checkLauncherUpdate() { await initUpdateLog(); await writeUpdateLog('INFO', '[checkLauncherUpdate] 开始检查启动器更新'); @@ -113,6 +173,26 @@ export async function checkLauncherUpdate() { 'AI_LEARNING_ASSISTANT_LAUNCHER', ); const latestVersion = latestVersionInfo.version; + + // 版本号规范校验 + if (!semver.valid(currentVersion)) { + await writeUpdateLog( + 'ERROR', + '[checkLauncherUpdate] 当前版本号格式无效', + { currentVersion }, + ); + throw new Error(`当前版本号格式无效: ${currentVersion}`); + } + + if (!semver.valid(latestVersion)) { + await writeUpdateLog( + 'ERROR', + '[checkLauncherUpdate] 最新版本号格式无效', + { latestVersion }, + ); + throw new Error(`最新版本号格式无效: ${latestVersion}`); + } + const haveNew = semver.lt(currentVersion, latestVersion); await writeUpdateLog('INFO', '[checkLauncherUpdate] 检查启动器更新完成', { @@ -121,6 +201,11 @@ export async function checkLauncherUpdate() { haveNew, }); + // 如果已是最新版本,清理更新临时目录 + if (!haveNew) { + await cleanupUpdateTempDir(); + } + return { currentVersion, latestVersion, @@ -150,16 +235,23 @@ export async function downloadLauncherUpdate() { 'AI_LEARNING_ASSISTANT_LAUNCHER', ); + // 空值检查:确保 dlcInfo 和 magnet 存在 + if (!latestVersionInfo.dlcInfo?.magnet) { + throw new Error('未找到有效的更新包信息(dlcInfo 或 magnet 缺失)'); + } + + const { magnet } = latestVersionInfo.dlcInfo; + await writeUpdateLog( 'DEBUG', '[downloadLauncherUpdate] 获取到最新版本信息', { version: latestVersionInfo.version, - magnet: latestVersionInfo.dlcInfo?.magnet?.substring(0, 50) + '...', + magnet: magnet.substring(0, 50) + '...', }, ); - await startWebtorrent(latestVersionInfo.dlcInfo.magnet); + await startWebtorrent(magnet); await writeUpdateLog('DEBUG', '[downloadLauncherUpdate] WebTorrent 已启动'); const torrent = await waitTorrentDone( @@ -206,7 +298,13 @@ export async function downloadLauncherUpdate() { /** * 生成自定义更新批处理脚本 - * 脚本功能:等待应用退出、备份用户数据、解压更新包、替换文件、恢复数据、启动新版本 + * 脚本功能:等待应用退出、解压更新包、校验完整性、备份旧版本、替换文件、恢复数据、启动新版本 + * + * 安全策略:采用"解压 -> 校验 -> 备份 -> 替换"模式 + * 1. 先在临时目录完整解压并校验 + * 2. 校验失败则直接启动旧版本,不做任何修改 + * 3. 校验成功后备份整个旧版本目录 + * 4. 替换失败时可从备份恢复 */ async function generateUpdateScript( zipPath: string, @@ -222,9 +320,14 @@ set "ZIP_PATH=${zipPath.replace(/\//g, '\\')}" set "APP_DIR=${appDir.replace(/\//g, '\\')}" set "TEMP_DIR=${tempDir.replace(/\//g, '\\')}" set "BACKUP_DIR=${tempDir.replace(/\//g, '\\')}\\external-resources-backup" +set "APP_BACKUP_DIR=${tempDir.replace(/\//g, '\\')}\\app-backup" set "EXTRACT_DIR=${tempDir.replace(/\//g, '\\')}\\extracted" +set "UPDATE_LOG=${tempDir.replace(/\//g, '\\')}\\update-script.log" -echo [更新] 等待应用退出... +:: 日志函数 +call :log "========== 更新脚本开始 ==========" + +call :log "等待应用退出..." :waitloop tasklist /FI "IMAGENAME eq AI Learning Assistant Launcher.exe" 2>NUL | find /I "AI Learning Assistant Launcher.exe" >NUL if not errorlevel 1 ( @@ -232,54 +335,142 @@ if not errorlevel 1 ( goto waitloop ) -echo [更新] 应用已退出,开始更新... +call :log "应用已退出,开始更新..." -:: 创建临时目录 -if not exist "%TEMP_DIR%" mkdir "%TEMP_DIR%" -if not exist "%EXTRACT_DIR%" mkdir "%EXTRACT_DIR%" +:: ========== 阶段1: 准备和解压 ========== +call :log "[阶段1] 准备临时目录..." +if exist "%TEMP_DIR%" rd /s /q "%TEMP_DIR%" 2>nul +mkdir "%TEMP_DIR%" +mkdir "%EXTRACT_DIR%" -:: 备份 external-resources -echo [更新] 备份用户数据... -if exist "%APP_DIR%\\external-resources" ( - xcopy "%APP_DIR%\\external-resources" "%BACKUP_DIR%\\" /E /I /H /Y /Q +call :log "[阶段1] 解压更新包到临时目录..." +powershell -Command "try { Expand-Archive -Path '%ZIP_PATH%' -DestinationPath '%EXTRACT_DIR%' -Force -ErrorAction Stop; exit 0 } catch { Write-Host $_.Exception.Message; exit 1 }" +if errorlevel 1 ( + call :log "[错误] 解压失败!启动旧版本..." + goto :launch_old_version ) -:: 解压 ZIP 文件 -echo [更新] 解压更新包... -powershell -Command "Expand-Archive -Path '%ZIP_PATH%' -DestinationPath '%EXTRACT_DIR%' -Force" - :: 查找解压后的目录(ZIP 内可能有一层目录) +set "SOURCE_DIR=" for /d %%D in ("%EXTRACT_DIR%\\*") do set "SOURCE_DIR=%%D" if not defined SOURCE_DIR set "SOURCE_DIR=%EXTRACT_DIR%" -:: 删除旧文件(保留 external-resources) -echo [更新] 清理旧版本... +call :log "[阶段1] 解压完成,源目录: %SOURCE_DIR%" + +:: ========== 阶段2: 校验解压完整性 ========== +call :log "[阶段2] 校验更新包完整性..." + +:: 检查关键文件是否存在 +if not exist "%SOURCE_DIR%\\AI Learning Assistant Launcher.exe" ( + call :log "[错误] 校验失败:主程序文件不存在!启动旧版本..." + goto :launch_old_version +) + +if not exist "%SOURCE_DIR%\\resources" ( + call :log "[错误] 校验失败:resources 目录不存在!启动旧版本..." + goto :launch_old_version +) + +call :log "[阶段2] 校验通过,更新包完整" + +:: ========== 阶段3: 备份用户数据和旧版本 ========== +call :log "[阶段3] 备份用户数据 external-resources..." +if exist "%APP_DIR%\\external-resources" ( + xcopy "%APP_DIR%\\external-resources" "%BACKUP_DIR%\\" /E /I /H /Y /Q >nul 2>&1 + if errorlevel 1 ( + call :log "[警告] 用户数据备份可能不完整" + ) else ( + call :log "[阶段3] 用户数据备份完成" + ) +) + +call :log "[阶段3] 备份旧版本应用..." +mkdir "%APP_BACKUP_DIR%" 2>nul +xcopy "%APP_DIR%\\*" "%APP_BACKUP_DIR%\\" /E /I /H /Y /Q >nul 2>&1 +if errorlevel 1 ( + call :log "[警告] 旧版本备份可能不完整,但继续更新" +) else ( + call :log "[阶段3] 旧版本备份完成" +) + +:: ========== 阶段4: 替换文件 ========== +call :log "[阶段4] 清理旧版本文件(保留 external-resources)..." for /d %%D in ("%APP_DIR%\\*") do ( - if /I not "%%~nxD"=="external-resources" rd /s /q "%%D" 2>nul + if /I not "%%~nxD"=="external-resources" ( + rd /s /q "%%D" 2>nul + ) ) for %%F in ("%APP_DIR%\\*") do ( del /f /q "%%F" 2>nul ) -:: 复制新文件 -echo [更新] 安装新版本... -xcopy "%SOURCE_DIR%\\*" "%APP_DIR%\\" /E /I /H /Y /Q +call :log "[阶段4] 安装新版本..." +xcopy "%SOURCE_DIR%\\*" "%APP_DIR%\\" /E /I /H /Y /Q >nul 2>&1 +if errorlevel 1 ( + call :log "[错误] 复制新文件失败!尝试从备份恢复..." + goto :restore_from_backup +) + +:: 验证安装结果 +if not exist "%APP_DIR%\\AI Learning Assistant Launcher.exe" ( + call :log "[错误] 安装验证失败:主程序不存在!尝试从备份恢复..." + goto :restore_from_backup +) + +call :log "[阶段4] 新版本安装完成" -:: 恢复 external-resources(合并) -echo [更新] 恢复用户数据... +:: ========== 阶段5: 恢复用户数据 ========== +call :log "[阶段5] 恢复用户数据..." if exist "%BACKUP_DIR%" ( - xcopy "%BACKUP_DIR%\\*" "%APP_DIR%\\external-resources\\" /E /I /H /Y /Q + xcopy "%BACKUP_DIR%\\*" "%APP_DIR%\\external-resources\\" /E /I /H /Y /Q >nul 2>&1 + call :log "[阶段5] 用户数据恢复完成" ) -:: 清理临时文件 -echo [更新] 清理临时文件... -rd /s /q "%TEMP_DIR%" 2>nul +:: ========== 阶段6: 清理和启动 ========== +call :log "[阶段6] 更新成功!清理临时文件..." +:: 延迟删除临时目录,避免影响日志 +start /b cmd /c "timeout /t 5 /nobreak >nul & rd /s /q "%TEMP_DIR%" 2>nul" -:: 启动新版本 -echo [更新] 启动新版本... +call :log "[阶段6] 启动新版本..." +call :log "========== 更新脚本结束 ==========" start "" "%APP_DIR%\\AI Learning Assistant Launcher.exe" +exit +:: ========== 错误处理: 从备份恢复 ========== +:restore_from_backup +call :log "[恢复] 开始从备份恢复旧版本..." +if exist "%APP_BACKUP_DIR%\\AI Learning Assistant Launcher.exe" ( + :: 清理可能的残留 + for /d %%D in ("%APP_DIR%\\*") do rd /s /q "%%D" 2>nul + for %%F in ("%APP_DIR%\\*") do del /f /q "%%F" 2>nul + + :: 恢复备份 + xcopy "%APP_BACKUP_DIR%\\*" "%APP_DIR%\\" /E /I /H /Y /Q >nul 2>&1 + if exist "%APP_DIR%\\AI Learning Assistant Launcher.exe" ( + call :log "[恢复] 旧版本恢复成功" + goto :launch_old_version + ) +) +call :log "[恢复] 恢复失败,请手动重新安装应用" +pause +exit + +:: ========== 错误处理: 启动旧版本 ========== +:launch_old_version +call :log "[回退] 启动旧版本..." +if exist "%APP_DIR%\\AI Learning Assistant Launcher.exe" ( + start "" "%APP_DIR%\\AI Learning Assistant Launcher.exe" +) else ( + call :log "[错误] 旧版本也不存在,请手动重新安装" + pause +) exit + +:: ========== 日志函数 ========== +:log +echo [%date% %time%] %~1 +echo [%date% %time%] %~1 >> "%UPDATE_LOG%" 2>nul +exit /b 0 `; // 确保临时目录存在 @@ -398,6 +589,13 @@ export async function installLauncherUpdate() { const latestVersionInfo = getLatestVersion( 'AI_LEARNING_ASSISTANT_LAUNCHER', ); + + // 空值检查:确保 dlcInfo 和 magnet 存在 + if (!latestVersionInfo.dlcInfo?.magnet) { + throw new Error('未找到有效的更新包信息(dlcInfo 或 magnet 缺失)'); + } + + const { magnet } = latestVersionInfo.dlcInfo; const version = latestVersionInfo.version; await writeUpdateLog('DEBUG', '[installLauncherUpdate] 最新版本:', version); @@ -449,7 +647,7 @@ export async function installLauncherUpdate() { 'DEBUG', '[installLauncherUpdate] 销毁 WebTorrent 释放文件句柄...', ); - await destroyWebtorrentForInstall(latestVersionInfo.dlcInfo.magnet); + await destroyWebtorrentForInstall(magnet); // 使用重试机制等待文件句柄释放 await waitForFileRelease(zipPath);