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/12] =?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/12] =?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/12] =?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/12] =?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/12] =?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/12] =?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/12] =?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 902a45fcd11d86e257219aa27fb3ede655d889cb Mon Sep 17 00:00:00 2001 From: forceOfPx <3239735744@qq.com> Date: Fri, 23 Jan 2026 18:45:35 +0800 Subject: [PATCH 08/12] 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 76c2c9cb85fa207f885061995ab412c84b8587e8 Mon Sep 17 00:00:00 2001 From: forceOfPx <3239735744@qq.com> Date: Fri, 6 Feb 2026 20:51:08 +0800 Subject: [PATCH 09/12] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 127 +++++------ package.json | 1 + src/renderer/pages/joint-build/index.scss | 64 ++++-- src/renderer/pages/joint-build/index.tsx | 256 ++++++++++++++-------- 4 files changed, 268 insertions(+), 180 deletions(-) diff --git a/package-lock.json b/package-lock.json index d0092e3..e525f2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@langchain/anthropic": "^0.3.30", + "@langchain/core": "^1.1.19", "@langchain/deepseek": "^0.1.0", "@langchain/google-genai": "^0.2.18", "@langchain/ollama": "^0.2.4", @@ -19,7 +20,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", @@ -254,7 +256,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2471,7 +2472,6 @@ "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", @@ -3208,7 +3208,6 @@ "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^3.0.1", "@inquirer/confirm": "^4.0.1", @@ -3363,7 +3362,6 @@ "resolved": "https://registry.npmmirror.com/@langchain/anthropic/-/anthropic-0.3.34.tgz", "integrity": "sha512-8bOW1A2VHRCjbzdYElrjxutKNs9NSIxYRGtR+OJWVzluMqoKKh2NmmFrpPizEyqCUEG2tTq5xt6XA1lwfqMJRA==", "license": "MIT", - "peer": true, "dependencies": { "@anthropic-ai/sdk": "^0.65.0", "fast-xml-parser": "^4.4.1" @@ -3376,27 +3374,58 @@ } }, "node_modules/@langchain/core": { - "version": "0.3.80", - "resolved": "https://registry.npmmirror.com/@langchain/core/-/core-0.3.80.tgz", - "integrity": "sha512-vcJDV2vk1AlCwSh3aBm/urQ1ZrlXFFBocv11bz/NBUfLWD5/UDNMzwPdaAd2dKvNmTWa9FM2lirLU3+JCf4cRA==", + "version": "1.1.19", + "resolved": "https://registry.npmmirror.com/@langchain/core/-/core-1.1.19.tgz", + "integrity": "sha512-hmNAcgeLLqNLnu8UK+HVTfB8170eCmAfsy4gFLBYeE+kdsnyO0Hd/Kd42pZi8Cgfux4OMX3yxl+g+/a1ktGe0A==", "license": "MIT", - "peer": true, "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", - "langsmith": "^0.3.67", + "langsmith": ">=0.4.0 <1.0.0", "mustache": "^4.2.0", "p-queue": "^6.6.2", - "p-retry": "4", "uuid": "^10.0.0", - "zod": "^3.25.32", - "zod-to-json-schema": "^3.22.3" + "zod": "^3.25.76 || ^4" }, "engines": { - "node": ">=18" + "node": ">=20" + } + }, + "node_modules/@langchain/core/node_modules/langsmith": { + "version": "0.4.12", + "resolved": "https://registry.npmmirror.com/langsmith/-/langsmith-0.4.12.tgz", + "integrity": "sha512-YWt0jcGvKqjUgIvd78rd4QcdMss0lUkeUaqp0UpVRq7H2yNDx8H5jOUO/laWUmaPtWGgcip0qturykXe1g9Gqw==", + "license": "MIT", + "dependencies": { + "@types/uuid": "^10.0.0", + "chalk": "^4.1.2", + "console-table-printer": "^2.12.1", + "p-queue": "^6.6.2", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "*", + "@opentelemetry/exporter-trace-otlp-proto": "*", + "@opentelemetry/sdk-trace-base": "*", + "openai": "*" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "openai": { + "optional": true + } } }, "node_modules/@langchain/core/node_modules/uuid": { @@ -3417,7 +3446,6 @@ "resolved": "https://registry.npmmirror.com/@langchain/deepseek/-/deepseek-0.1.0.tgz", "integrity": "sha512-5VzaG/oi2oSfBt+JUdXWjavZcmOzjk14+N5sCFnbrY6xeAjHrtBq7VeBcBJcHqkrjafxADK1aVGV0hpbhiFyJg==", "license": "MIT", - "peer": true, "dependencies": { "@langchain/openai": "^0.6.0" }, @@ -3433,7 +3461,6 @@ "resolved": "https://registry.npmmirror.com/@langchain/google-genai/-/google-genai-0.2.18.tgz", "integrity": "sha512-m9EiN3VKC01A7/625YQ6Q1Lqq8zueewADX4W5Tcme4RImN75zkg2Z7FYbD1Fo6Zwolc4wBNO6LUtbg3no4rv1Q==", "license": "MIT", - "peer": true, "dependencies": { "@google/generative-ai": "^0.24.0", "uuid": "^11.1.0" @@ -5462,7 +5489,6 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -5831,7 +5857,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5995,7 +6020,6 @@ "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.10.4.tgz", "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -6057,7 +6081,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6261,7 +6284,6 @@ "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", @@ -6691,7 +6713,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6791,7 +6812,6 @@ "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", @@ -6912,7 +6932,6 @@ "integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ant-design/colors": "^7.2.1", "@ant-design/cssinjs": "^1.23.0", @@ -8089,7 +8108,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -9786,8 +9804,7 @@ "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", @@ -11040,6 +11057,17 @@ "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", @@ -11333,7 +11361,6 @@ "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", @@ -11390,7 +11417,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -11924,7 +11950,6 @@ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -11982,22 +12007,6 @@ "express": "^4.0.0 || ^5.0.0-alpha.1" } }, - "node_modules/express-ws/node_modules/utf-8-validate": { - "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, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/express-ws/node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.10.tgz", @@ -17329,7 +17338,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -17492,7 +17500,6 @@ "integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -18543,7 +18550,6 @@ "resolved": "https://registry.npmmirror.com/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -18553,7 +18559,6 @@ "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -21006,7 +21011,6 @@ "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", @@ -21646,7 +21650,6 @@ "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -22123,7 +22126,6 @@ "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -22197,7 +22199,6 @@ "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", @@ -22315,7 +22316,6 @@ "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", @@ -22408,7 +22408,6 @@ "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", @@ -22801,9 +22800,8 @@ "version": "8.18.3", "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -22870,8 +22868,7 @@ "integrity": "sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==", "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/xterm-addon-fit": { "version": "0.5.0", @@ -23042,19 +23039,9 @@ "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" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25 || ^4" - } } } } diff --git a/package.json b/package.json index b325b13..1d8140d 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ }, "dependencies": { "@langchain/anthropic": "^0.3.30", + "@langchain/core": "^1.1.19", "@langchain/deepseek": "^0.1.0", "@langchain/google-genai": "^0.2.18", "@langchain/ollama": "^0.2.4", diff --git a/src/renderer/pages/joint-build/index.scss b/src/renderer/pages/joint-build/index.scss index deccc6d..93933b5 100644 --- a/src/renderer/pages/joint-build/index.scss +++ b/src/renderer/pages/joint-build/index.scss @@ -93,7 +93,7 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 12px; + margin-bottom: 8px; .disk-path { font-size: 14px; @@ -101,6 +101,12 @@ } } + .disk-stats { + font-size: 12px; + color: #999; + margin-bottom: 8px; + } + .modules-list { display: flex; flex-direction: column; @@ -118,7 +124,8 @@ .module-info { display: flex; flex-direction: column; - gap: 4px; + gap: 6px; + flex: 1; .module-header { display: flex; @@ -131,29 +138,58 @@ color: #333; } - .module-recommend { + .module-version { font-size: 12px; color: #999; - display: flex; - align-items: center; - gap: 2px; + background: #f5f5f5; + padding: 2px 8px; + border-radius: 4px; + } + } - .recommend-fire { - opacity: 0.3; - font-size: 12px; + .module-status { + display: flex; + align-items: center; + gap: 12px; + + .status-badge { + font-size: 13px; + font-weight: 500; + } - &.active { - opacity: 1; - } - } + .download-progress { + font-size: 12px; + color: #1890ff; + font-weight: 500; } } - .module-disk { + .module-stats { + display: flex; + gap: 16px; font-size: 12px; color: #999; + + span { + display: flex; + align-items: center; + gap: 4px; + } } } + + .module-actions { + display: flex; + gap: 8px; + flex-shrink: 0; + } + } + + .empty-tip { + text-align: center; + padding: 40px 20px; + color: #999; + font-size: 14px; } } } diff --git a/src/renderer/pages/joint-build/index.tsx b/src/renderer/pages/joint-build/index.tsx index 2e4efc3..bdfac81 100644 --- a/src/renderer/pages/joint-build/index.tsx +++ b/src/renderer/pages/joint-build/index.tsx @@ -3,19 +3,10 @@ import { NavLink } from 'react-router-dom'; import { useState, useEffect, useCallback } from 'react'; import { LeftOutlined } from '@ant-design/icons'; import jointBuildIcon from '../../../../icons/joint_build.png'; +import { DLCIndex, OneDLCInfo } from '../../../main/dlc/type-info'; import './index.scss'; -interface ModuleItem { - id: string; - name: string; - description: string; - diskUsage: string; - recommendLevel: number; // 1-3 - enabled: boolean; -} - 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'; // 欢迎弹窗用户选择 @@ -24,39 +15,62 @@ const bytesToGB = (bytes: number): number => { return Math.round((bytes / (1024 * 1024 * 1024)) * 10) / 10; }; +// 格式化字节大小 +const formatBytes = (bytes: number): string => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; +}; + +// 获取做种状态(只关心已下载完成的资源) +function getSeedingState(version: OneDLCInfo['versions'][string]): '未下载' | '已暂停做种' | '做种中' { + if (version.progress && version.progress.done) { + if (version.progress.paused) { + return '已暂停做种'; + } else { + return '做种中'; + } + } + return '未下载'; +} + +// 获取状态颜色 +function getStateColor(state: string): string { + switch (state) { + case '做种中': + return '#52c41a'; + case '已暂停做种': + return '#faad14'; + default: + return '#999'; + } +} + export default function JointBuild() { const [masterSwitch, setMasterSwitch] = useState(false); const [diskPath, setDiskPath] = useState('C:\\'); const [diskUsed, setDiskUsed] = useState(0); const [diskTotal, setDiskTotal] = useState(0); const [diskLoading, setDiskLoading] = useState(false); - // todo: 这些数据从哪获取?还是写死 - const [modules, setModules] = useState([ - { - id: 'launcher', - name: 'AI学习助手启动器', - description: '磁盘占用 200MB', - diskUsage: '200MB', - recommendLevel: 3, - enabled: true, - }, - { - id: 'training-package', - name: '学科培训安装包', - description: '磁盘占用 200MB', - diskUsage: '200MB', - recommendLevel: 3, - enabled: false, - }, - { - id: 'voice-model', - name: '学科培训语音模型', - description: '磁盘占用 2.1GB', - diskUsage: '2.1GB', - recommendLevel: 2, - enabled: false, - }, - ]); + const [dLCIndex, setDLCIndex] = useState([]); + const [refreshTrigger, setRefreshTrigger] = useState(1); + + // 定时刷新DLC数据 + useEffect(() => { + window.mainHandle + .queryWebtorrentHandle() + .then((newDLCIndex) => setDLCIndex(newDLCIndex)) + .catch(console.error); + const timeout = setTimeout( + () => setRefreshTrigger(refreshTrigger + 1), + 1000, + ); + return () => { + clearTimeout(timeout); + }; + }, [refreshTrigger]); // 获取磁盘信息 const fetchDiskInfo = useCallback(async (path: string) => { @@ -92,21 +106,6 @@ export default function JointBuild() { // 初始化时同步托盘状态到主进程 window.mainHandle.setTrayEnabled(initialMasterSwitch).catch(console.error); - const savedModules = localStorage.getItem(STORAGE_KEY_MODULES); - if (savedModules) { - try { - const parsed = JSON.parse(savedModules); - setModules((prev) => - prev.map((mod) => { - const saved = parsed.find((s: ModuleItem) => s.id === mod.id); - return saved ? { ...mod, enabled: saved.enabled } : mod; - }) - ); - } catch (e) { - console.error('Failed to parse saved modules:', e); - } - } - const savedDiskPath = localStorage.getItem(STORAGE_KEY_DISK_PATH); const initialPath = savedDiskPath || 'C:\\'; setDiskPath(initialPath); @@ -129,17 +128,13 @@ export default function JointBuild() { }, []); // 保存配置到 localStorage - const saveConfig = (master: boolean, mods: ModuleItem[]) => { + const saveConfig = (master: boolean) => { localStorage.setItem(STORAGE_KEY_MASTER, String(master)); - localStorage.setItem( - STORAGE_KEY_MODULES, - JSON.stringify(mods.map((m) => ({ id: m.id, enabled: m.enabled }))) - ); }; const handleMasterSwitchChange = async (checked: boolean) => { setMasterSwitch(checked); - saveConfig(checked, modules); + saveConfig(checked); // 同步托盘状态到主进程 try { @@ -155,12 +150,26 @@ export default function JointBuild() { } }; - const handleModuleToggle = (id: string, checked: boolean) => { - const newModules = modules.map((mod) => - mod.id === id ? { ...mod, enabled: checked } : mod - ); - setModules(newModules); - saveConfig(masterSwitch, newModules); + // 处理开始做种 + const handleStartSeeding = async (magnet: string) => { + try { + await window.mainHandle.startWebtorrentHandle(magnet); + message.success('开始做种'); + } catch (error) { + console.error('启动做种失败:', error); + message.error('启动做种失败'); + } + }; + + // 处理暂停做种 + const handlePauseSeeding = async (magnet: string) => { + try { + await window.mainHandle.pauseWebtorrentHandle(magnet); + message.info('已暂停做种'); + } catch (error) { + console.error('暂停失败:', error); + message.error('暂停失败'); + } }; const handleChangePath = async () => { @@ -179,22 +188,25 @@ export default function JointBuild() { } }; - const renderRecommendLevel = (level: number) => { - const fires = []; - for (let i = 0; i < 3; i++) { - fires.push( - - 🔥 - - ); - } - return fires; + // 获取最新版本 + const getLatestVersion = (dlc: OneDLCInfo): { key: string; version: OneDLCInfo['versions'][string] } | null => { + const versionKeys = Object.keys(dlc.versions); + if (versionKeys.length === 0) return null; + // 按版本号排序,取最新的 + const sortedKeys = versionKeys.sort((a, b) => { + const partsA = a.split('.').map(Number); + const partsB = b.split('.').map(Number); + for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) { + const numA = partsA[i] || 0; + const numB = partsB[i] || 0; + if (numA !== numB) return numB - numA; + } + return 0; + }); + return { key: sortedKeys[0], version: dlc.versions[sortedKeys[0]] }; }; - const diskUsedPercent = Math.round((diskUsed / diskTotal) * 100); + const diskUsedPercent = diskTotal > 0 ? Math.round((diskUsed / diskTotal) * 100) : 0; return (
@@ -226,6 +238,9 @@ export default function JointBuild() { 磁盘路径:{diskPath}
+
+ 已用 {diskUsed} GB / 总计 {diskTotal} GB +
-

共建参与模块设置

-

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

+

共建参与模块

+

以下是您已下载的资源包,开启做种可以帮助其他学习者更快获取资源

- {modules.map((mod) => ( -
-
-
- {mod.name} - - 推荐指数 {renderRecommendLevel(mod.recommendLevel)} - + {dLCIndex.map((dlc) => { + const latestVersionInfo = getLatestVersion(dlc); + if (!latestVersionInfo) return null; + + const { key: versionKey, version } = latestVersionInfo; + const state = getSeedingState(version); + const progress = version.progress; + + // 只显示已下载完成的资源 + if (state === '未下载') return null; + + return ( +
+
+
+ {dlc.name} + v{versionKey} +
+
+ + {state} + +
+ {progress && ( +
+ {progress.uploadSpeed > 0 && ( + ↑ {formatBytes(progress.uploadSpeed)}/s + )} + {progress.numPeers > 0 && ( + 节点: {progress.numPeers} + )} +
+ )} +
+
+ {state === '已暂停做种' && ( + + )} + {state === '做种中' && ( + + )}
- 磁盘占用 {mod.diskUsage}
- handleModuleToggle(mod.id, checked)} - disabled={!masterSwitch} - /> -
- ))} + ); + })} + {dLCIndex.filter(dlc => { + const latestVersionInfo = getLatestVersion(dlc); + if (!latestVersionInfo) return false; + return getSeedingState(latestVersionInfo.version) !== '未下载'; + }).length === 0 && ( +
暂无已下载的资源包可供做种
+ )}
From 2913f0b3e1798763d58a468838d7f4020b3045ff Mon Sep 17 00:00:00 2001 From: forceOfPx <3239735744@qq.com> Date: Sat, 7 Feb 2026 00:48:57 +0800 Subject: [PATCH 10/12] =?UTF-8?q?=E5=81=9A=E7=A7=8D=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=E7=A8=B3=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 20 +-- package.json | 2 + src/main/dlc/index.ts | 149 +++++++++++++++++++---- src/renderer/pages/joint-build/index.tsx | 35 ++++-- 4 files changed, 154 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index e525f2a..69ff5cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,11 @@ "@langchain/google-genai": "^0.2.18", "@langchain/ollama": "^0.2.4", "@langchain/openai": "^0.6.14", + "@types/parse-torrent": "^5.8.8", "@types/webtorrent": "^0.110.1", "electron-squirrel-startup": "^1.0.1", "langchain": "^0.3.35", + "parse-torrent": "^11.0.19", "pdf-lib": "^1.17.1", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -5697,7 +5699,6 @@ "version": "1.0.5", "resolved": "https://registry.npmmirror.com/@thaunknown/thirty-two/-/thirty-two-1.0.5.tgz", "integrity": "sha512-Q53KyCXweV1CS62EfqtPDqfpksn5keQ59PGqzzkK+g8Vif1jB4inoBCcs/BUSdsqddhE3G+2Fn+4RX3S6RqT0A==", - "dev": true, "license": "MIT", "dependencies": { "uint8-util": "^2.2.5" @@ -6684,7 +6685,6 @@ "version": "3.0.0", "resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, "license": "MIT", "dependencies": { "event-target-shim": "^5.0.0" @@ -7516,7 +7516,6 @@ "version": "1.0.2", "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6.0" @@ -7573,7 +7572,6 @@ "version": "4.0.0", "resolved": "https://registry.npmmirror.com/bencode/-/bencode-4.0.0.tgz", "integrity": "sha512-AERXw18df0pF3ziGOCyUjqKZBVNH8HV3lBxnx5w0qtgMIk4a1wb9BkcCQbkp9Zstfrn/dzRwl7MmUHHocX3sRQ==", - "dev": true, "license": "MIT", "dependencies": { "uint8-util": "^2.2.2" @@ -7586,7 +7584,6 @@ "version": "2.0.0", "resolved": "https://registry.npmmirror.com/bep53-range/-/bep53-range-2.0.0.tgz", "integrity": "sha512-sMm2sV5PRs0YOVk0LTKtjuIprVzxgTQUsrGX/7Yph2Rm4FO2Fqqtq7hNjsOB5xezM4v4+5rljCgK++UeQJZguA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.20.0" @@ -9507,7 +9504,6 @@ "version": "1.0.3", "resolved": "https://registry.npmmirror.com/cross-fetch-ponyfill/-/cross-fetch-ponyfill-1.0.3.tgz", "integrity": "sha512-uOBkDhUAGAbx/FEzNKkOfx3w57H8xReBBXoZvUnOKTI0FW0Xvrj3GrYv2iZXUqlffC1LMGfQzhmBM/ke+6eTDA==", - "dev": true, "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", @@ -9518,7 +9514,6 @@ "version": "3.3.2", "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dev": true, "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", @@ -9739,7 +9734,6 @@ "version": "4.0.1", "resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -11781,7 +11775,6 @@ "version": "5.0.1", "resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -12239,7 +12232,6 @@ "version": "3.2.0", "resolved": "https://registry.npmmirror.com/fetch-blob/-/fetch-blob-3.2.0.tgz", "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "dev": true, "funding": [ { "type": "github", @@ -12526,7 +12518,6 @@ "version": "4.0.10", "resolved": "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dev": true, "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" @@ -12892,7 +12883,6 @@ "version": "9.0.0", "resolved": "https://registry.npmmirror.com/get-stdin/-/get-stdin-9.0.0.tgz", "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -15481,7 +15471,6 @@ "version": "7.0.7", "resolved": "https://registry.npmmirror.com/magnet-uri/-/magnet-uri-7.0.7.tgz", "integrity": "sha512-z/+dB2NQsXaDuxVBjoPLpZT8ePaacUmoontoFheRBl++nALHYs4qV9MmhTur9e4SaMbkCR/uPX43UMzEOoeyaw==", - "dev": true, "funding": [ { "type": "github", @@ -16181,7 +16170,6 @@ "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", "deprecated": "Use your platform's native DOMException instead", - "dev": true, "funding": [ { "type": "github", @@ -17072,7 +17060,6 @@ "version": "11.0.19", "resolved": "https://registry.npmmirror.com/parse-torrent/-/parse-torrent-11.0.19.tgz", "integrity": "sha512-T0lEkDdFVQsy0YxHIKjzDHSgt/yl57f3INs5jl7OZqAm77XDF0FgRgrv3LCKgSqsTOrMwYaF0t2761WKdvhgig==", - "dev": true, "funding": [ { "type": "github", @@ -17725,7 +17712,6 @@ "version": "1.2.3", "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -21662,7 +21648,6 @@ "version": "2.2.5", "resolved": "https://registry.npmmirror.com/uint8-util/-/uint8-util-2.2.5.tgz", "integrity": "sha512-/QxVQD7CttWpVUKVPz9znO+3Dd4BdTSnFQ7pv/4drVhC9m4BaL2LFHTkJn6EsYoxT79VDq/2Gg8L0H22PrzyMw==", - "dev": true, "license": "MIT", "dependencies": { "base64-arraybuffer": "^1.0.2" @@ -22107,7 +22092,6 @@ "version": "3.3.3", "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" diff --git a/package.json b/package.json index 1d8140d..9d83c53 100644 --- a/package.json +++ b/package.json @@ -88,9 +88,11 @@ "@langchain/google-genai": "^0.2.18", "@langchain/ollama": "^0.2.4", "@langchain/openai": "^0.6.14", + "@types/parse-torrent": "^5.8.8", "@types/webtorrent": "^0.110.1", "electron-squirrel-startup": "^1.0.1", "langchain": "^0.3.35", + "parse-torrent": "^11.0.19", "pdf-lib": "^1.17.1", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/src/main/dlc/index.ts b/src/main/dlc/index.ts index 869cebe..756b7dc 100644 --- a/src/main/dlc/index.ts +++ b/src/main/dlc/index.ts @@ -11,6 +11,7 @@ import { } from './type-info'; import { ipcHandle } from '../ipc-util'; import WebTorrent, * as allExports from 'webtorrent'; +import crypto from 'crypto'; import path from 'path'; import { appPath } from '../exec'; import fs, { existsSync, mkdirSync, readFileSync, unlinkSync } from 'fs'; @@ -60,9 +61,14 @@ const getWebTorrent = async () => { export default async function init(ipcMain: IpcMain) { client = await getWebTorrent(); - ipcHandle(ipcMain, startWebtorrentHandle, async (_event, magnet: string) => - startWebtorrent(magnet), - ); + ipcHandle(ipcMain, startWebtorrentHandle, async (_event, magnet: string) => { + const torrent = await startWebtorrent(magnet); + // 返回可序列化的对象而不是 torrent 实例 + if (torrent) { + return { infoHash: torrent.infoHash, name: torrent.name }; + } + return null; + }); ipcHandle(ipcMain, queryWebtorrentHandle, async (_event) => queryWebtorrent(), ); @@ -79,6 +85,64 @@ export default async function init(ipcMain: IpcMain) { await restoreTorrentsFromFiles(); } +/** + * 从torrent文件中提取infoHash + * 简单实现:查找"4:info"标记,提取info字典并计算SHA1 + */ +function extractInfoHashFromTorrent(buffer: Buffer): string | undefined { + // 查找 "4:info" 的位置 + const infoKeyPattern = Buffer.from('4:infod'); + let infoStart = buffer.indexOf(infoKeyPattern); + if (infoStart === -1) { + return undefined; + } + // info字典从 "4:info" 之后的 'd' 开始 + infoStart = infoStart + 6; // 跳过 "4:info" + + // 从infoStart开始,找到对应的字典结束位置 + // bencode 字典以 'd' 开始,以 'e' 结束 + let depth = 0; + let infoEnd = infoStart; + + for (let i = infoStart; i < buffer.length; i++) { + const byte = buffer[i]; + + if (byte === 0x64 || byte === 0x6c) { // 'd' or 'l' (dict or list start) + depth++; + } else if (byte === 0x65) { // 'e' (end) + depth--; + if (depth === 0) { + infoEnd = i + 1; + break; + } + } else if (byte >= 0x30 && byte <= 0x39) { // '0'-'9' (string length) + // 解析字符串长度 + let lengthStr = ''; + let j = i; + while (j < buffer.length && buffer[j] >= 0x30 && buffer[j] <= 0x39) { + lengthStr += String.fromCharCode(buffer[j]); + j++; + } + if (buffer[j] === 0x3a) { // ':' + const strLength = parseInt(lengthStr, 10); + i = j + strLength; // 跳过字符串内容 + } + } else if (byte === 0x69) { // 'i' (integer start) + // 跳过整数直到 'e' + while (i < buffer.length && buffer[i] !== 0x65) { + i++; + } + } + } + + if (infoEnd <= infoStart) { + return undefined; + } + + const infoBuffer = buffer.subarray(infoStart, infoEnd); + return crypto.createHash('sha1').update(infoBuffer).digest('hex'); +} + export async function startWebtorrent(magnet: string) { const torrent = await client.get(magnet); const dLCInfo = getIndexVersionByMegnet(magnet); @@ -117,6 +181,7 @@ export async function startWebtorrent(magnet: string) { } } else { console.warn('没找到链接对应的索引信息', magnet); + return null; } } @@ -305,9 +370,8 @@ function getProgress(magnet?: string) { const params = new URL(magnet).searchParams; const xt = params.get('xt'); const torrent = client.torrents.filter((torrent) => { - const hash = xt.split(':')?.pop(); - // console.debug('hash', hash, 'infohash', torrent.infoHash); - return torrent.infoHash === hash; + const hash = xt.split(':')?.pop()?.toLowerCase(); + return torrent.infoHash.toLowerCase() === hash; })[0]; if (torrent) { @@ -484,14 +548,28 @@ async function restoreTorrentsFromFiles() { for (const torrentFile of torrentFiles) { const torrentPath = path.join(versionPath, torrentFile); - const infoHash = torrentFile.replace('.torrent', ''); + + // 读取并解析torrent文件以获取真实的infoHash + let torrentBuffer: Buffer; + let infoHash: string | undefined; + try { + torrentBuffer = fs.readFileSync(torrentPath); + infoHash = extractInfoHashFromTorrent(torrentBuffer); + } catch (error) { + console.error(`解析种子文件失败 ${torrentPath}:`, error); + continue; + } + + if (!infoHash) { + console.warn(`种子文件 ${torrentPath} 没有有效的infoHash,跳过`); + continue; + } // 检查是否已经添加了这个种子 const existingTorrent = client.torrents.find( - (t) => t.infoHash === infoHash, + (t) => t.infoHash.toLowerCase() === infoHash.toLowerCase(), ); if (existingTorrent) { - console.debug(`种子 ${infoHash} 已经存在,跳过`); continue; } @@ -509,31 +587,52 @@ async function restoreTorrentsFromFiles() { (file) => file !== torrentFile && !file.endsWith('.torrent'), ); if (otherFiles.length === 0) { - console.debug( - `版本 ${dlcId}/${version} 没有实际文件,跳过种子 ${infoHash}`, - ); continue; } try { - // 读取.torrent文件 - const torrentBuffer = fs.readFileSync(torrentPath); - - // 添加种子到WebTorrent + // 添加种子到WebTorrent(使用之前已读取的torrentBuffer) client.add(torrentBuffer, { path: versionPath }, (torrent) => { console.debug( `恢复种子成功: ${torrent.name} (${torrent.infoHash})`, ); - console.debug(`存储路径: ${torrent.path}`); - - // 根据用户要求,恢复种子后让下载处于暂停状态 - console.debug(`恢复种子后暂停下载: ${torrent.name}`); - torrent.pause(); - // 如果文件已经完整,切换到做种模式 - if (torrent.progress === 1) { - console.debug(`文件已完整,切换到做种模式: ${torrent.name}`); - torrent.deselect(0, torrent.pieces.length - 1, 0); + // 监听做种相关事件 + torrent.on('wire', (wire) => { + console.debug(`[${torrent.name}] 新连接: ${wire.remoteAddress}`); + }); + + torrent.on('upload', (bytes) => { + console.debug(`[${torrent.name}] 上传: ${bytes} bytes, 总上传: ${torrent.uploaded}, 速度: ${torrent.uploadSpeed} B/s`); + }); + + // 定时打印状态 + const statusInterval = setInterval(() => { + // @ts-ignore - destroyed 属性存在但类型定义中没有 + if (torrent.destroyed) { + clearInterval(statusInterval); + return; + } + console.debug( + `[${torrent.name}] 状态: peers=${torrent.numPeers}, 上传=${torrent.uploaded}, 上传速度=${torrent.uploadSpeed} B/s, ratio=${torrent.ratio.toFixed(2)}` + ); + }, 30000); // 每30秒打印一次 + + // 等待验证完成后开始做种 + const handleReady = () => { + // 验证完成后恢复做种(不暂停) + console.debug(`[${torrent.name}] 验证完成,开始做种`); + torrent.resume(); + }; + + // 如果已经验证完成则直接处理 + if (torrent.done) { + handleReady(); + } else { + // 等待 ready 事件(验证完成) + torrent.once('ready', handleReady); + // 如果部分文件已下载,也监听 done 事件 + torrent.once('done', handleReady); } }); diff --git a/src/renderer/pages/joint-build/index.tsx b/src/renderer/pages/joint-build/index.tsx index bdfac81..2ba985a 100644 --- a/src/renderer/pages/joint-build/index.tsx +++ b/src/renderer/pages/joint-build/index.tsx @@ -24,13 +24,25 @@ const formatBytes = (bytes: number): string => { return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; }; -// 获取做种状态(只关心已下载完成的资源) -function getSeedingState(version: OneDLCInfo['versions'][string]): '未下载' | '已暂停做种' | '做种中' { - if (version.progress && version.progress.done) { - if (version.progress.paused) { - return '已暂停做种'; - } else { - return '做种中'; +// 获取做种状态(只要种子添加到 WebTorrent 中就可以显示) +function getSeedingState(version: OneDLCInfo['versions'][string]): '未下载' | '已暂停做种' | '做种中' | '验证中' { + // 只要 progress 存在,就说明种子已添加到 WebTorrent + if (version.progress) { + // 完全完成或进度 >= 99% + if (version.progress.done || version.progress.progress >= 0.99) { + if (version.progress.paused) { + return '已暂停做种'; + } else { + return '做种中'; + } + } + // 有进度但未完成(验证中或部分下载) + if (version.progress.progress > 0) { + if (version.progress.paused) { + return '已暂停做种'; + } else { + return '验证中'; + } } } return '未下载'; @@ -43,6 +55,8 @@ function getStateColor(state: string): string { return '#52c41a'; case '已暂停做种': return '#faad14'; + case '验证中': + return '#1890ff'; default: return '#999'; } @@ -287,18 +301,21 @@ export default function JointBuild() { {progress.numPeers > 0 && ( 节点: {progress.numPeers} )} + {state === '验证中' && ( + 进度: {Math.round(progress.progress * 100)}% + )}
)}
- {state === '已暂停做种' && ( + {(state === '已暂停做种' || state === '验证中') && ( )} {state === '做种中' && ( From 777d39f8a6e41d4fef3a63ad8837d6b46586f86c Mon Sep 17 00:00:00 2001 From: forceOfPx <3239735744@qq.com> Date: Sat, 7 Feb 2026 01:20:44 +0800 Subject: [PATCH 11/12] =?UTF-8?q?=E9=99=90=E9=80=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/dlc/index.ts | 42 ++++++++++++++++++++--- src/main/dlc/type-info.ts | 4 +++ src/main/preload.ts | 8 +++++ src/renderer/pages/joint-build/index.scss | 25 ++++++++++++++ src/renderer/pages/joint-build/index.tsx | 41 +++++++++++++++++++++- 5 files changed, 114 insertions(+), 6 deletions(-) diff --git a/src/main/dlc/index.ts b/src/main/dlc/index.ts index 756b7dc..281d7d5 100644 --- a/src/main/dlc/index.ts +++ b/src/main/dlc/index.ts @@ -5,6 +5,8 @@ import { queryWebtorrentHandle, pauseWebtorrentHandle, removeWebtorrentHandle, + setUploadLimitHandle, + getUploadLimitHandle, DLCIndex, DLCId, OneDLCInfo, @@ -18,6 +20,8 @@ import fs, { existsSync, mkdirSync, readFileSync, unlinkSync } from 'fs'; import { compare } from 'semver'; let client: WebTorrent.Instance | null = null; +// 上传速度限制 (bytes/s),0 表示不限速 +let uploadSpeedLimit = 0; const getWebTorrent = async () => { const WebTorrentClass: WebTorrent.WebTorrent = // @ts-ignore @@ -81,6 +85,12 @@ export default async function init(ipcMain: IpcMain) { ipcHandle(ipcMain, logsWebtorrentHandle, async (_event, magnet: string) => logsWebtorrent(magnet), ); + ipcHandle(ipcMain, setUploadLimitHandle, async (_event, limit: number) => + setUploadLimit(limit), + ); + ipcHandle(ipcMain, getUploadLimitHandle, async (_event) => + getUploadLimit(), + ); // 检查种子文件和对应的实际文件,如果有文件,就添加到webtorrent中 await restoreTorrentsFromFiles(); } @@ -238,6 +248,32 @@ export async function logsWebtorrent(magnet: string) { return null; } +/** + * 设置上传速度限制 + * @param limit 限制速度 (bytes/s),0 表示不限速 + */ +export function setUploadLimit(limit: number) { + uploadSpeedLimit = limit; + if (client) { + // WebTorrent 支持 throttleUpload 方法来动态设置上传限速 + // @ts-ignore - throttleUpload 方法存在但类型定义中没有 + if (typeof client.throttleUpload === 'function') { + // @ts-ignore + // -1 表示禁用限速,正数表示限速值 (bytes/s) + client.throttleUpload(limit > 0 ? limit : -1); + } + } + console.debug(`上传速度限制设置为: ${limit > 0 ? (limit / 1024).toFixed(1) + ' KB/s' : '不限速'}`); + return { success: true, limit }; +} + +/** + * 获取当前上传速度限制 + */ +export function getUploadLimit(): number { + return uploadSpeedLimit; +} + const dlcIndexPath = path.join( appPath, 'external-resources', @@ -602,11 +638,7 @@ async function restoreTorrentsFromFiles() { console.debug(`[${torrent.name}] 新连接: ${wire.remoteAddress}`); }); - torrent.on('upload', (bytes) => { - console.debug(`[${torrent.name}] 上传: ${bytes} bytes, 总上传: ${torrent.uploaded}, 速度: ${torrent.uploadSpeed} B/s`); - }); - - // 定时打印状态 + // 定时打印状态(包含上传信息) const statusInterval = setInterval(() => { // @ts-ignore - destroyed 属性存在但类型定义中没有 if (torrent.destroyed) { diff --git a/src/main/dlc/type-info.ts b/src/main/dlc/type-info.ts index 37d0a5d..d8afc53 100644 --- a/src/main/dlc/type-info.ts +++ b/src/main/dlc/type-info.ts @@ -13,6 +13,10 @@ export const queryWebtorrentHandle = `${channel}query`; export const logsWebtorrentHandle = `${channel}logs`; +export const setUploadLimitHandle = `${channel}setUploadLimit`; + +export const getUploadLimitHandle = `${channel}getUploadLimit`; + export const dLCIds = [ 'PDF_TAR', 'VOICE_TAR', diff --git a/src/main/preload.ts b/src/main/preload.ts index e456e93..23c3547 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -34,6 +34,8 @@ import { queryWebtorrentHandle, removeWebtorrentHandle, startWebtorrentHandle, + setUploadLimitHandle, + getUploadLimitHandle, DLCId, } from './dlc/type-info'; @@ -171,6 +173,12 @@ const mainHandle = { logsWebtorrentHandle: async (url: string) => { return ipcInvoke(logsWebtorrentHandle, url); }, + setUploadLimit: async (limit: number) => { + return ipcInvoke<{ success: boolean; limit: number }>(setUploadLimitHandle, limit); + }, + getUploadLimit: async () => { + return ipcInvoke(getUploadLimitHandle); + }, }; export type MainHandle = typeof mainHandle; diff --git a/src/renderer/pages/joint-build/index.scss b/src/renderer/pages/joint-build/index.scss index 93933b5..b3fa2d5 100644 --- a/src/renderer/pages/joint-build/index.scss +++ b/src/renderer/pages/joint-build/index.scss @@ -86,6 +86,31 @@ .switch-label { font-size: 14px; color: #333; + white-space: nowrap; + } + + &.upload-limit-card { + margin-top: 12px; + + .slider-container { + display: flex; + align-items: center; + gap: 16px; + flex: 1; + margin-left: 24px; + + .ant-slider { + flex: 1; + margin: 0; + } + + .limit-value { + font-size: 13px; + color: #666; + min-width: 80px; + text-align: right; + } + } } } diff --git a/src/renderer/pages/joint-build/index.tsx b/src/renderer/pages/joint-build/index.tsx index 2ba985a..096870f 100644 --- a/src/renderer/pages/joint-build/index.tsx +++ b/src/renderer/pages/joint-build/index.tsx @@ -1,4 +1,4 @@ -import { Switch, Button, Progress, message } from 'antd'; +import { Switch, Button, Progress, message, Slider } from 'antd'; import { NavLink } from 'react-router-dom'; import { useState, useEffect, useCallback } from 'react'; import { LeftOutlined } from '@ant-design/icons'; @@ -9,6 +9,7 @@ import './index.scss'; const STORAGE_KEY_MASTER = 'joint_build_master_switch'; const STORAGE_KEY_DISK_PATH = 'joint_build_disk_path'; const STORAGE_KEY_WELCOME = 'ai_learning_assistant_welcome_shown'; // 欢迎弹窗用户选择 +const STORAGE_KEY_UPLOAD_LIMIT = 'joint_build_upload_limit'; // 字节转GB const bytesToGB = (bytes: number): number => { @@ -70,6 +71,7 @@ export default function JointBuild() { const [diskLoading, setDiskLoading] = useState(false); const [dLCIndex, setDLCIndex] = useState([]); const [refreshTrigger, setRefreshTrigger] = useState(1); + const [uploadLimit, setUploadLimit] = useState(0); // 0 表示不限速,单位 KB/s // 定时刷新DLC数据 useEffect(() => { @@ -124,6 +126,13 @@ export default function JointBuild() { const initialPath = savedDiskPath || 'C:\\'; setDiskPath(initialPath); + // 加载上传限速设置 + const savedUploadLimit = localStorage.getItem(STORAGE_KEY_UPLOAD_LIMIT); + const initialLimit = savedUploadLimit ? parseInt(savedUploadLimit, 10) : 0; + setUploadLimit(initialLimit); + // 同步到后端 + window.mainHandle.setUploadLimit(initialLimit * 1024).catch(console.error); // 转换为 bytes/s + // 初始化时获取磁盘信息 fetchDiskInfo(initialPath); }, [fetchDiskInfo]); @@ -164,6 +173,17 @@ export default function JointBuild() { } }; + // 处理上传限速变化 + const handleUploadLimitChange = async (value: number) => { + setUploadLimit(value); + localStorage.setItem(STORAGE_KEY_UPLOAD_LIMIT, String(value)); + try { + await window.mainHandle.setUploadLimit(value * 1024); // 转换为 bytes/s + } catch (error) { + console.error('设置上传限速失败:', error); + } + }; + // 处理开始做种 const handleStartSeeding = async (magnet: string) => { try { @@ -244,6 +264,25 @@ export default function JointBuild() { onChange={handleMasterSwitchChange} />
+
+ 上传限速 +
+ value === 0 ? '不限速' : `${value} KB/s` + }} + style={{ flex: 1 }} + /> + + {uploadLimit === 0 ? '不限速' : `${uploadLimit} KB/s`} + +
+
From b7e1705c4f7ac9dd5a6e543570b5432628ec6303 Mon Sep 17 00:00:00 2001 From: forceOfPx <3239735744@qq.com> Date: Sat, 7 Feb 2026 01:35:10 +0800 Subject: [PATCH 12/12] =?UTF-8?q?ui=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/pages/joint-build/index.scss | 2 +- src/renderer/pages/joint-build/index.tsx | 43 ++++++++++++++++------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/renderer/pages/joint-build/index.scss b/src/renderer/pages/joint-build/index.scss index b3fa2d5..aea46c9 100644 --- a/src/renderer/pages/joint-build/index.scss +++ b/src/renderer/pages/joint-build/index.scss @@ -96,7 +96,7 @@ display: flex; align-items: center; gap: 16px; - flex: 1; + width: 30%; margin-left: 24px; .ant-slider { diff --git a/src/renderer/pages/joint-build/index.tsx b/src/renderer/pages/joint-build/index.tsx index 096870f..071c2f3 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, Slider } from 'antd'; import { NavLink } from 'react-router-dom'; -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { LeftOutlined } from '@ant-design/icons'; import jointBuildIcon from '../../../../icons/joint_build.png'; import { DLCIndex, OneDLCInfo } from '../../../main/dlc/type-info'; @@ -72,16 +72,22 @@ export default function JointBuild() { const [dLCIndex, setDLCIndex] = useState([]); const [refreshTrigger, setRefreshTrigger] = useState(1); const [uploadLimit, setUploadLimit] = useState(0); // 0 表示不限速,单位 KB/s + const uploadLimitTimerRef = useRef | null>(null); // 定时刷新DLC数据 useEffect(() => { window.mainHandle .queryWebtorrentHandle() .then((newDLCIndex) => setDLCIndex(newDLCIndex)) - .catch(console.error); + .catch((err) => { + // 降低日志频率,只在首次或间隔打印 + if (refreshTrigger === 1 || refreshTrigger % 10 === 0) { + console.error('查询WebTorrent状态失败:', err); + } + }); const timeout = setTimeout( () => setRefreshTrigger(refreshTrigger + 1), - 1000, + 3000, // 从1秒改为3秒,降低刷新和日志频率 ); return () => { clearTimeout(timeout); @@ -147,6 +153,10 @@ export default function JointBuild() { return () => { window.removeEventListener('joint-build-status-changed', handleStatusChange as EventListener); + // 清理上传限速防抖定时器 + if (uploadLimitTimerRef.current) { + clearTimeout(uploadLimitTimerRef.current); + } }; }, []); @@ -173,15 +183,24 @@ export default function JointBuild() { } }; - // 处理上传限速变化 - const handleUploadLimitChange = async (value: number) => { + // 处理上传限速变化(防抖:UI 实时更新,后端调用延迟) + const handleUploadLimitChange = (value: number) => { setUploadLimit(value); - localStorage.setItem(STORAGE_KEY_UPLOAD_LIMIT, String(value)); - try { - await window.mainHandle.setUploadLimit(value * 1024); // 转换为 bytes/s - } catch (error) { - console.error('设置上传限速失败:', error); + + // 清除之前的定时器 + if (uploadLimitTimerRef.current) { + clearTimeout(uploadLimitTimerRef.current); } + + // 防抖:500ms 后才同步到后端 + uploadLimitTimerRef.current = setTimeout(async () => { + localStorage.setItem(STORAGE_KEY_UPLOAD_LIMIT, String(value)); + try { + await window.mainHandle.setUploadLimit(value * 1024); // 转换为 bytes/s + } catch (error) { + console.error('设置上传限速失败:', error); + } + }, 500); }; // 处理开始做种 @@ -269,14 +288,14 @@ export default function JointBuild() {
value === 0 ? '不限速' : `${value} KB/s` }} - style={{ flex: 1 }} + style={{ width: '50%' }} /> {uploadLimit === 0 ? '不限速' : `${uploadLimit} KB/s`}