diff --git a/.husky/pre-push b/.husky/pre-push index 0c95cbfdac..e7b0a02e9c 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -8,11 +8,9 @@ if git diff --cached --name-only | grep -q '^src-tauri/'; then fi fi -remote_name="$1" -remote_url=$(git remote get-url "$remote_name") - -if [[ "$remote_url" =~ github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$ ]]; then - echo "[pre-push] Detected push to clash-verge-rev/clash-verge-rev ($remote_url)" +# 检查所有 remote url 是否有目标仓库 +if git remote -v | grep -Eq 'github\\.com[:/]+clash-verge-rev/clash-verge-rev(\\.git)?'; then + echo "[pre-push] Detected push to clash-verge-rev/clash-verge-rev" echo "[pre-push] Running pnpm format:check..." pnpm format:check diff --git a/UPDATELOG.md b/UPDATELOG.md index ae48099dcd..fab7ad3620 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -3,15 +3,25 @@ ### 🐞 修复问题 - 修复系统代理端口不同步问题 -- 修复 自定义 `css` 背景图无法生效问题 +- 修复自定义 `css` 背景图无法生效问题 +- 修复在轻量模式下快速点击托盘图标带来的竞争态卡死问题 +- 修复同时开启静默启动与自动进入轻量模式后,自动进入轻量模式失效的问题 +- 修复静默启动时托盘工具栏轻量模式开启与关闭状态的同步 +- 修复导入订阅时非 http 协议链接被错误尝试导入 ### ✨ 新增功能 - `sidecar` 模式下清理多余的内核进程,防止运行出现异常 +- 新 macOS 下 TUN 和系统代理模式托盘图标(暂测) ### 🚀 优化改进 - 优化重构订阅切换逻辑,可以随时中断载入过程,防止卡死 +- 引入事件驱动代理管理器,优化代理配置更新逻辑,防止卡死 + +### 🗑️ 移除内容 + +- 移除了 macOS tray 图标显示网络速率 ## v2.3.1 diff --git a/package.json b/package.json index 6106f1395d..3c720f8e00 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,10 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@juggle/resize-observer": "^3.4.0", - "@mui/icons-material": "^7.1.1", - "@mui/lab": "7.0.0-beta.13", - "@mui/material": "^7.1.1", - "@mui/x-data-grid": "^8.5.2", + "@mui/icons-material": "^7.1.2", + "@mui/lab": "7.0.0-beta.14", + "@mui/material": "^7.1.2", + "@mui/x-data-grid": "^8.5.3", "@tauri-apps/api": "2.5.0", "@tauri-apps/plugin-clipboard-manager": "^2.2.3", "@tauri-apps/plugin-dialog": "^2.2.2", @@ -45,24 +45,21 @@ "@tauri-apps/plugin-shell": "2.2.2", "@tauri-apps/plugin-updater": "2.8.1", "@tauri-apps/plugin-window-state": "^2.2.3", - "@types/d3-shape": "^3.1.7", "@types/json-schema": "^7.0.15", + "json-schema": "^0.4.0", "ahooks": "^3.8.5", "axios": "^1.10.0", "chart.js": "^4.5.0", "cli-color": "^2.0.4", - "d3-shape": "^3.2.0", "dayjs": "1.11.13", "foxact": "^0.2.49", "glob": "^11.0.3", "i18next": "^25.2.1", - "js-base64": "^3.7.7", "js-yaml": "^4.1.0", "lodash-es": "^4.17.21", "monaco-editor": "^0.52.2", "monaco-yaml": "^5.4.0", "nanoid": "^5.1.5", - "peggy": "^5.0.3", "react": "19.1.0", "react-chartjs-2": "^5.3.0", "react-dom": "19.1.0", @@ -82,7 +79,6 @@ "devDependencies": { "@actions/github": "^6.0.1", "@tauri-apps/cli": "2.5.0", - "@types/js-cookie": "^3.0.6", "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", "@types/react": "19.1.8", @@ -93,13 +89,11 @@ "commander": "^14.0.0", "cross-env": "^7.0.3", "https-proxy-agent": "^7.0.6", - "husky": "^9.1.7", "meta-json-schema": "^1.19.10", "node-fetch": "^3.3.2", "prettier": "^3.5.3", - "pretty-quick": "^4.2.2", "sass": "^1.89.2", - "terser": "^5.43.0", + "terser": "^5.43.1", "typescript": "^5.8.3", "vite": "^6.3.5", "vite-plugin-monaco-editor": "^1.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 078f7d22b8..568e04c6d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,17 +27,17 @@ importers: specifier: ^3.4.0 version: 3.4.0 '@mui/icons-material': - specifier: ^7.1.1 - version: 7.1.1(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + specifier: ^7.1.2 + version: 7.1.2(@mui/material@7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) '@mui/lab': - specifier: 7.0.0-beta.13 - version: 7.0.0-beta.13(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 7.0.0-beta.14 + version: 7.0.0-beta.14(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mui/material': - specifier: ^7.1.1 - version: 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^7.1.2 + version: 7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mui/x-data-grid': - specifier: ^8.5.2 - version: 8.5.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^8.5.3 + version: 8.5.3(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tauri-apps/api': specifier: 2.5.0 version: 2.5.0 @@ -68,9 +68,6 @@ importers: '@tauri-apps/plugin-window-state': specifier: ^2.2.3 version: 2.2.3 - '@types/d3-shape': - specifier: ^3.1.7 - version: 3.1.7 '@types/json-schema': specifier: ^7.0.15 version: 7.0.15 @@ -86,9 +83,6 @@ importers: cli-color: specifier: ^2.0.4 version: 2.0.4 - d3-shape: - specifier: ^3.2.0 - version: 3.2.0 dayjs: specifier: 1.11.13 version: 1.11.13 @@ -101,12 +95,12 @@ importers: i18next: specifier: ^25.2.1 version: 25.2.1(typescript@5.8.3) - js-base64: - specifier: ^3.7.7 - version: 3.7.7 js-yaml: specifier: ^4.1.0 version: 4.1.0 + json-schema: + specifier: ^0.4.0 + version: 0.4.0 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -119,9 +113,6 @@ importers: nanoid: specifier: ^5.1.5 version: 5.1.5 - peggy: - specifier: ^5.0.3 - version: 5.0.3 react: specifier: 19.1.0 version: 19.1.0 @@ -174,9 +165,6 @@ importers: '@tauri-apps/cli': specifier: 2.5.0 version: 2.5.0 - '@types/js-cookie': - specifier: ^3.0.6 - version: 3.0.6 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -191,10 +179,10 @@ importers: version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-legacy': specifier: ^6.1.1 - version: 6.1.1(terser@5.43.0)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)) + version: 6.1.1(terser@5.43.1)(vite@6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1)) '@vitejs/plugin-react': specifier: 4.5.2 - version: 4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)) + version: 4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1)) adm-zip: specifier: ^0.5.16 version: 0.5.16 @@ -207,9 +195,6 @@ importers: https-proxy-agent: specifier: ^7.0.6 version: 7.0.6 - husky: - specifier: ^9.1.7 - version: 9.1.7 meta-json-schema: specifier: ^1.19.10 version: 1.19.10 @@ -219,27 +204,24 @@ importers: prettier: specifier: ^3.5.3 version: 3.5.3 - pretty-quick: - specifier: ^4.2.2 - version: 4.2.2(prettier@3.5.3) sass: specifier: ^1.89.2 version: 1.89.2 terser: - specifier: ^5.43.0 - version: 5.43.0 + specifier: ^5.43.1 + version: 5.43.1 typescript: specifier: ^5.8.3 version: 5.8.3 vite: specifier: ^6.3.5 - version: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1) + version: 6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1) vite-plugin-monaco-editor: specifier: ^1.1.0 version: 1.1.0(monaco-editor@0.52.2) vite-plugin-svgr: specifier: ^4.3.0 - version: 4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)) + version: 4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1)) packages: @@ -1023,27 +1005,27 @@ packages: '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} - '@mui/core-downloads-tracker@7.1.1': - resolution: {integrity: sha512-yBckQs4aQ8mqukLnPC6ivIRv6guhaXi8snVl00VtyojBbm+l6VbVhyTSZ68Abcx7Ah8B+GZhrB7BOli+e+9LkQ==} + '@mui/core-downloads-tracker@7.1.2': + resolution: {integrity: sha512-0gLO1PvbJwSYe5ji021tGj6HFqrtEPMGKK4L1zWwRbhzrWWUumUJvMvJUsIgWQIYQsgOnhq9k2Fc1BxLGHDsAg==} - '@mui/icons-material@7.1.1': - resolution: {integrity: sha512-X37+Yc8QpEnl0sYmz+WcLFy2dWgNRzbswDzLPXG7QU1XDVlP5TPp1HXjdmCupOWLL/I9m1fyhcyZl8/HPpp/Cg==} + '@mui/icons-material@7.1.2': + resolution: {integrity: sha512-slqJByDub7Y1UcokrM17BoMBMvn8n7daXFXVoTv0MEH5k3sHjmsH8ql/Mt3s9vQ20cORDr83UZ448TEGcbrXtw==} engines: {node: '>=14.0.0'} peerDependencies: - '@mui/material': ^7.1.1 + '@mui/material': ^7.1.2 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': optional: true - '@mui/lab@7.0.0-beta.13': - resolution: {integrity: sha512-wLSeePenug3+/kek4cFMIF3QZVC2fHt2Z3O3HwOFvakgErmT39WltYsNpWNojCnXUqcIExUp9xNW0Wk+tJShgA==} + '@mui/lab@7.0.0-beta.14': + resolution: {integrity: sha512-pn+ZvylDcBKQOo17oa/PhtIA/UFQFq8RvpN+r/jHrztz/CjMDju2CWBne0txvQ5JIS8uTIGp2/IsTa7II1g5wg==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 '@emotion/styled': ^11.3.0 - '@mui/material': ^7.1.1 + '@mui/material': ^7.1.2 '@mui/material-pigment-css': ^7.1.1 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1058,8 +1040,8 @@ packages: '@types/react': optional: true - '@mui/material@7.1.1': - resolution: {integrity: sha512-mTpdmdZCaHCGOH3SrYM41+XKvNL0iQfM9KlYgpSjgadXx/fEKhhvOktxm8++Xw6FFeOHoOiV+lzOI8X1rsv71A==} + '@mui/material@7.1.2': + resolution: {integrity: sha512-Z5PYKkA6Kd8vS04zKxJNpwuvt6IoMwqpbidV7RCrRQQKwczIwcNcS8L6GnN4pzFYfEs+N9v6co27DmG07rcnoA==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1135,8 +1117,8 @@ packages: '@types/react': optional: true - '@mui/x-data-grid@8.5.2': - resolution: {integrity: sha512-4KzawLZqRKp3KcGKsTDVz7zkEjACllQD5Zb8ds1QKlA6C3/oIoSU7PsemFLj+RL3rT5aORsLMBl97/egQ5tUhA==} + '@mui/x-data-grid@8.5.3': + resolution: {integrity: sha512-rA+de5yre16KFIGKRBUwb8kYIdn7SPPrZsBy1P3QxisqhC+Wz2AQg/W6WWv71aFHwplmGwsFUjU6d47Fy/wvXg==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.9.0 @@ -1151,8 +1133,8 @@ packages: '@emotion/styled': optional: true - '@mui/x-internals@8.5.2': - resolution: {integrity: sha512-5YhB2AekK7G8d0YrAjg3WNf0uy3V73JD98WNxJhbIlCraQgl8QOQzr2zNO7MAf/X7mZQtjpjuAsiG3+gI2NVyg==} + '@mui/x-internals@8.5.3': + resolution: {integrity: sha512-ImCg4E3DT3XoDIZO0pNCbB7iw14N+YCFY3J1V28POwCD7P2f3HSIz4jwzM006oYxI6bqeE6LMfpdPRDW6s6dQw==} engines: {node: '>=14.0.0'} peerDependencies: '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 @@ -1288,14 +1270,6 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} - '@peggyjs/from-mem@2.0.0': - resolution: {integrity: sha512-f+pL/s2DiT+2dxwheSoJT0P/KJy/s0klzE+ZqRdXHlkeyFk/DpKtyjLZIiA79kx56g3oEPA8Zu9EzEKzAwuvhw==} - engines: {node: '>=20'} - - '@pkgr/core@0.2.7': - resolution: {integrity: sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -1592,12 +1566,6 @@ packages: '@types/babel__traverse@7.20.7': resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} - '@types/d3-path@3.1.1': - resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} - - '@types/d3-shape@3.1.7': - resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1610,9 +1578,6 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - '@types/js-cookie@3.0.6': - resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} - '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -1875,14 +1840,6 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} - - d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} - d@1.0.2: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} @@ -2132,11 +2089,6 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - husky@9.1.7: - resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} - engines: {node: '>=18'} - hasBin: true - i18next@25.2.1: resolution: {integrity: sha512-+UoXK5wh+VlE1Zy5p6MjcvctHXAhRwQKCxiJD8noKZzIXmnAX8gdHX5fLPA3MEVxEN4vbZkQFy8N0LyD9tUqPw==} peerDependencies: @@ -2145,10 +2097,6 @@ packages: typescript: optional: true - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - immutable@5.1.2: resolution: {integrity: sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==} @@ -2211,9 +2159,6 @@ packages: resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} engines: {node: 20 || >=22} - js-base64@3.7.7: - resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} - js-cookie@3.0.5: resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} engines: {node: '>=14'} @@ -2238,6 +2183,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -2435,10 +2383,6 @@ packages: peerDependencies: monaco-editor: '>=0.36' - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2512,11 +2456,6 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - peggy@5.0.3: - resolution: {integrity: sha512-QErYmLjj/ehiNNJRqx2qb36hzkanuascpMqREs2RQqaXhU3cflIRScP/u2BoobIfu/FaeI3GGxNB/vFX/Ar9lg==} - engines: {node: '>=20'} - hasBin: true - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2537,13 +2476,6 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-quick@4.2.2: - resolution: {integrity: sha512-uAh96tBW1SsD34VhhDmWuEmqbpfYc/B3j++5MC/6b3Cb8Ow7NJsvKFhg0eoGu2xXX+o9RkahkTK6sUdd8E7g5w==} - engines: {node: '>=14'} - hasBin: true - peerDependencies: - prettier: ^3.0.0 - prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -2717,11 +2649,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} - engines: {node: '>=10'} - hasBin: true - server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} @@ -2746,10 +2673,6 @@ packages: sockette@2.0.6: resolution: {integrity: sha512-W6iG8RGV6Zife3Cj+FhuyHV447E6fqFM2hKmnaQrTvg3OydINV3Msj3WPFbX76blUlUxvQSMMMdrJxce8NqI5Q==} - source-map-generator@2.0.0: - resolution: {integrity: sha512-4KomB7QsJti7dFBAVF6SXHzuCNQauk4gE2CummcqPzl+eJqXz1CkkiBdVXXW3g8VGh23bxcdEVACOzrxpIqnUg==} - engines: {node: '>=20'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2815,8 +2738,8 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} - terser@5.43.0: - resolution: {integrity: sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==} + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} engines: {node: '>=10'} hasBin: true @@ -2824,9 +2747,6 @@ packages: resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} engines: {node: '>=0.12'} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.13: resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} engines: {node: '>=12.0.0'} @@ -3943,20 +3863,20 @@ snapshots: '@kurkle/color@0.3.4': {} - '@mui/core-downloads-tracker@7.1.1': {} + '@mui/core-downloads-tracker@7.1.2': {} - '@mui/icons-material@7.1.1(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': + '@mui/icons-material@7.1.2(@mui/material@7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 - '@mui/material': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/material': 7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 optionalDependencies: '@types/react': 19.1.8 - '@mui/lab@7.0.0-beta.13(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mui/lab@7.0.0-beta.14(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 - '@mui/material': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/material': 7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) '@mui/types': 7.4.3(@types/react@19.1.8) '@mui/utils': 7.1.1(@types/react@19.1.8)(react@19.1.0) @@ -3969,10 +3889,10 @@ snapshots: '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) '@types/react': 19.1.8 - '@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mui/material@7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 - '@mui/core-downloads-tracker': 7.1.1 + '@mui/core-downloads-tracker': 7.1.2 '@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) '@mui/types': 7.4.3(@types/react@19.1.8) '@mui/utils': 7.1.1(@types/react@19.1.8)(react@19.1.0) @@ -4046,13 +3966,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 - '@mui/x-data-grid@8.5.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mui/x-data-grid@8.5.3(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 - '@mui/material': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/material': 7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) '@mui/utils': 7.1.1(@types/react@19.1.8)(react@19.1.0) - '@mui/x-internals': 8.5.2(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + '@mui/x-internals': 8.5.3(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) clsx: 2.1.1 prop-types: 15.8.1 react: 19.1.0 @@ -4064,7 +3984,7 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@mui/x-internals@8.5.2(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': + '@mui/x-internals@8.5.3(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 '@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) @@ -4193,12 +4113,6 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true - '@peggyjs/from-mem@2.0.0': - dependencies: - semver: 7.7.1 - - '@pkgr/core@0.2.7': {} - '@popperjs/core@2.11.8': {} '@rolldown/pluginutils@1.0.0-beta.11': {} @@ -4447,12 +4361,6 @@ snapshots: dependencies: '@babel/types': 7.27.6 - '@types/d3-path@3.1.1': {} - - '@types/d3-shape@3.1.7': - dependencies: - '@types/d3-path': 3.1.1 - '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -4467,8 +4375,6 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/js-cookie@3.0.6': {} - '@types/js-yaml@4.0.9': {} '@types/json-schema@7.0.15': {} @@ -4507,7 +4413,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-legacy@6.1.1(terser@5.43.0)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1))': + '@vitejs/plugin-legacy@6.1.1(terser@5.43.1)(vite@6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1))': dependencies: '@babel/core': 7.27.4 '@babel/preset-env': 7.27.2(@babel/core@7.27.4) @@ -4517,12 +4423,12 @@ snapshots: magic-string: 0.30.17 regenerator-runtime: 0.14.1 systemjs: 6.15.1 - terser: 5.43.0 - vite: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1) + terser: 5.43.1 + vite: 6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1))': + '@vitejs/plugin-react@4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1))': dependencies: '@babel/core': 7.27.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) @@ -4530,7 +4436,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.11 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1) + vite: 6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1) transitivePeerDependencies: - supports-color @@ -4728,12 +4634,6 @@ snapshots: csstype@3.1.3: {} - d3-path@3.1.0: {} - - d3-shape@3.2.0: - dependencies: - d3-path: 3.1.0 - d@1.0.2: dependencies: es5-ext: 0.10.64 @@ -5020,16 +4920,12 @@ snapshots: transitivePeerDependencies: - supports-color - husky@9.1.7: {} - i18next@25.2.1(typescript@5.8.3): dependencies: '@babel/runtime': 7.27.6 optionalDependencies: typescript: 5.8.3 - ignore@7.0.5: {} - immutable@5.1.2: {} import-fresh@3.3.1: @@ -5081,8 +4977,6 @@ snapshots: dependencies: '@isaacs/cliui': 8.0.2 - js-base64@3.7.7: {} - js-cookie@3.0.5: {} js-tokens@4.0.0: {} @@ -5097,6 +4991,8 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema@0.4.0: {} + json5@2.2.3: {} jsonc-parser@3.3.1: {} @@ -5429,8 +5325,6 @@ snapshots: vscode-uri: 3.1.0 yaml: 2.7.1 - mri@1.2.0: {} - ms@2.1.3: {} nanoid@3.3.11: {} @@ -5499,12 +5393,6 @@ snapshots: path-type@4.0.0: {} - peggy@5.0.3: - dependencies: - '@peggyjs/from-mem': 2.0.0 - commander: 14.0.0 - source-map-generator: 2.0.0 - picocolors@1.1.1: {} picomatch@2.3.1: @@ -5520,17 +5408,6 @@ snapshots: prettier@3.5.3: {} - pretty-quick@4.2.2(prettier@3.5.3): - dependencies: - '@pkgr/core': 0.2.7 - ignore: 7.0.5 - mri: 1.2.0 - picocolors: 1.1.1 - picomatch: 4.0.2 - prettier: 3.5.3 - tinyexec: 0.3.2 - tslib: 2.8.1 - prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -5726,8 +5603,6 @@ snapshots: semver@6.3.1: {} - semver@7.7.1: {} - server-only@0.0.1: {} set-cookie-parser@2.7.1: {} @@ -5747,8 +5622,6 @@ snapshots: sockette@2.0.6: {} - source-map-generator@2.0.0: {} - source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -5818,7 +5691,7 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 - terser@5.43.0: + terser@5.43.1: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.14.1 @@ -5830,8 +5703,6 @@ snapshots: es5-ext: 0.10.64 next-tick: 1.1.0 - tinyexec@0.3.2: {} - tinyglobby@0.2.13: dependencies: fdir: 6.4.4(picomatch@4.0.2) @@ -5930,18 +5801,18 @@ snapshots: dependencies: monaco-editor: 0.52.2 - vite-plugin-svgr@4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)): + vite-plugin-svgr@4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1)): dependencies: '@rollup/pluginutils': 5.1.4(rollup@4.40.2) '@svgr/core': 8.1.0(typescript@5.8.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3)) - vite: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1) + vite: 6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1) transitivePeerDependencies: - rollup - supports-color - typescript - vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1): + vite@6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1): dependencies: esbuild: 0.25.4 fdir: 6.4.4(picomatch@4.0.2) @@ -5952,7 +5823,7 @@ snapshots: optionalDependencies: fsevents: 2.3.3 sass: 1.89.2 - terser: 5.43.0 + terser: 5.43.1 yaml: 2.7.1 void-elements@3.1.0: {} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 173f7a46f6..659f5dabc6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1078,6 +1078,7 @@ dependencies = [ "reqwest", "reqwest_dav", "runas", + "scopeguard", "serde", "serde_json", "serde_yaml", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6908bba03c..cd95b11b81 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -80,6 +80,7 @@ gethostname = "1.0.2" hmac = "0.12.1" sha2 = "0.10.9" hex = "0.4.3" +scopeguard = "1.2.0" [target.'cfg(windows)'.dependencies] runas = "=1.2.0" @@ -93,6 +94,10 @@ winapi = { version = "0.3.9", features = [ "errhandlingapi", "minwindef", "winerror", + "tlhelp32", + "processthreadsapi", + "winhttp", + "winreg", ] } [target.'cfg(target_os = "linux")'.dependencies] diff --git a/src-tauri/icons/tray-icon-sys-mono-new.ico b/src-tauri/icons/tray-icon-sys-mono-new.ico new file mode 100644 index 0000000000..d28cc7cbcc Binary files /dev/null and b/src-tauri/icons/tray-icon-sys-mono-new.ico differ diff --git a/src-tauri/icons/tray-icon-tun-mono-new.ico b/src-tauri/icons/tray-icon-tun-mono-new.ico new file mode 100644 index 0000000000..d580583abd Binary files /dev/null and b/src-tauri/icons/tray-icon-tun-mono-new.ico differ diff --git a/src-tauri/src/cmd/network.rs b/src-tauri/src/cmd/network.rs index 23a9ded664..d3b438b725 100644 --- a/src-tauri/src/cmd/network.rs +++ b/src-tauri/src/cmd/network.rs @@ -1,18 +1,15 @@ use super::CmdResult; -use crate::core::EventDrivenProxyManager; +use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager}; use crate::wrap_err; use network_interface::NetworkInterface; use serde_yaml::Mapping; -use sysproxy::Sysproxy; -use tokio::task::spawn_blocking; /// get the system proxy #[tauri::command] pub async fn get_sys_proxy() -> CmdResult { - let current = spawn_blocking(Sysproxy::get_system_proxy) - .await - .map_err(|e| format!("Failed to spawn blocking task for sysproxy: {}", e))? - .map_err(|e| format!("Failed to get system proxy: {}", e))?; + log::debug!(target: "app", "异步获取系统代理配置"); + + let current = AsyncProxyQuery::get_system_proxy().await; let mut map = Mapping::new(); map.insert("enable".into(), current.enable.into()); @@ -22,6 +19,7 @@ pub async fn get_sys_proxy() -> CmdResult { ); map.insert("bypass".into(), current.bypass.into()); + log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port); Ok(map) } diff --git a/src-tauri/src/core/async_proxy_query.rs b/src-tauri/src/core/async_proxy_query.rs new file mode 100644 index 0000000000..5fabaf217d --- /dev/null +++ b/src-tauri/src/core/async_proxy_query.rs @@ -0,0 +1,531 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use tokio::time::{timeout, Duration}; + +#[cfg(target_os = "linux")] +use anyhow::anyhow; +#[cfg(not(target_os = "windows"))] +use tokio::process::Command; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct AsyncAutoproxy { + pub enable: bool, + pub url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AsyncSysproxy { + pub enable: bool, + pub host: String, + pub port: u16, + pub bypass: String, +} + +impl Default for AsyncSysproxy { + fn default() -> Self { + Self { + enable: false, + host: "127.0.0.1".to_string(), + port: 7890, + bypass: String::new(), + } + } +} + +pub struct AsyncProxyQuery; + +impl AsyncProxyQuery { + /// 异步获取自动代理配置(PAC) + pub async fn get_auto_proxy() -> AsyncAutoproxy { + match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await { + Ok(Ok(proxy)) => { + log::debug!(target: "app", "异步获取自动代理成功: enable={}, url={}", proxy.enable, proxy.url); + proxy + } + Ok(Err(e)) => { + log::warn!(target: "app", "异步获取自动代理失败: {}", e); + AsyncAutoproxy::default() + } + Err(_) => { + log::warn!(target: "app", "异步获取自动代理超时"); + AsyncAutoproxy::default() + } + } + } + + /// 异步获取系统代理配置 + pub async fn get_system_proxy() -> AsyncSysproxy { + match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await { + Ok(Ok(proxy)) => { + log::debug!(target: "app", "异步获取系统代理成功: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port); + proxy + } + Ok(Err(e)) => { + log::warn!(target: "app", "异步获取系统代理失败: {}", e); + AsyncSysproxy::default() + } + Err(_) => { + log::warn!(target: "app", "异步获取系统代理超时"); + AsyncSysproxy::default() + } + } + } + + #[cfg(target_os = "windows")] + async fn get_auto_proxy_impl() -> Result { + // Windows: 从注册表读取PAC配置 + tokio::task::spawn_blocking(move || -> Result { + Self::get_pac_config_from_registry() + }) + .await? + } + + #[cfg(target_os = "windows")] + fn get_pac_config_from_registry() -> Result { + use std::ptr; + use winapi::shared::minwindef::{DWORD, HKEY}; + use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; + use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER}; + + unsafe { + let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0" + .encode_utf16() + .collect::>(); + + let mut hkey: HKEY = ptr::null_mut(); + let result = + RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey); + + if result != 0 { + log::debug!(target: "app", "无法打开注册表项"); + return Ok(AsyncAutoproxy::default()); + } + + // 1. 检查自动配置是否启用 (AutoConfigURL 存在且不为空即表示启用) + let auto_config_url_name = "AutoConfigURL\0".encode_utf16().collect::>(); + let mut url_buffer = vec![0u16; 1024]; + let mut url_buffer_size: DWORD = (url_buffer.len() * 2) as DWORD; + let mut url_value_type: DWORD = 0; + + let url_query_result = RegQueryValueExW( + hkey, + auto_config_url_name.as_ptr(), + ptr::null_mut(), + &mut url_value_type, + url_buffer.as_mut_ptr() as *mut u8, + &mut url_buffer_size, + ); + + let mut pac_url = String::new(); + if url_query_result == 0 && url_value_type == REG_SZ && url_buffer_size > 0 { + let end_pos = url_buffer + .iter() + .position(|&x| x == 0) + .unwrap_or(url_buffer.len()); + pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]); + log::debug!(target: "app", "从注册表读取到PAC URL: {}", pac_url); + } + + // 2. 检查自动检测设置是否启用 + let auto_detect_name = "AutoDetect\0".encode_utf16().collect::>(); + let mut auto_detect: DWORD = 0; + let mut detect_buffer_size: DWORD = 4; + let mut detect_value_type: DWORD = 0; + + let detect_query_result = RegQueryValueExW( + hkey, + auto_detect_name.as_ptr(), + ptr::null_mut(), + &mut detect_value_type, + &mut auto_detect as *mut DWORD as *mut u8, + &mut detect_buffer_size, + ); + + RegCloseKey(hkey); + + // PAC 启用的条件:AutoConfigURL 不为空,或 AutoDetect 被启用 + let pac_enabled = !pac_url.is_empty() + || (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0); + + if pac_enabled { + log::debug!(target: "app", "PAC配置启用: URL={}, AutoDetect={}", pac_url, auto_detect); + + if pac_url.is_empty() && auto_detect != 0 { + pac_url = "auto-detect".to_string(); + } + + Ok(AsyncAutoproxy { + enable: true, + url: pac_url, + }) + } else { + log::debug!(target: "app", "PAC配置未启用"); + Ok(AsyncAutoproxy::default()) + } + } + } + + #[cfg(target_os = "macos")] + async fn get_auto_proxy_impl() -> Result { + // macOS: 使用 scutil --proxy 命令 + let output = Command::new("scutil").args(["--proxy"]).output().await?; + + if !output.status.success() { + return Ok(AsyncAutoproxy::default()); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + log::debug!(target: "app", "scutil output: {}", stdout); + + let mut pac_enabled = false; + let mut pac_url = String::new(); + + // 解析 scutil 输出 + for line in stdout.lines() { + let line = line.trim(); + if line.contains("ProxyAutoConfigEnable") && line.contains("1") { + pac_enabled = true; + } else if line.contains("ProxyAutoConfigURLString") { + // 正确解析包含冒号的URL + // 格式: "ProxyAutoConfigURLString : http://127.0.0.1:11233/commands/pac" + if let Some(colon_pos) = line.find(" : ") { + pac_url = line[colon_pos + 3..].trim().to_string(); + } + } + } + + log::debug!(target: "app", "解析结果: pac_enabled={}, pac_url={}", pac_enabled, pac_url); + + Ok(AsyncAutoproxy { + enable: pac_enabled && !pac_url.is_empty(), + url: pac_url, + }) + } + + #[cfg(target_os = "linux")] + async fn get_auto_proxy_impl() -> Result { + // Linux: 检查环境变量和GNOME设置 + + // 首先检查环境变量 + if let Ok(auto_proxy) = std::env::var("auto_proxy") { + if !auto_proxy.is_empty() { + return Ok(AsyncAutoproxy { + enable: true, + url: auto_proxy, + }); + } + } + + // 尝试使用 gsettings 获取 GNOME 代理设置 + let output = Command::new("gsettings") + .args(["get", "org.gnome.system.proxy", "mode"]) + .output() + .await; + + if let Ok(output) = output { + if output.status.success() { + let mode = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if mode.contains("auto") { + // 获取 PAC URL + let pac_output = Command::new("gsettings") + .args(["get", "org.gnome.system.proxy", "autoconfig-url"]) + .output() + .await; + + if let Ok(pac_output) = pac_output { + if pac_output.status.success() { + let pac_url = String::from_utf8_lossy(&pac_output.stdout) + .trim() + .trim_matches('\'') + .trim_matches('"') + .to_string(); + + if !pac_url.is_empty() { + return Ok(AsyncAutoproxy { + enable: true, + url: pac_url, + }); + } + } + } + } + } + } + + Ok(AsyncAutoproxy::default()) + } + + #[cfg(target_os = "windows")] + async fn get_system_proxy_impl() -> Result { + // Windows: 使用注册表直接读取代理设置 + tokio::task::spawn_blocking(move || -> Result { + Self::get_system_proxy_from_registry() + }) + .await? + } + + #[cfg(target_os = "windows")] + fn get_system_proxy_from_registry() -> Result { + use std::ptr; + use winapi::shared::minwindef::{DWORD, HKEY}; + use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; + use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER}; + + unsafe { + let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0" + .encode_utf16() + .collect::>(); + + let mut hkey: HKEY = ptr::null_mut(); + let result = + RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey); + + if result != 0 { + return Ok(AsyncSysproxy::default()); + } + + // 检查代理是否启用 + let proxy_enable_name = "ProxyEnable\0".encode_utf16().collect::>(); + let mut proxy_enable: DWORD = 0; + let mut buffer_size: DWORD = 4; + let mut value_type: DWORD = 0; + + let enable_result = RegQueryValueExW( + hkey, + proxy_enable_name.as_ptr(), + ptr::null_mut(), + &mut value_type, + &mut proxy_enable as *mut DWORD as *mut u8, + &mut buffer_size, + ); + + if enable_result != 0 || value_type != REG_DWORD || proxy_enable == 0 { + RegCloseKey(hkey); + return Ok(AsyncSysproxy::default()); + } + + // 读取代理服务器设置 + let proxy_server_name = "ProxyServer\0".encode_utf16().collect::>(); + let mut buffer = vec![0u16; 1024]; + let mut buffer_size: DWORD = (buffer.len() * 2) as DWORD; + let mut value_type: DWORD = 0; + + let server_result = RegQueryValueExW( + hkey, + proxy_server_name.as_ptr(), + ptr::null_mut(), + &mut value_type, + buffer.as_mut_ptr() as *mut u8, + &mut buffer_size, + ); + + let mut proxy_server = String::new(); + if server_result == 0 && value_type == REG_SZ && buffer_size > 0 { + let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len()); + proxy_server = String::from_utf16_lossy(&buffer[..end_pos]); + } + + // 读取代理绕过列表 + let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::>(); + let mut bypass_buffer = vec![0u16; 1024]; + let mut bypass_buffer_size: DWORD = (bypass_buffer.len() * 2) as DWORD; + let mut bypass_value_type: DWORD = 0; + + let override_result = RegQueryValueExW( + hkey, + proxy_override_name.as_ptr(), + ptr::null_mut(), + &mut bypass_value_type, + bypass_buffer.as_mut_ptr() as *mut u8, + &mut bypass_buffer_size, + ); + + let mut bypass_list = String::new(); + if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 { + let end_pos = bypass_buffer + .iter() + .position(|&x| x == 0) + .unwrap_or(bypass_buffer.len()); + bypass_list = String::from_utf16_lossy(&bypass_buffer[..end_pos]); + } + + RegCloseKey(hkey); + + if !proxy_server.is_empty() { + // 解析服务器地址和端口 + let (host, port) = if let Some(colon_pos) = proxy_server.rfind(':') { + let host = proxy_server[..colon_pos].to_string(); + let port = proxy_server[colon_pos + 1..].parse::().unwrap_or(8080); + (host, port) + } else { + (proxy_server, 8080) + }; + + log::debug!(target: "app", "从注册表读取到代理设置: {}:{}, bypass: {}", host, port, bypass_list); + + Ok(AsyncSysproxy { + enable: true, + host, + port, + bypass: bypass_list, + }) + } else { + Ok(AsyncSysproxy::default()) + } + } + } + + #[cfg(target_os = "macos")] + async fn get_system_proxy_impl() -> Result { + let output = Command::new("scutil").args(["--proxy"]).output().await?; + + if !output.status.success() { + return Ok(AsyncSysproxy::default()); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + log::debug!(target: "app", "scutil proxy output: {}", stdout); + + let mut http_enabled = false; + let mut http_host = String::new(); + let mut http_port = 8080u16; + let mut exceptions = Vec::new(); + + for line in stdout.lines() { + let line = line.trim(); + if line.contains("HTTPEnable") && line.contains("1") { + http_enabled = true; + } else if line.contains("HTTPProxy") && !line.contains("Port") { + if let Some(host_part) = line.split(':').nth(1) { + http_host = host_part.trim().to_string(); + } + } else if line.contains("HTTPPort") { + if let Some(port_part) = line.split(':').nth(1) { + if let Ok(port) = port_part.trim().parse::() { + http_port = port; + } + } + } else if line.contains("ExceptionsList") { + // 解析异常列表 + if let Some(list_part) = line.split(':').nth(1) { + let list = list_part.trim(); + if !list.is_empty() { + exceptions.push(list.to_string()); + } + } + } + } + + Ok(AsyncSysproxy { + enable: http_enabled && !http_host.is_empty(), + host: http_host, + port: http_port, + bypass: exceptions.join(","), + }) + } + + #[cfg(target_os = "linux")] + async fn get_system_proxy_impl() -> Result { + // Linux: 检查环境变量和桌面环境设置 + + // 首先检查环境变量 + if let Ok(http_proxy) = std::env::var("http_proxy") { + if let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) { + return Ok(proxy_info); + } + } + + if let Ok(https_proxy) = std::env::var("https_proxy") { + if let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) { + return Ok(proxy_info); + } + } + + // 尝试使用 gsettings 获取 GNOME 代理设置 + let mode_output = Command::new("gsettings") + .args(["get", "org.gnome.system.proxy", "mode"]) + .output() + .await; + + if let Ok(mode_output) = mode_output { + if mode_output.status.success() { + let mode = String::from_utf8_lossy(&mode_output.stdout) + .trim() + .to_string(); + if mode.contains("manual") { + // 获取HTTP代理设置 + let host_result = Command::new("gsettings") + .args(["get", "org.gnome.system.proxy.http", "host"]) + .output() + .await; + + let port_result = Command::new("gsettings") + .args(["get", "org.gnome.system.proxy.http", "port"]) + .output() + .await; + + if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) { + if host_output.status.success() && port_output.status.success() { + let host = String::from_utf8_lossy(&host_output.stdout) + .trim() + .trim_matches('\'') + .trim_matches('"') + .to_string(); + + let port = String::from_utf8_lossy(&port_output.stdout) + .trim() + .parse::() + .unwrap_or(8080); + + if !host.is_empty() { + return Ok(AsyncSysproxy { + enable: true, + host, + port, + bypass: String::new(), + }); + } + } + } + } + } + } + + Ok(AsyncSysproxy::default()) + } + + #[cfg(target_os = "linux")] + fn parse_proxy_url(proxy_url: &str) -> Result { + // 解析形如 "http://proxy.example.com:8080" 的URL + let url = proxy_url.trim(); + + // 移除协议前缀 + let url = if let Some(stripped) = url.strip_prefix("http://") { + stripped + } else if let Some(stripped) = url.strip_prefix("https://") { + stripped + } else { + url + }; + + // 解析主机和端口 + let (host, port) = if let Some(colon_pos) = url.rfind(':') { + let host = url[..colon_pos].to_string(); + let port = url[colon_pos + 1..].parse::().unwrap_or(8080); + (host, port) + } else { + (url.to_string(), 8080) + }; + + if host.is_empty() { + return Err(anyhow!("无效的代理URL")); + } + + Ok(AsyncSysproxy { + enable: true, + host, + port, + bypass: std::env::var("no_proxy").unwrap_or_default(), + }) + } +} diff --git a/src-tauri/src/core/core.rs b/src-tauri/src/core/core.rs index 12623176dd..4c4589823d 100644 --- a/src-tauri/src/core/core.rs +++ b/src-tauri/src/core/core.rs @@ -1,5 +1,3 @@ -#[cfg(target_os = "macos")] -use crate::core::tray::Tray; use crate::{ config::*, core::{ @@ -516,70 +514,102 @@ impl CoreManager { Ok(()) } - /// 根据进程名查找进程PID列表 + /// 根据进程名查找进程PID列 async fn find_processes_by_name( &self, process_name: String, _target: &str, ) -> Result<(Vec, String)> { - let output = if cfg!(windows) { - tokio::process::Command::new("tasklist") - .args(&[ - "/FI", - &format!("IMAGENAME eq {}", process_name), - "/FO", - "CSV", - "/NH", - ]) - .output() - .await? - } else if cfg!(target_os = "macos") { - tokio::process::Command::new("pgrep") - .arg(&process_name) - .output() - .await? - } else { - // Linux - tokio::process::Command::new("pidof") - .arg(&process_name) - .output() - .await? - }; + #[cfg(windows)] + { + use std::mem; + use winapi::um::handleapi::CloseHandle; + use winapi::um::tlhelp32::{ + CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, + TH32CS_SNAPPROCESS, + }; + use winapi::um::winnt::HANDLE; - if !output.status.success() { - return Ok((Vec::new(), process_name)); - } + let process_name_clone = process_name.clone(); + let pids = tokio::task::spawn_blocking(move || -> Result> { + let mut pids = Vec::new(); - let stdout = String::from_utf8_lossy(&output.stdout); - let mut pids = Vec::new(); - - if cfg!(windows) { - // 解析CSV格式输出: "进程名","PID","会话名","会话#","内存使用" - for line in stdout.lines() { - if !line.is_empty() && line.contains(&process_name) { - let fields: Vec<&str> = line.split(',').collect(); - if fields.len() >= 2 { - // 移除引号并解析PID - let pid_str = fields[1].trim_matches('"'); - if let Ok(pid) = pid_str.parse::() { - pids.push(pid); + unsafe { + // 创建进程快照 + let snapshot: HANDLE = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if snapshot == winapi::um::handleapi::INVALID_HANDLE_VALUE { + return Err(anyhow::anyhow!("Failed to create process snapshot")); + } + + let mut pe32: PROCESSENTRY32W = mem::zeroed(); + pe32.dwSize = mem::size_of::() as u32; + + // 获取第一个进程 + if Process32FirstW(snapshot, &mut pe32) != 0 { + loop { + // 将宽字符转换为String + let end_pos = pe32 + .szExeFile + .iter() + .position(|&x| x == 0) + .unwrap_or(pe32.szExeFile.len()); + let exe_file = String::from_utf16_lossy(&pe32.szExeFile[..end_pos]); + + // 检查进程名是否匹配 + if exe_file.eq_ignore_ascii_case(&process_name_clone) { + pids.push(pe32.th32ProcessID); + } + if Process32NextW(snapshot, &mut pe32) == 0 { + break; + } } } + + // 关闭句柄 + CloseHandle(snapshot); } + + Ok(pids) + }) + .await??; + + Ok((pids, process_name)) + } + + #[cfg(not(windows))] + { + let output = if cfg!(target_os = "macos") { + tokio::process::Command::new("pgrep") + .arg(&process_name) + .output() + .await? + } else { + // Linux + tokio::process::Command::new("pidof") + .arg(&process_name) + .output() + .await? + }; + + if !output.status.success() { + return Ok((Vec::new(), process_name)); } - } else { + + let stdout = String::from_utf8_lossy(&output.stdout); + let mut pids = Vec::new(); + // Unix系统直接解析PID列表 - for pid_str in stdout.trim().split_whitespace() { + for pid_str in stdout.split_whitespace() { if let Ok(pid) = pid_str.parse::() { pids.push(pid); } } - } - Ok((pids, process_name)) + Ok((pids, process_name)) + } } - /// 终止进程并验证结果 + /// 终止进程并验证结果 - 使用Windows API直接终止,更优雅高效 async fn kill_process_with_verification(&self, pid: u32, process_name: String) -> bool { logging!( info, @@ -590,16 +620,32 @@ impl CoreManager { pid ); - let success = if cfg!(windows) { - tokio::process::Command::new("taskkill") - .args(&["/F", "/PID", &pid.to_string()]) - .output() - .await - .map(|output| output.status.success()) - .unwrap_or(false) - } else { + #[cfg(windows)] + let success = { + use winapi::um::handleapi::CloseHandle; + use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess}; + use winapi::um::winnt::{HANDLE, PROCESS_TERMINATE}; + + tokio::task::spawn_blocking(move || -> bool { + unsafe { + let process_handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid); + if process_handle.is_null() { + return false; + } + let result = TerminateProcess(process_handle, 1); + CloseHandle(process_handle); + + result != 0 + } + }) + .await + .unwrap_or(false) + }; + + #[cfg(not(windows))] + let success = { tokio::process::Command::new("kill") - .args(&["-9", &pid.to_string()]) + .args(["-9", &pid.to_string()]) .output() .await .map(|output| output.status.success()) @@ -645,34 +691,49 @@ impl CoreManager { } } - /// 检查进程是否仍在运行 + /// Windows API检查进程 async fn is_process_running(&self, pid: u32) -> Result { - let output = if cfg!(windows) { - tokio::process::Command::new("tasklist") - .args(&["/FI", &format!("PID eq {}", pid), "/FO", "CSV", "/NH"]) - .output() - .await? - } else { - tokio::process::Command::new("ps") - .args(&["-p", &pid.to_string()]) + #[cfg(windows)] + { + use winapi::shared::minwindef::DWORD; + use winapi::um::handleapi::CloseHandle; + use winapi::um::processthreadsapi::GetExitCodeProcess; + use winapi::um::processthreadsapi::OpenProcess; + use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION}; + + let result = tokio::task::spawn_blocking(move || -> Result { + unsafe { + let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid); + if process_handle.is_null() { + return Ok(false); + } + let mut exit_code: DWORD = 0; + let result = GetExitCodeProcess(process_handle, &mut exit_code); + CloseHandle(process_handle); + + if result == 0 { + return Ok(false); + } + Ok(exit_code == 259) + } + }) + .await?; + + result + } + + #[cfg(not(windows))] + { + let output = tokio::process::Command::new("ps") + .args(["-p", &pid.to_string()]) .output() - .await? - }; + .await?; - Ok(output.status.success() && !output.stdout.is_empty()) + Ok(output.status.success() && !output.stdout.is_empty()) + } } async fn start_core_by_sidecar(&self) -> Result<()> { - if let Err(e) = self.cleanup_orphaned_mihomo_processes().await { - logging!( - warn, - Type::Core, - true, - "清理多余 mihomo 进程时发生错误: {}", - e - ); - } - logging!(trace, Type::Core, true, "Running core by sidecar"); let config_file = &Config::generate_file(ConfigType::Run)?; let app_handle = handle::Handle::global() @@ -980,9 +1041,8 @@ impl CoreManager { } logging!(trace, Type::Core, "Initied core logic completed"); - #[cfg(target_os = "macos")] - logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await); - + // #[cfg(target_os = "macos")] + // logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await); Ok(()) } @@ -1036,17 +1096,6 @@ impl CoreManager { pub async fn restart_core(&self) -> Result<()> { self.stop_core().await?; - // 在重启时也清理多余的 mihomo 进程 - if let Err(e) = self.cleanup_orphaned_mihomo_processes().await { - logging!( - warn, - Type::Core, - true, - "重启时清理多余 mihomo 进程失败: {}", - e - ); - } - self.start_core().await?; Ok(()) } diff --git a/src-tauri/src/core/event_driven_proxy.rs b/src-tauri/src/core/event_driven_proxy.rs index e7a22c661c..09bbc165d9 100644 --- a/src-tauri/src/core/event_driven_proxy.rs +++ b/src-tauri/src/core/event_driven_proxy.rs @@ -4,6 +4,7 @@ use tokio::sync::{mpsc, oneshot}; use tokio::time::{sleep, timeout, Duration}; use crate::config::{Config, IVerge}; +use crate::core::async_proxy_query::AsyncProxyQuery; use crate::logging_error; use crate::utils::logging::Type; use once_cell::sync::Lazy; @@ -84,7 +85,7 @@ struct ProxyConfig { guard_enabled: bool, } -static PROXY_MANAGER: Lazy = Lazy::new(|| EventDrivenProxyManager::new()); +static PROXY_MANAGER: Lazy = Lazy::new(EventDrivenProxyManager::new); impl EventDrivenProxyManager { pub fn global() -> &'static EventDrivenProxyManager { @@ -393,56 +394,24 @@ impl EventDrivenProxyManager { } async fn get_auto_proxy_with_timeout() -> Autoproxy { - let result = timeout( - Duration::from_secs(2), - tokio::task::spawn_blocking(|| Autoproxy::get_auto_proxy()), - ) - .await; - - match result { - Ok(Ok(Ok(proxy))) => proxy, - Ok(Ok(Err(e))) => { - log::warn!(target: "app", "获取自动代理失败: {}", e); - Autoproxy { - enable: false, - url: "".to_string(), - } - } - Ok(Err(e)) => { - log::error!(target: "app", "spawn_blocking失败: {}", e); - Autoproxy { - enable: false, - url: "".to_string(), - } - } - Err(_) => { - log::warn!(target: "app", "获取自动代理超时"); - Autoproxy { - enable: false, - url: "".to_string(), - } - } + let async_proxy = AsyncProxyQuery::get_auto_proxy().await; + + // 转换为兼容的结构 + Autoproxy { + enable: async_proxy.enable, + url: async_proxy.url, } } async fn get_sys_proxy_with_timeout() -> Sysproxy { - let result = timeout( - Duration::from_secs(2), - tokio::task::spawn_blocking(|| Sysproxy::get_system_proxy()), - ) - .await; - - match result { - Ok(Ok(Ok(proxy))) => proxy, - _ => { - log::warn!(target: "app", "获取系统代理失败或超时"); - Sysproxy { - enable: false, - host: "127.0.0.1".to_string(), - port: 7890, - bypass: "".to_string(), - } - } + let async_proxy = AsyncProxyQuery::get_system_proxy().await; + + // 转换为兼容的结构 + Sysproxy { + enable: async_proxy.enable, + host: async_proxy.host, + port: async_proxy.port, + bypass: async_proxy.bypass, } } @@ -557,16 +526,10 @@ impl EventDrivenProxyManager { #[cfg(target_os = "windows")] async fn execute_sysproxy_command(args: &[&str]) { - use crate::{core::handle::Handle, utils::dirs}; - use tauri_plugin_shell::ShellExt; - - let app_handle = match Handle::global().app_handle() { - Ok(handle) => handle, - Err(e) => { - log::error!(target: "app", "获取应用句柄失败: {}", e); - return; - } - }; + use crate::utils::dirs; + #[allow(unused_imports)] // creation_flags必须 + use std::os::windows::process::CommandExt; + use tokio::process::Command; let binary_path = match dirs::service_path() { Ok(path) => path, @@ -582,10 +545,9 @@ impl EventDrivenProxyManager { return; } - let shell = app_handle.shell(); - let output = shell - .command(sysproxy_exe.as_path().to_str().unwrap()) + let output = Command::new(sysproxy_exe) .args(args) + .creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口 .output() .await; @@ -593,6 +555,12 @@ impl EventDrivenProxyManager { Ok(output) => { if !output.status.success() { log::error!(target: "app", "执行sysproxy命令失败: {:?}", args); + let stderr = String::from_utf8_lossy(&output.stderr); + if !stderr.is_empty() { + log::error!(target: "app", "sysproxy错误输出: {}", stderr); + } + } else { + log::debug!(target: "app", "成功执行sysproxy命令: {:?}", args); } } Err(e) => { diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index 070a049bbe..33c8fa965c 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -1,10 +1,6 @@ use crate::{ - config::Config, - core::handle, - feat, logging, logging_error, - module::lightweight::entry_lightweight_mode, - process::AsyncHandler, - utils::{logging::Type, resolve}, + config::Config, core::handle, feat, logging, logging_error, + module::lightweight::entry_lightweight_mode, utils::logging::Type, }; use anyhow::{bail, Result}; use once_cell::sync::OnceCell; @@ -14,7 +10,7 @@ use tauri::Manager; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState}; pub struct Hotkey { - current: Arc>>, // 保存当前的热键设置 + current: Arc>>, } impl Hotkey { @@ -38,7 +34,6 @@ impl Hotkey { enable_global_hotkey ); - // 如果全局热键被禁用,则不注册热键 if !enable_global_hotkey { return Ok(()); } @@ -153,76 +148,14 @@ impl Hotkey { "=== Hotkey Dashboard Window Operation Start ===" ); - // 检查是否在轻量模式下,如果是,需要同步处理 - if crate::module::lightweight::is_in_lightweight_mode() { - logging!( - info, - Type::Hotkey, - true, - "In lightweight mode, calling open_or_close_dashboard directly" - ); - crate::feat::open_or_close_dashboard(); - } else { - AsyncHandler::spawn(move || async move { - logging!( - debug, - Type::Hotkey, - true, - "Toggle dashboard window visibility (async)" - ); + logging!( + info, + Type::Hotkey, + true, + "Using unified WindowManager for hotkey operation (bypass debounce)" + ); - // 检查窗口是否存在 - if let Some(window) = handle::Handle::global().get_window() { - // 如果窗口可见,则隐藏 - match window.is_visible() { - Ok(visible) => { - if visible { - logging!( - info, - Type::Window, - true, - "Window is visible, hiding it" - ); - let _ = window.hide(); - } else { - // 如果窗口不可见,则显示 - logging!( - info, - Type::Window, - true, - "Window is hidden, showing it" - ); - if window.is_minimized().unwrap_or(false) { - let _ = window.unminimize(); - } - let _ = window.show(); - let _ = window.set_focus(); - } - } - Err(e) => { - logging!( - warn, - Type::Window, - true, - "Failed to check window visibility: {}", - e - ); - let _ = window.show(); - let _ = window.set_focus(); - } - } - } else { - // 如果窗口不存在,创建一个新窗口 - logging!( - info, - Type::Window, - true, - "Window does not exist, creating a new one" - ); - resolve::create_window(true); - } - }); - } + crate::feat::open_or_close_dashboard_hotkey(); logging!( debug, @@ -261,10 +194,8 @@ impl Hotkey { } } } else { - // 直接执行函数,不做任何状态检查 logging!(debug, Type::Hotkey, "Executing function directly"); - // 获取全局热键状态 let is_enable_global_hotkey = Config::verge() .latest() .enable_global_hotkey @@ -274,7 +205,6 @@ impl Hotkey { f(); } else { use crate::utils::window_manager::WindowManager; - // 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键 let is_visible = WindowManager::is_main_window_visible(); let is_focused = WindowManager::is_main_window_focused(); diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 5cc2fa4492..044abaf9c5 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,3 +1,4 @@ +pub mod async_proxy_query; pub mod backup; #[allow(clippy::module_inception)] mod core; diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index 71cca516e7..4cca93f366 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -9,6 +9,7 @@ use crate::{ use anyhow::Result; use once_cell::sync::OnceCell; use std::sync::Arc; +#[cfg(not(target_os = "windows"))] use sysproxy::{Autoproxy, Sysproxy}; use tauri::async_runtime::Mutex as TokioMutex; use tauri_plugin_autostart::ManagerExt; diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 62414efab3..0fb4334400 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -12,15 +12,7 @@ use crate::{ }; use anyhow::Result; -#[cfg(target_os = "macos")] -use futures::StreamExt; use parking_lot::Mutex; -#[cfg(target_os = "macos")] -use parking_lot::RwLock; -#[cfg(target_os = "macos")] -pub use speed_rate::{SpeedRate, Traffic}; -#[cfg(target_os = "macos")] -use std::sync::Arc; use std::{ fs, sync::atomic::{AtomicBool, Ordering}, @@ -31,20 +23,37 @@ use tauri::{ tray::{MouseButton, MouseButtonState, TrayIconEvent}, AppHandle, Wry, }; -#[cfg(target_os = "macos")] -use tokio::sync::broadcast; use super::handle; #[derive(Clone)] struct TrayState {} +// 托盘点击防抖机制 +static TRAY_CLICK_DEBOUNCE: OnceCell> = OnceCell::new(); +const TRAY_CLICK_DEBOUNCE_MS: u64 = 300; + +fn get_tray_click_debounce() -> &'static Mutex { + TRAY_CLICK_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1))) +} + +fn should_handle_tray_click() -> bool { + let debounce_lock = get_tray_click_debounce(); + let mut last_click = debounce_lock.lock(); + let now = Instant::now(); + + if now.duration_since(*last_click) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) { + *last_click = now; + true + } else { + log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms", + now.duration_since(*last_click).as_millis()); + false + } +} + #[cfg(target_os = "macos")] pub struct Tray { - pub speed_rate: Arc>>, - shutdown_tx: Arc>>>, - is_subscribed: Arc>, - pub rate_cache: Arc>>, last_menu_update: Mutex>, menu_updating: AtomicBool, } @@ -105,7 +114,7 @@ impl TrayState { if tray_icon_colorful == "monochrome" { ( false, - include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(), + include_bytes!("../../../icons/tray-icon-sys-mono-new.ico").to_vec(), ) } else { ( @@ -139,7 +148,7 @@ impl TrayState { if tray_icon_colorful == "monochrome" { ( false, - include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(), + include_bytes!("../../../icons/tray-icon-tun-mono-new.ico").to_vec(), ) } else { ( @@ -164,10 +173,6 @@ impl Tray { #[cfg(target_os = "macos")] return TRAY.get_or_init(|| Tray { - speed_rate: Arc::new(Mutex::new(None)), - shutdown_tx: Arc::new(RwLock::new(None)), - is_subscribed: Arc::new(RwLock::new(false)), - rate_cache: Arc::new(Mutex::new(None)), last_menu_update: Mutex::new(None), menu_updating: AtomicBool::new(false), }); @@ -180,11 +185,6 @@ impl Tray { } pub fn init(&self) -> Result<()> { - #[cfg(target_os = "macos")] - { - let mut speed_rate = self.speed_rate.lock(); - *speed_rate = Some(SpeedRate::new()); - } Ok(()) } @@ -291,7 +291,7 @@ impl Tray { /// 更新托盘图标 #[cfg(target_os = "macos")] - pub fn update_icon(&self, rate: Option) -> Result<()> { + pub fn update_icon(&self, _rate: Option) -> Result<()> { let app_handle = match handle::Handle::global().app_handle() { Some(handle) => handle, None => { @@ -312,55 +312,18 @@ impl Tray { let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); - let (is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) { + let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) { (true, true) => TrayState::get_tun_tray_icon(), (true, false) => TrayState::get_sysproxy_tray_icon(), (false, true) => TrayState::get_tun_tray_icon(), (false, false) => TrayState::get_common_tray_icon(), }; - let enable_tray_speed = verge.enable_tray_speed.unwrap_or(false); - let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true); let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string()); let is_colorful = colorful == "colorful"; - if !enable_tray_speed { - let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?)); - let _ = tray.set_icon_as_template(!is_colorful); - return Ok(()); - } - - let rate = if let Some(rate) = rate { - Some(rate) - } else { - let guard = self.speed_rate.lock(); - if let Some(guard) = guard.as_ref() { - if let Some(rate) = guard.get_curent_rate() { - Some(rate) - } else { - Some(Rate::default()) - } - } else { - Some(Rate::default()) - } - }; - - let mut rate_guard = self.rate_cache.lock(); - if *rate_guard != rate { - *rate_guard = rate; - - let bytes = if enable_tray_icon { - Some(icon_bytes) - } else { - None - }; - - let rate = rate_guard.as_ref(); - if let Ok(rate_bytes) = SpeedRate::add_speed_text(is_custom_icon, bytes, rate) { - let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?)); - let _ = tray.set_icon_as_template(!is_custom_icon && !is_colorful); - } - } + let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?)); + let _ = tray.set_icon_as_template(!is_colorful); Ok(()) } @@ -475,155 +438,9 @@ impl Tray { Ok(()) } - /// 订阅流量数据 - #[cfg(target_os = "macos")] - pub async fn subscribe_traffic(&self) -> Result<()> { - log::info!(target: "app", "subscribe traffic"); - - // 如果已经订阅,先取消订阅 - if *self.is_subscribed.read() { - self.unsubscribe_traffic(); - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - - let (shutdown_tx, shutdown_rx) = broadcast::channel(3); - *self.shutdown_tx.write() = Some(shutdown_tx); - *self.is_subscribed.write() = true; - - let speed_rate = Arc::clone(&self.speed_rate); - let is_subscribed = Arc::clone(&self.is_subscribed); - - // 使用单线程防止阻塞主线程 - std::thread::Builder::new() - .name("traffic-monitor".into()) - .spawn(move || { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("Failed to build tokio runtime for traffic monitor"); - // 在单独的运行时中执行异步任务 - rt.block_on(async move { - let mut shutdown = shutdown_rx; - let speed_rate = speed_rate.clone(); - let is_subscribed = is_subscribed.clone(); - let mut consecutive_errors = 0; - let max_consecutive_errors = 5; - - let mut interval = tokio::time::interval(std::time::Duration::from_secs(10)); - - 'outer: loop { - if !*is_subscribed.read() { - log::info!(target: "app", "Traffic subscription has been cancelled"); - break; - } - - match tokio::time::timeout( - std::time::Duration::from_secs(5), - Traffic::get_traffic_stream() - ).await { - Ok(stream_result) => { - match stream_result { - Ok(mut stream) => { - consecutive_errors = 0; - - loop { - tokio::select! { - traffic_result = stream.next() => { - match traffic_result { - Some(Ok(traffic)) => { - if let Ok(Some(rate)) = tokio::time::timeout( - std::time::Duration::from_millis(50), - async { - let guard = speed_rate.try_lock(); - if let Some(guard) = guard { - if let Some(sr) = guard.as_ref() { - sr.update_and_check_changed(traffic.up, traffic.down) - } else { - None - } - } else { - None - } - } - ).await { - let _ = tokio::time::timeout( - std::time::Duration::from_millis(100), - async { let _ = Tray::global().update_icon(Some(rate)); } - ).await; - } - }, - Some(Err(e)) => { - log::error!(target: "app", "Traffic stream error: {}", e); - consecutive_errors += 1; - if consecutive_errors >= max_consecutive_errors { - log::error!(target: "app", "Too many errors, reconnecting traffic stream"); - break; - } - }, - None => { - log::info!(target: "app", "Traffic stream ended, reconnecting"); - break; - } - } - }, - _ = shutdown.recv() => { - log::info!(target: "app", "Received shutdown signal for traffic stream"); - break 'outer; - }, - _ = interval.tick() => { - if !*is_subscribed.read() { - log::info!(target: "app", "Traffic monitor detected subscription cancelled"); - break 'outer; - } - log::debug!(target: "app", "Traffic subscription periodic health check"); - }, - _ = tokio::time::sleep(std::time::Duration::from_secs(60)) => { - log::info!(target: "app", "Traffic stream max active time reached, reconnecting"); - break; - } - } - } - }, - Err(e) => { - log::error!(target: "app", "Failed to get traffic stream: {}", e); - consecutive_errors += 1; - if consecutive_errors >= max_consecutive_errors { - log::error!(target: "app", "Too many consecutive errors, pausing traffic monitoring"); - tokio::time::sleep(std::time::Duration::from_secs(30)).await; - consecutive_errors = 0; - } else { - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - } - } - } - }, - Err(_) => { - log::error!(target: "app", "Traffic stream initialization timed out"); - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - } - } - - if !*is_subscribed.read() { - break; - } - } - log::info!(target: "app", "Traffic subscription thread terminated"); - }); - }) - .expect("Failed to spawn traffic monitor thread"); - - Ok(()) - } - /// 取消订阅 traffic 数据 #[cfg(target_os = "macos")] - pub fn unsubscribe_traffic(&self) { - log::info!(target: "app", "unsubscribe traffic"); - *self.is_subscribed.write() = false; - if let Some(tx) = self.shutdown_tx.write().take() { - drop(tx); - } - } + pub fn unsubscribe_traffic(&self) {} pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> { log::info!(target: "app", "正在从AppHandle创建系统托盘"); @@ -664,6 +481,11 @@ impl Tray { .. } = event { + // 添加防抖检查,防止快速连击 + if !should_handle_tray_click() { + return; + } + match tray_event.as_str() { "system_proxy" => feat::toggle_system_proxy(), "tun_mode" => feat::toggle_tun_mode(None), @@ -949,12 +771,15 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { "open_window" => { use crate::utils::window_manager::WindowManager; log::info!(target: "app", "托盘菜单点击: 打开窗口"); - // 如果在轻量模式中,先退出轻量模式 + + if !should_handle_tray_click() { + return; + } + if crate::module::lightweight::is_in_lightweight_mode() { log::info!(target: "app", "当前在轻量模式,正在退出"); crate::module::lightweight::exit_lightweight_mode(); } - // 使用统一的窗口管理器显示窗口 let result = WindowManager::show_main_window(); log::info!(target: "app", "窗口显示结果: {:?}", result); } @@ -977,7 +802,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { "restart_clash" => feat::restart_clash_core(), "restart_app" => feat::restart_app(), "entry_lightweight_mode" => { - // 处理轻量模式的切换 + if !should_handle_tray_click() { + return; + } + let was_lightweight = crate::module::lightweight::is_in_lightweight_mode(); if was_lightweight { crate::module::lightweight::exit_lightweight_mode(); @@ -985,7 +813,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { crate::module::lightweight::entry_lightweight_mode(); } - // 退出轻量模式后显示主窗口 if was_lightweight { use crate::utils::window_manager::WindowManager; let result = WindowManager::show_main_window(); @@ -1002,7 +829,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { _ => {} } - // 统一调用状态更新 if let Err(e) = Tray::global().update_all_states() { log::warn!(target: "app", "更新托盘状态失败: {}", e); } diff --git a/src-tauri/src/core/tray/speed_rate.rs b/src-tauri/src/core/tray/speed_rate.rs index 06cbf61014..8b13789179 100644 --- a/src-tauri/src/core/tray/speed_rate.rs +++ b/src-tauri/src/core/tray/speed_rate.rs @@ -1,336 +1 @@ -use crate::{ - module::mihomo::{MihomoManager, Rate}, - utils::help::format_bytes_speed, -}; -use ab_glyph::FontArc; -use anyhow::Result; -use futures::Stream; -use image::{GenericImageView, Rgba, RgbaImage}; -use imageproc::drawing::draw_text_mut; -use parking_lot::Mutex; -use std::{io::Cursor, sync::Arc}; -use tokio_tungstenite::tungstenite::http; -use tungstenite::client::IntoClientRequest; -#[derive(Debug, Clone)] -pub struct SpeedRate { - rate: Arc>, - last_update: Arc>, -} -impl SpeedRate { - pub fn new() -> Self { - Self { - rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))), - last_update: Arc::new(Mutex::new(std::time::Instant::now())), - } - } - - pub fn update_and_check_changed(&self, up: u64, down: u64) -> Option { - let mut rates = self.rate.lock(); - let mut last_update = self.last_update.lock(); - let now = std::time::Instant::now(); - - // 限制更新频率为每秒最多2次(500ms) - if now.duration_since(*last_update).as_millis() < 500 { - return None; - } - - let (current, previous) = &mut *rates; - - // Avoid unnecessary float conversions for small value checks - let should_update = if current.up < 1000 && down < 1000 { - // For small values, always update to ensure accuracy - current.up != up || current.down != down - } else { - // For larger values, use integer math to check for >5% change - // Multiply by 20 instead of dividing by 0.05 to avoid floating point - let up_threshold = current.up / 20; - let down_threshold = current.down / 20; - - (up > current.up && up - current.up > up_threshold) - || (up < current.up && current.up - up > up_threshold) - || (down > current.down && down - current.down > down_threshold) - || (down < current.down && current.down - down > down_threshold) - }; - - if !should_update { - return None; - } - - *previous = current.clone(); - current.up = up; - current.down = down; - *last_update = now; - - if previous != current { - Some(current.clone()) - } else { - None - } - } - - pub fn get_curent_rate(&self) -> Option { - let rates = self.rate.lock(); - let (current, _) = &*rates; - Some(current.clone()) - } - - // 分离图标加载和速率渲染 - pub fn add_speed_text( - is_custom_icon: bool, - icon_bytes: Option>, - rate: Option<&Rate>, - ) -> Result> { - let rate = rate.unwrap_or(&Rate { up: 0, down: 0 }); - - let (mut icon_width, mut icon_height) = (0, 256); - let icon_image = if let Some(bytes) = icon_bytes.clone() { - let icon_image = image::load_from_memory(&bytes)?; - icon_width = icon_image.width(); - icon_height = icon_image.height(); - icon_image - } else { - // 返回一个空的 RGBA 图像 - image::DynamicImage::new_rgba8(0, 0) - }; - - let total_width = match (is_custom_icon, icon_bytes.is_some()) { - (true, true) => 510, - (true, false) => 740, - (false, false) => 740, - (false, true) => icon_width + 740, - }; - - // println!( - // "icon_height: {}, icon_wight: {}, total_width: {}", - // icon_height, icon_width, total_width - // ); - - // 创建新的透明画布 - let mut combined_image = RgbaImage::new(total_width, icon_height); - - // 将原始图标绘制到新画布的左侧 - if icon_bytes.is_some() { - for y in 0..icon_height { - for x in 0..icon_width { - let pixel = icon_image.get_pixel(x, y); - combined_image.put_pixel(x, y, pixel); - } - } - } - - let is_colorful = if let Some(bytes) = icon_bytes.clone() { - !crate::utils::help::is_monochrome_image_from_bytes(&bytes).unwrap_or(false) - } else { - false - }; - - // 选择文本颜色 - let (text_color, shadow_color) = if is_colorful { - ( - Rgba([144u8, 144u8, 144u8, 255u8]), - // Rgba([255u8, 255u8, 255u8, 128u8]), - Rgba([0u8, 0u8, 0u8, 128u8]), - ) - // ( - // Rgba([160u8, 160u8, 160u8, 255u8]), - // // Rgba([255u8, 255u8, 255u8, 128u8]), - // Rgba([0u8, 0u8, 0u8, 255u8]), - // ) - } else { - ( - Rgba([255u8, 255u8, 255u8, 255u8]), - Rgba([0u8, 0u8, 0u8, 128u8]), - ) - }; - // 减小字体大小以适应文本区域 - let font_data = include_bytes!("../../../assets/fonts/SF-Pro.ttf"); - let font = FontArc::try_from_vec(font_data.to_vec()).unwrap(); - let font_size = icon_height as f32 * 0.6; // 稍微减小字体 - let scale = ab_glyph::PxScale::from(font_size); - - // 使用更简洁的速率格式 - let up_text = format!("↑ {}", format_bytes_speed(rate.up)); - let down_text = format!("↓ {}", format_bytes_speed(rate.down)); - - // For test rate display - // let down_text = format!("↓ {}", format_bytes_speed(102 * 1020 * 1024)); - - // 计算文本位置,确保垂直间距合适 - // 修改文本位置为居右显示 - // 计算右对齐的文本位置 - // let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32; - // let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32; - // let up_text_x = total_width - up_text_width; - // let down_text_x = total_width - down_text_width; - - // 计算左对齐的文本位置 - let (up_text_x, down_text_x) = { - if is_custom_icon || icon_bytes.is_some() { - let text_left_offset = 30; - let left_begin = icon_width + text_left_offset; - (left_begin, left_begin) - } else { - (icon_width, icon_width) - } - }; - - // 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小 - let text_height = font_size as i32; - let total_text_height = text_height * 2; - let up_y = (icon_height as i32 - total_text_height) / 2; - let down_y = up_y + text_height; - - // 绘制速率文本(先阴影后文字) - let shadow_offset = 1; - - // 绘制上行速率 - draw_text_mut( - &mut combined_image, - shadow_color, - up_text_x as i32 + shadow_offset, - up_y + shadow_offset, - scale, - &font, - &up_text, - ); - draw_text_mut( - &mut combined_image, - text_color, - up_text_x as i32, - up_y, - scale, - &font, - &up_text, - ); - - // 绘制下行速率 - draw_text_mut( - &mut combined_image, - shadow_color, - down_text_x as i32 + shadow_offset, - down_y + shadow_offset, - scale, - &font, - &down_text, - ); - draw_text_mut( - &mut combined_image, - text_color, - down_text_x as i32, - down_y, - scale, - &font, - &down_text, - ); - - // 将结果转换为 PNG 数据 - let mut bytes = Vec::new(); - combined_image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?; - Ok(bytes) - } -} - -#[derive(Debug, Clone)] -pub struct Traffic { - pub up: u64, - pub down: u64, -} - -impl Traffic { - pub async fn get_traffic_stream() -> Result>> - { - use futures::{ - future::FutureExt, - stream::{self, StreamExt}, - }; - use std::time::Duration; - - // 先处理错误和超时情况 - let stream = Box::pin( - stream::unfold((), move |_| async move { - 'retry: loop { - log::info!(target: "app", "establishing traffic websocket connection"); - let (url, token) = MihomoManager::get_traffic_ws_url(); - let mut request = match url.into_client_request() { - Ok(req) => req, - Err(e) => { - log::error!(target: "app", "failed to create websocket request: {}", e); - tokio::time::sleep(Duration::from_secs(2)).await; - continue 'retry; - } - }; - - request.headers_mut().insert(http::header::AUTHORIZATION, token); - - match tokio::time::timeout(Duration::from_secs(3), - tokio_tungstenite::connect_async(request) - ).await { - Ok(Ok((ws_stream, _))) => { - log::info!(target: "app", "traffic websocket connection established"); - // 设置流超时控制 - let traffic_stream = ws_stream - .take_while(|msg| { - let continue_stream = msg.is_ok(); - async move { continue_stream }.boxed() - }) - .filter_map(|msg| async move { - match msg { - Ok(msg) => { - if !msg.is_text() { - return None; - } - - match tokio::time::timeout( - Duration::from_millis(200), - async { msg.into_text() } - ).await { - Ok(Ok(text)) => { - match serde_json::from_str::(&text) { - Ok(json) => { - let up = json["up"].as_u64().unwrap_or(0); - let down = json["down"].as_u64().unwrap_or(0); - Some(Ok(Traffic { up, down })) - }, - Err(e) => { - log::warn!(target: "app", "traffic json parse error: {} for {}", e, text); - None - } - } - }, - Ok(Err(e)) => { - log::warn!(target: "app", "traffic text conversion error: {}", e); - None - }, - Err(_) => { - log::warn!(target: "app", "traffic text processing timeout"); - None - } - } - }, - Err(e) => { - log::error!(target: "app", "traffic websocket error: {}", e); - None - } - } - }); - - return Some((traffic_stream, ())); - }, - Ok(Err(e)) => { - log::error!(target: "app", "traffic websocket connection failed: {}", e); - }, - Err(_) => { - log::error!(target: "app", "traffic websocket connection timed out"); - } - } - - tokio::time::sleep(Duration::from_secs(2)).await; - } - }) - .flatten(), - ); - - Ok(stream) - } -} diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index 4ac7505818..9d43ff4d56 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -11,11 +11,43 @@ use crate::{ /// Open or close the dashboard window #[allow(dead_code)] pub fn open_or_close_dashboard() { + open_or_close_dashboard_internal(false) +} + +/// Open or close the dashboard window (hotkey call, dispatched to main thread) +#[allow(dead_code)] +pub fn open_or_close_dashboard_hotkey() { + open_or_close_dashboard_internal(true) +} + +/// Internal implementation for opening/closing dashboard +fn open_or_close_dashboard_internal(bypass_debounce: bool) { + use crate::process::AsyncHandler; use crate::utils::window_manager::WindowManager; - log::info!(target: "app", "Attempting to open/close dashboard"); + log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {})", bypass_debounce); - // 检查是否在轻量模式下 + // 热键调用调度到主线程执行,避免 WebView 创建死锁 + if bypass_debounce { + log::info!(target: "app", "热键调用,调度到主线程执行窗口操作"); + + AsyncHandler::spawn(move || async move { + log::info!(target: "app", "主线程中执行热键窗口操作"); + + if crate::module::lightweight::is_in_lightweight_mode() { + log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode"); + crate::module::lightweight::exit_lightweight_mode(); + log::info!(target: "app", "Creating new window after exiting lightweight mode"); + let result = WindowManager::show_main_window(); + log::info!(target: "app", "Window operation result: {:?}", result); + return; + } + + let result = WindowManager::toggle_main_window(); + log::info!(target: "app", "Window toggle result: {:?}", result); + }); + return; + } if crate::module::lightweight::is_in_lightweight_mode() { log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode"); crate::module::lightweight::exit_lightweight_mode(); @@ -25,7 +57,6 @@ pub fn open_or_close_dashboard() { return; } - // 使用统一的窗口管理器切换窗口状态 let result = WindowManager::toggle_main_window(); log::info!(target: "app", "Window toggle result: {:?}", result); } diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index b6ebed7cf5..0b8553e530 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -13,11 +13,17 @@ use crate::AppHandleManager; use anyhow::{Context, Result}; use delay_timer::prelude::TaskBuilder; -use std::sync::Mutex; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Mutex, +}; use tauri::{Listener, Manager}; const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task"; +// 添加退出轻量模式的锁,防止并发调用 +static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false); + fn with_lightweight_status(f: F) -> R where F: FnOnce(&mut LightWeightState) -> R, @@ -30,20 +36,20 @@ where pub fn run_once_auto_lightweight() { LightWeightState::default().run_once_time(|| { - let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(true); + let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(false); let enable_auto = Config::verge() .data() .enable_auto_light_weight_mode - .unwrap_or(true); + .unwrap_or(false); if enable_auto && is_silent_start { logging!( info, Type::Lightweight, true, - "正常创建窗口和添加定时器监听器" + "在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器" ); set_lightweight_mode(false); - disable_auto_light_weight_mode(); + enable_auto_light_weight_mode(); // 触发托盘更新 if let Err(e) = Tray::global().update_part() { @@ -59,8 +65,13 @@ pub fn auto_lightweight_mode_init() { let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false); let enable_auto = { Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false); - if enable_auto && is_silent_start { - logging!(info, Type::Lightweight, true, "自动轻量模式静默启动"); + if enable_auto && !is_silent_start { + logging!( + info, + Type::Lightweight, + true, + "非静默启动直接挂载自动进入轻量模式监听器!" + ); set_lightweight_mode(true); enable_auto_light_weight_mode(); @@ -78,7 +89,7 @@ pub fn is_in_lightweight_mode() -> bool { } // 设置轻量模式状态 -fn set_lightweight_mode(value: bool) { +pub fn set_lightweight_mode(value: bool) { with_lightweight_status(|state| { state.set_lightweight_mode(value); }); @@ -120,7 +131,6 @@ pub fn entry_lightweight_mode() { } #[cfg(target_os = "macos")] AppHandleManager::global().set_activation_policy_accessory(); - logging!(info, Type::Lightweight, true, "轻量模式已开启"); } set_lightweight_mode(true); let _ = cancel_light_weight_timer(); @@ -131,6 +141,25 @@ pub fn entry_lightweight_mode() { // 添加从轻量模式恢复的函数 pub fn exit_lightweight_mode() { + // 使用原子操作检查是否已经在退出过程中,防止并发调用 + if EXITING_LIGHTWEIGHT + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_err() + { + logging!( + info, + Type::Lightweight, + true, + "轻量模式退出操作已在进行中,跳过重复调用" + ); + return; + } + + // 使用defer确保无论如何都会重置标志 + let _guard = scopeguard::guard((), |_| { + EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst); + }); + // 确保当前确实处于轻量模式才执行退出操作 if !is_in_lightweight_mode() { logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出"); @@ -138,7 +167,6 @@ pub fn exit_lightweight_mode() { } set_lightweight_mode(false); - logging!(info, Type::Lightweight, true, "正在退出轻量模式"); // macOS激活策略 #[cfg(target_os = "macos")] diff --git a/src-tauri/src/module/mihomo.rs b/src-tauri/src/module/mihomo.rs index 0fc40d31f3..37eb57a752 100644 --- a/src-tauri/src/module/mihomo.rs +++ b/src-tauri/src/module/mihomo.rs @@ -4,8 +4,6 @@ use once_cell::sync::Lazy; use parking_lot::{Mutex, RwLock}; use std::time::{Duration, Instant}; use tauri::http::HeaderMap; -#[cfg(target_os = "macos")] -use tauri::http::HeaderValue; // 缓存的最大有效期(5秒) const CACHE_TTL: Duration = Duration::from_secs(5); @@ -106,31 +104,5 @@ impl MihomoManager { Some((server, headers)) } - // 提供默认值的版本,避免在connection_info为None时panic - #[cfg(target_os = "macos")] - fn get_clash_client_info_or_default() -> (String, HeaderMap) { - Self::get_clash_client_info().unwrap_or_else(|| { - let mut headers = HeaderMap::new(); - headers.insert("Content-Type", "application/json".parse().unwrap()); - ("http://127.0.0.1:9090".to_string(), headers) - }) - } - - #[cfg(target_os = "macos")] - pub fn get_traffic_ws_url() -> (String, HeaderValue) { - let (url, headers) = MihomoManager::get_clash_client_info_or_default(); - let ws_url = url.replace("http://", "ws://") + "/traffic"; - let auth = headers - .get("Authorization") - .map(|val| val.to_str().unwrap_or("").to_string()) - .unwrap_or_default(); - - // 创建默认的空HeaderValue而不是使用unwrap_or_default - let token = match HeaderValue::from_str(&auth) { - Ok(v) => v, - Err(_) => HeaderValue::from_static(""), - }; - - (ws_url, token) - } + // 已移除未使用的 get_clash_client_info_or_default 和 get_traffic_ws_url 方法 } diff --git a/src-tauri/src/utils/autostart.rs b/src-tauri/src/utils/autostart.rs index 565f715051..df04b8100a 100644 --- a/src-tauri/src/utils/autostart.rs +++ b/src-tauri/src/utils/autostart.rs @@ -4,7 +4,7 @@ use anyhow::{anyhow, Result}; use log::info; #[cfg(target_os = "windows")] -use std::{fs, path::Path, path::PathBuf}; +use std::{fs, os::windows::process::CommandExt, path::Path, path::PathBuf}; /// Windows 下的开机启动文件夹路径 #[cfg(target_os = "windows")] @@ -59,6 +59,8 @@ pub fn create_shortcut() -> Result<()> { let output = std::process::Command::new("powershell") .args(["-Command", &powershell_command]) + // 隐藏 PowerShell 窗口 + .creation_flags(0x08000000) // CREATE_NO_WINDOW .output() .map_err(|e| anyhow!("执行 PowerShell 命令失败: {}", e))?; diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs index 0df0573bee..4c30b0f1a8 100644 --- a/src-tauri/src/utils/help.rs +++ b/src-tauri/src/utils/help.rs @@ -125,19 +125,6 @@ pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> { Ok(()) } -#[cfg(target_os = "macos")] -pub fn is_monochrome_image_from_bytes(data: &[u8]) -> anyhow::Result { - let img = image::load_from_memory(data)?; - let rgb_img = img.to_rgb8(); - - for pixel in rgb_img.pixels() { - if pixel[0] != pixel[1] || pixel[1] != pixel[2] { - return Ok(false); - } - } - Ok(true) -} - #[cfg(target_os = "linux")] pub fn linux_elevator() -> String { use std::process::Command; @@ -176,39 +163,3 @@ macro_rules! t { } }; } - -/// 将字节数转换为可读的流量字符串 -/// 支持 B/s、KB/s、MB/s、GB/s 的自动转换 -/// -/// # Examples -/// ```not_run -/// format_bytes_speed(1000) // returns "1000B/s" -/// format_bytes_speed(1024) // returns "1.0KB/s" -/// format_bytes_speed(1024 * 1024) // returns "1.0MB/s" -/// ``` -/// ``` -#[cfg(target_os = "macos")] -pub fn format_bytes_speed(speed: u64) -> String { - const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"]; - let mut size = speed as f64; - let mut unit_index = 0; - - while size >= 1000.0 && unit_index < UNITS.len() - 1 { - size /= 1024.0; - unit_index += 1; - } - - format!("{:.1}{}/s", size, UNITS[unit_index]) -} - -#[cfg(target_os = "macos")] -#[test] -fn test_format_bytes_speed() { - assert_eq!(format_bytes_speed(0), "0.0B/s"); - assert_eq!(format_bytes_speed(1023), "1.0KB/s"); - assert_eq!(format_bytes_speed(1024), "1.0KB/s"); - assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s"); - assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s"); - assert_eq!(format_bytes_speed(1024 * 500), "500.0KB/s"); - assert_eq!(format_bytes_speed(1024 * 1024 * 2), "2.0MB/s"); -} diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs index 8e1c0ca7ac..06701846f3 100644 --- a/src-tauri/src/utils/resolve.rs +++ b/src-tauri/src/utils/resolve.rs @@ -13,6 +13,7 @@ use anyhow::{bail, Result}; use once_cell::sync::OnceCell; use parking_lot::{Mutex, RwLock}; use percent_encoding::percent_decode_str; +use scopeguard; use serde_yaml::Mapping; use std::{ sync::Arc, @@ -294,6 +295,7 @@ pub fn create_window(is_show: bool) -> bool { if !is_show { logging!(info, Type::Window, true, "静默模式启动时不创建窗口"); + lightweight::set_lightweight_mode(true); handle::Handle::notify_startup_completed(); return false; } @@ -337,6 +339,12 @@ pub fn create_window(is_show: bool) -> bool { *creating = (true, Instant::now()); + // ScopeGuard 确保创建状态重置,防止 webview 卡死 + let _guard = scopeguard::guard(creating, |mut creating_guard| { + *creating_guard = (false, Instant::now()); + logging!(debug, Type::Window, true, "[ScopeGuard] 窗口创建状态已重置"); + }); + match tauri::WebviewWindowBuilder::new( &handle::Handle::global().app_handle().unwrap(), "main", /* the unique window label */ @@ -419,8 +427,6 @@ pub fn create_window(is_show: bool) -> bool { Ok(newly_created_window) => { logging!(debug, Type::Window, true, "主窗口实例创建成功"); - *creating = (false, Instant::now()); - update_ui_ready_stage(UiReadyStage::NotStarted); AsyncHandler::spawn(move || async move { @@ -534,7 +540,6 @@ pub fn create_window(is_show: bool) -> bool { } Err(e) => { logging!(error, Type::Window, true, "主窗口构建失败: {}", e); - *creating = (false, Instant::now()); // Reset the creating state if window creation failed false } } diff --git a/src-tauri/src/utils/window_manager.rs b/src-tauri/src/utils/window_manager.rs index d00656f647..f627e213d4 100644 --- a/src-tauri/src/utils/window_manager.rs +++ b/src-tauri/src/utils/window_manager.rs @@ -4,6 +4,14 @@ use tauri::{Manager, WebviewWindow, Wry}; #[cfg(target_os = "macos")] use crate::AppHandleManager; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use scopeguard; +use std::{ + sync::atomic::{AtomicBool, Ordering}, + time::{Duration, Instant}, +}; + /// 窗口操作结果 #[derive(Debug, Clone, Copy, PartialEq)] pub enum WindowOperationResult { @@ -34,25 +42,71 @@ pub enum WindowState { NotExist, } +// 窗口操作防抖机制 +static WINDOW_OPERATION_DEBOUNCE: OnceCell> = OnceCell::new(); +static WINDOW_OPERATION_IN_PROGRESS: AtomicBool = AtomicBool::new(false); +const WINDOW_OPERATION_DEBOUNCE_MS: u64 = 500; + +fn get_window_operation_debounce() -> &'static Mutex { + WINDOW_OPERATION_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1))) +} + +fn should_handle_window_operation() -> bool { + if WINDOW_OPERATION_IN_PROGRESS.load(Ordering::Acquire) { + log::warn!(target: "app", "[防抖] 窗口操作已在进行中,跳过重复调用"); + return false; + } + + let debounce_lock = get_window_operation_debounce(); + let mut last_operation = debounce_lock.lock(); + let now = Instant::now(); + let elapsed = now.duration_since(*last_operation); + + log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)", + elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS); + + if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) { + *last_operation = now; + WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release); + log::info!(target: "app", "[防抖] 窗口操作被允许执行"); + true + } else { + log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms", + elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS); + false + } +} + +fn finish_window_operation() { + WINDOW_OPERATION_IN_PROGRESS.store(false, Ordering::Release); +} + /// 统一的窗口管理器 pub struct WindowManager; impl WindowManager { pub fn get_main_window_state() -> WindowState { - if let Some(window) = Self::get_main_window() { - if window.is_minimized().unwrap_or(false) { - WindowState::Minimized - } else if window.is_visible().unwrap_or(false) { - if window.is_focused().unwrap_or(false) { + match Self::get_main_window() { + Some(window) => { + let is_minimized = window.is_minimized().unwrap_or(false); + let is_visible = window.is_visible().unwrap_or(false); + let is_focused = window.is_focused().unwrap_or(false); + + if is_minimized { + return WindowState::Minimized; + } + + if !is_visible { + return WindowState::Hidden; + } + + if is_focused { WindowState::VisibleFocused } else { WindowState::VisibleUnfocused } - } else { - WindowState::Hidden } - } else { - WindowState::NotExist + None => WindowState::NotExist, } } @@ -65,6 +119,14 @@ impl WindowManager { /// 智能显示主窗口 pub fn show_main_window() -> WindowOperationResult { + // 防抖检查 + if !should_handle_window_operation() { + return WindowOperationResult::NoAction; + } + let _guard = scopeguard::guard((), |_| { + finish_window_operation(); + }); + logging!(info, Type::Window, true, "开始智能显示主窗口"); logging!( debug, @@ -80,8 +142,11 @@ impl WindowManager { WindowState::NotExist => { logging!(info, Type::Window, true, "窗口不存在,创建新窗口"); if Self::create_new_window() { + logging!(info, Type::Window, true, "窗口创建成功"); + std::thread::sleep(std::time::Duration::from_millis(100)); WindowOperationResult::Created } else { + logging!(warn, Type::Window, true, "窗口创建失败"); WindowOperationResult::Failed } } @@ -91,6 +156,16 @@ impl WindowManager { } WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => { if let Some(window) = Self::get_main_window() { + let state_after_check = Self::get_main_window_state(); + if state_after_check == WindowState::VisibleFocused { + logging!( + info, + Type::Window, + true, + "窗口在检查期间已变为可见和有焦点状态" + ); + return WindowOperationResult::NoAction; + } Self::activate_window(&window) } else { WindowOperationResult::Failed @@ -101,6 +176,14 @@ impl WindowManager { /// 切换主窗口显示状态(显示/隐藏) pub fn toggle_main_window() -> WindowOperationResult { + // 防抖检查 + if !should_handle_window_operation() { + return WindowOperationResult::NoAction; + } + let _guard = scopeguard::guard((), |_| { + finish_window_operation(); + }); + logging!(info, Type::Window, true, "开始切换主窗口显示状态"); let current_state = Self::get_main_window_state(); @@ -108,37 +191,61 @@ impl WindowManager { info, Type::Window, true, - "当前窗口状态: {:?}", - current_state + "当前窗口状态: {:?} | 详细状态: {}", + current_state, + Self::get_window_status_info() ); match current_state { WindowState::NotExist => { // 窗口不存在,创建新窗口 + logging!(info, Type::Window, true, "窗口不存在,将创建新窗口"); + // 由于已经有防抖保护,直接调用内部方法 if Self::create_new_window() { WindowOperationResult::Created } else { WindowOperationResult::Failed } } - WindowState::VisibleFocused => { - // 窗口可见且有焦点,隐藏它 - if let Some(window) = Self::get_main_window() { - if window.hide().is_ok() { - logging!(info, Type::Window, true, "窗口已隐藏"); - WindowOperationResult::Hidden + WindowState::VisibleFocused | WindowState::VisibleUnfocused => { + logging!( + info, + Type::Window, + true, + "窗口可见(焦点状态: {}),将隐藏窗口", + if current_state == WindowState::VisibleFocused { + "有焦点" } else { - WindowOperationResult::Failed + "无焦点" + } + ); + if let Some(window) = Self::get_main_window() { + match window.hide() { + Ok(_) => { + logging!(info, Type::Window, true, "窗口已成功隐藏"); + WindowOperationResult::Hidden + } + Err(e) => { + logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e); + WindowOperationResult::Failed + } } } else { + logging!(warn, Type::Window, true, "无法获取窗口实例"); WindowOperationResult::Failed } } - WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => { - // 窗口存在但不可见或无焦点,激活它 + WindowState::Minimized | WindowState::Hidden => { + logging!( + info, + Type::Window, + true, + "窗口存在但被隐藏或最小化,将激活窗口" + ); if let Some(window) = Self::get_main_window() { Self::activate_window(&window) } else { + logging!(warn, Type::Window, true, "无法获取窗口实例"); WindowOperationResult::Failed } } @@ -216,17 +323,21 @@ impl WindowManager { pub fn hide_main_window() -> WindowOperationResult { logging!(info, Type::Window, true, "开始隐藏主窗口"); - if let Some(window) = Self::get_main_window() { - if window.hide().is_ok() { - logging!(info, Type::Window, true, "窗口已隐藏"); - WindowOperationResult::Hidden - } else { - logging!(warn, Type::Window, true, "隐藏窗口失败"); - WindowOperationResult::Failed + match Self::get_main_window() { + Some(window) => match window.hide() { + Ok(_) => { + logging!(info, Type::Window, true, "窗口已隐藏"); + WindowOperationResult::Hidden + } + Err(e) => { + logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e); + WindowOperationResult::Failed + } + }, + None => { + logging!(info, Type::Window, true, "窗口不存在,无需隐藏"); + WindowOperationResult::NoAction } - } else { - logging!(info, Type::Window, true, "窗口不存在,无需隐藏"); - WindowOperationResult::NoAction } } @@ -251,7 +362,7 @@ impl WindowManager { .unwrap_or(false) } - /// 创建新窗口现有的实现 + /// 创建新窗口,防抖避免重复调用 fn create_new_window() -> bool { use crate::utils::resolve; resolve::create_window(true) diff --git a/src/components/home/clash-info-card.tsx b/src/components/home/clash-info-card.tsx index 029f174c82..dcdd22a8e5 100644 --- a/src/components/home/clash-info-card.tsx +++ b/src/components/home/clash-info-card.tsx @@ -17,7 +17,7 @@ const formatUptime = (uptimeMs: number) => { export const ClashInfoCard = () => { const { t } = useTranslation(); const { version: clashVersion } = useClash(); - const { clashConfig, sysproxy, rules, uptime } = useAppData(); + const { clashConfig, rules, uptime, systemProxyAddress } = useAppData(); // 使用useMemo缓存格式化后的uptime,避免频繁计算 const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]); @@ -42,7 +42,7 @@ export const ClashInfoCard = () => { {t("System Proxy Address")} - {sysproxy?.server || "-"} + {systemProxyAddress} @@ -74,7 +74,14 @@ export const ClashInfoCard = () => { ); - }, [clashConfig, clashVersion, t, formattedUptime, rules.length, sysproxy]); + }, [ + clashConfig, + clashVersion, + t, + formattedUptime, + rules.length, + systemProxyAddress, + ]); return ( { const { verge } = useVerge(); const { isAdminMode } = useSystemState(); + const { indicator: systemProxyIndicator } = useSystemProxyState(); - const { enable_system_proxy, enable_tun_mode } = verge ?? {}; + const { enable_tun_mode } = verge ?? {}; const updateLocalStatus = async () => { try { @@ -180,7 +182,7 @@ export const ProxyTunCard: FC = () => { const tabDescription = useMemo(() => { if (activeTab === "system") { return { - text: enable_system_proxy + text: systemProxyIndicator ? t("System Proxy Enabled") : t("System Proxy Disabled"), tooltip: t("System Proxy Info"), @@ -195,7 +197,7 @@ export const ProxyTunCard: FC = () => { tooltip: t("TUN Mode Intercept Info"), }; } - }, [activeTab, enable_system_proxy, enable_tun_mode, isTunAvailable, t]); + }, [activeTab, systemProxyIndicator, enable_tun_mode, isTunAvailable, t]); return ( @@ -214,7 +216,7 @@ export const ProxyTunCard: FC = () => { onClick={() => handleTabChange("system")} icon={ComputerRounded} label={t("System Proxy")} - hasIndicator={enable_system_proxy} + hasIndicator={systemProxyIndicator} /> ((props, ref) => { diff --git a/src/components/setting/mods/layout-viewer.tsx b/src/components/setting/mods/layout-viewer.tsx index 55425e459c..2b498b76ee 100644 --- a/src/components/setting/mods/layout-viewer.tsx +++ b/src/components/setting/mods/layout-viewer.tsx @@ -209,7 +209,7 @@ export const LayoutViewer = forwardRef((props, ref) => { )} - {OS === "macos" && ( + {/* {OS === "macos" && ( ((props, ref) => { - )} + )} */} {OS === "macos" && ( diff --git a/src/components/setting/mods/sysproxy-viewer.tsx b/src/components/setting/mods/sysproxy-viewer.tsx index 4481e8ad28..7582e8c8cc 100644 --- a/src/components/setting/mods/sysproxy-viewer.tsx +++ b/src/components/setting/mods/sysproxy-viewer.tsx @@ -3,6 +3,7 @@ import { BaseFieldset } from "@/components/base/base-fieldset"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { EditorViewer } from "@/components/profile/editor-viewer"; import { useVerge } from "@/hooks/use-verge"; +import { useAppData } from "@/providers/app-data-provider"; import { getClashConfig } from "@/services/api"; import { getAutotemProxy, @@ -172,6 +173,35 @@ export const SysproxyViewer = forwardRef((props, ref) => { } }; + const { systemProxyAddress } = useAppData(); + + // 为当前状态计算系统代理地址 + const getSystemProxyAddress = useMemo(() => { + if (!clashConfig) return "-"; + + const isPacMode = value.pac ?? false; + + if (isPacMode) { + const host = value.proxy_host || "127.0.0.1"; + const port = verge?.verge_mixed_port || clashConfig["mixed-port"] || 7897; + return `${host}:${port}`; + } else { + return systemProxyAddress; + } + }, [ + value.pac, + value.proxy_host, + verge?.verge_mixed_port, + clashConfig, + systemProxyAddress, + ]); + const getCurrentPacUrl = useMemo(() => { + const host = value.proxy_host || "127.0.0.1"; + // 根据环境判断PAC端口 + const port = import.meta.env.DEV ? 11233 : 33331; + return `http://${host}:${port}/commands/pac`; + }, [value.proxy_host]); + useImperativeHandle(ref, () => ({ open: () => { setOpen(true); @@ -417,7 +447,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { {t("Server Addr")} - {sysproxy?.server ? sysproxy.server : t("Not available")} + {getSystemProxyAddress} @@ -425,7 +455,9 @@ export const SysproxyViewer = forwardRef((props, ref) => { {value.pac && ( {t("PAC URL")} - {autoproxy?.url || "-"} + + {getCurrentPacUrl || "-"} + )} diff --git a/src/components/setting/setting-system.tsx b/src/components/setting/setting-system.tsx index f892bc8ebf..2e94f01f2c 100644 --- a/src/components/setting/setting-system.tsx +++ b/src/components/setting/setting-system.tsx @@ -1,5 +1,5 @@ -import useSWR, { mutate } from "swr"; -import { useRef, useEffect, useState } from "react"; +import { mutate } from "swr"; +import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { SettingsRounded, @@ -10,24 +10,18 @@ import { DeleteForeverRounded, } from "@mui/icons-material"; import { useVerge } from "@/hooks/use-verge"; +import { useSystemProxyState } from "@/hooks/use-system-proxy-state"; import { DialogRef, Switch } from "@/components/base"; import { SettingList, SettingItem } from "./mods/setting-comp"; import { GuardState } from "./mods/guard-state"; import { SysproxyViewer } from "./mods/sysproxy-viewer"; import { TunViewer } from "./mods/tun-viewer"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; -import { - getSystemProxy, - getAutotemProxy, - installService, - uninstallService, - restartCore, - stopCore, -} from "@/services/cmds"; +import { uninstallService, restartCore, stopCore } from "@/services/cmds"; import { useLockFn } from "ahooks"; import { Button, Tooltip } from "@mui/material"; import { useSystemState } from "@/hooks/use-system-state"; -import { closeAllConnections } from "@/services/api"; + import { showNotice } from "@/services/noticeService"; import { useServiceInstaller } from "@/hooks/useServiceInstaller"; @@ -40,9 +34,11 @@ const SettingSystem = ({ onError }: Props) => { const { verge, mutateVerge, patchVerge } = useVerge(); const { installServiceAndRestartCore } = useServiceInstaller(); - - const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy); - const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy); + const { + actualState: systemProxyActualState, + indicator: systemProxyIndicator, + toggleSystemProxy, + } = useSystemProxyState(); const { isAdminMode, isServiceMode, mutateRunningMode } = useSystemState(); @@ -52,26 +48,14 @@ const SettingSystem = ({ onError }: Props) => { const sysproxyRef = useRef(null); const tunRef = useRef(null); - const { - enable_tun_mode, - enable_auto_launch, - enable_silent_start, - enable_system_proxy, - proxy_auto_config, - enable_hover_jump_navigator, - } = verge ?? {}; + const { enable_tun_mode, enable_auto_launch, enable_silent_start } = + verge ?? {}; const onSwitchFormat = (_e: any, value: boolean) => value; const onChangeData = (patch: Partial) => { mutateVerge({ ...verge, ...patch }, false); }; - const updateProxyStatus = async () => { - await new Promise((resolve) => setTimeout(resolve, 100)); - await mutate("getSystemProxy"); - await mutate("getAutotemProxy"); - }; - // 抽象服务操作逻辑 const handleServiceOperation = useLockFn( async ({ @@ -195,13 +179,7 @@ const SettingSystem = ({ onError }: Props) => { icon={SettingsRounded} onClick={() => sysproxyRef.current?.open()} /> - {proxy_auto_config ? ( - autoproxy?.enable ? ( - - ) : ( - - ) - ) : sysproxy?.enable ? ( + {systemProxyIndicator ? ( ) : ( @@ -210,44 +188,13 @@ const SettingSystem = ({ onError }: Props) => { } > { - if (autoproxy?.enable === false && sysproxy?.enable === false) { - onChangeData({ enable_system_proxy: !enable_system_proxy }); - } else { - onChangeData({ enable_system_proxy: e }); - } - }} - onGuard={async (e) => { - if (autoproxy?.enable === false && sysproxy?.enable === false) { - await patchVerge({ enable_system_proxy: !enable_system_proxy }); - await updateProxyStatus(); - return; - } - if (!e && verge?.auto_close_connection) { - closeAllConnections(); - } - await patchVerge({ enable_system_proxy: e }); - await updateProxyStatus(); - }} + onGuard={(e) => toggleSystemProxy(e)} > - + diff --git a/src/components/shared/ProxyControlSwitches.tsx b/src/components/shared/ProxyControlSwitches.tsx index 18a2b1375d..f96d5cbeb5 100644 --- a/src/components/shared/ProxyControlSwitches.tsx +++ b/src/components/shared/ProxyControlSwitches.tsx @@ -1,6 +1,6 @@ import { useRef } from "react"; import { useTranslation } from "react-i18next"; -import useSWR, { mutate } from "swr"; +import useSWR from "swr"; import { SettingsRounded, PlayCircleOutlineRounded, @@ -20,16 +20,8 @@ import { GuardState } from "@/components/setting/mods/guard-state"; import { SysproxyViewer } from "@/components/setting/mods/sysproxy-viewer"; import { TunViewer } from "@/components/setting/mods/tun-viewer"; import { useVerge } from "@/hooks/use-verge"; -import { - getSystemProxy, - getAutotemProxy, - getRunningMode, - installService, - restartCore, - isServiceAvailable, -} from "@/services/cmds"; -import { useLockFn } from "ahooks"; -import { closeAllConnections } from "@/services/api"; +import { useSystemProxyState } from "@/hooks/use-system-proxy-state"; +import { getRunningMode } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; import { useServiceInstaller } from "@/hooks/useServiceInstaller"; @@ -48,12 +40,13 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => { const theme = useTheme(); const { installServiceAndRestartCore } = useServiceInstaller(); - const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy); - const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy); - const { data: runningMode, mutate: mutateRunningMode } = useSWR( - "getRunningMode", - getRunningMode, - ); + const { + actualState: systemProxyActualState, + indicator: systemProxyIndicator, + toggleSystemProxy, + } = useSystemProxyState(); + + const { data: runningMode } = useSWR("getRunningMode", getRunningMode); // 是否以sidecar模式运行 const isSidecarMode = runningMode === "Sidecar"; @@ -61,8 +54,7 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => { const sysproxyRef = useRef(null); const tunRef = useRef(null); - const { enable_tun_mode, enable_system_proxy, proxy_auto_config } = - verge ?? {}; + const { enable_tun_mode, enable_system_proxy } = verge ?? {}; // 确定当前显示哪个开关 const isSystemProxyMode = label === t("System Proxy") || !label; @@ -73,12 +65,6 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => { mutateVerge({ ...verge, ...patch }, false); }; - const updateProxyStatus = async () => { - await new Promise((resolve) => setTimeout(resolve, 100)); - await mutate("getSystemProxy"); - await mutate("getAutotemProxy"); - }; - // 安装系统服务 const onInstallService = installServiceAndRestartCore; @@ -113,7 +99,7 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => { }} > - {enable_system_proxy ? ( + {systemProxyIndicator ? ( @@ -155,18 +141,11 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => { onChangeData({ enable_system_proxy: e })} - onGuard={async (e) => { - if (!e && verge?.auto_close_connection) { - closeAllConnections(); - } - await patchVerge({ enable_system_proxy: e }); - await updateProxyStatus(); - }} + onGuard={(e) => toggleSystemProxy(e)} > diff --git a/src/hooks/use-log-data.ts b/src/hooks/use-log-data.ts index 8ceebb3cf0..cecf7168a7 100644 --- a/src/hooks/use-log-data.ts +++ b/src/hooks/use-log-data.ts @@ -1,10 +1,4 @@ -import { useEffect } from "react"; -import { useEnableLog } from "../services/states"; -import { createSockette, createAuthSockette } from "../utils/websocket"; -import { useClashInfo } from "./use-clash"; -import dayjs from "dayjs"; import { create } from "zustand"; -import { useVisibility } from "./use-visibility"; import { useGlobalLogData, clearGlobalLogs, diff --git a/src/hooks/use-system-proxy-state.ts b/src/hooks/use-system-proxy-state.ts new file mode 100644 index 0000000000..2e4621ba18 --- /dev/null +++ b/src/hooks/use-system-proxy-state.ts @@ -0,0 +1,72 @@ +import useSWR, { mutate } from "swr"; +import { useVerge } from "@/hooks/use-verge"; +import { getAutotemProxy } from "@/services/cmds"; +import { useAppData } from "@/providers/app-data-provider"; +import { closeAllConnections } from "@/services/api"; + +// 系统代理状态检测统一逻辑 +export const useSystemProxyState = () => { + const { verge, mutateVerge, patchVerge } = useVerge(); + const { sysproxy } = useAppData(); + const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy, { + revalidateOnFocus: true, + revalidateOnReconnect: true, + }); + + const { enable_system_proxy, proxy_auto_config } = verge ?? {}; + + const getSystemProxyActualState = () => { + const userEnabled = enable_system_proxy ?? false; + + if (userEnabled) { + return true; + } + + return autoproxy?.enable === false && sysproxy?.enable === false + ? false + : userEnabled; + }; + + const getSystemProxyIndicator = () => { + if (proxy_auto_config) { + return autoproxy?.enable ?? false; + } else { + return sysproxy?.enable ?? false; + } + }; + + const updateProxyStatus = async () => { + await new Promise((resolve) => setTimeout(resolve, 100)); + await mutate("getSystemProxy"); + await mutate("getAutotemProxy"); + }; + + const toggleSystemProxy = (enabled: boolean) => { + mutateVerge({ ...verge, enable_system_proxy: enabled }, false); + + setTimeout(async () => { + try { + if (!enabled && verge?.auto_close_connection) { + closeAllConnections(); + } + await patchVerge({ enable_system_proxy: enabled }); + + updateProxyStatus(); + } catch (error) { + mutateVerge({ ...verge, enable_system_proxy: !enabled }, false); + } + }, 0); + + return Promise.resolve(); + }; + + return { + actualState: getSystemProxyActualState(), + indicator: getSystemProxyIndicator(), + configState: enable_system_proxy ?? false, + sysproxy, + autoproxy, + proxy_auto_config, + toggleSystemProxy, + }; +}; diff --git a/src/locales/en.json b/src/locales/en.json index eb3f8c6e9d..740911dd53 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -623,5 +623,6 @@ "Originals Only": "Originals Only", "No (IP Banned By Disney+)": "No (IP Banned By Disney+)", "Unsupported Country/Region": "Unsupported Country/Region", - "Failed (Network Connection)": "Failed (Network Connection)" + "Failed (Network Connection)": "Failed (Network Connection)", + "Invalid Profile URL": "Invalid profile URL. Please enter a URL starting with http:// or https://" } diff --git a/src/locales/ru.json b/src/locales/ru.json index f4aa2350ca..0bdeab4871 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -581,5 +581,6 @@ "Originals Only": "Только Originals", "No (IP Banned By Disney+)": "Нет (IP забанен Disney+)", "Unsupported Country/Region": "Страна/регион не поддерживается", - "Failed (Network Connection)": "Ошибка подключения" + "Failed (Network Connection)": "Ошибка подключения", + "Invalid Profile URL": "Недопустимая ссылка на профиль, введите адрес, начинающийся с http:// или https://" } diff --git a/src/locales/zh.json b/src/locales/zh.json index 4689bfe634..aa13fc3b68 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -623,5 +623,6 @@ "Originals Only": "仅限原创", "No (IP Banned By Disney+)": "不支持(IP被Disney+禁止)", "Unsupported Country/Region": "不支持的国家/地区", - "Failed (Network Connection)": "测试失败(网络连接问题)" + "Failed (Network Connection)": "测试失败(网络连接问题)", + "Invalid Profile URL": "无效的订阅链接,请输入以 http:// 或 https:// 开头的地址" } diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 2d763f5a38..50657fc2d8 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -21,7 +21,6 @@ import { useCustomTheme } from "@/components/layout/use-custom-theme"; import getSystem from "@/utils/get-system"; import "dayjs/locale/ru"; import "dayjs/locale/zh-cn"; -import { getPortableFlag } from "@/services/cmds"; import React from "react"; import { useListen } from "@/hooks/use-listen"; import { listen } from "@tauri-apps/api/event"; diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index b0da5d125d..4c8b6d4c11 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -19,10 +19,7 @@ import { ConnectionDetailRef, } from "@/components/connection/connection-detail"; import parseTraffic from "@/utils/parse-traffic"; -import { - BaseSearchBox, - type SearchState, -} from "@/components/base/base-search-box"; +import { BaseSearchBox } from "@/components/base/base-search-box"; import { BaseStyledSelect } from "@/components/base/base-styled-select"; import { useTheme } from "@mui/material/styles"; import { useVisibility } from "@/hooks/use-visibility"; diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 7e910f1251..1d9fbaa864 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -37,11 +37,7 @@ import { BasePage } from "@/components/base"; import { ClashInfoCard } from "@/components/home/clash-info-card"; import { SystemInfoCard } from "@/components/home/system-info-card"; import { useLockFn } from "ahooks"; -import { - entry_lightweight_mode, - openWebUrl, - patchVergeConfig, -} from "@/services/cmds"; +import { entry_lightweight_mode, openWebUrl } from "@/services/cmds"; import { TestCard } from "@/components/home/test-card"; import { IpInfoCard } from "@/components/home/ip-info-card"; diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 7d75e401bb..71bfd73b27 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -45,7 +45,7 @@ import { ProfileMore } from "@/components/profile/profile-more"; import { ProfileItem } from "@/components/profile/profile-item"; import { useProfiles } from "@/hooks/use-profiles"; import { ConfigViewer } from "@/components/setting/mods/config-viewer"; -import { add, throttle } from "lodash-es"; +import { throttle } from "lodash-es"; import { BaseStyledTextField } from "@/components/base/base-styled-text-field"; import { readTextFile } from "@tauri-apps/plugin-fs"; import { readText } from "@tauri-apps/plugin-clipboard-manager"; @@ -227,8 +227,12 @@ const ProfilePage = () => { const onImport = async () => { if (!url) return; + // 校验url是否为http/https + if (!/^https?:\/\//i.test(url)) { + showNotice("error", t("Invalid Profile URL")); + return; + } setLoading(true); - try { // 尝试正常导入 await importProfile(url); @@ -240,14 +244,12 @@ const ProfilePage = () => { // 首次导入失败,尝试使用自身代理 const errmsg = err.message || err.toString(); showNotice("info", t("Import failed, retrying with Clash proxy...")); - try { // 使用自身代理尝试导入 await importProfile(url, { with_proxy: false, self_proxy: true, }); - // 回退导入成功 showNotice("success", t("Profile Imported with Clash proxy")); setUrl(""); diff --git a/src/providers/app-data-provider.tsx b/src/providers/app-data-provider.tsx index 166b8057b4..9e7ab3afd8 100644 --- a/src/providers/app-data-provider.tsx +++ b/src/providers/app-data-provider.tsx @@ -1,4 +1,5 @@ -import { createContext, useContext, useMemo, useEffect } from "react"; +import React, { createContext, useContext, useEffect, useMemo } from "react"; +import { useVerge } from "@/hooks/use-verge"; import useSWR from "swr"; import useSWRSubscription from "swr/subscription"; import { @@ -37,6 +38,8 @@ interface AppDataContextType { }; traffic: { up: number; down: number }; memory: { inuse: number }; + systemProxyAddress: string; + refreshProxy: () => Promise; refreshClashConfig: () => Promise; refreshRules: () => Promise; @@ -55,8 +58,9 @@ export const AppDataProvider = ({ }: { children: React.ReactNode; }) => { - const { clashInfo } = useClashInfo(); const pageVisible = useVisibility(); + const { clashInfo } = useClashInfo(); + const { verge } = useVerge(); // 基础数据 - 中频率更新 (5秒) const { data: proxiesData, mutate: refreshProxy } = useSWR( @@ -64,7 +68,7 @@ export const AppDataProvider = ({ getProxies, { refreshInterval: 5000, - revalidateOnFocus: false, + revalidateOnFocus: true, suspense: false, errorRetryCount: 3, }, @@ -239,7 +243,8 @@ export const AppDataProvider = ({ "getSystemProxy", getSystemProxy, { - revalidateOnFocus: false, + revalidateOnFocus: true, + revalidateOnReconnect: true, suspense: false, errorRetryCount: 3, }, @@ -508,8 +513,39 @@ export const AppDataProvider = ({ }; // 聚合所有数据 - const value = useMemo( - () => ({ + const value = useMemo(() => { + // 计算系统代理地址 + const calculateSystemProxyAddress = () => { + if (!verge || !clashConfig) return "-"; + + const isPacMode = verge.proxy_auto_config ?? false; + + if (isPacMode) { + // PAC模式:显示我们期望设置的代理地址 + const proxyHost = verge.proxy_host || "127.0.0.1"; + const proxyPort = + verge.verge_mixed_port || clashConfig["mixed-port"] || 7897; + return `${proxyHost}:${proxyPort}`; + } else { + // HTTP代理模式:优先使用系统地址,但如果格式不正确则使用期望地址 + const systemServer = sysproxy?.server; + if ( + systemServer && + systemServer !== "-" && + !systemServer.startsWith(":") + ) { + return systemServer; + } else { + // 系统地址无效,返回期望的代理地址 + const proxyHost = verge.proxy_host || "127.0.0.1"; + const proxyPort = + verge.verge_mixed_port || clashConfig["mixed-port"] || 7897; + return `${proxyHost}:${proxyPort}`; + } + } + }; + + return { // 数据 proxies: proxiesData, clashConfig, @@ -534,6 +570,8 @@ export const AppDataProvider = ({ traffic: trafficData, memory: memoryData, + systemProxyAddress: calculateSystemProxyAddress(), + // 刷新方法 refreshProxy, refreshClashConfig, @@ -542,27 +580,27 @@ export const AppDataProvider = ({ refreshProxyProviders, refreshRuleProviders, refreshAll, - }), - [ - proxiesData, - clashConfig, - rulesData, - sysproxy, - runningMode, - uptimeData, - connectionsData, - trafficData, - memoryData, - proxyProviders, - ruleProviders, - refreshProxy, - refreshClashConfig, - refreshRules, - refreshSysproxy, - refreshProxyProviders, - refreshRuleProviders, - ], - ); + }; + }, [ + proxiesData, + clashConfig, + rulesData, + sysproxy, + runningMode, + uptimeData, + connectionsData, + trafficData, + memoryData, + proxyProviders, + ruleProviders, + verge, + refreshProxy, + refreshClashConfig, + refreshRules, + refreshSysproxy, + refreshProxyProviders, + refreshRuleProviders, + ]); return ( {children} diff --git a/src/services/api.ts b/src/services/api.ts index cebaec84d4..f108ebb306 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,7 +1,6 @@ import axios, { AxiosInstance } from "axios"; import { getClashInfo } from "./cmds"; import { invoke } from "@tauri-apps/api/core"; -import { useLockFn } from "ahooks"; let instancePromise: Promise = null!; diff --git a/src/services/global-log-service.ts b/src/services/global-log-service.ts index b8d8731c77..8e94832521 100644 --- a/src/services/global-log-service.ts +++ b/src/services/global-log-service.ts @@ -1,8 +1,7 @@ // 全局日志服务,使应用在任何页面都能收集日志 import { create } from "zustand"; -import { createSockette, createAuthSockette } from "@/utils/websocket"; +import { createAuthSockette } from "@/utils/websocket"; import dayjs from "dayjs"; -import { useState, useEffect } from "react"; // 最大日志数量 const MAX_LOG_NUM = 1000; diff --git a/src/utils/uri-parser.ts b/src/utils/uri-parser.ts index eb8b630420..7aba40a5a5 100644 --- a/src/utils/uri-parser.ts +++ b/src/utils/uri-parser.ts @@ -730,7 +730,7 @@ function URI_Trojan(line: string): IProxyTrojanConfig { function URI_Hysteria2(line: string): IProxyHysteria2Config { line = line.split(/(hysteria2|hy2):\/\//)[2]; - // eslint-disable-next-line no-unused-vars + let [__, password, server, ___, port, ____, addons = "", name] = /^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || []; let portNum = parseInt(`${port}`, 10);