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__}
+
-
window.open('https://ai-learning-assistant.com/joint-build', '_blank')}
- >
-
- 共建计划
-
+
+
+
+ 共建计划
+
+
([
+ {
+ 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,
+ },
+ ]);
+
+ // 从 localStorage 加载配置
+ useEffect(() => {
+ const savedMasterSwitch = localStorage.getItem(STORAGE_KEY_MASTER);
+ if (savedMasterSwitch !== null) {
+ setMasterSwitch(savedMasterSwitch === 'true');
+ }
+
+ 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);
+ if (savedDiskPath) {
+ setDiskPath(savedDiskPath);
+ }
+ }, []);
+
+ // 保存配置到 localStorage
+ const saveConfig = (master: boolean, mods: ModuleItem[]) => {
+ 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 = (checked: boolean) => {
+ setMasterSwitch(checked);
+ saveConfig(checked, modules);
+ if (checked) {
+ message.success('共建计划已开启');
+ } else {
+ message.info('共建计划已关闭');
+ }
+ };
+
+ const handleModuleToggle = (id: string, checked: boolean) => {
+ const newModules = modules.map((mod) =>
+ mod.id === id ? { ...mod, enabled: checked } : mod
+ );
+ setModules(newModules);
+ saveConfig(masterSwitch, newModules);
+ };
+
+ const handleChangePath = async () => {
+ // 这里可以调用 electron 的对话框选择文件夹
+ // 暂时用 prompt 模拟
+ const newPath = window.prompt('请输入新的磁盘路径:', diskPath);
+ if (newPath) {
+ setDiskPath(newPath);
+ localStorage.setItem(STORAGE_KEY_DISK_PATH, newPath);
+ message.success('路径已更新');
+ }
+ };
+
+ const renderRecommendLevel = (level: number) => {
+ const fires = [];
+ for (let i = 0; i < 3; i++) {
+ fires.push(
+
+ 🔥
+
+ );
+ }
+ return fires;
+ };
+
+ const diskUsedPercent = Math.round((diskUsed / diskTotal) * 100);
+
+ return (
+
+
+
+
+
+

+
+
共建计划
+
通过闲时分享少量带宽,不仅能加速您的下载,也能帮助其他学习者更快获取大模型与课程资源。
+
+
+
+
+
+
+
可用磁盘空间
+
+ 磁盘路径:{diskPath}
+ 更换路径
+
+
+
+
+
+
共建参与模块设置
+
管理参与哪些模块的共建计划,请根据您电脑的磁盘空余选择开启
+
+ {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() {
共建计划
+
P2P测试
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
+
- {state === '已暂停做种' && (
+ {(state === '已暂停做种' || state === '验证中') && (
handleStartSeeding(version.magnet)}
disabled={!masterSwitch}
>
- 开始做种
+ {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`}