mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-15 22:00:56 +08:00
Compare commits
289
Commits
v2.3.0-alpha.4
...
v2.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9464ff776 | ||
|
|
335ca817d2 | ||
|
|
6d112c387d | ||
|
|
e4c243de2d | ||
|
|
02f67961a9 | ||
|
|
7d5fd295ed | ||
|
|
daa0b1592d | ||
|
|
b411783bbe | ||
|
|
40a59bbc1a | ||
|
|
93fc4932ee | ||
|
|
2277d7232e | ||
|
|
435318cf1d | ||
|
|
a9a9d8a78f | ||
|
|
a2544d237e | ||
|
|
9397ac0174 | ||
|
|
4c719da096 | ||
|
|
7613417c33 | ||
|
|
a2a65cade7 | ||
|
|
3a07402aa2 | ||
|
|
59b67f5d3f | ||
|
|
e30cfc3a2f | ||
|
|
52655d9702 | ||
|
|
e93846ddc1 | ||
|
|
4cf2f6b1e6 | ||
|
|
43a3cb74ac | ||
|
|
3360339c08 | ||
|
|
f0dbe9fa60 | ||
|
|
756751b765 | ||
|
|
85a9f6c8d4 | ||
|
|
7fe0381850 | ||
|
|
537d27d10b | ||
|
|
a5fdd3f1a2 | ||
|
|
be8a632a09 | ||
|
|
95c34f1df5 | ||
|
|
8b53a7bd99 | ||
|
|
b169ee8149 | ||
|
|
9cc6dde999 | ||
|
|
cf1fbb63c4 | ||
|
|
2aa629ff5d | ||
|
|
1e2b453c24 | ||
|
|
331e4a4970 | ||
|
|
ee3ffaef1d | ||
|
|
7cc3bc83a0 | ||
|
|
558e28ddaf | ||
|
|
45e69543b3 | ||
|
|
67ee41c5ea | ||
|
|
160ed05178 | ||
|
|
5ecfe121b3 | ||
|
|
a654137af9 | ||
|
|
cb591f19fb | ||
|
|
4823a348be | ||
|
|
32da6ae808 | ||
|
|
7eb70b0f0d | ||
|
|
52e8e45daf | ||
|
|
319c5b84fa | ||
|
|
6a93ff1fc1 | ||
|
|
6069b654d1 | ||
|
|
2af8c32497 | ||
|
|
a3957289c8 | ||
|
|
3f5cd6c26a | ||
|
|
8046dad56d | ||
|
|
cdc1fd2d87 | ||
|
|
499626b946 | ||
|
|
a9cfb2cfaa | ||
|
|
7b976c16eb | ||
|
|
44e8a035aa | ||
|
|
6b57607926 | ||
|
|
c3675e48fd | ||
|
|
a66393c609 | ||
|
|
776abaf56d | ||
|
|
18808004f4 | ||
|
|
db8761946d | ||
|
|
2194a96145 | ||
|
|
ecd396d70f | ||
|
|
5ab0438397 | ||
|
|
bcde047695 | ||
|
|
109f5f9648 | ||
|
|
d16c691c0f | ||
|
|
3eb2a5b3ef | ||
|
|
569e2d5192 | ||
|
|
0077157d28 | ||
|
|
9e19bab5a7 | ||
|
|
b1e2940db6 | ||
|
|
4113cd619c | ||
|
|
1f78d576a3 | ||
|
|
e2a548f6a5 | ||
|
|
2956725e66 | ||
|
|
5131d37d58 | ||
|
|
1dfba159e0 | ||
|
|
631718a138 | ||
|
|
3172ab2d1c | ||
|
|
6a6bde3764 | ||
|
|
f756b37f97 | ||
|
|
a26c28517a | ||
|
|
894428642b | ||
|
|
36d58d05b3 | ||
|
|
80de055fc2 | ||
|
|
4905b44c8a | ||
|
|
02e19bb132 | ||
|
|
84989e0ea3 | ||
|
|
9661c5fd82 | ||
|
|
c8dfdb7a5a | ||
|
|
df5897c908 | ||
|
|
8d0af75145 | ||
|
|
36c6f0ca8d | ||
|
|
3048a2ae08 | ||
|
|
27535c7bb7 | ||
|
|
15a1770ee9 | ||
|
|
f580409ade | ||
|
|
f209d17e3c | ||
|
|
5047b0f614 | ||
|
|
ad623da86d | ||
|
|
3ec7772e5d | ||
|
|
e6b57561e3 | ||
|
|
3b1c2c95ec | ||
|
|
364abafffe | ||
|
|
389281b96c | ||
|
|
fed64bcd08 | ||
|
|
17a5c4d094 | ||
|
|
f2317c7816 | ||
|
|
7fce7ca62f | ||
|
|
167bcb222d | ||
|
|
108a599666 | ||
|
|
57476741cd | ||
|
|
077f3e79f8 | ||
|
|
2c9aa4bca7 | ||
|
|
9cf0f1e0a7 | ||
|
|
660c03a564 | ||
|
|
1d49d79af2 | ||
|
|
4ccb17dde6 | ||
|
|
8bc433711d | ||
|
|
2e82bd8624 | ||
|
|
4b57d513a2 | ||
|
|
d36c3a83a9 | ||
|
|
ac3afe4dee | ||
|
|
667844aa12 | ||
|
|
6d192233d1 | ||
|
|
764ef48fd1 | ||
|
|
3f95c81243 | ||
|
|
7c385d329d | ||
|
|
553438d219 | ||
|
|
941921904c | ||
|
|
7d808ed798 | ||
|
|
7704072a65 | ||
|
|
9076baf1c8 | ||
|
|
954ff53d9b | ||
|
|
37d268bb16 | ||
|
|
5befa90f81 | ||
|
|
6e3313f00d | ||
|
|
7b5afb7afe | ||
|
|
fe13dad06f | ||
|
|
b41bad0ae2 | ||
|
|
18ef7f0272 | ||
|
|
4435a5aee4 | ||
|
|
d9e3a47894 | ||
|
|
c96be18187 | ||
|
|
47416dd3f8 | ||
|
|
4486f734bb | ||
|
|
bea6a2c8f7 | ||
|
|
82af9ed78e | ||
|
|
2b9e38d259 | ||
|
|
7c4222aed2 | ||
|
|
a574ced428 | ||
|
|
cf437e6d94 | ||
|
|
e1bb8aa125 | ||
|
|
c11bdd81e9 | ||
|
|
f1192c95a8 | ||
|
|
ae187cc21a | ||
|
|
a7875718f7 | ||
|
|
5db1f7cda7 | ||
|
|
cb98b17052 | ||
|
|
7aee1c6d6e | ||
|
|
f22199b7d9 | ||
|
|
0a8d6e5147 | ||
|
|
6d519dac1e | ||
|
|
d5a174c71b | ||
|
|
628de70e89 | ||
|
|
fee08f3826 | ||
|
|
bdfc383a18 | ||
|
|
f6b5524e0e | ||
|
|
e7461fccab | ||
|
|
a92872c831 | ||
|
|
094feb74ec | ||
|
|
9b1c660306 | ||
|
|
4b860ba897 | ||
|
|
3d8b2cf35f | ||
|
|
41fc13cfe2 | ||
|
|
5fde5dcc7c | ||
|
|
034885d810 | ||
|
|
3f7a7b8cd2 | ||
|
|
98dc50a9ed | ||
|
|
5dd820d12e | ||
|
|
bc30db2875 | ||
|
|
abe914d446 | ||
|
|
cc65ce6812 | ||
|
|
1a6454ee79 | ||
|
|
b72f397369 | ||
|
|
e698fe8d18 | ||
|
|
c8cad1c295 | ||
|
|
c423fe90e2 | ||
|
|
623461b649 | ||
|
|
1f9ba4de5c | ||
|
|
10f792b8bc | ||
|
|
357b64e9a0 | ||
|
|
f3ad6cee41 | ||
|
|
972310ea4e | ||
|
|
f5315fd737 | ||
|
|
c40c1960ed | ||
|
|
c8ae37811c | ||
|
|
15f05748a5 | ||
|
|
0a0a25913b | ||
|
|
945999889c | ||
|
|
ce7e1d6456 | ||
|
|
72c4b9de8a | ||
|
|
a8257e8cb2 | ||
|
|
15d3c765ac | ||
|
|
e7d3d7e0ae | ||
|
|
c0f3d35e13 | ||
|
|
fc30fab9cd | ||
|
|
a67e8388a9 | ||
|
|
ac7307b1f7 | ||
|
|
4068e5ec9c | ||
|
|
ea71181692 | ||
|
|
9b46c1a991 | ||
|
|
26acce94a4 | ||
|
|
2a0e1e206e | ||
|
|
f83d1d1582 | ||
|
|
77a090d5eb | ||
|
|
a9561e0ded | ||
|
|
dd505e4d58 | ||
|
|
dd0e9d4e1b | ||
|
|
8bc451ff08 | ||
|
|
032e5bf32e | ||
|
|
41ba8f6c7a | ||
|
|
799783d3ef | ||
|
|
77fb40506f | ||
|
|
5858f05c13 | ||
|
|
0432cad112 | ||
|
|
47ec8a348c | ||
|
|
eef348b8dc | ||
|
|
e78a02d0ba | ||
|
|
4e54b61380 | ||
|
|
cc39b2734e | ||
|
|
16eaccb10e | ||
|
|
dbc6f54a77 | ||
|
|
9bd5950f60 | ||
|
|
2c7c416f60 | ||
|
|
3eb0ab4197 | ||
|
|
b7cb511032 | ||
|
|
18850bb51a | ||
|
|
4d718e0cea | ||
|
|
3a2f567055 | ||
|
|
4f474e1098 | ||
|
|
22eac1a832 | ||
|
|
07738dd82d | ||
|
|
f07d508018 | ||
|
|
b591a90100 | ||
|
|
db3cfdf66f | ||
|
|
5cf3e1a817 | ||
|
|
25cfd162f6 | ||
|
|
7001ef2030 | ||
|
|
cbcab72a7c | ||
|
|
d05b1c3130 | ||
|
|
fbdcda9a7f | ||
|
|
0ebc482698 | ||
|
|
09969d95de | ||
|
|
689042df60 | ||
|
|
564fe15df2 | ||
|
|
30015dfd67 | ||
|
|
698c47da69 | ||
|
|
46b7a520cd | ||
|
|
cadd6c3497 | ||
|
|
c4682ab6e9 | ||
|
|
59594855b8 | ||
|
|
44b7af0c6e | ||
|
|
80f550d67e | ||
|
|
44f085604a | ||
|
|
1144f8017a | ||
|
|
d8fa17a3e7 | ||
|
|
bf868e0ae2 | ||
|
|
e1dac63f69 | ||
|
|
2657234761 | ||
|
|
4a345fdd41 | ||
|
|
fee6a586d7 | ||
|
|
7c738483b7 | ||
|
|
c3d8ed28a2 | ||
|
|
f47b4b961b | ||
|
|
7763abf6c2 | ||
|
|
9d9d078346 |
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"name": "Clash Verge Rev Development Environment",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-22.04",
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "20"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "latest",
|
||||
"profile": "default"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/git:1": {},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
|
||||
},
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"rust-lang.rust-analyzer",
|
||||
"tauri-apps.tauri-vscode",
|
||||
"ms-vscode.vscode-typescript-next",
|
||||
"esbenp.prettier-vscode",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"ms-vscode.vscode-json",
|
||||
"redhat.vscode-yaml",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"ms-vscode.hexeditor",
|
||||
"christian-kohler.path-intellisense",
|
||||
"yzhang.markdown-all-in-one",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"ms-vscode.vscode-eslint"
|
||||
],
|
||||
"settings": {
|
||||
"rust-analyzer.cargo.features": ["verge-dev"],
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[rust]": {
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[yaml]": {
|
||||
"editor.defaultFormatter": "redhat.vscode-yaml"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"forwardPorts": [1420, 3000, 8080, 9090, 7890, 7891],
|
||||
|
||||
"portsAttributes": {
|
||||
"1420": {
|
||||
"label": "Tauri Dev Server",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"3000": {
|
||||
"label": "Vite Dev Server",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"7890": {
|
||||
"label": "Clash HTTP Proxy",
|
||||
"onAutoForward": "silent"
|
||||
},
|
||||
"7891": {
|
||||
"label": "Clash SOCKS Proxy",
|
||||
"onAutoForward": "silent"
|
||||
},
|
||||
"9090": {
|
||||
"label": "Clash API",
|
||||
"onAutoForward": "silent"
|
||||
}
|
||||
},
|
||||
|
||||
"postCreateCommand": "bash .devcontainer/post-create.sh",
|
||||
|
||||
"mounts": [
|
||||
"source=clash-verge-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume",
|
||||
"source=clash-verge-cargo-registry,target=/usr/local/cargo/registry,type=volume",
|
||||
"source=clash-verge-cargo-git,target=/usr/local/cargo/git,type=volume"
|
||||
],
|
||||
|
||||
"containerEnv": {
|
||||
"RUST_BACKTRACE": "1",
|
||||
"NODE_OPTIONS": "--max-old-space-size=4096",
|
||||
"TAURI_DEV_WATCHER_IGNORE_FILE": ".taurignore"
|
||||
},
|
||||
|
||||
"remoteUser": "vscode",
|
||||
"workspaceFolder": "/workspaces/clash-verge-rev",
|
||||
"shutdownAction": "stopContainer"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
github: clash-verge-rev
|
||||
@@ -9,13 +9,16 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
## 在提交问题之前,请确认以下事项:
|
||||
|
||||
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||
4. 请 **务必** 查看 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本更新日志
|
||||
5. 请 **务必** 尝试 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本,确定问题是否仍然存在
|
||||
6. 请 **务必** 按照模板规范详细描述问题以及尝试更新 Alpha 版本,否则issue将会被直接关闭
|
||||
|
||||
## Before submitting the issue, please make sure of the following checklist:
|
||||
|
||||
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide/term.html) and [FAQ](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
|
||||
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
|
||||
@@ -34,6 +37,7 @@ body:
|
||||
attributes:
|
||||
label: 软件版本 / Verge Version
|
||||
description: 请提供Verge的具体版本,如果是alpha版本,请注明下载时间(精确到小时分钟) / Please provide the specific version of Verge. If it is an alpha version, please indicate the download time (accurate to hours and minutes)
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -42,13 +46,13 @@ body:
|
||||
description: 请提供复现问题的步骤 / Steps to reproduce the behavior
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 操作系统 / OS
|
||||
options:
|
||||
- Windows
|
||||
- Linux
|
||||
- MacOS
|
||||
- label: Windows
|
||||
- label: Linux
|
||||
- label: MacOS
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -61,5 +65,9 @@ body:
|
||||
attributes:
|
||||
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
|
||||
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到debug,并重启Verge生效。日志文件在“软件左侧菜单”->“设置”->“日志目录”下) / Please provide a complete or relevant part of the Debug log (please adjust the "Log level" to debug in "Software left menu" -> "Settings" -> "Log level". If there is a Verge error, please adjust "Miscellaneous settings" -> "app log level" to debug, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
|
||||
placeholder: |
|
||||
日志目录一般位于 Clash Verge Rev 安装目录的 "logs/" 子目录中,请将日志内容粘贴到此处。
|
||||
Log directory is usually located in the "logs/" subdirectory of the Clash Verge Rev installation directory, please paste the log content here.
|
||||
render: log
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
name: I18N / 多语言相关
|
||||
title: "[I18N] "
|
||||
description: 用于多语言翻译、国际化相关问题或建议 / For issues or suggestions related to translations and internationalization
|
||||
labels: ["I18n"]
|
||||
type: "Task"
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## I18N 相关问题/建议
|
||||
请用此模板提交翻译错误、缺失、建议或新增语言请求。
|
||||
Please use this template for translation errors, missing translations, suggestions, or new language requests.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 问题描述 / Description
|
||||
description: 详细描述你的 I18N 问题或建议 / Please describe your I18N issue or suggestion in detail
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: language
|
||||
attributes:
|
||||
label: 相关语言 / Language
|
||||
description: 例如 zh, en, jp, ru, ... / e.g. zh, en, jp, ru, ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: suggestion
|
||||
attributes:
|
||||
label: 建议或修正内容 / Suggestion or Correction
|
||||
description: 如果是翻译修正或建议,请填写建议的内容 / If this is a translation correction or suggestion, please provide the suggested content
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: i18n-type
|
||||
attributes:
|
||||
label: 问题类型 / Issue Type
|
||||
description: 请选择适用类型(可多选) / Please select the applicable type(s)
|
||||
options:
|
||||
- label: 翻译错误 / Translation error
|
||||
- label: 翻译缺失 / Missing translation
|
||||
- label: 建议优化 / Suggestion
|
||||
- label: 新增语言 / New language
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: verge-version
|
||||
attributes:
|
||||
label: 软件版本 / Verge Version
|
||||
description: 请提供你使用的 Verge 具体版本 / Please provide the specific version of Verge you are using
|
||||
validations:
|
||||
required: true
|
||||
+178
-141
@@ -1,123 +1,176 @@
|
||||
name: Alpha Build
|
||||
|
||||
on:
|
||||
# 因为 alpha 不再负责频繁构建,且需要相对于 autobuild 更稳定使用环境
|
||||
# 所以不再使用 workflow_dispatch 触发
|
||||
# 应当通过 git tag 来触发构建
|
||||
# TODO 手动控制版本号
|
||||
workflow_dispatch:
|
||||
# schedule:
|
||||
# UTC+8 0,6,12,18
|
||||
# - cron: "0 16,22,4,10 * * *"
|
||||
# inputs:
|
||||
# tag_name:
|
||||
# description: "Alpha tag name (e.g. v1.2.3-alpha.1)"
|
||||
# required: true
|
||||
# type: string
|
||||
|
||||
# push:
|
||||
# # 应当限制在 dev 分支上触发发布。
|
||||
# branches:
|
||||
# - dev
|
||||
# # 应当限制 v*.*.*-alpha* 的 tag 来触发发布。
|
||||
# tags:
|
||||
# - "v*.*.*-alpha*"
|
||||
permissions: write-all
|
||||
env:
|
||||
TAG_NAME: alpha
|
||||
TAG_CHANNEL: Alpha
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
concurrency:
|
||||
# only allow per workflow per commit (and not pr) to run at a time
|
||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
check_commit:
|
||||
check_alpha_tag:
|
||||
name: Check Alpha Tag package.json Version Consistency
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check if version changed or src changed
|
||||
id: check
|
||||
- name: Check tag and package.json version
|
||||
id: check_tag
|
||||
run: |
|
||||
# For manual workflow_dispatch, always run
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
TAG_REF="${GITHUB_REF##*/}"
|
||||
echo "Current tag: $TAG_REF"
|
||||
if [[ ! "$TAG_REF" =~ -alpha ]]; then
|
||||
echo "Current tag is not an alpha tag."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Store current version from package.json
|
||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
# Get the previous commit's package.json version
|
||||
git checkout HEAD~1 package.json
|
||||
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
|
||||
echo "Previous version: $PREVIOUS_VERSION"
|
||||
|
||||
# Reset back to current commit
|
||||
git checkout HEAD package.json
|
||||
|
||||
# Check if version changed
|
||||
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
||||
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
PKG_VERSION=$(jq -r .version package.json)
|
||||
echo "package.json version: $PKG_VERSION"
|
||||
if [[ "$PKG_VERSION" != *alpha* ]]; then
|
||||
echo "package.json version is not an alpha version."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if src or src-tauri directories changed
|
||||
CURRENT_SRC_HASH=$(git rev-parse HEAD:src)
|
||||
PREVIOUS_SRC_HASH=$(git rev-parse HEAD~1:src 2>/dev/null || echo "")
|
||||
CURRENT_TAURI_HASH=$(git rev-parse HEAD:src-tauri 2>/dev/null || echo "")
|
||||
PREVIOUS_TAURI_HASH=$(git rev-parse HEAD~1:src-tauri 2>/dev/null || echo "")
|
||||
|
||||
echo "Current src hash: $CURRENT_SRC_HASH"
|
||||
echo "Previous src hash: $PREVIOUS_SRC_HASH"
|
||||
echo "Current tauri hash: $CURRENT_TAURI_HASH"
|
||||
echo "Previous tauri hash: $PREVIOUS_TAURI_HASH"
|
||||
|
||||
if [ "$CURRENT_SRC_HASH" != "$PREVIOUS_SRC_HASH" ] || [ "$CURRENT_TAURI_HASH" != "$PREVIOUS_TAURI_HASH" ]; then
|
||||
echo "Source directories changed"
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Version and source directories unchanged"
|
||||
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||
if [[ "$TAG_REF" != "v$PKG_VERSION" ]]; then
|
||||
echo "Tag ($TAG_REF) does not match package.json version (v$PKG_VERSION)."
|
||||
exit 1
|
||||
fi
|
||||
echo "Alpha tag and package.json version are consistent."
|
||||
|
||||
delete_old_assets:
|
||||
needs: check_commit
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||
name: Delete Old Alpha Release Assets and Tags
|
||||
needs: check_alpha_tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete Old Alpha Release Assets
|
||||
- name: Delete Old Alpha Tags Except Latest
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const releaseTag = 'alpha';
|
||||
const tagPattern = /-alpha.*/; // 匹配带有 -alpha 的 tag
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
try {
|
||||
// Get the release by tag name
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag: releaseTag
|
||||
// 获取所有 tag
|
||||
const { data: tags } = await github.rest.repos.listTags({
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100 // 调整 per_page 以获取更多 tag
|
||||
});
|
||||
|
||||
console.log(`Found release with ID: ${release.id}`);
|
||||
// 过滤出包含 -alpha 的 tag
|
||||
const alphaTags = (await Promise.all(
|
||||
tags
|
||||
.filter(tag => tagPattern.test(tag.name))
|
||||
.map(async tag => {
|
||||
// 获取每个 tag 的 commit 信息以获得日期
|
||||
const { data: commit } = await github.rest.repos.getCommit({
|
||||
owner,
|
||||
repo,
|
||||
ref: tag.commit.sha
|
||||
});
|
||||
return {
|
||||
...tag,
|
||||
commitDate: commit.committer && commit.committer.date ? commit.committer.date : commit.commit.author.date
|
||||
};
|
||||
})
|
||||
)).sort((a, b) => {
|
||||
// 按 commit 日期降序排序(最新的在前面)
|
||||
return new Date(b.commitDate) - new Date(a.commitDate);
|
||||
});
|
||||
|
||||
// Delete each asset
|
||||
if (release.assets && release.assets.length > 0) {
|
||||
console.log(`Deleting ${release.assets.length} assets`);
|
||||
console.log(`Found ${alphaTags.length} alpha tags`);
|
||||
|
||||
for (const asset of release.assets) {
|
||||
console.log(`Deleting asset: ${asset.name} (${asset.id})`);
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
asset_id: asset.id
|
||||
if (alphaTags.length === 0) {
|
||||
console.log('No alpha tags found');
|
||||
return;
|
||||
}
|
||||
|
||||
// 保留最新的 tag
|
||||
const latestTag = alphaTags[0];
|
||||
console.log(`Keeping latest alpha tag: ${latestTag.name}`);
|
||||
|
||||
// 处理其他旧的 alpha tag
|
||||
for (const tag of alphaTags.slice(1)) {
|
||||
console.log(`Processing tag: ${tag.name}`);
|
||||
|
||||
// 获取与 tag 关联的 release
|
||||
try {
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
owner,
|
||||
repo,
|
||||
tag: tag.name
|
||||
});
|
||||
}
|
||||
|
||||
console.log('All assets deleted successfully');
|
||||
} else {
|
||||
console.log('No assets found to delete');
|
||||
// 删除 release 下的所有资产
|
||||
if (release.assets && release.assets.length > 0) {
|
||||
console.log(`Deleting ${release.assets.length} assets for release ${tag.name}`);
|
||||
for (const asset of release.assets) {
|
||||
console.log(`Deleting asset: ${asset.name} (${asset.id})`);
|
||||
await github.rest.repos.deleteReleaseAsset({
|
||||
owner,
|
||||
repo,
|
||||
asset_id: asset.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 删除 release
|
||||
console.log(`Deleting release for tag: ${tag.name}`);
|
||||
await github.rest.repos.deleteRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: release.id
|
||||
});
|
||||
|
||||
// 删除 tag
|
||||
console.log(`Deleting tag: ${tag.name}`);
|
||||
await github.rest.git.deleteRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `tags/${tag.name}`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
console.log(`No release found for tag ${tag.name}, deleting tag directly`);
|
||||
await github.rest.git.deleteRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `tags/${tag.name}`
|
||||
});
|
||||
} else {
|
||||
console.error(`Error processing tag ${tag.name}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Old alpha tags and releases deleted successfully');
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
console.log('Release not found, nothing to delete');
|
||||
} else {
|
||||
console.error('Error:', error);
|
||||
throw error;
|
||||
}
|
||||
console.error('Error:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
update_tag:
|
||||
@@ -128,22 +181,18 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch Alpha update logs
|
||||
id: fetch_alpha_logs
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
run: |
|
||||
# Check if UPDATELOG.md exists
|
||||
if [ -f "UPDATELOG.md" ]; then
|
||||
# Extract the section starting with ## and containing -alpha until the next ## or end of file
|
||||
# ALPHA_LOGS=$(awk '/^## .*-alpha/{flag=1; print; next} /^## /{flag=0} flag' UPDATELOG.md)
|
||||
ALPHA_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||
|
||||
if [ -n "$ALPHA_LOGS" ]; then
|
||||
echo "Found alpha update logs"
|
||||
echo "ALPHA_LOGS<<EOF" >> $GITHUB_ENV
|
||||
echo "$ALPHA_LOGS" >> $GITHUB_ENV
|
||||
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||
if [ -n "$UPDATE_LOGS" ]; then
|
||||
echo "Found update logs"
|
||||
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
|
||||
echo "$UPDATE_LOGS" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
else
|
||||
echo "No alpha sections found in UPDATELOG.md"
|
||||
echo "No update sections found in UPDATELOG.md"
|
||||
fi
|
||||
else
|
||||
echo "UPDATELOG.md file not found"
|
||||
@@ -156,17 +205,15 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- run: |
|
||||
# 检查 ALPHA_LOGS 是否存在,如果不存在则使用默认消息
|
||||
if [ -z "$ALPHA_LOGS" ]; then
|
||||
echo "No alpha logs found, using default message"
|
||||
ALPHA_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||
if [ -z "$UPDATE_LOGS" ]; then
|
||||
echo "No update logs found, using default message"
|
||||
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||
else
|
||||
echo "Using found alpha logs"
|
||||
echo "Using found update logs"
|
||||
fi
|
||||
|
||||
# 生成 release.txt 文件
|
||||
cat > release.txt << EOF
|
||||
$ALPHA_LOGS
|
||||
$UPDATE_LOGS
|
||||
|
||||
## 我应该下载哪个版本?
|
||||
|
||||
@@ -189,7 +236,6 @@ jobs:
|
||||
- arm64架构: arm64_fixed_webview2-setup.exe
|
||||
|
||||
### FAQ
|
||||
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
### 稳定机场VPN推荐
|
||||
@@ -201,14 +247,15 @@ jobs:
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||
body_path: release.txt
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
generate_release_notes: true
|
||||
|
||||
alpha:
|
||||
alpha-x86-windows-macos-linux:
|
||||
name: Alpha x86 Windows, MacOS and Linux
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -224,7 +271,6 @@ jobs:
|
||||
target: x86_64-apple-darwin
|
||||
- os: ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
@@ -240,8 +286,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
save-if: false
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
@@ -262,13 +307,10 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
|
||||
- name: Patch Alpha Hash
|
||||
run: pnpm fix-alpha-version
|
||||
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
# run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
@@ -284,15 +326,16 @@ jobs:
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
tagName: alpha
|
||||
releaseName: "Clash Verge Rev Alpha"
|
||||
tagName: ${{ env.TAG_NAME }}
|
||||
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||
releaseBody: "More new features are now supported."
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
|
||||
alpha-for-linux-arm:
|
||||
alpha-arm-linux:
|
||||
name: Alpha ARM Linux
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -319,7 +362,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -334,16 +377,13 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
# run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
|
||||
- name: Patch Alpha Hash
|
||||
run: pnpm fix-alpha-version
|
||||
|
||||
- name: "Setup for linux"
|
||||
run: |-
|
||||
- name: Setup for linux
|
||||
run: |
|
||||
sudo ls -lR /etc/apt/
|
||||
|
||||
cat > /tmp/sources.list << EOF
|
||||
@@ -377,14 +417,14 @@ jobs:
|
||||
patchelf:${{ matrix.arch }} \
|
||||
librsvg2-dev:${{ matrix.arch }}
|
||||
|
||||
- name: "Install aarch64 tools"
|
||||
- name: Install aarch64 tools
|
||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
run: |
|
||||
sudo apt install -y \
|
||||
gcc-aarch64-linux-gnu \
|
||||
g++-aarch64-linux-gnu
|
||||
|
||||
- name: "Install armv7 tools"
|
||||
- name: Install armv7 tools
|
||||
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
||||
run: |
|
||||
sudo apt install -y \
|
||||
@@ -417,15 +457,16 @@ jobs:
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||
|
||||
alpha-for-fixed-webview2:
|
||||
alpha-x86-arm-windows_webview2:
|
||||
name: Alpha x86 and ARM Windows with WebView2
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -449,8 +490,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
save-if: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -465,13 +505,10 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
|
||||
- name: Patch Alpha Hash
|
||||
run: pnpm fix-alpha-version
|
||||
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
# run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
|
||||
- name: Download WebView2 Runtime
|
||||
run: |
|
||||
@@ -515,13 +552,13 @@ jobs:
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: alpha
|
||||
name: "Clash Verge Rev Alpha"
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||
|
||||
- name: Portable Bundle
|
||||
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --alpha
|
||||
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
name: Autobuild Check Logic Test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check_autobuild_logic:
|
||||
name: Check Autobuild Should Run Logic
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check if version or source changed, or assets already exist
|
||||
id: check
|
||||
run: |
|
||||
# # 仅用于测试逻辑,手动触发自动跳过
|
||||
# if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
# echo "should_run=skip" >> $GITHUB_OUTPUT
|
||||
# echo "🟡 手动触发,跳过 should_run 检查"
|
||||
# exit 0
|
||||
# fi
|
||||
|
||||
# 确保有 HEAD~1
|
||||
if ! git rev-parse HEAD~1 > /dev/null 2>&1; then
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
echo "🟢 没有前一个提交,默认需要构建"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 版本号变更判断
|
||||
CURRENT_VERSION=$(jq -r '.version' package.json)
|
||||
PREVIOUS_VERSION=$(git show HEAD~1:package.json | jq -r '.version' 2>/dev/null || echo "")
|
||||
|
||||
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
echo "🟢 版本号变更: $PREVIOUS_VERSION → $CURRENT_VERSION"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 检查 src 变更(排除常见产物与缓存)
|
||||
SRC_DIFF=$(git diff --name-only HEAD~1 HEAD -- src/ | grep -Ev '^src/(dist|build|node_modules|\.next|\.cache)' || true)
|
||||
TAURI_DIFF=$(git diff --name-only HEAD~1 HEAD -- src-tauri/ | grep -Ev '^src-tauri/(target|node_modules|dist|\.cache)' || true)
|
||||
|
||||
if [ -n "$SRC_DIFF" ] || [ -n "$TAURI_DIFF" ]; then
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
echo "🟢 源码变更 detected"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 找到最后一个修改 Tauri 相关文件的 commit
|
||||
echo "🔍 查找最后一个 Tauri 相关变更的 commit..."
|
||||
|
||||
LAST_TAURI_COMMIT=""
|
||||
for commit in $(git rev-list HEAD --max-count=50); do
|
||||
# 检查此 commit 是否修改了 Tauri 相关文件
|
||||
CHANGED_FILES=$(git show --name-only --pretty=format: $commit | tr '\n' ' ')
|
||||
HAS_TAURI_CHANGES=false
|
||||
|
||||
# 检查各个模式
|
||||
if echo "$CHANGED_FILES" | grep -q "src/" && echo "$CHANGED_FILES" | grep -qvE "src/(dist|build|node_modules|\.next|\.cache)"; then
|
||||
HAS_TAURI_CHANGES=true
|
||||
elif echo "$CHANGED_FILES" | grep -qE "src-tauri/(src|Cargo\.(toml|lock)|tauri\..*\.conf\.json|build\.rs|capabilities)"; then
|
||||
HAS_TAURI_CHANGES=true
|
||||
fi
|
||||
|
||||
if [ "$HAS_TAURI_CHANGES" = true ]; then
|
||||
LAST_TAURI_COMMIT=$(git rev-parse --short $commit)
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$LAST_TAURI_COMMIT" ]; then
|
||||
echo "⚠️ 最近的 commits 中未找到 Tauri 相关变更,使用当前 commit"
|
||||
LAST_TAURI_COMMIT=$(git rev-parse --short HEAD)
|
||||
fi
|
||||
|
||||
CURRENT_COMMIT=$(git rev-parse --short HEAD)
|
||||
echo "📝 最后 Tauri 相关 commit: $LAST_TAURI_COMMIT"
|
||||
echo "📝 当前 commit: $CURRENT_COMMIT"
|
||||
|
||||
# 检查 autobuild release 是否存在
|
||||
AUTOBUILD_RELEASE_EXISTS=$(gh release view "autobuild" --json id -q '.id' 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$AUTOBUILD_RELEASE_EXISTS" ]; then
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
echo "🟢 没有 autobuild release,需构建"
|
||||
else
|
||||
# 检查 latest.json 是否存在
|
||||
LATEST_JSON_EXISTS=$(gh release view "autobuild" --json assets -q '.assets[] | select(.name == "latest.json") | .name' 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$LATEST_JSON_EXISTS" ]; then
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
echo "🟢 没有 latest.json,需构建"
|
||||
else
|
||||
# 下载并解析 latest.json 检查版本和 commit hash
|
||||
echo "📥 下载 latest.json 检查版本..."
|
||||
LATEST_JSON_URL=$(gh release view "autobuild" --json assets -q '.assets[] | select(.name == "latest.json") | .browser_download_url' 2>/dev/null)
|
||||
|
||||
if [ -n "$LATEST_JSON_URL" ]; then
|
||||
LATEST_JSON_CONTENT=$(curl -s "$LATEST_JSON_URL" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$LATEST_JSON_CONTENT" ]; then
|
||||
LATEST_VERSION=$(echo "$LATEST_JSON_CONTENT" | jq -r '.version' 2>/dev/null || echo "")
|
||||
echo "📦 最新 autobuild 版本: $LATEST_VERSION"
|
||||
|
||||
# 从版本字符串中提取 commit hash (格式: X.Y.Z+autobuild.MMDD.commit)
|
||||
LATEST_COMMIT=$(echo "$LATEST_VERSION" | sed -n 's/.*+autobuild\.[0-9]\{4\}\.\([a-f0-9]*\)$/\1/p' || echo "")
|
||||
echo "📝 最新 autobuild commit: $LATEST_COMMIT"
|
||||
|
||||
if [ "$LAST_TAURI_COMMIT" != "$LATEST_COMMIT" ]; then
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
echo "🟢 Tauri commit hash 不匹配 ($LAST_TAURI_COMMIT != $LATEST_COMMIT),需构建"
|
||||
else
|
||||
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||
echo "🔴 相同 Tauri commit hash ($LAST_TAURI_COMMIT),不需构建"
|
||||
fi
|
||||
else
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
echo "⚠️ 无法下载或解析 latest.json,需构建"
|
||||
fi
|
||||
else
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
echo "⚠️ 无法获取 latest.json 下载 URL,需构建"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Output should_run result
|
||||
run: |
|
||||
echo "Result: ${{ steps.check.outputs.should_run }}"
|
||||
+201
-101
@@ -8,6 +8,7 @@ on:
|
||||
permissions: write-all
|
||||
env:
|
||||
TAG_NAME: autobuild
|
||||
TAG_CHANNEL: AutoBuild
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
concurrency:
|
||||
@@ -17,55 +18,10 @@ concurrency:
|
||||
jobs:
|
||||
check_commit:
|
||||
name: Check Commit Needs Build
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check if version changed or src changed
|
||||
id: check
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
git checkout HEAD~1 package.json
|
||||
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
|
||||
echo "Previous version: $PREVIOUS_VERSION"
|
||||
|
||||
git checkout HEAD package.json
|
||||
|
||||
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
||||
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CURRENT_SRC_HASH=$(git rev-parse HEAD:src)
|
||||
PREVIOUS_SRC_HASH=$(git rev-parse HEAD~1:src 2>/dev/null || echo "")
|
||||
CURRENT_TAURI_HASH=$(git rev-parse HEAD:src-tauri 2>/dev/null || echo "")
|
||||
PREVIOUS_TAURI_HASH=$(git rev-parse HEAD~1:src-tauri 2>/dev/null || echo "")
|
||||
|
||||
echo "Current src hash: $CURRENT_SRC_HASH"
|
||||
echo "Previous src hash: $PREVIOUS_SRC_HASH"
|
||||
echo "Current tauri hash: $CURRENT_TAURI_HASH"
|
||||
echo "Previous tauri hash: $PREVIOUS_TAURI_HASH"
|
||||
|
||||
if [ "$CURRENT_SRC_HASH" != "$PREVIOUS_SRC_HASH" ] || [ "$CURRENT_TAURI_HASH" != "$PREVIOUS_TAURI_HASH" ]; then
|
||||
echo "Source directories changed"
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Version and source directories unchanged"
|
||||
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
uses: clash-verge-rev/clash-verge-rev/.github/workflows/check-commit-needs-build.yml@dev
|
||||
with:
|
||||
tag_name: autobuild
|
||||
force_build: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
update_tag:
|
||||
name: Update tag
|
||||
@@ -94,9 +50,28 @@ jobs:
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Release AutoBuild Version
|
||||
run: pnpm release-version autobuild-latest
|
||||
|
||||
- name: Set Env
|
||||
run: |
|
||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||
VERSION=$(jq -r .version package.json)
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- run: |
|
||||
@@ -110,25 +85,24 @@ jobs:
|
||||
cat > release.txt << EOF
|
||||
$UPDATE_LOGS
|
||||
|
||||
## 我应该下载哪个版本?
|
||||
|
||||
### MacOS
|
||||
- MacOS intel芯片: x64.dmg
|
||||
- MacOS apple M芯片: aarch64.dmg
|
||||
|
||||
### Linux
|
||||
- Linux 64位: amd64.deb/amd64.rpm
|
||||
- Linux arm64 architecture: arm64.deb/aarch64.rpm
|
||||
- Linux armv7架构: armhf.deb/armhfp.rpm
|
||||
## 下载地址
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- 64位: x64-setup.exe
|
||||
- arm64架构: arm64-setup.exe
|
||||
#### 便携版问题很多不再提供
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||
|
||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||
- 64位: x64_fixed_webview2-setup.exe
|
||||
- arm64架构: arm64_fixed_webview2-setup.exe
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
|
||||
|
||||
### macOS
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
|
||||
|
||||
### Linux
|
||||
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
|
||||
|
||||
### FAQ
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
@@ -143,15 +117,26 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
name: "Clash Verge Rev AutoBuild"
|
||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||
body_path: release.txt
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
generate_release_notes: true
|
||||
|
||||
clean_old_assets:
|
||||
name: Clean Old Release Assets
|
||||
needs: [check_commit, update_tag]
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' && needs.update_tag.result == 'success' }}
|
||||
|
||||
uses: clash-verge-rev/clash-verge-rev/.github/workflows/clean-old-assets.yml@dev
|
||||
with:
|
||||
tag_name: autobuild
|
||||
dry_run: false
|
||||
|
||||
autobuild-x86-windows-macos-linux:
|
||||
name: Autobuild x86 Windows, MacOS and Linux
|
||||
needs: update_tag
|
||||
needs: [check_commit, update_tag]
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -182,7 +167,8 @@ jobs:
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-shared
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
@@ -190,25 +176,26 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release Autobuild Version
|
||||
run: pnpm release-version autobuild
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: pnpm release-version autobuild-latest
|
||||
|
||||
- name: Tauri build
|
||||
- name: Tauri build for Windows-macOS-Linux
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
@@ -223,16 +210,18 @@ jobs:
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
tagName: ${{ env.TAG_NAME }}
|
||||
releaseName: "Clash Verge Rev AutoBuild"
|
||||
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||
releaseBody: "More new features are now supported."
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
includeUpdaterJson: true
|
||||
|
||||
autobuild-arm-linux:
|
||||
name: Autobuild ARM Linux
|
||||
needs: update_tag
|
||||
needs: [check_commit, update_tag]
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -259,24 +248,27 @@ jobs:
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-shared
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release Autobuild Version
|
||||
run: pnpm release-version autobuild
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: pnpm release-version autobuild-latest
|
||||
|
||||
- name: Setup for linux
|
||||
run: |
|
||||
@@ -327,7 +319,7 @@ jobs:
|
||||
gcc-arm-linux-gnueabihf \
|
||||
g++-arm-linux-gnueabihf
|
||||
|
||||
- name: Build for Linux
|
||||
- name: Tauri Build for Linux
|
||||
run: |
|
||||
export PKG_CONFIG_ALLOW_CROSS=1
|
||||
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
|
||||
@@ -354,7 +346,7 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
name: "Clash Verge Rev AutoBuild"
|
||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
@@ -363,7 +355,8 @@ jobs:
|
||||
|
||||
autobuild-x86-arm-windows_webview2:
|
||||
name: Autobuild x86 and ARM Windows with WebView2
|
||||
needs: update_tag
|
||||
needs: [check_commit, update_tag]
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -387,25 +380,27 @@ jobs:
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-shared
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release Autobuild Version
|
||||
run: pnpm release-version autobuild
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: pnpm release-version autobuild-latest
|
||||
|
||||
- name: Download WebView2 Runtime
|
||||
run: |
|
||||
@@ -414,7 +409,7 @@ jobs:
|
||||
Remove-Item .\src-tauri\tauri.windows.conf.json
|
||||
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
|
||||
|
||||
- name: Tauri build
|
||||
- name: Tauri build for Windows
|
||||
id: build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
@@ -425,6 +420,7 @@ jobs:
|
||||
with:
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
includeUpdaterJson: true
|
||||
|
||||
- name: Rename
|
||||
run: |
|
||||
@@ -450,12 +446,116 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
name: "Clash Verge Rev AutoBuild"
|
||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||
|
||||
- name: Portable Bundle
|
||||
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --autobuild
|
||||
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
notify-telegram:
|
||||
name: Notify Telegram
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
update_tag,
|
||||
autobuild-x86-windows-macos-linux,
|
||||
autobuild-arm-linux,
|
||||
autobuild-x86-arm-windows_webview2,
|
||||
]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
run: |
|
||||
if [ -f "UPDATELOG.md" ]; then
|
||||
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||
if [ -n "$UPDATE_LOGS" ]; then
|
||||
echo "Found update logs"
|
||||
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
|
||||
echo "$UPDATE_LOGS" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
else
|
||||
echo "No update sections found in UPDATELOG.md"
|
||||
fi
|
||||
else
|
||||
echo "UPDATELOG.md file not found"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Release AutoBuild Version
|
||||
run: pnpm release-version autobuild-latest
|
||||
|
||||
- name: Get Version and Release Info
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install jq
|
||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild" >> $GITHUB_ENV
|
||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate release.txt
|
||||
run: |
|
||||
if [ -z "$UPDATE_LOGS" ]; then
|
||||
echo "No update logs found, using default message"
|
||||
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||
else
|
||||
echo "Using found update logs"
|
||||
fi
|
||||
|
||||
cat > release.txt << EOF
|
||||
$UPDATE_LOGS
|
||||
|
||||
## 下载地址
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||
|
||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
|
||||
|
||||
### macOS
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
|
||||
|
||||
### Linux
|
||||
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
|
||||
|
||||
### FAQ
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
### 稳定机场VPN推荐
|
||||
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
Created at ${{ env.BUILDTIME }}.
|
||||
EOF
|
||||
|
||||
- name: Send Telegram Notification
|
||||
run: node scripts/telegram.mjs
|
||||
env:
|
||||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
BUILD_TYPE: autobuild
|
||||
VERSION: ${{ env.VERSION }}
|
||||
DOWNLOAD_URL: ${{ env.DOWNLOAD_URL }}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
name: Check Commit Needs Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: "Release tag name to check against (default: autobuild)"
|
||||
required: false
|
||||
default: "autobuild"
|
||||
type: string
|
||||
force_build:
|
||||
description: "Force build regardless of checks"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: "Release tag name to check against (default: autobuild)"
|
||||
required: false
|
||||
default: "autobuild"
|
||||
type: string
|
||||
force_build:
|
||||
description: "Force build regardless of checks"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
outputs:
|
||||
should_run:
|
||||
description: "Whether the build should run"
|
||||
value: ${{ jobs.check_commit.outputs.should_run }}
|
||||
last_tauri_commit:
|
||||
description: "The last commit hash with Tauri-related changes"
|
||||
value: ${{ jobs.check_commit.outputs.last_tauri_commit }}
|
||||
autobuild_version:
|
||||
description: "The generated autobuild version string"
|
||||
value: ${{ jobs.check_commit.outputs.autobuild_version }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
env:
|
||||
TAG_NAME: ${{ inputs.tag_name || 'autobuild' }}
|
||||
|
||||
jobs:
|
||||
check_commit:
|
||||
name: Check Commit Needs Build
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
last_tauri_commit: ${{ steps.check.outputs.last_tauri_commit }}
|
||||
autobuild_version: ${{ steps.check.outputs.autobuild_version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
|
||||
- name: Check if version changed or src changed
|
||||
id: check
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Force build if requested
|
||||
if [ "${{ inputs.force_build }}" == "true" ]; then
|
||||
echo "🚀 Force build requested"
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||
echo "📦 Current version: $CURRENT_VERSION"
|
||||
|
||||
git checkout HEAD~1 package.json
|
||||
PREVIOUS_VERSION=$(cat package.json | jq -r '.version')
|
||||
echo "📦 Previous version: $PREVIOUS_VERSION"
|
||||
|
||||
git checkout HEAD package.json
|
||||
|
||||
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
||||
echo "✅ Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Use get_latest_tauri_commit.bash to find the latest Tauri-related commit
|
||||
echo "🔍 Finding last commit with Tauri-related changes using script..."
|
||||
|
||||
# Make script executable
|
||||
chmod +x scripts-workflow/get_latest_tauri_commit.bash
|
||||
|
||||
# Get the latest Tauri-related commit hash (full hash)
|
||||
LAST_TAURI_COMMIT_FULL=$(./scripts-workflow/get_latest_tauri_commit.bash)
|
||||
if [[ $? -ne 0 ]] || [[ -z "$LAST_TAURI_COMMIT_FULL" ]]; then
|
||||
echo "❌ Failed to get Tauri-related commit, using current commit"
|
||||
LAST_TAURI_COMMIT_FULL=$(git rev-parse HEAD)
|
||||
fi
|
||||
|
||||
# Get short hash for display and version tagging
|
||||
LAST_TAURI_COMMIT=$(git rev-parse --short "$LAST_TAURI_COMMIT_FULL")
|
||||
|
||||
echo "📝 Last Tauri-related commit: $LAST_TAURI_COMMIT"
|
||||
|
||||
# Generate autobuild version using autobuild-latest format
|
||||
CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g')
|
||||
MONTH=$(date +%m)
|
||||
DAY=$(date +%d)
|
||||
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
|
||||
|
||||
echo "🏷️ Autobuild version: $AUTOBUILD_VERSION"
|
||||
echo "📝 Last Tauri commit: $LAST_TAURI_COMMIT"
|
||||
|
||||
# Set outputs for other jobs to use
|
||||
echo "last_tauri_commit=$LAST_TAURI_COMMIT" >> $GITHUB_OUTPUT
|
||||
echo "autobuild_version=$AUTOBUILD_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# Check if autobuild release exists
|
||||
echo "🔍 Checking autobuild release and latest.json..."
|
||||
AUTOBUILD_RELEASE_EXISTS=$(gh release view "${{ env.TAG_NAME }}" --json id -q '.id' 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$AUTOBUILD_RELEASE_EXISTS" ]; then
|
||||
echo "✅ No autobuild release exists, build needed"
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# Check if latest.json exists in the release
|
||||
LATEST_JSON_EXISTS=$(gh release view "${{ env.TAG_NAME }}" --json assets -q '.assets[] | select(.name == "latest.json") | .name' 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$LATEST_JSON_EXISTS" ]; then
|
||||
echo "✅ No latest.json found in autobuild release, build needed"
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# Download and parse latest.json to check version and commit hash
|
||||
echo "📥 Downloading latest.json to check version..."
|
||||
LATEST_JSON_URL="https://github.com/clash-verge-rev/clash-verge-rev/releases/download/autobuild/latest.json"
|
||||
|
||||
LATEST_JSON_CONTENT=$(curl -sL "$LATEST_JSON_URL" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$LATEST_JSON_CONTENT" ]; then
|
||||
LATEST_VERSION=$(echo "$LATEST_JSON_CONTENT" | jq -r '.version' 2>/dev/null || echo "")
|
||||
echo "📦 Latest autobuild version: $LATEST_VERSION"
|
||||
|
||||
# Extract commit hash from version string (format: X.Y.Z+autobuild.MMDD.commit)
|
||||
LATEST_COMMIT=$(echo "$LATEST_VERSION" | sed -n 's/.*+autobuild\.[0-9]\{4\}\.\([a-f0-9]*\)$/\1/p' || echo "")
|
||||
echo "📝 Latest autobuild commit: $LATEST_COMMIT"
|
||||
|
||||
if [ "$LAST_TAURI_COMMIT" != "$LATEST_COMMIT" ]; then
|
||||
echo "✅ Tauri commit hash mismatch ($LAST_TAURI_COMMIT != $LATEST_COMMIT), build needed"
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "❌ Same Tauri commit hash ($LAST_TAURI_COMMIT), no build needed"
|
||||
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Failed to download or parse latest.json, build needed"
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,220 @@
|
||||
name: Clean Old Assets
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: "Release tag name to clean (default: autobuild)"
|
||||
required: false
|
||||
default: "autobuild"
|
||||
type: string
|
||||
dry_run:
|
||||
description: "Dry run mode (only show what would be deleted)"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: "Release tag name to clean (default: autobuild)"
|
||||
required: false
|
||||
default: "autobuild"
|
||||
type: string
|
||||
dry_run:
|
||||
description: "Dry run mode (only show what would be deleted)"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions: write-all
|
||||
|
||||
env:
|
||||
TAG_NAME: ${{ inputs.tag_name || 'autobuild' }}
|
||||
TAG_CHANNEL: AutoBuild
|
||||
|
||||
jobs:
|
||||
check_current_version:
|
||||
name: Check Current Version and Commit
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
current_version: ${{ steps.check.outputs.current_version }}
|
||||
last_tauri_commit: ${{ steps.check.outputs.last_tauri_commit }}
|
||||
autobuild_version: ${{ steps.check.outputs.autobuild_version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
|
||||
- name: Get current version and find last Tauri commit
|
||||
id: check
|
||||
run: |
|
||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||
echo "📦 Current version: $CURRENT_VERSION"
|
||||
|
||||
# Find the last commit that changed Tauri-related files
|
||||
echo "🔍 Finding last commit with Tauri-related changes..."
|
||||
|
||||
# Define patterns for Tauri-related files
|
||||
TAURI_PATTERNS="src/ src-tauri/src src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.*.conf.json src-tauri/build.rs src-tauri/capabilities"
|
||||
|
||||
# Get the last commit that changed any of these patterns (excluding build artifacts)
|
||||
LAST_TAURI_COMMIT=""
|
||||
for commit in $(git rev-list HEAD --max-count=50); do
|
||||
# Check if this commit changed any Tauri-related files
|
||||
CHANGED_FILES=$(git show --name-only --pretty=format: $commit | tr '\n' ' ')
|
||||
HAS_TAURI_CHANGES=false
|
||||
|
||||
# Check each pattern
|
||||
if echo "$CHANGED_FILES" | grep -q "src/" && echo "$CHANGED_FILES" | grep -qvE "src/(dist|build|node_modules|\.next|\.cache)"; then
|
||||
HAS_TAURI_CHANGES=true
|
||||
elif echo "$CHANGED_FILES" | grep -qE "src-tauri/(src|Cargo\.(toml|lock)|tauri\..*\.conf\.json|build\.rs|capabilities)"; then
|
||||
HAS_TAURI_CHANGES=true
|
||||
fi
|
||||
|
||||
if [ "$HAS_TAURI_CHANGES" = true ]; then
|
||||
LAST_TAURI_COMMIT=$(git rev-parse --short $commit)
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$LAST_TAURI_COMMIT" ]; then
|
||||
echo "⚠️ No Tauri-related changes found in recent commits, using current commit"
|
||||
LAST_TAURI_COMMIT=$(git rev-parse --short HEAD)
|
||||
fi
|
||||
|
||||
echo "📝 Last Tauri-related commit: $LAST_TAURI_COMMIT"
|
||||
echo "📝 Current commit: $(git rev-parse --short HEAD)"
|
||||
|
||||
# Generate autobuild version for consistency
|
||||
CURRENT_BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-(alpha|beta|rc)(\.[0-9]+)?//g' | sed -E 's/\+[a-zA-Z0-9.-]+//g')
|
||||
MONTH=$(date +%m)
|
||||
DAY=$(date +%d)
|
||||
AUTOBUILD_VERSION="${CURRENT_BASE_VERSION}+autobuild.${MONTH}${DAY}.${LAST_TAURI_COMMIT}"
|
||||
|
||||
echo "🏷️ Current autobuild version: $AUTOBUILD_VERSION"
|
||||
|
||||
# Set outputs for other jobs to use
|
||||
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "last_tauri_commit=$LAST_TAURI_COMMIT" >> $GITHUB_OUTPUT
|
||||
echo "autobuild_version=$AUTOBUILD_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
clean_old_assets:
|
||||
name: Clean Old Release Assets
|
||||
runs-on: ubuntu-latest
|
||||
needs: check_current_version
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Clean old assets from release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ env.TAG_NAME }}
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
run: |
|
||||
# Use values from check_current_version job
|
||||
CURRENT_AUTOBUILD_VERSION="${{ needs.check_current_version.outputs.autobuild_version }}"
|
||||
LAST_TAURI_COMMIT="${{ needs.check_current_version.outputs.last_tauri_commit }}"
|
||||
CURRENT_VERSION="${{ needs.check_current_version.outputs.current_version }}"
|
||||
|
||||
echo "📦 Current version: $CURRENT_VERSION"
|
||||
echo "📦 Current autobuild version: $CURRENT_AUTOBUILD_VERSION"
|
||||
echo "📝 Last Tauri commit: $LAST_TAURI_COMMIT"
|
||||
echo "🏷️ Target tag: $TAG_NAME"
|
||||
echo "🔍 Dry run mode: $DRY_RUN"
|
||||
|
||||
# Check if release exists
|
||||
RELEASE_EXISTS=$(gh release view "$TAG_NAME" --json id -q '.id' 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$RELEASE_EXISTS" ]; then
|
||||
echo "❌ Release '$TAG_NAME' not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Found release '$TAG_NAME'"
|
||||
|
||||
# Get all assets
|
||||
echo "📋 Getting list of all assets..."
|
||||
assets=$(gh release view "$TAG_NAME" --json assets -q '.assets[].name' || true)
|
||||
|
||||
if [ -z "$assets" ]; then
|
||||
echo "ℹ️ No assets found in release '$TAG_NAME'"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "📋 Found assets:"
|
||||
echo "$assets" | sed 's/^/ - /'
|
||||
|
||||
# Count assets to keep and delete
|
||||
ASSETS_TO_KEEP=""
|
||||
ASSETS_TO_DELETE=""
|
||||
|
||||
for asset in $assets; do
|
||||
# Keep assets that match current autobuild version or are non-versioned files (like latest.json)
|
||||
if [[ "$asset" == *"$CURRENT_AUTOBUILD_VERSION"* ]] || [[ "$asset" == "latest.json" ]]; then
|
||||
ASSETS_TO_KEEP="$ASSETS_TO_KEEP$asset\n"
|
||||
else
|
||||
ASSETS_TO_DELETE="$ASSETS_TO_DELETE$asset\n"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "🔒 Assets to keep (current version: $CURRENT_AUTOBUILD_VERSION):"
|
||||
if [ -n "$ASSETS_TO_KEEP" ]; then
|
||||
echo -e "$ASSETS_TO_KEEP" | grep -v '^$' | sed 's/^/ - /'
|
||||
else
|
||||
echo " - None"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🗑️ Assets to delete:"
|
||||
if [ -n "$ASSETS_TO_DELETE" ]; then
|
||||
echo -e "$ASSETS_TO_DELETE" | grep -v '^$' | sed 's/^/ - /'
|
||||
else
|
||||
echo " - None"
|
||||
echo "ℹ️ No old assets to clean"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo ""
|
||||
echo "🔍 DRY RUN MODE: No assets will actually be deleted"
|
||||
echo " To actually delete these assets, run this workflow again with dry_run=false"
|
||||
else
|
||||
echo ""
|
||||
echo "🗑️ Deleting old assets..."
|
||||
|
||||
DELETED_COUNT=0
|
||||
FAILED_COUNT=0
|
||||
|
||||
for asset in $assets; do
|
||||
# Skip assets that should be kept
|
||||
if [[ "$asset" == *"$CURRENT_AUTOBUILD_VERSION"* ]] || [[ "$asset" == "latest.json" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " Deleting: $asset"
|
||||
if gh release delete-asset "$TAG_NAME" "$asset" -y 2>/dev/null; then
|
||||
DELETED_COUNT=$((DELETED_COUNT + 1))
|
||||
else
|
||||
echo " ⚠️ Failed to delete $asset"
|
||||
FAILED_COUNT=$((FAILED_COUNT + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "📊 Cleanup summary:"
|
||||
echo " - Deleted: $DELETED_COUNT assets"
|
||||
if [ $FAILED_COUNT -gt 0 ]; then
|
||||
echo " - Failed: $FAILED_COUNT assets"
|
||||
fi
|
||||
echo " - Kept: $(echo -e "$ASSETS_TO_KEEP" | grep -v '^$' | wc -l) assets"
|
||||
|
||||
if [ $FAILED_COUNT -gt 0 ]; then
|
||||
echo "⚠️ Some assets failed to delete. Please check the logs above."
|
||||
exit 1
|
||||
else
|
||||
echo "✅ Cleanup completed successfully!"
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,66 @@
|
||||
name: Clippy Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
clippy:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
- os: ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
save-if: false
|
||||
cache-all-crates: false
|
||||
shared-key: autobuild-shared
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Build Web Assets
|
||||
run: pnpm run web:build
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
|
||||
- name: Run Clippy
|
||||
run: cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --all-features -- -D warnings
|
||||
@@ -0,0 +1,64 @@
|
||||
name: Cross Platform Cargo Check
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# pull_request:
|
||||
# push:
|
||||
# branches: [main, dev]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cargo-check:
|
||||
# Treat all Rust compiler warnings as errors
|
||||
env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
save-if: false
|
||||
|
||||
- name: Cargo Check (deny warnings)
|
||||
working-directory: src-tauri
|
||||
run: |
|
||||
cargo check --target ${{ matrix.target }} --workspace --all-features
|
||||
+45
-13
@@ -2,12 +2,25 @@ name: Development Test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_windows:
|
||||
description: "运行 Windows"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
run_macos_aarch64:
|
||||
description: "运行 macOS aarch64"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions: write-all
|
||||
env:
|
||||
TAG_NAME: deploytest
|
||||
TAG_CHANNEL: DeployTest
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
concurrency:
|
||||
# only allow per workflow per commit (and not pr) to run at a time
|
||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
@@ -20,47 +33,66 @@ jobs:
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
bundle: nsis
|
||||
id: windows
|
||||
input: run_windows
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
bundle: dmg
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
bundle: dmg
|
||||
id: macos-aarch64
|
||||
input: run_macos_aarch64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Skip job if not selected
|
||||
if: github.event.inputs[matrix.input] != 'true'
|
||||
run: echo "Job ${{ matrix.id }} skipped as requested"
|
||||
|
||||
- name: Checkout Repository
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust Stable
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Add Rust Target
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
save-if: false
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
shared-key: autobuild-shared
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm install and check
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
|
||||
- name: Tauri build
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
@@ -78,7 +110,7 @@ jobs:
|
||||
args: --target ${{ matrix.target }} -b ${{ matrix.bundle }}
|
||||
|
||||
- name: Upload Artifacts
|
||||
if: matrix.os == 'macos-latest'
|
||||
if: matrix.os == 'macos-latest' && github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
@@ -86,7 +118,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Artifacts
|
||||
if: matrix.os == 'windows-latest'
|
||||
if: matrix.os == 'windows-latest' && github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
name: Check Formatting
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: install Rust stable and rustfmt
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
|
||||
- name: run cargo fmt
|
||||
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
|
||||
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
- run: corepack enable
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm format:check
|
||||
|
||||
# taplo:
|
||||
# name: taplo (.toml files)
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
|
||||
# - name: install Rust stable
|
||||
# uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# - name: install taplo-cli
|
||||
# uses: taiki-e/install-action@v2
|
||||
# with:
|
||||
# tool: taplo-cli
|
||||
|
||||
# - run: taplo fmt --check --diff
|
||||
+253
-15
@@ -1,7 +1,12 @@
|
||||
name: Release Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# ! 为了避免重复发布版本,应当通过独特 git tag 触发。
|
||||
# ! 不再使用 workflow_dispatch 触发。
|
||||
# workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
permissions: write-all
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
@@ -12,7 +17,137 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
check_tag_version:
|
||||
name: Check Release Tag and package.json Version Consistency
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if tag is from main branch
|
||||
run: |
|
||||
TAG_REF="${GITHUB_REF##*/}"
|
||||
echo "Checking if tag $TAG_REF is from main branch..."
|
||||
|
||||
TAG_COMMIT=$(git rev-list -n 1 $TAG_REF)
|
||||
MAIN_COMMITS=$(git rev-list origin/main)
|
||||
|
||||
if echo "$MAIN_COMMITS" | grep -q "$TAG_COMMIT"; then
|
||||
echo "✅ Tag $TAG_REF is from main branch"
|
||||
else
|
||||
echo "❌ Tag $TAG_REF is not from main branch"
|
||||
echo "This release workflow only accepts tags from main branch."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check tag and package.json version
|
||||
run: |
|
||||
TAG_REF="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}"
|
||||
echo "Current tag: $TAG_REF"
|
||||
|
||||
PKG_VERSION=$(jq -r .version package.json)
|
||||
echo "package.json version: $PKG_VERSION"
|
||||
|
||||
EXPECTED_TAG="v$PKG_VERSION"
|
||||
|
||||
if [[ "$TAG_REF" != "$EXPECTED_TAG" ]]; then
|
||||
echo "❌ Version mismatch:"
|
||||
echo " Git tag : $TAG_REF"
|
||||
echo " package.json : $EXPECTED_TAG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Tag and package.json version are consistent."
|
||||
|
||||
update_tag:
|
||||
name: Update tag
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release, release-for-linux-arm, release-for-fixed-webview2]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
run: |
|
||||
if [ -f "UPDATELOG.md" ]; then
|
||||
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||
if [ -n "$UPDATE_LOGS" ]; then
|
||||
echo "Found update logs"
|
||||
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
|
||||
echo "$UPDATE_LOGS" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
else
|
||||
echo "No update sections found in UPDATELOG.md"
|
||||
fi
|
||||
else
|
||||
echo "UPDATELOG.md file not found"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Set Env
|
||||
run: |
|
||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||
TAG_REF="${GITHUB_REF##*/}"
|
||||
echo "TAG_NAME=$TAG_REF" >> $GITHUB_ENV
|
||||
VERSION=$(echo "$TAG_REF" | sed 's/^v//')
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/$TAG_REF" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- run: |
|
||||
if [ -z "$UPDATE_LOGS" ]; then
|
||||
echo "No update logs found, using default message"
|
||||
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||
else
|
||||
echo "Using found update logs"
|
||||
fi
|
||||
|
||||
cat > release.txt << EOF
|
||||
$UPDATE_LOGS
|
||||
|
||||
## 下载地址
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||
|
||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
|
||||
|
||||
### macOS
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
|
||||
|
||||
### Linux
|
||||
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
|
||||
|
||||
### FAQ
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
### 稳定机场VPN推荐
|
||||
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
Created at ${{ env.BUILDTIME }}.
|
||||
EOF
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
name: "Clash Verge Rev ${{ env.TAG_NAME }}"
|
||||
body_path: release.txt
|
||||
prerelease: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# generate_release_notes: true
|
||||
|
||||
release:
|
||||
name: Release Build
|
||||
needs: [check_tag_version]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -43,8 +178,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
save-if: false
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
@@ -65,7 +199,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
@@ -81,13 +215,20 @@ jobs:
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
tagName: v__VERSION__
|
||||
releaseName: "Clash Verge Rev v__VERSION__"
|
||||
releaseBody: "More new features are now supported."
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: "Clash Verge Rev ${{ github.ref_name }}"
|
||||
releaseBody: "Draft release, will be updated later."
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
includeUpdaterJson: true
|
||||
|
||||
|
||||
|
||||
release-for-linux-arm:
|
||||
name: Release Build for Linux ARM
|
||||
needs: [check_tag_version]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -113,7 +254,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -128,7 +269,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: "Setup for linux"
|
||||
run: |-
|
||||
@@ -202,13 +343,15 @@ jobs:
|
||||
with:
|
||||
tag_name: v${{env.VERSION}}
|
||||
name: "Clash Verge Rev v${{env.VERSION}}"
|
||||
body: "More new features are now supported."
|
||||
body: "See release notes for detailed changelog."
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||
|
||||
release-for-fixed-webview2:
|
||||
name: Release Build for Fixed WebView2
|
||||
needs: [check_tag_version]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -231,8 +374,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
save-if: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -247,7 +389,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Download WebView2 Runtime
|
||||
run: |
|
||||
@@ -293,7 +435,7 @@ jobs:
|
||||
with:
|
||||
tag_name: v${{steps.build.outputs.appVersion}}
|
||||
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}"
|
||||
body: "More new features are now supported."
|
||||
body: "See release notes for detailed changelog."
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||
|
||||
@@ -303,6 +445,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release-update:
|
||||
name: Release Update
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release, release-for-linux-arm]
|
||||
steps:
|
||||
@@ -353,8 +496,9 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
submit-to-winget:
|
||||
name: Submit to Winget
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release-update]
|
||||
needs: [update_tag, release-update]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -373,3 +517,97 @@ jobs:
|
||||
release-tag: v${{env.VERSION}}
|
||||
installers-regex: '_(arm64|x64|x86)-setup\.exe$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
notify-telegram:
|
||||
name: Notify Telegram
|
||||
runs-on: ubuntu-latest
|
||||
needs: [update_tag, release-update, release-update-for-fixed-webview2, submit-to-winget]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
run: |
|
||||
if [ -f "UPDATELOG.md" ]; then
|
||||
UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||
if [ -n "$UPDATE_LOGS" ]; then
|
||||
echo "Found update logs"
|
||||
echo "UPDATE_LOGS<<EOF" >> $GITHUB_ENV
|
||||
echo "$UPDATE_LOGS" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
else
|
||||
echo "No update sections found in UPDATELOG.md"
|
||||
fi
|
||||
else
|
||||
echo "UPDATELOG.md file not found"
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Get Version and Release Info
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install jq
|
||||
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||
echo "DOWNLOAD_URL=https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
|
||||
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate release.txt
|
||||
run: |
|
||||
if [ -z "$UPDATE_LOGS" ]; then
|
||||
echo "No update logs found, using default message"
|
||||
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
|
||||
else
|
||||
echo "Using found update logs"
|
||||
fi
|
||||
|
||||
cat > release.txt << EOF
|
||||
$UPDATE_LOGS
|
||||
|
||||
## 下载地址
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||
|
||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
|
||||
|
||||
### macOS
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
|
||||
|
||||
### Linux
|
||||
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhfp.rpm)
|
||||
|
||||
### FAQ
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
### 稳定机场VPN推荐
|
||||
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
Created at ${{ env.BUILDTIME }}.
|
||||
EOF
|
||||
|
||||
- name: Send Telegram Notification
|
||||
run: node scripts/telegram.mjs
|
||||
env:
|
||||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
BUILD_TYPE: release
|
||||
VERSION: ${{ env.VERSION }}
|
||||
DOWNLOAD_URL: ${{ env.DOWNLOAD_URL }}
|
||||
|
||||
+17
-9
@@ -2,17 +2,25 @@
|
||||
|
||||
#pnpm pretty-quick --staged
|
||||
|
||||
# 运行 clippy fmt
|
||||
cd src-tauri
|
||||
cargo fmt
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "rustfmt failed to format the code. Please fix the issues and try again."
|
||||
exit 1
|
||||
if git diff --cached --name-only | grep -q '^src/'; then
|
||||
pnpm format:check
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Code format check failed in src/. Please fix formatting issues."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
cd ..
|
||||
|
||||
git add .
|
||||
if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
||||
cd src-tauri
|
||||
cargo fmt
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "rustfmt failed to format the code. Please fix the issues and try again."
|
||||
exit 1
|
||||
fi
|
||||
cd ..
|
||||
fi
|
||||
|
||||
#git add .
|
||||
|
||||
# 允许提交
|
||||
exit 0
|
||||
|
||||
+29
-8
@@ -1,13 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 运行 clippy
|
||||
# cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix
|
||||
# $1: remote name (e.g., origin)
|
||||
# $2: remote url (e.g., git@github.com:clash-verge-rev/clash-verge-rev.git)
|
||||
|
||||
# 如果 clippy 失败,阻止 push
|
||||
# if [ $? -ne 0 ]; then
|
||||
# echo "Clippy found issues in sub_crate. Please fix them before pushing."
|
||||
# exit 1
|
||||
# fi
|
||||
if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
||||
cargo clippy --manifest-path ./src-tauri/Cargo.toml
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Clippy found issues in src-tauri. Please fix them before pushing."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# Only run format check if the remote exists and is the main repo
|
||||
remote_name="$1"
|
||||
if git remote get-url "$remote_name" >/dev/null 2>&1; then
|
||||
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)"
|
||||
echo "[pre-push] Running pnpm format:check..."
|
||||
pnpm format:check
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Code format check failed. Please fix formatting before pushing."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "[pre-push] Not pushing to target repo. Skipping format check."
|
||||
fi
|
||||
else
|
||||
echo "[pre-push] Remote $remote_name does not exist. Skipping format check."
|
||||
fi
|
||||
|
||||
# 允许 push
|
||||
exit 0
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# README.md
|
||||
# UPDATELOG.md
|
||||
# CONTRIBUTING.md
|
||||
|
||||
pnpm-lock.yaml
|
||||
|
||||
src-tauri/target/
|
||||
src-tauri/gen/
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"proseWrap": "preserve",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"endOfLine": "lf",
|
||||
"embeddedLanguageFormatting": "auto"
|
||||
}
|
||||
+26
-11
@@ -8,10 +8,15 @@ Before you start contributing to the project, you need to set up your developmen
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/v1/guides/getting-started/prerequisites) to install them on your system.
|
||||
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/start/prerequisites/) to install them on your system.
|
||||
|
||||
### Setup for Windows Users
|
||||
|
||||
> [!NOTE]
|
||||
> **If you are using a Windows ARM device, you additionally need to install [LLVM](https://github.com/llvm/llvm-project/releases) (including clang) and set the environment variable.**
|
||||
>
|
||||
> Because the `ring` crate is compiled based on `clang` under Windows ARM.
|
||||
|
||||
If you're a Windows user, you may need to perform some additional steps:
|
||||
|
||||
- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary.
|
||||
@@ -33,12 +38,15 @@ npm install pnpm -g
|
||||
```
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
Install node packages
|
||||
|
||||
```shell
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Install apt packages ONLY for Ubuntu
|
||||
|
||||
```shell
|
||||
apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
```
|
||||
@@ -48,11 +56,13 @@ apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev
|
||||
You have two options for downloading the clash binary:
|
||||
|
||||
- Automatically download it via the provided script:
|
||||
|
||||
```shell
|
||||
pnpm run check
|
||||
pnpm run prebuild
|
||||
# Use '--force' to force update to the latest version
|
||||
# pnpm run check --force
|
||||
# pnpm run prebuild --force
|
||||
```
|
||||
|
||||
- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
|
||||
|
||||
### Run the Development Server
|
||||
@@ -105,20 +115,25 @@ pnpm portable
|
||||
|
||||
If you changed the rust code, it's recommanded to execute code style formatting and quailty checks.
|
||||
|
||||
1. Code style formatting
|
||||
1. Code quailty checks
|
||||
|
||||
```bash
|
||||
# For rust backend
|
||||
$ clash-verge-rev: pnpm clippy
|
||||
# For frontend (not yet).
|
||||
```
|
||||
|
||||
2. Code style formatting
|
||||
|
||||
```bash
|
||||
# For rust backend
|
||||
$ clash-verge-rev: cd src-tauri
|
||||
$ clash-verge-rev/src-tauri: cargo fmt
|
||||
# For frontend
|
||||
$ clash-verge-rev: pnpm format:check
|
||||
$ clash-verge-rev: pnpm format
|
||||
```
|
||||
|
||||
2. Code quailty checks
|
||||
|
||||
```bash
|
||||
$ clash-verge-rev: pnpm clippy
|
||||
```
|
||||
|
||||
|
||||
Once you have made your changes:
|
||||
|
||||
1. Fork the repository.
|
||||
|
||||
@@ -18,10 +18,18 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
|
||||
## Install
|
||||
|
||||
请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
|
||||
Go to the [release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
|
||||
Go to the [Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
|
||||
Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||
|
||||
### 安装说明和常见问题,请到[文档页](https://clash-verge-rev.github.io/)查看:[Doc](https://clash-verge-rev.github.io/)
|
||||
#### 我应当怎样选择发行版
|
||||
|
||||
| 版本 | 特征 | 链接 |
|
||||
| :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
|
||||
| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
|
||||
| Alpha | 早期测试版,功能未完善,可能存在缺陷。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
|
||||
| AutoBuild | 滚动更新版,持续集成更新,适合开发测试。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
|
||||
|
||||
#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看
|
||||
|
||||
---
|
||||
|
||||
@@ -41,11 +49,12 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||
- 解锁流媒体及 ChatGPT
|
||||
- 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
|
||||
#### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
|
||||
|
||||
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
|
||||
|
||||
🧩 YXVM 独立服务器优势:
|
||||
|
||||
- 🌎 优质网络,回程优化,下载快到飞起
|
||||
- 🔧 物理机独享资源,非VPS可比,性能拉满
|
||||
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
|
||||
@@ -79,7 +88,7 @@ To run the development server, execute the following commands after all prerequi
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
pnpm run check
|
||||
pnpm run prebuild
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
|
||||
+287
-136
@@ -1,143 +1,284 @@
|
||||
## v2.4.0
|
||||
|
||||
**发行代号:融**
|
||||
代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。
|
||||
|
||||
### 🏆 重大改进
|
||||
|
||||
- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性
|
||||
- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示
|
||||
- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.12**
|
||||
- 新增版本信息复制按钮
|
||||
- 增强型流量监控,支持更详细的数据分析
|
||||
- 新增流量图表多种显示模式
|
||||
- 新增强制刷新配置和节点缓存功能
|
||||
- 首页流量统计支持查看刻度线详情
|
||||
|
||||
### 🚀 性能优化
|
||||
|
||||
- 全面提升数据传输和处理效率
|
||||
- 优化内存使用,减少系统资源消耗
|
||||
- 改进流量图表渲染性能
|
||||
- 优化配置和节点刷新策略,从5秒延长到60秒
|
||||
- 改进数据缓存机制,减少重复请求
|
||||
- 优化异步程序性能
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复系统代理状态检测和显示不一致问题
|
||||
- 修复系统主题窗口颜色不一致问题
|
||||
- 修复特殊字符 URL 处理问题
|
||||
- 修复配置修改后缓存不同步问题
|
||||
- 修复 Windows 安装器自启设置问题
|
||||
- 修复 macOS 下 Dock 图标恢复窗口问题
|
||||
- 修复 linux 下 KDE/Plasma 异常标题栏按钮
|
||||
- 修复架构升级后节点测速功能异常
|
||||
- 修复架构升级后流量统计功能异常
|
||||
- 修复架构升级后日志功能异常
|
||||
- 修复外部控制器跨域配置保存问题
|
||||
- 修复首页端口显示不一致问题
|
||||
- 修复首页流量统计刻度线显示问题
|
||||
- 修复日志页面按钮功能混淆问题
|
||||
- 修复日志等级设置保存问题
|
||||
- 修复日志等级异常过滤
|
||||
- 修复清理日志天数功能异常
|
||||
- 修复偶发性启动卡死问题
|
||||
- 修复首页虚拟网卡开关在管理模式下的状态问题
|
||||
|
||||
### 🔧 技术改进
|
||||
|
||||
- 统一使用新的内核通信方式
|
||||
- 新增外部控制器配置界面
|
||||
- 改进跨平台兼容性支持
|
||||
|
||||
## v2.3.2
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复系统代理端口不同步问题
|
||||
- 修复自定义 `css` 背景图无法生效问题
|
||||
- 修复在轻量模式下快速点击托盘图标带来的竞争态卡死问题
|
||||
- 修复同时开启静默启动与自动进入轻量模式后,自动进入轻量模式失效的问题
|
||||
- 修复静默启动时托盘工具栏轻量模式开启与关闭状态的同步
|
||||
- 修复导入订阅时非 http 协议链接被错误尝试导入
|
||||
- 修复切换节点后页面长时间 loading 及缓存过期导致的数据不同步问题
|
||||
- 修复将快捷键名称更名为 `Clash Verge`之后无法删除图标和无法删除注册表
|
||||
- 修复`DNS`覆写 `fallback` `proxy server` `nameserver` `direct Nameserver` 字段支持留空
|
||||
- 修复`DNS`覆写 `nameserver-policy` 字段无法正确识别 `geo` 库
|
||||
- 修复搜索框输入特殊字符崩溃
|
||||
- 修复 Windows 下 Start UP 名称与 exe 名称不统一
|
||||
- 修复显示 Mihomo 内核日志等级应该大于设置等级
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- `sidecar` 模式下清理多余的内核进程,防止运行出现异常
|
||||
- 新 macOS 下 TUN 和系统代理模式托盘图标(暂测)
|
||||
- 快捷键事件通过系统通知
|
||||
- 添加外部 `cors` 控制面板
|
||||
|
||||
### 🚀 优化改进
|
||||
|
||||
- 优化重构订阅切换逻辑,可以随时中断载入过程,防止卡死
|
||||
- 引入事件驱动代理管理器,优化代理配置更新逻辑,防止卡死
|
||||
- 改进主页订阅卡流量已使用比例计算精度
|
||||
- 优化后端缓存刷新机制,支持毫秒级 TTL(默认 3000ms),减少重复请求并提升性能,切换节点时强制刷新后端数据,前端 UI 实时更新,操作更流畅
|
||||
- 解耦前端数据拉取与后端缓存刷新,提升节点切换速度和一致性
|
||||
|
||||
### 🗑️ 移除内容
|
||||
|
||||
- 移除了 macOS tray 图标显示网络速率
|
||||
|
||||
### 🌐 国际化更新
|
||||
|
||||
- 修复部分翻译缺失和不一致问题
|
||||
|
||||
## v2.3.1
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 增加配置文件校验,修复从古老版本升级上来的"No such file or directory (os error 2)"错误
|
||||
- 修复扩展脚本转义错误
|
||||
- 修复 macOS Intel X86 架构构建错误导致无法运行
|
||||
- 修复 Linux 下界面边框白边问题
|
||||
- 修复 托盘 无响应问题
|
||||
- 修复 托盘 无法从轻量模式退出并恢复窗口
|
||||
- 修复 快速切换订阅可能导致的卡死问题
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- 新增 window-state 窗口状态管理和恢复
|
||||
|
||||
### 🚀 优化改进
|
||||
|
||||
- 优化 托盘 统一响应
|
||||
- 优化 静默启动+自启动轻量模式 运行方式
|
||||
- 降低前端潜在内存泄漏风险,提升运行时性能
|
||||
- 优化 React 状态、副作用、数据获取、清理等流程。
|
||||
|
||||
## v2.3.0
|
||||
|
||||
尽管外部控制密钥已自动补全默认值且不允许为空。仍然推荐自行修改外部控制密钥。
|
||||
**发行代号:御**
|
||||
代号释义: 「御」,象征掌控与守护,寓意本次版本对系统稳定性、安全性与用户体验的全面驾驭与提升。
|
||||
|
||||
#### ⚠️ 已知问题
|
||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
|
||||
- 窗口状态管理器已确定上游存在缺陷,暂时移除。当前不再内置窗口大小和位置记忆。
|
||||
- MacOS 下卸载服务后需手动重启软件才能与内核通信。
|
||||
尽管 `external-controller` 密钥现已自动补全默认值且不允许为空,**仍建议手动修改密钥以提高安全性**。
|
||||
|
||||
### 2.3.0 相对于 2.2.3
|
||||
#### 🐞 修复问题
|
||||
- 首页"代理模式"快速切换导致的卡死问题
|
||||
- 解锁测试报错信息
|
||||
- Macos 快捷键关闭窗口无法启用自动轻量模式
|
||||
- 静默启动异常窗口创建和关闭流程
|
||||
- Windows 错误的全局快捷键 `Ctrl+Q` 注册
|
||||
- Vless URL 解码时网络类型错误
|
||||
- 切换自定义代理地址导致系统代理状态异常
|
||||
- Macos TUN 默认无效网卡名称
|
||||
- 托盘更改订阅后 UI 不同步的问题
|
||||
- 修复提权漏洞,改用带认证的 IPC 通信
|
||||
- 编辑器中连字符问题
|
||||
- 安装服务模式后无法立即开启 TUN 模式
|
||||
- 同步更新多语言翻译
|
||||
- 修复 .window-state.json 无法删除的问题
|
||||
- 无法修改配置更新 HTTP 请求超时
|
||||
- 修复 getDelayFix 钩子问题
|
||||
- 使用外部扩展脚本覆写代理组时首页无法显示代理组
|
||||
- 导出诊断 Verge 版本与设置页面不同步
|
||||
### ⚠️ 已知问题
|
||||
|
||||
#### ✨ 新增功能
|
||||
- Mihomo(Meta)内核升级至 1.19.10
|
||||
- 允许代理主机地址设置为非 127.0.0.1 对 WSL 代理友好
|
||||
- 关闭系统代理时关闭已建立的网络连接
|
||||
- 托盘显示当前轻量模式状态
|
||||
- Webdav 请求加入 UA
|
||||
- Webdav 支持目录重定向
|
||||
- Webdav 备份目录检查和文件上传重试机制
|
||||
- 系统代理守卫可检查意外设置变更并恢复
|
||||
- 定时自动订阅更新也能自动回退使用代理
|
||||
- 订阅请求超时机制,防止订阅更新的时候卡死
|
||||
- 订阅卡片点击时间可切换下次自动更新时间,自动更新触发后页面有明确的成功与否提示
|
||||
- 添加网络管理器以优化网络请求处理,防止资源竞争导致的启动时 UI 卡死
|
||||
- 更新依赖,替换弃用元素
|
||||
- 首页当前节点增加排序功能
|
||||
- DNS 覆写下增加 Hosts 设置功能
|
||||
- 修复服务模式安装后无法立即开启 TUN 模式的问题
|
||||
- 支持手动卸载服务模式,回退到 Sidecar 模式
|
||||
- 添加了土耳其语,日本语,德语,西班牙语,繁体中文的支持
|
||||
- 卸载服务的按钮
|
||||
- 添加了Zashboard的一键跳转URL
|
||||
- 使用操作系统默认的窗口管理器
|
||||
- 切换、升级、重启内核的状态管理
|
||||
- 更精细化控制自动日志清理,新增1天选项
|
||||
- Winodws 快捷键名称改为 `Clash Verge`
|
||||
- 配置加载阶段自动补全 external-controller secret 字段。
|
||||
- 添加 TCP Concurrency (TCP并发)
|
||||
- 仅在 Ubuntu 22.04/24.04、Fedora 41 的 **GNOME 桌面环境** 做过简单测试,不保证其他 Linux 发行版兼容,后续将逐步适配和优化。
|
||||
- macOS:
|
||||
- MacOS 下自动升级成功后请关闭程序等待 30 秒重启,因为 MacOS 的端口释放特性,卸载服务后需重启应用等 30 秒才能恢复内核通信。立即启动可能无法正常启动内核。
|
||||
- 墙贴主要为浅色,深色 Tray 图标存在闪烁问题;
|
||||
- 彩色 Tray 图标颜色偏淡;
|
||||
|
||||
#### 🚀 优化改进
|
||||
- 系统代理 Bypass 设置
|
||||
- Windows 下使用 Startup 文件夹的方式实现开机自启,解决管理员模式下开机自启的各种问题
|
||||
- 切换到规则页面时自动刷新规则数据
|
||||
- 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新
|
||||
- 编辑非激活订阅的时候不在触发当前订阅配置重载
|
||||
- 改进核心功能防止主进程阻塞、改进MihomoManager实现,以及优化窗口创建流程
|
||||
- 优化系统代理设置更新逻辑
|
||||
- 重构前端通知系统分离通知线程防止前端卡死
|
||||
- 优化网络请求和错误处理
|
||||
- 重构通知系统
|
||||
- 使用异步方法重构 UI 启动逻辑,解决启动软件过程中的各种卡死问题
|
||||
- MacOS 下默认关闭托盘速率显示
|
||||
- 优化服务操作流程,提升系统服务相关操作的稳定性和用户体验
|
||||
- 优化了其他语言的翻译问题
|
||||
- Mihomo 内核默认日志等级为 warn
|
||||
- Clash Verge Rev 应用默认日志等级为 warn
|
||||
- 重构了原来的 IP 信息请求重试机制,采用轮询检测,解决了 Network Error 和超时问题
|
||||
- 对轮询检测机制进行了优化,引入洗牌算法来增强随机性
|
||||
- 对获取系统信息的流程进行了优化,并添加了去重检测机制,确保剔除重复的信息
|
||||
- 优化窗口状态初始化逻辑和添加缺失的权限设置
|
||||
- 异步化配置:优化端口查找和配置保存逻辑
|
||||
- 重构事件通知机制到独立线程,避免前端卡死
|
||||
- 优化端口设置,每个端口可随机设置端口号
|
||||
- 优化了保存机制,使用平滑函数,防止客户端卡死
|
||||
- 优化端口设置退出和保存机制
|
||||
- 强制为 Mihomo 配置补全并覆盖 external-controller-cors 字段,默认不允许跨域和仅本地请求,提升 cors 安全性,升级配置时自动覆盖
|
||||
- 修改 端口检测范围 (1111-65536)
|
||||
- 配置文件缺失 secret 字段时自动填充默认值 set-your-secret
|
||||
- 优化异步处理,防止部分组件 UI 阻塞
|
||||
- 关闭 DNS 启用
|
||||
- 延迟测试链接更换为 Https 协议 https://cp.cloudflare.com/generate_204
|
||||
- 已确认窗口状态管理器存在上游缺陷,已暂时移除窗口大小与位置记忆功能。
|
||||
|
||||
#### 🗑️ 移除内容
|
||||
- 窗口状态管理器
|
||||
- Webdav 跨平台备份恢复限制
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复首页“代理模式”快速切换导致的卡死问题
|
||||
- 修复 MacOS 快捷键关闭窗口无法启用自动轻量模式
|
||||
- 修复静默启动异常窗口的创建与关闭流程
|
||||
- 修复 Windows 下错误注册的全局快捷键 `Ctrl+Q`
|
||||
- 修复解锁测试报错信息与 VLESS URL 解码时的网络类型错误
|
||||
- 修复切换自定义代理地址后系统代理状态异常
|
||||
- 修复 macOS TUN 默认无效网卡名称
|
||||
- 修复更改订阅后托盘 UI 不同步的问题
|
||||
- 修复服务模式安装后无法立即开启 TUN 模式
|
||||
- 修复无法删除 `.window-state.json`
|
||||
- 修复无法修改配置更新 HTTP 请求超时问题
|
||||
- 修复 `getDelayFix` 钩子异常
|
||||
- 修复外部扩展脚本覆写代理组时首页无法显示代理组
|
||||
- 修复 Verge 导出诊断版本与设置页面不同步
|
||||
- 修复切换语言时设置页面可能加载失败
|
||||
- 修复编辑器中连字符处理问题
|
||||
- 修复提权漏洞,改用带认证的 IPC 通信机制
|
||||
- 修复静默启动无法使用自动轻量模式
|
||||
- 修复 JS 脚本转义特殊字符报错
|
||||
- 修复 macOS 静默启动时异常启动 Dock 栏图标
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.10**
|
||||
- 支持设置代理地址为非 `127.0.0.1`,提升 WSL 兼容性
|
||||
- 系统代理守卫:可检测意外变更并自动恢复
|
||||
- 托盘新增当前轻量模式状态显示
|
||||
- 关闭系统代理时同时断开已建立的连接
|
||||
- 新增 WebDAV 功能:
|
||||
- 加入 UA 请求头
|
||||
- 支持目录重定向
|
||||
- 备份目录检查与上传重试机制
|
||||
|
||||
- 自动订阅更新机制:
|
||||
- 加入请求超时机制防止卡死
|
||||
- 支持在代理状态下自动重试订阅更新
|
||||
- 支持订阅卡片点击切换下次自动更新时间,并显示更新结果提示
|
||||
|
||||
- DNS 设置新增 Hosts 配置功能
|
||||
- 首页代理节点支持排序
|
||||
- 支持服务模式手动卸载,回退至 Sidecar 模式
|
||||
- 核心状态管理支持切换、升级、重启
|
||||
- 配置加载阶段自动补全 `external-controller secret`
|
||||
- 新增日志自动清理周期选项(含1天)
|
||||
- 新增 Zashboard 一键跳转入口
|
||||
- 使用系统默认窗口管理器
|
||||
|
||||
### 🚀 优化改进
|
||||
|
||||
- **系统相关:**
|
||||
- 系统代理 Bypass 设置优化
|
||||
- 优化代理设置更新逻辑与守卫机制
|
||||
- Windows 启动方式调整为 Startup 文件夹,解决管理员模式下自启问题
|
||||
|
||||
- **性能与稳定性:**
|
||||
- 全面异步化处理配置加载、UI 启动、事件通知等关键流程,解决卡顿问题
|
||||
- 优化 MihomoManager 实现与窗口创建流程
|
||||
- 改进内核日志等级为 `warn`,减少噪音输出
|
||||
- 重构主进程与通知系统,提升响应性与分离度
|
||||
- 优化网络请求与错误处理机制
|
||||
- 添加网络管理器防止资源竞争引发 UI 卡死
|
||||
- 优化配置文件加载内存使用
|
||||
- 优化缓存 Mihomo proxy 和 providers 信息内存使用
|
||||
|
||||
- **前端与界面体验:**
|
||||
- 切换规则页自动刷新数据
|
||||
- 非激活订阅编辑时不再触发配置重载
|
||||
- 优化托盘速率显示,macOS 下默认关闭
|
||||
- Windows 快捷键名称更名为 `Clash Verge`
|
||||
- 更新失败可回退至使用代理重试
|
||||
- 支持异步端口查找与保存,端口支持随机生成
|
||||
- 修改端口检测范围至 `1111-65536`
|
||||
- 优化保存机制,使用平滑函数防止卡顿
|
||||
|
||||
- **配置增强与安全性:**
|
||||
- 配置缺失 `secret` 字段时自动补全为 `set-your-secret`
|
||||
- 强制为 Mihomo 配置补全 `external-controller-cors` 字段(默认不允许跨域,限制本地访问)计划后续支持自定义 cors
|
||||
- 优化窗口权限设置与状态初始化逻辑
|
||||
- 网络延迟测试替换为 HTTPS 协议:`https://cp.cloudflare.com/generate_204`
|
||||
- 优化 IP 信息获取流程,添加去重机制与轮询检测算法
|
||||
|
||||
- 同步修复翻译错误与不一致项,优化整体语言体验
|
||||
- 加强语言切换后的页面稳定性,避免加载异常
|
||||
|
||||
### 🗑️ 移除内容
|
||||
|
||||
- 窗口状态管理器(上游存在缺陷)
|
||||
- WebDAV 跨平台备份恢复限制
|
||||
|
||||
---
|
||||
|
||||
## v2.2.3
|
||||
|
||||
#### 已知问题
|
||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙
|
||||
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
|
||||
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
|
||||
|
||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||
- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙
|
||||
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
|
||||
- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS
|
||||
|
||||
### 2.2.3 相对于 2.2.2
|
||||
|
||||
#### 修复了:
|
||||
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
|
||||
- “开机自启”和“DNS覆写”开关跳动问题
|
||||
- 自定义托盘图标未能应用更改
|
||||
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大
|
||||
- MacOS 托盘速率显示不全
|
||||
- Linux 在系统服务模式下无法拉起 Mihomo 内核
|
||||
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
|
||||
- 相同节点名称可能导致的页面渲染出错
|
||||
- URL Schemes被截断的问题
|
||||
- 首页流量统计卡更好的时间戳范围
|
||||
- 静默启动无法触发自动轻量化计时器
|
||||
|
||||
- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题
|
||||
- “开机自启”和“DNS覆写”开关跳动问题
|
||||
- 自定义托盘图标未能应用更改
|
||||
- MacOS 自定义托盘图标显示速率时图标和文本间隙过大
|
||||
- MacOS 托盘速率显示不全
|
||||
- Linux 在系统服务模式下无法拉起 Mihomo 内核
|
||||
- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃
|
||||
- 相同节点名称可能导致的页面渲染出错
|
||||
- URL Schemes被截断的问题
|
||||
- 首页流量统计卡更好的时间戳范围
|
||||
- 静默启动无法触发自动轻量化计时器
|
||||
|
||||
#### 新增了:
|
||||
- Mihomo(Meta)内核升级至 1.19.4
|
||||
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
|
||||
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
|
||||
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
|
||||
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
|
||||
- 代理组显示节点数量
|
||||
- 统一运行模式检测,支持管理员模式下开启TUN模式
|
||||
- 托盘切换代理模式会根据设置自动断开之前连接
|
||||
- 如订阅获取失败回退使用Clash内核代理再次尝试
|
||||
|
||||
- Mihomo(Meta)内核升级至 1.19.4
|
||||
- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限
|
||||
- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务
|
||||
- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置
|
||||
- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启
|
||||
- 代理组显示节点数量
|
||||
- 统一运行模式检测,支持管理员模式下开启TUN模式
|
||||
- 托盘切换代理模式会根据设置自动断开之前连接
|
||||
- 如订阅获取失败回退使用Clash内核代理再次尝试
|
||||
|
||||
#### 移除了:
|
||||
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
|
||||
|
||||
- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。
|
||||
|
||||
#### 优化了:
|
||||
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
|
||||
- 前端统一刷新应用数据,优化数据获取和刷新逻辑
|
||||
- 优化首页流量图表代码,调整图表文字边距
|
||||
- MacOS 托盘速率更好的显示样式和更新逻辑
|
||||
- 首页仅在有流量图表时显示流量图表区域
|
||||
- 更新DNS默认覆写配置
|
||||
- 移除测试目录,简化资源初始化逻辑
|
||||
|
||||
- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
|
||||
- 前端统一刷新应用数据,优化数据获取和刷新逻辑
|
||||
- 优化首页流量图表代码,调整图表文字边距
|
||||
- MacOS 托盘速率更好的显示样式和更新逻辑
|
||||
- 首页仅在有流量图表时显示流量图表区域
|
||||
- 更新DNS默认覆写配置
|
||||
- 移除测试目录,简化资源初始化逻辑
|
||||
|
||||
## v2.2.2
|
||||
|
||||
@@ -148,23 +289,29 @@
|
||||
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
|
||||
|
||||
#### 已知问题
|
||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||
|
||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||
|
||||
### 2.2.2 相对于 2.2.1(已下架不再提供)
|
||||
|
||||
#### 修复了:
|
||||
- 弹黑框的问题(原因是服务崩溃触发重装机制)
|
||||
- MacOS进入轻量模式以后隐藏Dock图标
|
||||
- 增加轻量模式缺失的tray翻译
|
||||
- Linux下的窗口边框被削掉的问题
|
||||
|
||||
- 弹黑框的问题(原因是服务崩溃触发重装机制)
|
||||
- MacOS进入轻量模式以后隐藏Dock图标
|
||||
- 增加轻量模式缺失的tray翻译
|
||||
- Linux下的窗口边框被削掉的问题
|
||||
|
||||
#### 新增了:
|
||||
- 加强服务检测和重装逻辑
|
||||
- 增强内核与服务保活机制
|
||||
- 增加服务模式下的僵尸进程清理机制
|
||||
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
|
||||
|
||||
- 加强服务检测和重装逻辑
|
||||
- 增强内核与服务保活机制
|
||||
- 增加服务模式下的僵尸进程清理机制
|
||||
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
|
||||
|
||||
### 2.2.1 相对于 2.2.0(已下架不再提供)
|
||||
|
||||
#### 修复了:
|
||||
|
||||
1. **首页**
|
||||
- 修复 Direct 模式首页无法渲染
|
||||
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
|
||||
@@ -181,6 +328,7 @@
|
||||
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
|
||||
|
||||
#### 新增了:
|
||||
|
||||
1. **首页**
|
||||
- 首页文本过长自动截断
|
||||
2. **轻量模式**
|
||||
@@ -197,6 +345,7 @@
|
||||
## 2.2.0(已下架不再提供)
|
||||
|
||||
#### 新增功能
|
||||
|
||||
1. **首页**
|
||||
- 新增首页功能,默认启动页面改为首页。
|
||||
- 首页流量图卡片显示上传/下载名称。
|
||||
@@ -228,6 +377,7 @@
|
||||
- 新增代理命令。
|
||||
|
||||
#### 修复
|
||||
|
||||
1. **系统**
|
||||
- 修复 Windows 热键崩溃。
|
||||
- 修复 macOS 无框标题。
|
||||
@@ -241,6 +391,7 @@
|
||||
- 修复构建失败问题。
|
||||
|
||||
#### 优化
|
||||
|
||||
1. **性能**
|
||||
- 重构后端,巨幅性能优化。
|
||||
- 优化首页组件性能。
|
||||
@@ -296,7 +447,7 @@
|
||||
|
||||
- 新增窗口状态实时监控与自动保存功能
|
||||
- 增强核心配置变更时的验证与错误处理机制
|
||||
- 支持通过环境变量`CLASH_VERGE_REV_IP`自定义复制IP地址
|
||||
- 支持通过环境变量 `CLASH_VERGE_REV_IP`自定义复制IP地址
|
||||
- 添加连接表列宽持久化设置与进程过滤功能
|
||||
- 新增代理组首字母导航与动态滚动定位功能
|
||||
- 实现连接追踪暂停/恢复功能
|
||||
@@ -617,7 +768,7 @@
|
||||
- 禁用部分 Webview2 快捷键
|
||||
- 热键配置新增连接符 + 号
|
||||
- 新增部分悬浮提示按钮,用于解释说明
|
||||
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||
- 设置页面右上角新增 TG 频道链接
|
||||
- 各种细节优化和界面性能优化
|
||||
|
||||
@@ -657,7 +808,7 @@
|
||||
- 禁用部分 Webview2 快捷键
|
||||
- 热键配置新增连接符 + 号
|
||||
- 新增部分悬浮提示按钮,用于解释说明
|
||||
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||
- 设置页面右上角新增 TG 频道链接
|
||||
- 各种细节优化和界面性能优化
|
||||
|
||||
@@ -693,7 +844,7 @@
|
||||
- 禁用部分 Webview2 快捷键
|
||||
- 热键配置新增连接符 + 号
|
||||
- 新增部分悬浮提示按钮,用于解释说明
|
||||
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||
- 设置页面右上角新增 TG 频道链接
|
||||
|
||||
### Bugs Fixes
|
||||
@@ -867,7 +1018,7 @@
|
||||
### Features
|
||||
|
||||
- 缓存代理组图标
|
||||
- 使用`boa_engine` 代替 `rquickjs`
|
||||
- 使用 `boa_engine` 代替 `rquickjs`
|
||||
- 支持 Linux armv7
|
||||
|
||||
### Bugs Fixes
|
||||
@@ -932,7 +1083,7 @@
|
||||
- 支持自定义托盘图标
|
||||
- 支持禁用代理组图标
|
||||
- 代理组显示当前代理
|
||||
- 修改 `打开面板` 快捷键为`打开/关闭面板`
|
||||
- 修改 `打开面板` 快捷键为 `打开/关闭面板`
|
||||
|
||||
---
|
||||
|
||||
@@ -1096,7 +1247,7 @@
|
||||
|
||||
### Bugs Fixes
|
||||
|
||||
- Windows 下更新时无法覆盖`clash-verge-service.exe`的问题(需要卸载重装一次服务,下次更新生效)
|
||||
- Windows 下更新时无法覆盖 `clash-verge-service.exe`的问题(需要卸载重装一次服务,下次更新生效)
|
||||
- 窗口最大化按钮变化问题
|
||||
- 窗口尺寸保存错误问题
|
||||
- 复制环境变量类型无法切换问题
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
files:
|
||||
- source: /src/locales/en.json
|
||||
translation: /src/locales
|
||||
multilingual: 1
|
||||
+58
-63
@@ -1,115 +1,110 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||
"dev:trace": "cross-env RUST_BACKTRACE=1 RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace",
|
||||
"build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build",
|
||||
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
|
||||
"tauri": "tauri",
|
||||
"web:dev": "vite",
|
||||
"web:build": "tsc --noEmit && vite build",
|
||||
"web:serve": "vite preview",
|
||||
"check": "node scripts/check.mjs",
|
||||
"prebuild": "node scripts/prebuild.mjs",
|
||||
"updater": "node scripts/updater.mjs",
|
||||
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
||||
"portable": "node scripts/portable.mjs",
|
||||
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
||||
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
|
||||
"release-version": "node scripts/release-version.mjs",
|
||||
"release-alpha-version": "node scripts/release-alpha_version.mjs",
|
||||
"prepare": "husky",
|
||||
"release:autobuild": "pnpm release-version autobuild",
|
||||
"release:deploytest": "pnpm release-version deploytest",
|
||||
"publish-version": "node scripts/publish-version.mjs",
|
||||
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
|
||||
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml"
|
||||
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^7.0.2",
|
||||
"@mui/lab": "7.0.0-beta.12",
|
||||
"@mui/material": "^7.0.2",
|
||||
"@mui/x-data-grid": "^8.2.0",
|
||||
"@tauri-apps/api": "2.5.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||
"@tauri-apps/plugin-fs": "^2.2.1",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
|
||||
"@tauri-apps/plugin-notification": "^2.2.2",
|
||||
"@tauri-apps/plugin-process": "^2.2.1",
|
||||
"@tauri-apps/plugin-shell": "2.2.1",
|
||||
"@tauri-apps/plugin-updater": "2.7.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@mui/icons-material": "^7.3.1",
|
||||
"@mui/lab": "7.0.0-beta.16",
|
||||
"@mui/material": "^7.3.1",
|
||||
"@mui/x-data-grid": "^8.10.2",
|
||||
"@tauri-apps/api": "2.8.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.3.3",
|
||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.3.0",
|
||||
"@tauri-apps/plugin-notification": "^2.3.0",
|
||||
"@tauri-apps/plugin-process": "^2.3.0",
|
||||
"@tauri-apps/plugin-shell": "2.3.0",
|
||||
"@tauri-apps/plugin-updater": "2.9.0",
|
||||
"@tauri-apps/plugin-window-state": "^2.4.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.8.3",
|
||||
"chart.js": "^4.4.9",
|
||||
"ahooks": "^3.9.3",
|
||||
"axios": "^1.11.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"cli-color": "^2.0.4",
|
||||
"d3-shape": "^3.2.0",
|
||||
"dayjs": "1.11.13",
|
||||
"foxact": "^0.2.44",
|
||||
"glob": "^11.0.1",
|
||||
"i18next": "^25.0.2",
|
||||
"js-base64": "^3.7.7",
|
||||
"foxact": "^0.2.49",
|
||||
"glob": "^11.0.3",
|
||||
"i18next": "^25.4.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-yaml": "^5.3.1",
|
||||
"monaco-yaml": "^5.4.0",
|
||||
"nanoid": "^5.1.5",
|
||||
"peggy": "^5.0.0",
|
||||
"react": "19.1.0",
|
||||
"react": "19.1.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-dom": "19.1.1",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-i18next": "15.5.1",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-i18next": "15.7.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-monaco-editor": "0.58.0",
|
||||
"react-router-dom": "7.6.0",
|
||||
"react-virtuoso": "^4.12.7",
|
||||
"react-monaco-editor": "0.59.0",
|
||||
"react-router-dom": "7.8.1",
|
||||
"react-virtuoso": "^4.14.0",
|
||||
"sockette": "^2.0.6",
|
||||
"swr": "^2.3.3",
|
||||
"swr": "^2.3.6",
|
||||
"tar": "^7.4.3",
|
||||
"types-pac": "^1.0.3",
|
||||
"zustand": "^5.0.3"
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.0",
|
||||
"@tauri-apps/cli": "2.2.7",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@tauri-apps/cli": "2.8.1",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "19.1.4",
|
||||
"@types/react-dom": "19.1.5",
|
||||
"@vitejs/plugin-legacy": "^6.0.2",
|
||||
"@vitejs/plugin-react": "4.4.1",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-react": "5.0.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"commander": "^14.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-env": "^10.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"husky": "^9.1.7",
|
||||
"meta-json-schema": "^1.19.3",
|
||||
"meta-json-schema": "^1.19.12",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.5.3",
|
||||
"pretty-quick": "^4.1.1",
|
||||
"sass": "^1.86.0",
|
||||
"terser": "^5.39.0",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.2",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-organize-imports": "^4.2.0",
|
||||
"sass": "^1.90.0",
|
||||
"terser": "^5.43.1",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.3",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"endOfLine": "lf"
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.13.2"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1256
-1227
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"extends": ["config:recommended", ":disableDependencyDashboard"],
|
||||
"baseBranches": ["dev"],
|
||||
"enabledManagers": ["cargo", "npm"],
|
||||
"labels": ["dependencies"],
|
||||
"ignorePaths": [
|
||||
"**/node_modules/**",
|
||||
"**/bower_components/**",
|
||||
"**/vendor/**",
|
||||
"**/__tests__/**",
|
||||
"**/test/**",
|
||||
"**/tests/**",
|
||||
"**/__fixtures__/**",
|
||||
"**/crate/**",
|
||||
"shared/**"
|
||||
],
|
||||
"rangeStrategy": "bump",
|
||||
"packageRules": [
|
||||
{
|
||||
"semanticCommitType": "chore",
|
||||
"matchPackageNames": ["*"]
|
||||
},
|
||||
{
|
||||
"description": "Disable node/pnpm version updates",
|
||||
"matchPackageNames": ["node", "pnpm"],
|
||||
"matchDepTypes": ["engines", "packageManager"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"description": "Group all cargo dependencies into a single PR",
|
||||
"matchManagers": ["cargo"],
|
||||
"groupName": "cargo dependencies"
|
||||
},
|
||||
{
|
||||
"description": "Group all npm dependencies into a single PR",
|
||||
"matchManagers": ["npm"],
|
||||
"groupName": "npm dependencies"
|
||||
}
|
||||
],
|
||||
"postUpdateOptions": ["pnpmDedupe"],
|
||||
"ignoreDeps": ["serde_yaml", "sysinfo", "zip", "criterion"]
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 获取最近一个和 Tauri 相关的改动的 commit hash
|
||||
# This script finds the latest commit that modified Tauri-related files
|
||||
|
||||
# Tauri 相关文件的模式
|
||||
TAURI_PATTERNS=(
|
||||
"src-tauri/"
|
||||
"Cargo.toml"
|
||||
"Cargo.lock"
|
||||
"tauri.*.conf.json"
|
||||
"package.json"
|
||||
"pnpm-lock.yaml"
|
||||
"src/"
|
||||
)
|
||||
|
||||
# 排除的文件模式(build artifacts 等)
|
||||
EXCLUDE_PATTERNS=(
|
||||
"src-tauri/target/"
|
||||
"src-tauri/gen/"
|
||||
"*.log"
|
||||
"*.tmp"
|
||||
"node_modules/"
|
||||
".git/"
|
||||
)
|
||||
|
||||
# 构建 git log 的路径过滤参数
|
||||
PATHS=""
|
||||
for pattern in "${TAURI_PATTERNS[@]}"; do
|
||||
if [[ -e "$pattern" ]]; then
|
||||
PATHS="$PATHS $pattern"
|
||||
fi
|
||||
done
|
||||
|
||||
# 如果没有找到相关路径,返回错误
|
||||
if [[ -z "$PATHS" ]]; then
|
||||
echo "Error: No Tauri-related paths found in current directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 获取最新的 commit hash
|
||||
# 使用 git log 查找最近修改了 Tauri 相关文件的提交
|
||||
LATEST_COMMIT=$(git log --format="%H" -n 1 -- $PATHS)
|
||||
|
||||
# 验证是否找到了 commit
|
||||
if [[ -z "$LATEST_COMMIT" ]]; then
|
||||
echo "Error: No commits found for Tauri-related files" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 输出结果
|
||||
echo "$LATEST_COMMIT"
|
||||
|
||||
# 如果需要更多信息,可以取消注释以下行
|
||||
# echo "Latest Tauri-related commit: $LATEST_COMMIT"
|
||||
# git show --stat --oneline "$LATEST_COMMIT"
|
||||
@@ -1,21 +1,21 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const LOCALES_DIR = path.resolve(__dirname, '../src/locales');
|
||||
const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
|
||||
const SRC_DIRS = [
|
||||
path.resolve(__dirname, '../src'),
|
||||
path.resolve(__dirname, '../src-tauri')
|
||||
path.resolve(__dirname, "../src"),
|
||||
path.resolve(__dirname, "../src-tauri"),
|
||||
];
|
||||
const exts = ['.js', '.ts', '.tsx', '.jsx', '.vue', '.rs'];
|
||||
const exts = [".js", ".ts", ".tsx", ".jsx", ".vue", ".rs"];
|
||||
|
||||
// 递归获取所有文件
|
||||
function getAllFiles(dir, exts) {
|
||||
let files = [];
|
||||
fs.readdirSync(dir).forEach(file => {
|
||||
fs.readdirSync(dir).forEach((file) => {
|
||||
const full = path.join(dir, file);
|
||||
if (fs.statSync(full).isDirectory()) {
|
||||
files = files.concat(getAllFiles(full, exts));
|
||||
@@ -28,21 +28,21 @@ function getAllFiles(dir, exts) {
|
||||
|
||||
// 读取所有源码内容为一个大字符串
|
||||
function getAllSourceContent() {
|
||||
const files = SRC_DIRS.flatMap(dir => getAllFiles(dir, exts));
|
||||
return files.map(f => fs.readFileSync(f, 'utf8')).join('\n');
|
||||
const files = SRC_DIRS.flatMap((dir) => getAllFiles(dir, exts));
|
||||
return files.map((f) => fs.readFileSync(f, "utf8")).join("\n");
|
||||
}
|
||||
|
||||
// 白名单 key,不检查这些 key 是否被使用
|
||||
const WHITELIST_KEYS = [
|
||||
'theme.light',
|
||||
'theme.dark',
|
||||
'theme.system',
|
||||
"Already Using Latest Core Version"
|
||||
"theme.light",
|
||||
"theme.dark",
|
||||
"theme.system",
|
||||
"Already Using Latest Core Version",
|
||||
];
|
||||
|
||||
// 主流程
|
||||
function processI18nFile(i18nPath, lang, allSource) {
|
||||
const i18n = JSON.parse(fs.readFileSync(i18nPath, 'utf8'));
|
||||
const i18n = JSON.parse(fs.readFileSync(i18nPath, "utf8"));
|
||||
const keys = Object.keys(i18n);
|
||||
|
||||
const used = {};
|
||||
@@ -50,7 +50,7 @@ function processI18nFile(i18nPath, lang, allSource) {
|
||||
|
||||
let checked = 0;
|
||||
const total = keys.length;
|
||||
keys.forEach(key => {
|
||||
keys.forEach((key) => {
|
||||
if (WHITELIST_KEYS.includes(key)) {
|
||||
used[key] = i18n[key];
|
||||
} else {
|
||||
@@ -65,8 +65,10 @@ function processI18nFile(i18nPath, lang, allSource) {
|
||||
checked++;
|
||||
if (checked % 20 === 0 || checked === total) {
|
||||
const percent = ((checked / total) * 100).toFixed(1);
|
||||
process.stdout.write(`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`);
|
||||
if (checked === total) process.stdout.write('\n');
|
||||
process.stdout.write(
|
||||
`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`,
|
||||
);
|
||||
if (checked === total) process.stdout.write("\n");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -74,25 +76,27 @@ function processI18nFile(i18nPath, lang, allSource) {
|
||||
console.log(`\n[${lang}] Unused keys:`, unused);
|
||||
|
||||
// 备份原文件
|
||||
const oldPath = i18nPath + '.old';
|
||||
const oldPath = i18nPath + ".old";
|
||||
fs.renameSync(i18nPath, oldPath);
|
||||
|
||||
// 写入精简后的 i18n 文件(保留原文件名)
|
||||
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), 'utf8');
|
||||
console.log(`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`);
|
||||
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), "utf8");
|
||||
console.log(
|
||||
`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`,
|
||||
);
|
||||
console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
// 支持 zhtw.json、zh-tw.json、zh_CN.json 等
|
||||
const files = fs.readdirSync(LOCALES_DIR).filter(f =>
|
||||
/^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith('.old')
|
||||
);
|
||||
const files = fs
|
||||
.readdirSync(LOCALES_DIR)
|
||||
.filter((f) => /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith(".old"));
|
||||
const allSource = getAllSourceContent();
|
||||
files.forEach(file => {
|
||||
const lang = path.basename(file, '.json');
|
||||
files.forEach((file) => {
|
||||
const lang = path.basename(file, ".json");
|
||||
processI18nFile(path.join(LOCALES_DIR, file), lang, allSource);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
main();
|
||||
|
||||
@@ -61,12 +61,12 @@ const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/down
|
||||
let META_ALPHA_VERSION;
|
||||
|
||||
const META_ALPHA_MAP = {
|
||||
"win32-x64": "mihomo-windows-amd64-compatible",
|
||||
"win32-x64": "mihomo-windows-amd64-v2",
|
||||
"win32-ia32": "mihomo-windows-386",
|
||||
"win32-arm64": "mihomo-windows-arm64",
|
||||
"darwin-x64": "mihomo-darwin-amd64-compatible",
|
||||
"darwin-x64": "mihomo-darwin-amd64-v2",
|
||||
"darwin-arm64": "mihomo-darwin-arm64",
|
||||
"linux-x64": "mihomo-linux-amd64-compatible",
|
||||
"linux-x64": "mihomo-linux-amd64-v2",
|
||||
"linux-ia32": "mihomo-linux-386",
|
||||
"linux-arm64": "mihomo-linux-arm64",
|
||||
"linux-arm": "mihomo-linux-armv7",
|
||||
@@ -108,12 +108,12 @@ const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
|
||||
let META_VERSION;
|
||||
|
||||
const META_MAP = {
|
||||
"win32-x64": "mihomo-windows-amd64-compatible",
|
||||
"win32-x64": "mihomo-windows-amd64-v2",
|
||||
"win32-ia32": "mihomo-windows-386",
|
||||
"win32-arm64": "mihomo-windows-arm64",
|
||||
"darwin-x64": "mihomo-darwin-amd64-compatible",
|
||||
"darwin-x64": "mihomo-darwin-amd64-v2",
|
||||
"darwin-arm64": "mihomo-darwin-arm64",
|
||||
"linux-x64": "mihomo-linux-amd64-compatible",
|
||||
"linux-x64": "mihomo-linux-amd64-v2",
|
||||
"linux-ia32": "mihomo-linux-386",
|
||||
"linux-arm64": "mihomo-linux-arm64",
|
||||
"linux-arm": "mihomo-linux-armv7",
|
||||
@@ -0,0 +1,66 @@
|
||||
// scripts/publish-version.mjs
|
||||
import { spawn } from "child_process";
|
||||
import { existsSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
const rootDir = process.cwd();
|
||||
const scriptPath = path.join(rootDir, "scripts", "release-version.mjs");
|
||||
|
||||
if (!existsSync(scriptPath)) {
|
||||
console.error("release-version.mjs not found!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const versionArg = process.argv[2];
|
||||
if (!versionArg) {
|
||||
console.error("Usage: pnpm publish-version <version>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 1. 调用 release-version.mjs
|
||||
const runRelease = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
const child = spawn("node", [scriptPath, versionArg], { stdio: "inherit" });
|
||||
child.on("exit", (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error("release-version failed"));
|
||||
});
|
||||
});
|
||||
|
||||
// 2. 判断是否需要打 tag
|
||||
function isSemver(version) {
|
||||
return /^v?\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/.test(version);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
await runRelease();
|
||||
|
||||
let tag = null;
|
||||
if (versionArg === "alpha") {
|
||||
// 读取 package.json 里的主版本
|
||||
const pkg = await import(path.join(rootDir, "package.json"), {
|
||||
assert: { type: "json" },
|
||||
});
|
||||
tag = `v${pkg.default.version}-alpha`;
|
||||
} else if (isSemver(versionArg)) {
|
||||
// 1.2.3 或 v1.2.3
|
||||
tag = versionArg.startsWith("v") ? versionArg : `v${versionArg}`;
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
// 打 tag 并推送
|
||||
const { execSync } = await import("child_process");
|
||||
try {
|
||||
execSync(`git tag ${tag}`, { stdio: "inherit" });
|
||||
execSync(`git push origin ${tag}`, { stdio: "inherit" });
|
||||
console.log(`[INFO]: Git tag ${tag} created and pushed.`);
|
||||
} catch (e) {
|
||||
console.error(`[ERROR]: Failed to create or push git tag: ${tag}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.log("[INFO]: No git tag created for this version.");
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
+111
-30
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
/**
|
||||
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json.
|
||||
*
|
||||
@@ -8,15 +6,19 @@
|
||||
*
|
||||
* <version> can be:
|
||||
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3+build)
|
||||
* - A tag: "alpha", "beta", "rc", or "autobuild"
|
||||
* - A tag: "alpha", "beta", "rc", "autobuild", "autobuild-latest", or "deploytest"
|
||||
* - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta)
|
||||
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3+autobuild.2406101530)
|
||||
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3+autobuild.0614.a1b2c3d)
|
||||
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3+deploytest.2406101530)
|
||||
*
|
||||
* Examples:
|
||||
* pnpm release-version 1.2.3
|
||||
* pnpm release-version v1.2.3-beta
|
||||
* pnpm release-version beta
|
||||
* pnpm release-version autobuild
|
||||
* pnpm release-version autobuild-latest
|
||||
* pnpm release-version deploytest
|
||||
*
|
||||
* The script will:
|
||||
* - Validate and normalize the version argument
|
||||
@@ -30,19 +32,55 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { program } from "commander";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
/**
|
||||
* 生成短时间戳(格式:YYMMDDHHMM)
|
||||
* 获取当前 git 短 commit hash
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateShortTimestamp() {
|
||||
function getGitShortCommit() {
|
||||
try {
|
||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||
} catch (e) {
|
||||
console.warn("[WARN]: Failed to get git short commit, fallback to 'nogit'");
|
||||
return "nogit";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新 Tauri 相关提交的短 hash
|
||||
* @returns {string}
|
||||
*/
|
||||
function getLatestTauriCommit() {
|
||||
try {
|
||||
const fullHash = execSync(
|
||||
"bash ./scripts-workflow/get_latest_tauri_commit.bash",
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
return execSync(`git rev-parse --short ${fullHash}`).toString().trim();
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"[WARN]: Failed to get latest Tauri commit, fallback to current git short commit",
|
||||
);
|
||||
return getGitShortCommit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成短时间戳(格式:YYMMDD)或带 commit(格式:YYMMDD.cc39b27)
|
||||
* @param {boolean} withCommit 是否带 commit
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateShortTimestamp(withCommit = false) {
|
||||
const now = new Date();
|
||||
const year = String(now.getFullYear()).slice(-2);
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(now.getDate()).padStart(2, "0");
|
||||
const hours = String(now.getHours()).padStart(2, "0");
|
||||
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||
return `${year}${month}${day}${hours}${minutes}`;
|
||||
if (withCommit) {
|
||||
const gitShort = getGitShortCommit();
|
||||
return `${month}${day}.${gitShort}`;
|
||||
}
|
||||
return `${month}${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,7 +89,9 @@ function generateShortTimestamp() {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidVersion(version) {
|
||||
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(version);
|
||||
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
|
||||
version,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,8 +109,8 @@ function normalizeVersion(version) {
|
||||
* @returns {string}
|
||||
*/
|
||||
function getBaseVersion(version) {
|
||||
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, '');
|
||||
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, '');
|
||||
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
|
||||
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
|
||||
return base;
|
||||
}
|
||||
|
||||
@@ -85,10 +125,21 @@ async function updatePackageVersion(newVersion) {
|
||||
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||
const packageJson = JSON.parse(data);
|
||||
|
||||
console.log("[INFO]: Current package.json version is: ", packageJson.version);
|
||||
packageJson.version = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion;
|
||||
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), "utf8");
|
||||
console.log(`[INFO]: package.json version updated to: ${packageJson.version}`);
|
||||
console.log(
|
||||
"[INFO]: Current package.json version is: ",
|
||||
packageJson.version,
|
||||
);
|
||||
packageJson.version = newVersion.startsWith("v")
|
||||
? newVersion.slice(1)
|
||||
: newVersion;
|
||||
await fs.writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(
|
||||
`[INFO]: package.json version updated to: ${packageJson.version}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error updating package.json version:", error);
|
||||
throw error;
|
||||
@@ -105,18 +156,22 @@ async function updateCargoVersion(newVersion) {
|
||||
try {
|
||||
const data = await fs.readFile(cargoTomlPath, "utf8");
|
||||
const lines = data.split("\n");
|
||||
const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion;
|
||||
const baseVersion = getBaseVersion(versionWithoutV);
|
||||
const versionWithoutV = newVersion.startsWith("v")
|
||||
? newVersion.slice(1)
|
||||
: newVersion;
|
||||
|
||||
const updatedLines = lines.map((line) => {
|
||||
if (line.trim().startsWith("version =")) {
|
||||
return line.replace(/version\s*=\s*"[^"]+"/, `version = "${baseVersion}"`);
|
||||
return line.replace(
|
||||
/version\s*=\s*"[^"]+"/,
|
||||
`version = "${versionWithoutV}"`,
|
||||
);
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
||||
console.log(`[INFO]: Cargo.toml version updated to: ${baseVersion}`);
|
||||
console.log(`[INFO]: Cargo.toml version updated to: ${versionWithoutV}`);
|
||||
} catch (error) {
|
||||
console.error("Error updating Cargo.toml version:", error);
|
||||
throw error;
|
||||
@@ -133,13 +188,26 @@ async function updateTauriConfigVersion(newVersion) {
|
||||
try {
|
||||
const data = await fs.readFile(tauriConfigPath, "utf8");
|
||||
const tauriConfig = JSON.parse(data);
|
||||
const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion;
|
||||
const baseVersion = getBaseVersion(versionWithoutV);
|
||||
const versionWithoutV = newVersion.startsWith("v")
|
||||
? newVersion.slice(1)
|
||||
: newVersion;
|
||||
|
||||
console.log("[INFO]: Current tauri.conf.json version is: ", tauriConfig.version);
|
||||
tauriConfig.version = baseVersion;
|
||||
await fs.writeFile(tauriConfigPath, JSON.stringify(tauriConfig, null, 2), "utf8");
|
||||
console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`);
|
||||
console.log(
|
||||
"[INFO]: Current tauri.conf.json version is: ",
|
||||
tauriConfig.version,
|
||||
);
|
||||
|
||||
// 使用完整版本信息,包含build metadata
|
||||
tauriConfig.version = versionWithoutV;
|
||||
|
||||
await fs.writeFile(
|
||||
tauriConfigPath,
|
||||
JSON.stringify(tauriConfig, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(
|
||||
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error updating tauri.conf.json version:", error);
|
||||
throw error;
|
||||
@@ -173,15 +241,29 @@ async function main(versionArg) {
|
||||
|
||||
try {
|
||||
let newVersion;
|
||||
const validTags = ["alpha", "beta", "rc", "autobuild"];
|
||||
const validTags = [
|
||||
"alpha",
|
||||
"beta",
|
||||
"rc",
|
||||
"autobuild",
|
||||
"autobuild-latest",
|
||||
"deploytest",
|
||||
];
|
||||
|
||||
if (validTags.includes(versionArg.toLowerCase())) {
|
||||
const currentVersion = await getCurrentVersion();
|
||||
const baseVersion = getBaseVersion(currentVersion);
|
||||
|
||||
if (versionArg.toLowerCase() === "autobuild") {
|
||||
const timestamp = generateShortTimestamp();
|
||||
newVersion = `${baseVersion}+autobuild.${timestamp}`;
|
||||
// 格式: 2.3.0+autobuild.250613.cc39b27
|
||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true)}`;
|
||||
} else if (versionArg.toLowerCase() === "autobuild-latest") {
|
||||
// 格式: 2.3.0+autobuild.0614.a1b2c3d (使用最新 Tauri 提交)
|
||||
const latestTauriCommit = getLatestTauriCommit();
|
||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`;
|
||||
} else if (versionArg.toLowerCase() === "deploytest") {
|
||||
// 格式: 2.3.0+deploytest.250613.cc39b27
|
||||
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true)}`;
|
||||
} else {
|
||||
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
||||
}
|
||||
@@ -210,4 +292,3 @@ program
|
||||
.argument("<version>", "version tag or full version")
|
||||
.action(main)
|
||||
.parse(process.argv);
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import axios from "axios";
|
||||
import { readFileSync } from "fs";
|
||||
import { log_success, log_error, log_info } from "./utils.mjs";
|
||||
|
||||
const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道
|
||||
const CHAT_ID_TEST = "@vergetest"; // 测试频道
|
||||
|
||||
async function sendTelegramNotification() {
|
||||
if (!process.env.TELEGRAM_BOT_TOKEN) {
|
||||
throw new Error("TELEGRAM_BOT_TOKEN is required");
|
||||
}
|
||||
|
||||
const version =
|
||||
process.env.VERSION ||
|
||||
(() => {
|
||||
const pkg = readFileSync("package.json", "utf-8");
|
||||
return JSON.parse(pkg).version;
|
||||
})();
|
||||
|
||||
const downloadUrl =
|
||||
process.env.DOWNLOAD_URL ||
|
||||
`https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v${version}`;
|
||||
|
||||
const isAutobuild =
|
||||
process.env.BUILD_TYPE === "autobuild" || version.includes("autobuild");
|
||||
const chatId = isAutobuild ? CHAT_ID_TEST : CHAT_ID_RELEASE;
|
||||
const buildType = isAutobuild ? "滚动更新版" : "正式版";
|
||||
|
||||
log_info(`Preparing Telegram notification for ${buildType} ${version}`);
|
||||
log_info(`Target channel: ${chatId}`);
|
||||
log_info(`Download URL: ${downloadUrl}`);
|
||||
|
||||
// 读取发布说明和下载地址
|
||||
let releaseContent = "";
|
||||
try {
|
||||
releaseContent = readFileSync("release.txt", "utf-8");
|
||||
log_info("成功读取 release.txt 文件");
|
||||
} catch (error) {
|
||||
log_error("无法读取 release.txt,使用默认发布说明");
|
||||
releaseContent = "更多新功能现已支持,详细更新日志请查看发布页面。";
|
||||
}
|
||||
|
||||
// Markdown 转换为 HTML
|
||||
function convertMarkdownToTelegramHTML(content) {
|
||||
return content
|
||||
.split("\n")
|
||||
.map((line) => {
|
||||
if (line.trim().length === 0) {
|
||||
return "";
|
||||
} else if (line.startsWith("## ")) {
|
||||
return `<b>${line.replace("## ", "")}</b>`;
|
||||
} else if (line.startsWith("### ")) {
|
||||
return `<b>${line.replace("### ", "")}</b>`;
|
||||
} else if (line.startsWith("#### ")) {
|
||||
return `<b>${line.replace("#### ", "")}</b>`;
|
||||
} else {
|
||||
let processedLine = line.replace(
|
||||
/\[([^\]]+)\]\(([^)]+)\)/g,
|
||||
'<a href="$2">$1</a>',
|
||||
);
|
||||
processedLine = processedLine.replace(
|
||||
/\*\*([^*]+)\*\*/g,
|
||||
"<b>$1</b>",
|
||||
);
|
||||
return processedLine;
|
||||
}
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
const formattedContent = convertMarkdownToTelegramHTML(releaseContent);
|
||||
|
||||
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
|
||||
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${version}">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
|
||||
|
||||
// 发送到 Telegram
|
||||
try {
|
||||
await axios.post(
|
||||
`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
text: content,
|
||||
link_preview_options: {
|
||||
is_disabled: false,
|
||||
url: `https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${version}`,
|
||||
prefer_large_media: true,
|
||||
},
|
||||
parse_mode: "HTML",
|
||||
},
|
||||
);
|
||||
log_success(`✅ Telegram 通知发送成功到 ${chatId}`);
|
||||
} catch (error) {
|
||||
log_error(
|
||||
`❌ Telegram 通知发送失败到 ${chatId}:`,
|
||||
error.response?.data || error.message,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行函数
|
||||
sendTelegramNotification().catch((error) => {
|
||||
log_error("脚本执行失败:", error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Generated
+1755
-1665
File diff suppressed because it is too large
Load Diff
+92
-46
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
||||
default-run = "clash-verge"
|
||||
@@ -13,79 +13,77 @@ build = "build.rs"
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.2.0", features = [] }
|
||||
tauri-build = { version = "2.4.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
warp = "0.3"
|
||||
anyhow = "1.0.98"
|
||||
warp = { version = "0.4.2", features = ["server"] }
|
||||
anyhow = "1.0.99"
|
||||
dirs = "6.0"
|
||||
open = "5.3"
|
||||
log = "0.4"
|
||||
dunce = "1.0"
|
||||
log4rs = "1"
|
||||
open = "5.3.2"
|
||||
log = "0.4.27"
|
||||
dunce = "1.0.5"
|
||||
log4rs = "1.3.0"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4.41"
|
||||
sysinfo = "0.35.0"
|
||||
sysinfo = { version = "=0.35.2", features = ["network", "system"] }
|
||||
boa_engine = "0.20.0"
|
||||
serde_json = "1.0.140"
|
||||
serde_json = "1.0.143"
|
||||
serde_yaml = "0.9.34"
|
||||
once_cell = "1.21.3"
|
||||
lazy_static = "1.5.0"
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.6"
|
||||
parking_lot = "0.12.3"
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3.1"
|
||||
tokio = { version = "1.44.2", features = [
|
||||
tokio = { version = "1.47.1", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
"sync",
|
||||
] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
reqwest = { version = "0.12.15", features = ["json", "rustls-tls", "cookies"] }
|
||||
reqwest = { version = "0.12.23", features = ["json", "rustls-tls", "cookies"] }
|
||||
regex = "1.11.1"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
||||
image = "0.25.6"
|
||||
imageproc = "0.25.0"
|
||||
tauri = { version = "2.5.1", features = [
|
||||
tauri = { version = "2.8.2", features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tray-icon",
|
||||
"image-ico",
|
||||
"image-png",
|
||||
] }
|
||||
network-interface = { version = "2.0.1", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
tauri-plugin-dialog = "2.2.1"
|
||||
tauri-plugin-fs = "2.2.1"
|
||||
tauri-plugin-process = "2.2.1"
|
||||
tauri-plugin-clipboard-manager = "2.2.2"
|
||||
tauri-plugin-deep-link = "2.2.1"
|
||||
tauri-plugin-devtools = "2.0.0"
|
||||
tauri-plugin-window-state = "2.2.2"
|
||||
zip = "2.6.1"
|
||||
network-interface = { version = "2.0.3", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.3.0"
|
||||
tauri-plugin-dialog = "2.3.3"
|
||||
tauri-plugin-fs = "2.4.2"
|
||||
tauri-plugin-process = "2.3.0"
|
||||
tauri-plugin-clipboard-manager = "2.3.0"
|
||||
tauri-plugin-deep-link = "2.4.1"
|
||||
tauri-plugin-devtools = "2.0.1"
|
||||
tauri-plugin-window-state = "2.4.0"
|
||||
zip = "=4.2.0"
|
||||
reqwest_dav = "0.2.1"
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
base64 = "0.22.1"
|
||||
getrandom = "0.3.2"
|
||||
tokio-tungstenite = "0.26.2"
|
||||
getrandom = "0.3.3"
|
||||
futures = "0.3.31"
|
||||
sys-locale = "0.3.2"
|
||||
async-trait = "0.1.88"
|
||||
mihomo_api = { path = "src_crates/crate_mihomo_api" }
|
||||
ab_glyph = "0.2.29"
|
||||
tungstenite = "0.26.2"
|
||||
libc = "0.2.172"
|
||||
libc = "0.2.175"
|
||||
gethostname = "1.0.2"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.9"
|
||||
hex = "0.4.3"
|
||||
scopeguard = "1.2.0"
|
||||
kode-bridge = "0.2.1-rc1"
|
||||
dashmap = "6.1.0"
|
||||
tauri-plugin-notification = "2.3.0"
|
||||
console-subscriber = { version = "0.4.1", optional = true }
|
||||
tokio-stream = "0.1.17"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
deelevate = "0.2.0"
|
||||
winreg = "0.55.0"
|
||||
winapi = { version = "0.3", features = [
|
||||
winapi = { version = "0.3.9", features = [
|
||||
"winbase",
|
||||
"fileapi",
|
||||
"winnt",
|
||||
@@ -93,20 +91,25 @@ winapi = { version = "0.3", features = [
|
||||
"errhandlingapi",
|
||||
"minwindef",
|
||||
"winerror",
|
||||
"tlhelp32",
|
||||
"processthreadsapi",
|
||||
"winhttp",
|
||||
"winreg",
|
||||
] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
users = "0.11.0"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-global-shortcut = "2.2.0"
|
||||
tauri-plugin-updater = "2.7.1"
|
||||
tauri-plugin-autostart = "2.5.0"
|
||||
tauri-plugin-global-shortcut = "2.3.0"
|
||||
tauri-plugin-updater = "2.9.0"
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
verge-dev = []
|
||||
tokio-trace = ["console-subscriber"]
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
@@ -136,12 +139,55 @@ name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.19.1"
|
||||
criterion = "0.7.0"
|
||||
|
||||
[workspace]
|
||||
members = ["src_crates/crate_mihomo_api"]
|
||||
[lints.clippy]
|
||||
# Core categories - most important for code safety and correctness
|
||||
correctness = { level = "deny", priority = -1 }
|
||||
suspicious = { level = "deny", priority = -1 }
|
||||
|
||||
# [patch.crates-io]
|
||||
# bitflags = { git = "https://github.com/bitflags/bitflags", rev = "2.9.0" }
|
||||
# zerocopy = { git = "https://github.com/google/zerocopy", rev = "v0.8.24" }
|
||||
# tungstenite = { git = "https://github.com/snapview/tungstenite-rs", rev = "v0.26.2" }
|
||||
# Critical safety lints - warn for now due to extensive existing usage
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
|
||||
# Development quality lints
|
||||
todo = "warn"
|
||||
dbg_macro = "warn"
|
||||
#print_stdout = "warn"
|
||||
#print_stderr = "warn"
|
||||
|
||||
# Performance lints for proxy application
|
||||
clone_on_ref_ptr = "warn"
|
||||
rc_clone_in_vec_init = "warn"
|
||||
large_stack_arrays = "warn"
|
||||
large_const_arrays = "warn"
|
||||
|
||||
# Security lints
|
||||
#integer_division = "warn"
|
||||
#lossy_float_literal = "warn"
|
||||
#default_numeric_fallback = "warn"
|
||||
|
||||
# Mutex and async lints - strict control
|
||||
async_yields_async = "deny" # Prevents missing await in async blocks
|
||||
mutex_atomic = "deny" # Use atomics instead of Mutex<bool/int>
|
||||
mutex_integer = "deny" # Use AtomicInt instead of Mutex<int>
|
||||
rc_mutex = "deny" # Single-threaded Rc with Mutex is wrong
|
||||
unused_async = "deny" # Too many false positives in Tauri/framework code
|
||||
await_holding_lock = "deny"
|
||||
large_futures = "deny"
|
||||
future_not_send = "deny"
|
||||
|
||||
# Common style improvements
|
||||
redundant_else = "deny" # Too many in existing code
|
||||
needless_continue = "deny" # Too many in existing code
|
||||
needless_raw_string_hashes = "deny" # Too many in existing code
|
||||
|
||||
# Disable noisy categories for existing codebase but keep them available
|
||||
#style = { level = "allow", priority = -1 }
|
||||
#complexity = { level = "allow", priority = -1 }
|
||||
#perf = { level = "allow", priority = -1 }
|
||||
#pedantic = { level = "allow", priority = -1 }
|
||||
#nursery = { level = "allow", priority = -1 }
|
||||
#restriction = { level = "allow", priority = -1 }
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use std::hint::black_box;
|
||||
|
||||
// 业务模型 & Draft
|
||||
use app_lib::config::Draft as DraftNew;
|
||||
use app_lib::config::IVerge;
|
||||
|
||||
// fn bench_apply_old(c: &mut Criterion) {
|
||||
// c.bench_function("apply_draft_old", |b| {
|
||||
// b.iter(|| {
|
||||
// let verge = Box::new(IVerge {
|
||||
// enable_auto_launch: Some(true),
|
||||
// enable_tun_mode: Some(false),
|
||||
// ..Default::default()
|
||||
// });
|
||||
|
||||
// let draft = DraftOld::from(black_box(verge));
|
||||
|
||||
// {
|
||||
// let mut d = draft.draft_mut();
|
||||
// d.enable_auto_launch = Some(false);
|
||||
// }
|
||||
|
||||
// let _ = draft.apply();
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// fn bench_discard_old(c: &mut Criterion) {
|
||||
// c.bench_function("discard_draft_old", |b| {
|
||||
// b.iter(|| {
|
||||
// let verge = Box::new(IVerge::default());
|
||||
// let draft = DraftOld::from(black_box(verge));
|
||||
|
||||
// {
|
||||
// let mut d = draft.draft_mut();
|
||||
// d.enable_auto_launch = Some(false);
|
||||
// }
|
||||
|
||||
// let _ = draft.discard();
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
/// 基准:修改草稿并 apply()
|
||||
fn bench_apply_new(c: &mut Criterion) {
|
||||
c.bench_function("apply_draft_new", |b| {
|
||||
b.iter(|| {
|
||||
let verge = Box::new(IVerge {
|
||||
enable_auto_launch: Some(true),
|
||||
enable_tun_mode: Some(false),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let draft = DraftNew::from(black_box(verge));
|
||||
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(false);
|
||||
}
|
||||
|
||||
let _ = draft.apply();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 基准:修改草稿并 discard()
|
||||
fn bench_discard_new(c: &mut Criterion) {
|
||||
c.bench_function("discard_draft_new", |b| {
|
||||
b.iter(|| {
|
||||
let verge = Box::new(IVerge::default());
|
||||
let draft = DraftNew::from(black_box(verge));
|
||||
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(false);
|
||||
}
|
||||
|
||||
let _ = draft.discard();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
// bench_apply_old,
|
||||
// bench_discard_old,
|
||||
bench_apply_new,
|
||||
bench_discard_new
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"identifier": "desktop-windows-capability",
|
||||
"description": "permissions for desktop windows applications",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:webview:allow-create-webview",
|
||||
"core:webview:allow-create-webview-window"
|
||||
]
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled",
|
||||
"core:window:allow-set-theme"
|
||||
"core:window:allow-set-theme",
|
||||
"notification:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
# This template contains all of the possible sections and their default values
|
||||
|
||||
# Note that all fields that take a lint level have these possible values:
|
||||
# * deny - An error will be produced and the check will fail
|
||||
# * warn - A warning will be produced, but the check will not fail
|
||||
# * allow - No warning or error will be produced, though in some cases a note
|
||||
# will be
|
||||
|
||||
# The values provided in this template are the default values that will be used
|
||||
# when any section or field is not specified in your own configuration
|
||||
|
||||
# Root options
|
||||
|
||||
# The graph table configures how the dependency graph is constructed and thus
|
||||
# which crates the checks are performed against
|
||||
[graph]
|
||||
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||
# only the specified targets will be checked when running `cargo deny check`.
|
||||
# This means, if a particular package is only ever used as a target specific
|
||||
# dependency, such as, for example, the `nix` crate only being used via the
|
||||
# `target_family = "unix"` configuration, that only having windows targets in
|
||||
# this list would mean the nix crate, as well as any of its exclusive
|
||||
# dependencies not shared by any other crates, would be ignored, as the target
|
||||
# list here is effectively saying which targets you are building for.
|
||||
targets = [
|
||||
# The triple can be any string, but only the target triples built in to
|
||||
# rustc (as of 1.40) can be checked against actual config expressions
|
||||
#"x86_64-unknown-linux-musl",
|
||||
# You can also specify which target_features you promise are enabled for a
|
||||
# particular target. target_features are currently not validated against
|
||||
# the actual valid features supported by the target architecture.
|
||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||
]
|
||||
# When creating the dependency graph used as the source of truth when checks are
|
||||
# executed, this field can be used to prune crates from the graph, removing them
|
||||
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
|
||||
# is pruned from the graph, all of its dependencies will also be pruned unless
|
||||
# they are connected to another crate in the graph that hasn't been pruned,
|
||||
# so it should be used with care. The identifiers are [Package ID Specifications]
|
||||
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
|
||||
#exclude = []
|
||||
# If true, metadata will be collected with `--all-features`. Note that this can't
|
||||
# be toggled off if true, if you want to conditionally enable `--all-features` it
|
||||
# is recommended to pass `--all-features` on the cmd line instead
|
||||
all-features = false
|
||||
# If true, metadata will be collected with `--no-default-features`. The same
|
||||
# caveat with `all-features` applies
|
||||
no-default-features = false
|
||||
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||
# is specified on the cmd line they will take precedence over this option.
|
||||
#features = []
|
||||
|
||||
# The output table provides options for how/if diagnostics are outputted
|
||||
[output]
|
||||
# When outputting inclusion graphs in diagnostics that include features, this
|
||||
# option can be used to specify the depth at which feature edges will be added.
|
||||
# This option is included since the graphs can be quite large and the addition
|
||||
# of features from the crate(s) to all of the graph roots can be far too verbose.
|
||||
# This option can be overridden via `--feature-depth` on the cmd line
|
||||
feature-depth = 1
|
||||
|
||||
# This section is considered when running `cargo deny check advisories`
|
||||
# More documentation for the advisories section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||
[advisories]
|
||||
# The path where the advisory databases are cloned/fetched into
|
||||
#db-path = "$CARGO_HOME/advisory-dbs"
|
||||
# The url(s) of the advisory databases to use
|
||||
#db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
#"RUSTSEC-0000-0000",
|
||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
"RUSTSEC-2024-0415",
|
||||
]
|
||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||
# If this is false, then it uses a built-in git library.
|
||||
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||
# See Git Authentication for more information about setting up git authentication.
|
||||
#git-fetch-with-cli = true
|
||||
|
||||
# This section is considered when running `cargo deny check licenses`
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
allow = [
|
||||
#"MIT",
|
||||
#"Apache-2.0",
|
||||
#"Apache-2.0 WITH LLVM-exception",
|
||||
]
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
# [possible values: any between 0.0 and 1.0].
|
||||
confidence-threshold = 0.85
|
||||
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||
# aren't accepted for every possible crate as with the normal allow list
|
||||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], crate = "adler32" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
#[[licenses.clarify]]
|
||||
# The package spec the clarification applies to
|
||||
#crate = "ring"
|
||||
# The SPDX expression for the license requirements of the crate
|
||||
#expression = "MIT AND ISC AND OpenSSL"
|
||||
# One or more files in the crate's source used as the "source of truth" for
|
||||
# the license expression. If the contents match, the clarification will be used
|
||||
# when running the license check, otherwise the clarification will be ignored
|
||||
# and the crate will be checked normally, which may produce warnings or errors
|
||||
# depending on the rest of your configuration
|
||||
#license-files = [
|
||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
#]
|
||||
|
||||
[licenses.private]
|
||||
# If true, ignores workspace crates that aren't published, or are only
|
||||
# published to private registries.
|
||||
# To see how to mark a crate as unpublished (to the official registry),
|
||||
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
||||
ignore = false
|
||||
# One or more private registries that you might publish crates to, if a crate
|
||||
# is only published to private registries, and ignore is true, the crate will
|
||||
# not have its license(s) checked
|
||||
registries = [
|
||||
#"https://sekretz.com/registry
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check bans`.
|
||||
# More documentation about the 'bans' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||
[bans]
|
||||
# Lint level for when multiple versions of the same crate are detected
|
||||
multiple-versions = "warn"
|
||||
# Lint level for when a crate version requirement is `*`
|
||||
wildcards = "allow"
|
||||
# The graph highlighting used when creating dotgraphs for crates
|
||||
# with multiple versions
|
||||
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# The default lint level for `default` features for crates that are members of
|
||||
# the workspace that is being checked. This can be overridden by allowing/denying
|
||||
# `default` on a crate-by-crate basis if desired.
|
||||
workspace-default-features = "allow"
|
||||
# The default lint level for `default` features for external crates that are not
|
||||
# members of the workspace. This can be overridden by allowing/denying `default`
|
||||
# on a crate-by-crate basis if desired.
|
||||
external-default-features = "allow"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||
]
|
||||
|
||||
# List of features to allow/deny
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#[[bans.features]]
|
||||
#crate = "reqwest"
|
||||
# Features to not allow
|
||||
#deny = ["json"]
|
||||
# Features to allow
|
||||
#allow = [
|
||||
# "rustls",
|
||||
# "__rustls",
|
||||
# "__tls",
|
||||
# "hyper-rustls",
|
||||
# "rustls",
|
||||
# "rustls-pemfile",
|
||||
# "rustls-tls-webpki-roots",
|
||||
# "tokio-rustls",
|
||||
# "webpki-roots",
|
||||
#]
|
||||
# If true, the allowed features must exactly match the enabled feature set. If
|
||||
# this is set there is no point setting `deny`
|
||||
#exact = true
|
||||
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite.
|
||||
skip-tree = [
|
||||
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
# More documentation about the 'sources' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||
[sources]
|
||||
# Lint level for what to happen when a crate from a crate registry that is not
|
||||
# in the allow list is encountered
|
||||
unknown-registry = "warn"
|
||||
# Lint level for what to happen when a crate from a git repository that is not
|
||||
# in the allow list is encountered
|
||||
unknown-git = "warn"
|
||||
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
||||
# if not specified. If it is specified but empty, no registries are allowed.
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
# List of URLs for allowed Git repositories
|
||||
allow-git = []
|
||||
|
||||
[sources.allow-org]
|
||||
# github.com organizations to allow git sources for
|
||||
github = []
|
||||
# gitlab.com organizations to allow git sources for
|
||||
gitlab = []
|
||||
# bitbucket.org organizations to allow git sources for
|
||||
bitbucket = []
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
@@ -523,7 +523,7 @@ FunctionEnd
|
||||
${If} $0 == 0
|
||||
Push $0
|
||||
${If} $1 == 0
|
||||
DetailPrint "Restart Clash Verge Service..."
|
||||
DetailPrint "Restart ${PRODUCTNAME} Service..."
|
||||
SimpleSC::StartService "clash_verge_service" "" 30
|
||||
${EndIf}
|
||||
${ElseIf} $0 != 0
|
||||
@@ -549,20 +549,20 @@ FunctionEnd
|
||||
${If} $0 == 0
|
||||
Push $0
|
||||
${If} $1 == 1
|
||||
DetailPrint "Stop Clash Verge Service..."
|
||||
DetailPrint "Stop ${PRODUCTNAME} Service..."
|
||||
SimpleSC::StopService "clash_verge_service" 1 30
|
||||
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
|
||||
${If} $0 == 0
|
||||
DetailPrint "Removing Clash Verge Service..."
|
||||
DetailPrint "Removing ${PRODUCTNAME} Service..."
|
||||
SimpleSC::RemoveService "clash_verge_service"
|
||||
${ElseIf} $0 != 0
|
||||
Push $0
|
||||
SimpleSC::GetErrorMessage
|
||||
Pop $0
|
||||
MessageBox MB_OK|MB_ICONSTOP "Clash Verge Service Stop Error ($0)"
|
||||
MessageBox MB_OK|MB_ICONSTOP "${PRODUCTNAME} Service Stop Error ($0)"
|
||||
${EndIf}
|
||||
${ElseIf} $1 == 0
|
||||
DetailPrint "Removing Clash Verge Service..."
|
||||
DetailPrint "Removing ${PRODUCTNAME} Service..."
|
||||
SimpleSC::RemoveService "clash_verge_service"
|
||||
${EndIf}
|
||||
${ElseIf} $0 != 0
|
||||
@@ -661,7 +661,7 @@ SectionEnd
|
||||
Pop $R0
|
||||
${If} $R0 = 0
|
||||
IfSilent kill 0
|
||||
${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "$(appRunningOkKill)" IDOK kill IDCANCEL cancel ${|}
|
||||
${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "${PRODUCTNAME} 正在运行 $\n点击确定以终止运行" IDOK kill IDCANCEL cancel ${|}
|
||||
kill:
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe"
|
||||
@@ -691,8 +691,6 @@ SectionEnd
|
||||
app_check_done:
|
||||
!macroend
|
||||
|
||||
|
||||
|
||||
Var VC_REDIST_URL
|
||||
Var VC_REDIST_EXE
|
||||
|
||||
@@ -701,25 +699,25 @@ Section CheckAndInstallVSRuntime
|
||||
${If} ${IsNativeARM64}
|
||||
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.arm64.exe"
|
||||
StrCpy $VC_REDIST_EXE "vc_redist.arm64.exe"
|
||||
|
||||
|
||||
; 检查关键DLL
|
||||
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
|
||||
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
|
||||
|
||||
|
||||
${ElseIf} ${RunningX64}
|
||||
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x64.exe"
|
||||
StrCpy $VC_REDIST_EXE "vc_redist.x64.exe"
|
||||
|
||||
|
||||
; 检查关键DLL
|
||||
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
|
||||
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
|
||||
|
||||
|
||||
${Else}
|
||||
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x86.exe"
|
||||
StrCpy $VC_REDIST_EXE "vc_redist.x86.exe"
|
||||
|
||||
|
||||
; 检查关键DLL
|
||||
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
|
||||
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
|
||||
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
|
||||
${EndIf}
|
||||
|
||||
@@ -754,30 +752,44 @@ Section CheckAndInstallVSRuntime
|
||||
${Else}
|
||||
DetailPrint "Visual C++ Redistributable 下载失败"
|
||||
${EndIf}
|
||||
|
||||
|
||||
Done:
|
||||
SectionEnd
|
||||
|
||||
|
||||
|
||||
Section Install
|
||||
SetOutPath $INSTDIR
|
||||
nsExec::Exec 'netsh int tcp res'
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
|
||||
; 修复系统级启动文件夹
|
||||
CreateDirectory "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup"
|
||||
DetailPrint "系统级启动文件夹已确保存在"
|
||||
|
||||
; 修复用户级启动文件夹
|
||||
SetShellVarContext current
|
||||
StrCpy $0 "$SMPROGRAMS\Startup"
|
||||
CreateDirectory "$0"
|
||||
DetailPrint "已确保用户级启动文件夹存在: $0"
|
||||
|
||||
; 删除 window-state.json 文件 .window-state.json 文件
|
||||
DetailPrint "开始删除 window-state.json or .window-state.json"
|
||||
SetShellVarContext current
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
|
||||
|
||||
; 清理自启动注册表项
|
||||
DetailPrint "Cleaning auto-launch registry entries..."
|
||||
|
||||
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
|
||||
|
||||
SetRegView 64
|
||||
|
||||
SetRegView 64
|
||||
; 清理旧版本的注册表项 (Clash Verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "Clash Verge"
|
||||
${EndIf}
|
||||
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "Clash Verge"
|
||||
@@ -788,7 +800,7 @@ Section Install
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "clash-verge"
|
||||
${EndIf}
|
||||
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "clash-verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "clash-verge"
|
||||
@@ -798,7 +810,7 @@ Section Install
|
||||
; Delete clash-verge.desktop
|
||||
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
|
||||
Delete "$INSTDIR\Clash Verge.exe"
|
||||
|
||||
|
||||
; Copy main executable
|
||||
File "${MAINBINARYSRCPATH}"
|
||||
|
||||
@@ -916,26 +928,29 @@ FunctionEnd
|
||||
!macroend
|
||||
|
||||
Section Uninstall
|
||||
;删除 .window-state.json 文件
|
||||
SetShellVarContext current
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
|
||||
|
||||
!insertmacro CheckIfAppIsRunning
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
!insertmacro RemoveVergeService
|
||||
|
||||
; 删除 window-state.json 文件 .window-state.json 文件
|
||||
DetailPrint "开始删除 window-state.json or .window-state.json"
|
||||
SetShellVarContext current
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
|
||||
|
||||
; 清理自启动注册表项
|
||||
DetailPrint "Cleaning auto-launch registry entries..."
|
||||
|
||||
|
||||
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
|
||||
|
||||
|
||||
SetRegView 64
|
||||
; 清理旧版本的注册表项 (Clash Verge)
|
||||
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "Clash Verge"
|
||||
${EndIf}
|
||||
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "Clash Verge"
|
||||
@@ -946,7 +961,7 @@ Section Uninstall
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKCU "$R1" "clash-verge"
|
||||
${EndIf}
|
||||
|
||||
|
||||
ReadRegStr $R2 HKLM "$R1" "clash-verge"
|
||||
${If} $R2 != ""
|
||||
DeleteRegValue HKLM "$R1" "clash-verge"
|
||||
@@ -978,24 +993,146 @@ Section Uninstall
|
||||
{{/each}}
|
||||
RMDir "$INSTDIR"
|
||||
|
||||
; 删除固定栏
|
||||
!insertmacro DeleteAppUserModelId
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
||||
!insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||
; 兼容旧名称快捷方式
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
|
||||
!insertmacro UnpinShortcut "$DESKTOP\clash-verge.lnk"
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
|
||||
!insertmacro UnpinShortcut "$DESKTOP\${MAINBINARYNAME}.lnk"
|
||||
|
||||
; Remove start menu shortcut
|
||||
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
|
||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
||||
; 兼容旧名称快捷方式
|
||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
|
||||
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
|
||||
; 删除所有用户的桌面快捷方式
|
||||
DetailPrint "开始删除所有用户桌面的 ${PRODUCTNAME} 快捷方式..."
|
||||
|
||||
; 删除公共桌面快捷方式
|
||||
Delete "C:\Users\Public\Desktop\Clash Verge.lnk"
|
||||
Delete "C:\Users\Public\Desktop\clash-verge.lnk"
|
||||
|
||||
; 枚举所有用户配置文件目录
|
||||
SetRegView 64
|
||||
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" ""
|
||||
|
||||
; 初始化循环
|
||||
StrCpy $R1 0
|
||||
Loop:
|
||||
EnumRegKey $R2 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" $R1
|
||||
${If} $R2 == ""
|
||||
Goto Done
|
||||
${EndIf}
|
||||
|
||||
; 读取用户配置文件路径
|
||||
ReadRegStr $R3 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$R2" "ProfileImagePath"
|
||||
${If} $R3 != ""
|
||||
; 构建用户桌面路径
|
||||
StrCpy $R4 "$R3\Desktop"
|
||||
|
||||
; 删除该用户桌面的快捷方式
|
||||
Delete "$R4\Clash Verge.lnk"
|
||||
Delete "$R4\clash-verge.lnk"
|
||||
|
||||
DetailPrint "尝试删除用户 '$R3' 桌面的 ${PRODUCTNAME} 快捷方式"
|
||||
${EndIf}
|
||||
|
||||
; 递增循环计数器
|
||||
IntOp $R1 $R1 + 1
|
||||
Goto Loop
|
||||
Done:
|
||||
|
||||
DetailPrint "所有用户桌面快捷方式删除完成"
|
||||
|
||||
; 删除用户级开始菜单中的应用程序文件夹和快捷方式
|
||||
DetailPrint "删除用户级开始菜单中的应用程序文件夹和快捷方式..."
|
||||
RMDir /r /REBOOTOK "$SMPROGRAMS\Clash Verge"
|
||||
RMDir /r /REBOOTOK "$SMPROGRAMS\clash-verge"
|
||||
DetailPrint "删除用户级开始菜单中的应用程序文件夹和快捷方式完成"
|
||||
|
||||
; 删除系统级开始菜单中的应用程序文件夹和快捷方式
|
||||
DetailPrint "删除系统级开始菜单中的应用程序文件夹和快捷方式..."
|
||||
RMDir /r /REBOOTOK "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Clash Verge"
|
||||
RMDir /r /REBOOTOK "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\clash-verge"
|
||||
DetailPrint "删除系统级开始菜单中的应用程序文件夹和快捷方式完成"
|
||||
|
||||
; 删除所有带 Clash Verge 或 clash-verge 的注册表项
|
||||
DetailPrint "开始清理所有 ${PRODUCTNAME} 相关的注册表项..."
|
||||
|
||||
; 设置注册表查看模式 (64位)
|
||||
SetRegView 64
|
||||
|
||||
; 清理 CurrentVersion\Run 中的自启动项
|
||||
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
|
||||
DeleteRegValue HKCU "$R1" "Clash Verge"
|
||||
DeleteRegValue HKCU "$R1" "clash-verge"
|
||||
DeleteRegValue HKLM "$R1" "Clash Verge"
|
||||
DeleteRegValue HKLM "$R1" "clash-verge"
|
||||
|
||||
; 清理 App Paths
|
||||
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Clash Verge.exe"
|
||||
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\clash-verge.exe"
|
||||
|
||||
; 删除指定的注册表路径
|
||||
DeleteRegKey HKLM "Software\Clash Verge Rev"
|
||||
DeleteRegKey HKCU "Software\Clash Verge Rev"
|
||||
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\ClashVerge"
|
||||
DeleteRegKey HKCU "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Clash Verge"
|
||||
|
||||
; 清理 Uninstall 信息
|
||||
StrCpy $R1 0
|
||||
EnumUninstallLoop:
|
||||
EnumRegKey $R2 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $R1
|
||||
${If} $R2 == ""
|
||||
Goto EnumUninstallDone
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R3 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$R2" "DisplayName"
|
||||
${If} $R3 != ""
|
||||
StrCmp $R3 "Clash Verge" 0 +3
|
||||
StrCmp $R3 "clash-verge" 0 +2
|
||||
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$R2"
|
||||
${EndIf}
|
||||
|
||||
IntOp $R1 $R1 + 1
|
||||
Goto EnumUninstallLoop
|
||||
EnumUninstallDone:
|
||||
|
||||
; 清理用户特定的注册表项
|
||||
StrCpy $R1 0
|
||||
EnumHKCULoop:
|
||||
EnumRegKey $R2 HKCU "SOFTWARE" $R1
|
||||
${If} $R2 == ""
|
||||
Goto EnumHKCUDone
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R3 HKCU "SOFTWARE\$R2" ""
|
||||
${If} $R3 != ""
|
||||
StrCmp $R3 "Clash Verge" 0 +3
|
||||
StrCmp $R3 "clash-verge" 0 +2
|
||||
DeleteRegKey HKCU "SOFTWARE\$R2"
|
||||
${EndIf}
|
||||
|
||||
IntOp $R1 $R1 + 1
|
||||
Goto EnumHKCULoop
|
||||
EnumHKCUDone:
|
||||
|
||||
; 清理系统范围的注册表项
|
||||
StrCpy $R1 0
|
||||
EnumHKLMLoop:
|
||||
EnumRegKey $R2 HKLM "SOFTWARE" $R1
|
||||
${If} $R2 == ""
|
||||
Goto EnumHKLMDone
|
||||
${EndIf}
|
||||
|
||||
ReadRegStr $R3 HKLM "SOFTWARE\$R2" ""
|
||||
${If} $R3 != ""
|
||||
StrCmp $R3 "Clash Verge" 0 +3
|
||||
StrCmp $R3 "clash-verge" 0 +2
|
||||
DeleteRegKey HKLM "SOFTWARE\$R2"
|
||||
${EndIf}
|
||||
|
||||
IntOp $R1 $R1 + 1
|
||||
Goto EnumHKLMLoop
|
||||
EnumHKLMDone:
|
||||
|
||||
DetailPrint "注册表清理完成"
|
||||
|
||||
; Remove desktop shortcuts
|
||||
Delete "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||
; 兼容旧名称快捷方式
|
||||
Delete "$DESKTOP\clash-verge.lnk"
|
||||
|
||||
; Remove registry information for add/remove programs
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
@@ -1015,10 +1152,6 @@ Section Uninstall
|
||||
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
|
||||
${EndIf}
|
||||
|
||||
;删除 .window-state.json 文件
|
||||
SetShellVarContext current
|
||||
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\.window-state.json"
|
||||
|
||||
${GetOptions} $CMDLINE "/P" $R0
|
||||
IfErrors +2 0
|
||||
SetAutoClose true
|
||||
@@ -1051,7 +1184,6 @@ FunctionEnd
|
||||
System::Free $4
|
||||
System::Free $5
|
||||
${IPropertyStore::Commit} $2 ""
|
||||
${IUnknown::Release} $2 ""
|
||||
${IPersistFile::Save} $1 '("${shortcut}",1)'
|
||||
${EndIf}
|
||||
${IUnknown::Release} $1 ""
|
||||
|
||||
@@ -11,4 +11,3 @@ merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
||||
imports_granularity = "Crate"
|
||||
|
||||
@@ -121,9 +121,8 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
||||
Err(_) => {
|
||||
if icon_path.exists() {
|
||||
return Ok(icon_path.to_string_lossy().to_string());
|
||||
} else {
|
||||
return Err("Failed to create temporary file".into());
|
||||
}
|
||||
return Err("Failed to create temporary file".into());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,7 +146,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
||||
Ok(icon_path.to_string_lossy().to_string())
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
Err(format!("下载的内容不是有效图片: {}", url))
|
||||
Err(format!("下载的内容不是有效图片: {url}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +216,7 @@ pub fn notify_ui_ready() -> CmdResult<()> {
|
||||
/// UI加载阶段
|
||||
#[tauri::command]
|
||||
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
log::info!(target: "app", "UI加载阶段更新: {}", stage);
|
||||
log::info!(target: "app", "UI加载阶段更新: {stage}");
|
||||
|
||||
use crate::utils::resolve::UiReadyStage;
|
||||
|
||||
@@ -228,8 +227,8 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
|
||||
"Ready" => UiReadyStage::Ready,
|
||||
_ => {
|
||||
log::warn!(target: "app", "未知的UI加载阶段: {}", stage);
|
||||
return Err(format!("未知的UI加载阶段: {}", stage));
|
||||
log::warn!(target: "app", "未知的UI加载阶段: {stage}");
|
||||
return Err(format!("未知的UI加载阶段: {stage}"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+389
-29
@@ -1,8 +1,19 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
config::*, core::*, feat, module::mihomo::MihomoManager, process::AsyncHandler, wrap_err,
|
||||
config::*,
|
||||
core::*,
|
||||
feat,
|
||||
ipc::{self, IpcManager},
|
||||
logging,
|
||||
process::AsyncHandler,
|
||||
state::proxy::ProxyRequestCache,
|
||||
utils::logging::Type,
|
||||
wrap_err,
|
||||
};
|
||||
use serde_yaml::Mapping;
|
||||
use std::time::Duration;
|
||||
|
||||
const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
/// 复制Clash环境变量
|
||||
#[tauri::command]
|
||||
@@ -14,7 +25,7 @@ pub fn copy_clash_env() -> CmdResult {
|
||||
/// 获取Clash信息
|
||||
#[tauri::command]
|
||||
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
Ok(Config::clash().latest().get_client_info())
|
||||
Ok(Config::clash().latest_ref().get_client_info())
|
||||
}
|
||||
|
||||
/// 修改Clash配置
|
||||
@@ -33,7 +44,7 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||
/// 切换Clash核心
|
||||
#[tauri::command]
|
||||
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
|
||||
log::info!(target: "app", "changing core to {clash_core}");
|
||||
logging!(info, Type::Config, "changing core to {clash_core}");
|
||||
|
||||
match CoreManager::global()
|
||||
.change_core(Some(clash_core.clone()))
|
||||
@@ -43,14 +54,18 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
||||
// 切换内核后重启内核
|
||||
match CoreManager::global().restart_core().await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "core changed and restarted to {clash_core}");
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
"core changed and restarted to {clash_core}"
|
||||
);
|
||||
handle::Handle::notice_message("config_core::change_success", &clash_core);
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(None)
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = format!("Core changed but failed to restart: {}", err);
|
||||
log::error!(target: "app", "{}", error_msg);
|
||||
let error_msg = format!("Core changed but failed to restart: {err}");
|
||||
logging!(error, Type::Core, "{error_msg}");
|
||||
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||
Ok(Some(error_msg))
|
||||
}
|
||||
@@ -58,7 +73,7 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = err.to_string();
|
||||
log::error!(target: "app", "failed to change core: {error_msg}");
|
||||
logging!(error, Type::Core, "failed to change core: {error_msg}");
|
||||
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||
Ok(Some(error_msg))
|
||||
}
|
||||
@@ -68,19 +83,31 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
||||
/// 启动核心
|
||||
#[tauri::command]
|
||||
pub async fn start_core() -> CmdResult {
|
||||
wrap_err!(CoreManager::global().start_core().await)
|
||||
let result = wrap_err!(CoreManager::global().start_core().await);
|
||||
if result.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// 关闭核心
|
||||
#[tauri::command]
|
||||
pub async fn stop_core() -> CmdResult {
|
||||
wrap_err!(CoreManager::global().stop_core().await)
|
||||
let result = wrap_err!(CoreManager::global().stop_core().await);
|
||||
if result.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// 重启核心
|
||||
#[tauri::command]
|
||||
pub async fn restart_core() -> CmdResult {
|
||||
wrap_err!(CoreManager::global().restart_core().await)
|
||||
let result = wrap_err!(CoreManager::global().restart_core().await);
|
||||
if result.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// 获取代理延迟
|
||||
@@ -90,9 +117,11 @@ pub async fn clash_api_get_proxy_delay(
|
||||
url: Option<String>,
|
||||
timeout: i32,
|
||||
) -> CmdResult<serde_json::Value> {
|
||||
MihomoManager::global()
|
||||
.test_proxy_delay(&name, url, timeout)
|
||||
.await
|
||||
wrap_err!(
|
||||
IpcManager::global()
|
||||
.test_proxy_delay(&name, url, timeout)
|
||||
.await
|
||||
)
|
||||
}
|
||||
|
||||
/// 测试URL延迟
|
||||
@@ -116,7 +145,7 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||
// 保存DNS配置到文件
|
||||
let yaml_str = serde_yaml::to_string(&dns_config).map_err(|e| e.to_string())?;
|
||||
fs::write(&dns_path, yaml_str).map_err(|e| e.to_string())?;
|
||||
log::info!(target: "app", "DNS config saved to {:?}", dns_path);
|
||||
logging!(info, Type::Config, "DNS config saved to {dns_path:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -137,20 +166,20 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
let dns_path = match dirs::app_home_dir() {
|
||||
Ok(path) => path.join("dns_config.yaml"),
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get home dir: {}", e);
|
||||
logging!(error, Type::Config, "Failed to get home dir: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !dns_path.exists() {
|
||||
log::warn!(target: "app", "DNS config file not found");
|
||||
logging!(warn, Type::Config, "DNS config file not found");
|
||||
return;
|
||||
}
|
||||
|
||||
let dns_yaml = match std::fs::read_to_string(&dns_path) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to read DNS config: {}", e);
|
||||
logging!(error, Type::Config, "Failed to read DNS config: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -163,51 +192,67 @@ pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
patch
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to parse DNS config: {}", e);
|
||||
logging!(error, Type::Config, "Failed to parse DNS config: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
log::info!(target: "app", "Applying DNS config from file");
|
||||
logging!(info, Type::Config, "Applying DNS config from file");
|
||||
|
||||
// 重新生成配置,确保DNS配置被正确应用
|
||||
// 这里不调用patch_clash以避免将DNS配置写入config.yaml
|
||||
Config::runtime()
|
||||
.latest()
|
||||
.draft_mut()
|
||||
.patch_config(patch_config.clone());
|
||||
|
||||
// 首先重新生成配置
|
||||
if let Err(err) = Config::generate().await {
|
||||
log::error!(target: "app", "Failed to regenerate config with DNS: {}", err);
|
||||
if let Err(err) = Config::generate() {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"Failed to regenerate config with DNS: {err}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 然后应用新配置
|
||||
if let Err(err) = CoreManager::global().update_config().await {
|
||||
log::error!(target: "app", "Failed to apply config with DNS: {}", err);
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"Failed to apply config with DNS: {err}"
|
||||
);
|
||||
} else {
|
||||
log::info!(target: "app", "DNS config successfully applied");
|
||||
logging!(info, Type::Config, "DNS config successfully applied");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
} else {
|
||||
// 当关闭DNS设置时,不需要对配置进行任何修改
|
||||
// 直接重新生成配置,让enhance函数自动跳过DNS配置的加载
|
||||
log::info!(target: "app", "DNS settings disabled, regenerating config");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"DNS settings disabled, regenerating config"
|
||||
);
|
||||
|
||||
// 重新生成配置
|
||||
if let Err(err) = Config::generate().await {
|
||||
log::error!(target: "app", "Failed to regenerate config: {}", err);
|
||||
if let Err(err) = Config::generate() {
|
||||
logging!(error, Type::Config, "Failed to regenerate config: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用新配置
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "Config regenerated successfully");
|
||||
logging!(info, Type::Config, "Config regenerated successfully");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "Failed to apply regenerated config: {}", err);
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"Failed to apply regenerated config: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -267,3 +312,318 @@ pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取Clash版本信息
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_version() -> CmdResult<serde_json::Value> {
|
||||
wrap_err!(IpcManager::global().get_version().await)
|
||||
}
|
||||
|
||||
/// 获取Clash配置
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_config() -> CmdResult<serde_json::Value> {
|
||||
let manager = IpcManager::global();
|
||||
let cache = ProxyRequestCache::global();
|
||||
let key = ProxyRequestCache::make_key("clash_config", "default");
|
||||
let value = cache
|
||||
.get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async {
|
||||
manager.get_config().await.unwrap_or_else(|e| {
|
||||
logging!(error, Type::Cmd, "Failed to fetch clash config: {e}");
|
||||
serde_json::Value::Object(serde_json::Map::new())
|
||||
})
|
||||
})
|
||||
.await;
|
||||
Ok((*value).clone())
|
||||
}
|
||||
|
||||
/// 强制刷新Clash配置缓存
|
||||
#[tauri::command]
|
||||
pub async fn force_refresh_clash_config() -> CmdResult<serde_json::Value> {
|
||||
let cache = ProxyRequestCache::global();
|
||||
let key = ProxyRequestCache::make_key("clash_config", "default");
|
||||
cache.map.remove(&key);
|
||||
get_clash_config().await
|
||||
}
|
||||
|
||||
/// 更新地理数据
|
||||
#[tauri::command]
|
||||
pub async fn update_geo_data() -> CmdResult {
|
||||
wrap_err!(IpcManager::global().update_geo_data().await)
|
||||
}
|
||||
|
||||
/// 升级Clash核心
|
||||
#[tauri::command]
|
||||
pub async fn upgrade_clash_core() -> CmdResult {
|
||||
wrap_err!(IpcManager::global().upgrade_core().await)
|
||||
}
|
||||
|
||||
/// 获取规则
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_rules() -> CmdResult<serde_json::Value> {
|
||||
wrap_err!(IpcManager::global().get_rules().await)
|
||||
}
|
||||
|
||||
/// 更新代理选择
|
||||
#[tauri::command]
|
||||
pub async fn update_proxy_choice(group: String, proxy: String) -> CmdResult {
|
||||
wrap_err!(IpcManager::global().update_proxy(&group, &proxy).await)
|
||||
}
|
||||
|
||||
/// 获取代理提供者
|
||||
#[tauri::command]
|
||||
pub async fn get_proxy_providers() -> CmdResult<serde_json::Value> {
|
||||
wrap_err!(IpcManager::global().get_providers_proxies().await)
|
||||
}
|
||||
|
||||
/// 获取规则提供者
|
||||
#[tauri::command]
|
||||
pub async fn get_rule_providers() -> CmdResult<serde_json::Value> {
|
||||
wrap_err!(IpcManager::global().get_rule_providers().await)
|
||||
}
|
||||
|
||||
/// 代理提供者健康检查
|
||||
#[tauri::command]
|
||||
pub async fn proxy_provider_health_check(name: String) -> CmdResult {
|
||||
wrap_err!(
|
||||
IpcManager::global()
|
||||
.proxy_provider_health_check(&name)
|
||||
.await
|
||||
)
|
||||
}
|
||||
|
||||
/// 更新代理提供者
|
||||
#[tauri::command]
|
||||
pub async fn update_proxy_provider(name: String) -> CmdResult {
|
||||
wrap_err!(IpcManager::global().update_proxy_provider(&name).await)
|
||||
}
|
||||
|
||||
/// 更新规则提供者
|
||||
#[tauri::command]
|
||||
pub async fn update_rule_provider(name: String) -> CmdResult {
|
||||
wrap_err!(IpcManager::global().update_rule_provider(&name).await)
|
||||
}
|
||||
|
||||
/// 获取连接
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_connections() -> CmdResult<serde_json::Value> {
|
||||
wrap_err!(IpcManager::global().get_connections().await)
|
||||
}
|
||||
|
||||
/// 删除连接
|
||||
#[tauri::command]
|
||||
pub async fn delete_clash_connection(id: String) -> CmdResult {
|
||||
wrap_err!(IpcManager::global().delete_connection(&id).await)
|
||||
}
|
||||
|
||||
/// 关闭所有连接
|
||||
#[tauri::command]
|
||||
pub async fn close_all_clash_connections() -> CmdResult {
|
||||
wrap_err!(IpcManager::global().close_all_connections().await)
|
||||
}
|
||||
|
||||
/// 获取流量数据 (使用新的IPC流式监控)
|
||||
#[tauri::command]
|
||||
pub async fn get_traffic_data() -> CmdResult<serde_json::Value> {
|
||||
let traffic = crate::ipc::get_current_traffic().await;
|
||||
let result = serde_json::json!({
|
||||
"up": traffic.total_up,
|
||||
"down": traffic.total_down,
|
||||
"up_rate": traffic.up_rate,
|
||||
"down_rate": traffic.down_rate,
|
||||
"last_updated": traffic.last_updated.elapsed().as_secs()
|
||||
});
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 获取内存数据 (使用新的IPC流式监控)
|
||||
#[tauri::command]
|
||||
pub async fn get_memory_data() -> CmdResult<serde_json::Value> {
|
||||
let memory = crate::ipc::get_current_memory().await;
|
||||
let usage_percent = if memory.oslimit > 0 {
|
||||
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let result = serde_json::json!({
|
||||
"inuse": memory.inuse,
|
||||
"oslimit": memory.oslimit,
|
||||
"usage_percent": usage_percent,
|
||||
"last_updated": memory.last_updated.elapsed().as_secs()
|
||||
});
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 启动流量监控服务 (IPC流式监控自动启动,此函数为兼容性保留)
|
||||
#[tauri::command]
|
||||
pub async fn start_traffic_service() -> CmdResult {
|
||||
logging!(trace, Type::Ipc, "启动流量监控服务 (IPC流式监控)");
|
||||
// 新的IPC监控在首次访问时自动启动
|
||||
// 触发一次访问以确保监控器已初始化
|
||||
let _ = crate::ipc::get_current_traffic().await;
|
||||
let _ = crate::ipc::get_current_memory().await;
|
||||
logging!(info, Type::Ipc, "IPC流式监控已激活");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止流量监控服务 (IPC流式监控无需显式停止,此函数为兼容性保留)
|
||||
#[tauri::command]
|
||||
pub async fn stop_traffic_service() -> CmdResult {
|
||||
logging!(trace, Type::Ipc, "停止流量监控服务请求 (IPC流式监控)");
|
||||
// 新的IPC监控是持久的,无需显式停止
|
||||
logging!(info, Type::Ipc, "IPC流式监控继续运行");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取格式化的流量数据 (包含单位,便于前端显示)
|
||||
#[tauri::command]
|
||||
pub async fn get_formatted_traffic_data() -> CmdResult<serde_json::Value> {
|
||||
logging!(trace, Type::Ipc, "获取格式化流量数据");
|
||||
let (up_rate, down_rate, total_up, total_down, is_fresh) =
|
||||
crate::ipc::get_formatted_traffic().await;
|
||||
let result = serde_json::json!({
|
||||
"up_rate_formatted": up_rate,
|
||||
"down_rate_formatted": down_rate,
|
||||
"total_up_formatted": total_up,
|
||||
"total_down_formatted": total_down,
|
||||
"is_fresh": is_fresh
|
||||
});
|
||||
logging!(
|
||||
debug,
|
||||
Type::Ipc,
|
||||
"格式化流量数据: ↑{up_rate}/s ↓{down_rate}/s (总计: ↑{total_up} ↓{total_down})"
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 获取格式化的内存数据 (包含单位,便于前端显示)
|
||||
#[tauri::command]
|
||||
pub async fn get_formatted_memory_data() -> CmdResult<serde_json::Value> {
|
||||
logging!(info, Type::Ipc, "获取格式化内存数据");
|
||||
let (inuse, oslimit, usage_percent, is_fresh) = crate::ipc::get_formatted_memory().await;
|
||||
let result = serde_json::json!({
|
||||
"inuse_formatted": inuse,
|
||||
"oslimit_formatted": oslimit,
|
||||
"usage_percent": usage_percent,
|
||||
"is_fresh": is_fresh
|
||||
});
|
||||
logging!(
|
||||
debug,
|
||||
Type::Ipc,
|
||||
"格式化内存数据: {inuse} / {oslimit} ({usage_percent:.1}%)"
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 获取系统监控概览 (流量+内存,便于前端一次性获取所有状态)
|
||||
#[tauri::command]
|
||||
pub async fn get_system_monitor_overview() -> CmdResult<serde_json::Value> {
|
||||
logging!(debug, Type::Ipc, "获取系统监控概览");
|
||||
|
||||
// 并发获取流量和内存数据
|
||||
let (traffic, memory) = tokio::join!(
|
||||
crate::ipc::get_current_traffic(),
|
||||
crate::ipc::get_current_memory()
|
||||
);
|
||||
|
||||
let (traffic_formatted, memory_formatted) = tokio::join!(
|
||||
crate::ipc::get_formatted_traffic(),
|
||||
crate::ipc::get_formatted_memory()
|
||||
);
|
||||
|
||||
let traffic_is_fresh = traffic.last_updated.elapsed().as_secs() < 5;
|
||||
let memory_is_fresh = memory.last_updated.elapsed().as_secs() < 10;
|
||||
|
||||
let result = serde_json::json!({
|
||||
"traffic": {
|
||||
"raw": {
|
||||
"up": traffic.total_up,
|
||||
"down": traffic.total_down,
|
||||
"up_rate": traffic.up_rate,
|
||||
"down_rate": traffic.down_rate
|
||||
},
|
||||
"formatted": {
|
||||
"up_rate": traffic_formatted.0,
|
||||
"down_rate": traffic_formatted.1,
|
||||
"total_up": traffic_formatted.2,
|
||||
"total_down": traffic_formatted.3
|
||||
},
|
||||
"is_fresh": traffic_is_fresh
|
||||
},
|
||||
"memory": {
|
||||
"raw": {
|
||||
"inuse": memory.inuse,
|
||||
"oslimit": memory.oslimit,
|
||||
"usage_percent": if memory.oslimit > 0 {
|
||||
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
},
|
||||
"formatted": {
|
||||
"inuse": memory_formatted.0,
|
||||
"oslimit": memory_formatted.1,
|
||||
"usage_percent": memory_formatted.2
|
||||
},
|
||||
"is_fresh": memory_is_fresh
|
||||
},
|
||||
"overall_status": if traffic_is_fresh && memory_is_fresh { "healthy" } else { "stale" }
|
||||
});
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 获取代理组延迟
|
||||
#[tauri::command]
|
||||
pub async fn get_group_proxy_delays(
|
||||
group_name: String,
|
||||
url: Option<String>,
|
||||
timeout: Option<i32>,
|
||||
) -> CmdResult<serde_json::Value> {
|
||||
wrap_err!(
|
||||
IpcManager::global()
|
||||
.get_group_proxy_delays(&group_name, url, timeout.unwrap_or(10000))
|
||||
.await
|
||||
)
|
||||
}
|
||||
|
||||
/// 检查调试是否启用
|
||||
#[tauri::command]
|
||||
pub async fn is_clash_debug_enabled() -> CmdResult<bool> {
|
||||
match IpcManager::global().is_debug_enabled().await {
|
||||
Ok(enabled) => Ok(enabled),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// 垃圾回收
|
||||
#[tauri::command]
|
||||
pub async fn clash_gc() -> CmdResult {
|
||||
wrap_err!(IpcManager::global().gc().await)
|
||||
}
|
||||
|
||||
/// 获取日志 (使用新的流式实现)
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_logs() -> CmdResult<serde_json::Value> {
|
||||
Ok(ipc::get_logs_json().await)
|
||||
}
|
||||
|
||||
/// 启动日志监控
|
||||
#[tauri::command]
|
||||
pub async fn start_logs_monitoring(level: Option<String>) -> CmdResult {
|
||||
ipc::start_logs_monitoring(level).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止日志监控
|
||||
#[tauri::command]
|
||||
pub async fn stop_logs_monitoring() -> CmdResult {
|
||||
ipc::stop_logs_monitoring().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 清除日志
|
||||
#[tauri::command]
|
||||
pub async fn clear_logs() -> CmdResult {
|
||||
ipc::clear_logs().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use chrono::Local;
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
@@ -37,7 +38,7 @@ fn country_code_to_emoji(country_code: &str) -> String {
|
||||
let c2 = 0x1F1E6 + (bytes[1] as u32) - ('A' as u32);
|
||||
|
||||
char::from_u32(c1)
|
||||
.and_then(|c1| char::from_u32(c2).map(|c2| format!("{}{}", c1, c2)))
|
||||
.and_then(|c1| char::from_u32(c2).map(|c2| format!("{c1}{c2}")))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -163,7 +164,7 @@ async fn check_chatgpt_combined(client: &Client) -> Vec<UnlockItem> {
|
||||
|
||||
map.get("loc").map(|loc| {
|
||||
let emoji = country_code_to_emoji(loc);
|
||||
format!("{}{}", emoji, loc)
|
||||
format!("{emoji}{loc}")
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -250,12 +251,28 @@ async fn check_gemini(client: &Client) -> UnlockItem {
|
||||
let status = if is_ok { "Yes" } else { "No" };
|
||||
|
||||
// 尝试提取国家代码
|
||||
let re = Regex::new(r#",2,1,200,"([A-Z]{3})""#).unwrap();
|
||||
let re = match Regex::new(r#",2,1,200,"([A-Z]{3})""#) {
|
||||
Ok(re) => re,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Gemini regex: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Gemini".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let region = re.captures(&body).and_then(|caps| {
|
||||
caps.get(1).map(|m| {
|
||||
let country_code = m.as_str();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
format!("{}{}", emoji, country_code)
|
||||
format!("{emoji}{country_code}")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -303,12 +320,28 @@ async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
||||
}
|
||||
} else if body_lower.contains("ad-free") {
|
||||
// 尝试解析国家代码
|
||||
let re = Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#).unwrap();
|
||||
let re = match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
|
||||
Ok(re) => re,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile YouTube Premium regex: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let region = re.captures(&body).and_then(|caps| {
|
||||
caps.get(1).map(|m| {
|
||||
let country_code = m.as_str().trim();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
format!("{}{}", emoji, country_code)
|
||||
format!("{emoji}{country_code}")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -350,11 +383,16 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
let cookie_store = Arc::new(reqwest::cookie::Jar::default());
|
||||
|
||||
// 使用带Cookie的客户端
|
||||
let client_with_cookies = reqwest::Client::builder()
|
||||
let client_with_cookies = match reqwest::Client::builder()
|
||||
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||
.cookie_provider(Arc::clone(&cookie_store))
|
||||
.build()
|
||||
.unwrap_or_else(|_| client.clone());
|
||||
.build() {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Network, "Failed to create client with cookies for Bahamut Anime: {}", e);
|
||||
client.clone()
|
||||
}
|
||||
};
|
||||
|
||||
// 第一步:获取设备ID (会自动保存Cookie)
|
||||
let device_url = "https://ani.gamer.com.tw/ajax/getdeviceid.php";
|
||||
@@ -363,10 +401,21 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
match response.text().await {
|
||||
Ok(text) => {
|
||||
// 使用正则提取deviceid
|
||||
let re = Regex::new(r#""deviceid"\s*:\s*"([^"]+)"#).unwrap();
|
||||
re.captures(&text)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
|
||||
.unwrap_or_default()
|
||||
match Regex::new(r#""deviceid"\s*:\s*"([^"]+)"#) {
|
||||
Ok(re) => re
|
||||
.captures(&text)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
|
||||
.unwrap_or_default(),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile deviceid regex for Bahamut Anime: {}",
|
||||
e
|
||||
);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => String::new(),
|
||||
}
|
||||
@@ -384,10 +433,8 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
}
|
||||
|
||||
// 第二步:使用设备ID检查访问权限 (使用相同的Cookie)
|
||||
let url = format!(
|
||||
"https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={}",
|
||||
device_id
|
||||
);
|
||||
let url =
|
||||
format!("https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={device_id}");
|
||||
|
||||
let token_result = match client_with_cookies.get(&url).send().await {
|
||||
Ok(response) => {
|
||||
@@ -423,17 +470,25 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
.await
|
||||
{
|
||||
Ok(response) => match response.text().await {
|
||||
Ok(body) => {
|
||||
let region_re = Regex::new(r#"data-geo="([^"]+)"#).unwrap();
|
||||
region_re
|
||||
Ok(body) => match Regex::new(r#"data-geo="([^"]+)"#) {
|
||||
Ok(region_re) => region_re
|
||||
.captures(&body)
|
||||
.and_then(|caps| caps.get(1))
|
||||
.map(|m| {
|
||||
let country_code = m.as_str();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
format!("{}{}", emoji, country_code)
|
||||
})
|
||||
}
|
||||
format!("{emoji}{country_code}")
|
||||
}),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile region regex for Bahamut Anime: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
@@ -470,7 +525,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
|
||||
// 检查连接失败情况
|
||||
if let Err(e) = &result1 {
|
||||
eprintln!("Netflix请求错误: {}", e);
|
||||
eprintln!("Netflix请求错误: {e}");
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
@@ -487,7 +542,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
.await;
|
||||
|
||||
if let Err(e) = &result2 {
|
||||
eprintln!("Netflix请求错误: {}", e);
|
||||
eprintln!("Netflix请求错误: {e}");
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
@@ -497,8 +552,40 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
}
|
||||
|
||||
// 获取状态码
|
||||
let status1 = result1.unwrap().status().as_u16();
|
||||
let status2 = result2.unwrap().status().as_u16();
|
||||
let status1 = match result1 {
|
||||
Ok(response) => response.status().as_u16(),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to get Netflix response 1: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let status2 = match result2 {
|
||||
Ok(response) => response.status().as_u16(),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to get Netflix response 2: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 根据状态码判断解锁状况
|
||||
if status1 == 404 && status2 == 404 {
|
||||
@@ -541,7 +628,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, region_code)),
|
||||
region: Some(format!("{emoji}{region_code}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -557,7 +644,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("获取Netflix区域信息失败: {}", e);
|
||||
eprintln!("获取Netflix区域信息失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes (但无法获取区域)".to_string(),
|
||||
@@ -570,7 +657,7 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
// 其他未知错误状态
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: format!("Failed (状态码: {}_{}", status1, status2),
|
||||
status: format!("Failed (状态码: {status1}_{status2}"),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
@@ -614,7 +701,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, country)),
|
||||
region: Some(format!("{emoji}{country}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -631,7 +718,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("解析Fast.com API响应失败: {}", e);
|
||||
eprintln!("解析Fast.com API响应失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (解析错误)".to_string(),
|
||||
@@ -642,7 +729,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Fast.com API请求失败: {}", e);
|
||||
eprintln!("Fast.com API请求失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (CDN API)".to_string(),
|
||||
@@ -687,7 +774,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
};
|
||||
}
|
||||
|
||||
let device_response = device_result.unwrap();
|
||||
let device_response = match device_result {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to get Disney+ device response: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 检查是否 403 错误
|
||||
if device_response.status().as_u16() == 403 {
|
||||
@@ -712,7 +815,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
};
|
||||
|
||||
// 提取 assertion
|
||||
let re = Regex::new(r#""assertion"\s*:\s*"([^"]+)"#).unwrap();
|
||||
let re = match Regex::new(r#""assertion"\s*:\s*"([^"]+)"#) {
|
||||
Ok(re) => re,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile assertion regex for Disney+: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Regex Error)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let assertion = match re.captures(&device_body) {
|
||||
Some(caps) => caps.get(1).map(|m| m.as_str().to_string()),
|
||||
None => None,
|
||||
@@ -731,7 +850,18 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
let token_url = "https://disney.api.edge.bamgrid.com/token";
|
||||
|
||||
// 构建请求体 - 使用表单数据格式而非 JSON
|
||||
let assertion_str = assertion.unwrap();
|
||||
let assertion_str = match assertion {
|
||||
Some(assertion) => assertion,
|
||||
None => {
|
||||
logging!(error, Type::Network, "No assertion found for Disney+");
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (No Assertion)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let token_body = [
|
||||
(
|
||||
"grant_type",
|
||||
@@ -764,7 +894,23 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
};
|
||||
}
|
||||
|
||||
let token_response = token_result.unwrap();
|
||||
let token_response = match token_result {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to get Disney+ token response: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let token_status = token_response.status();
|
||||
|
||||
// 保存原始响应用于调试
|
||||
@@ -800,10 +946,20 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
.map(|s| s.to_string()),
|
||||
Err(_) => {
|
||||
// 如果 JSON 解析失败,尝试使用正则表达式
|
||||
let refresh_token_re = Regex::new(r#""refresh_token"\s*:\s*"([^"]+)"#).unwrap();
|
||||
refresh_token_re
|
||||
.captures(&token_body_text)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
|
||||
match Regex::new(r#""refresh_token"\s*:\s*"([^"]+)"#) {
|
||||
Ok(refresh_token_re) => refresh_token_re
|
||||
.captures(&token_body_text)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile refresh_token regex for Disney+: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -827,7 +983,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
// GraphQL API 通常接受 JSON 格式
|
||||
let graphql_payload = format!(
|
||||
r#"{{"query":"mutation refreshToken($input: RefreshTokenInput!) {{ refreshToken(refreshToken: $input) {{ activeSession {{ sessionId }} }} }}","variables":{{"input":{{"refreshToken":"{}"}}}}}}"#,
|
||||
refresh_token.unwrap()
|
||||
refresh_token.unwrap_or_default()
|
||||
);
|
||||
|
||||
let graphql_result = client
|
||||
@@ -859,21 +1015,56 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
};
|
||||
|
||||
// 解析 GraphQL 响应获取区域信息
|
||||
let graphql_response = graphql_result.unwrap();
|
||||
let graphql_response = match graphql_result {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to get Disney+ GraphQL response: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let graphql_status = graphql_response.status();
|
||||
let graphql_body_text = (graphql_response.text().await).unwrap_or_default();
|
||||
let graphql_body_text = match graphql_response.text().await {
|
||||
Ok(text) => text,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to read Disney+ GraphQL response text: {}",
|
||||
e
|
||||
);
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
|
||||
// 如果 GraphQL 响应为空或明显错误,尝试直接获取区域信息
|
||||
if graphql_body_text.is_empty() || graphql_status.as_u16() >= 400 {
|
||||
// 尝试直接从主页获取区域信息
|
||||
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
|
||||
Ok(response) => match response.text().await {
|
||||
Ok(body) => {
|
||||
let region_re = Regex::new(r#"region"\s*:\s*"([^"]+)"#).unwrap();
|
||||
region_re
|
||||
Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
|
||||
Ok(region_re) => region_re
|
||||
.captures(&body)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
|
||||
}
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Disney+ main page region regex: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
@@ -884,7 +1075,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{} (from main page)", emoji, region)),
|
||||
region: Some(format!("{emoji}{region} (from main page)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -900,28 +1091,59 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
} else {
|
||||
}
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: format!(
|
||||
"Failed (GraphQL error: {}, status: {})",
|
||||
graphql_body_text.chars().take(50).collect::<String>() + "...",
|
||||
graphql_status.as_u16()
|
||||
),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
// 提取国家代码
|
||||
let region_re = match Regex::new(r#""countryCode"\s*:\s*"([^"]+)"#) {
|
||||
Ok(re) => re,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Disney+ countryCode regex: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: format!(
|
||||
"Failed (GraphQL error: {}, status: {})",
|
||||
graphql_body_text.chars().take(50).collect::<String>() + "...",
|
||||
graphql_status.as_u16()
|
||||
),
|
||||
status: "Failed (Regex Error)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 提取国家代码
|
||||
let region_re = Regex::new(r#""countryCode"\s*:\s*"([^"]+)"#).unwrap();
|
||||
};
|
||||
let region_code = region_re
|
||||
.captures(&graphql_body_text)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
|
||||
|
||||
// 提取支持状态
|
||||
let supported_re = Regex::new(r#""inSupportedLocation"\s*:\s*(false|true)"#).unwrap();
|
||||
let supported_re = match Regex::new(r#""inSupportedLocation"\s*:\s*(false|true)"#) {
|
||||
Ok(re) => re,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Disney+ supported location regex: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Regex Error)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let in_supported_location = supported_re
|
||||
.captures(&graphql_body_text)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str() == "true"));
|
||||
@@ -931,12 +1153,20 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
// 尝试直接从主页获取区域信息
|
||||
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
|
||||
Ok(response) => match response.text().await {
|
||||
Ok(body) => {
|
||||
let region_re = Regex::new(r#"region"\s*:\s*"([^"]+)"#).unwrap();
|
||||
region_re
|
||||
Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
|
||||
Ok(region_re) => region_re
|
||||
.captures(&body)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
|
||||
}
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Disney+ main page region regex: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
@@ -947,7 +1177,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{} (from main page)", emoji, region)),
|
||||
region: Some(format!("{emoji}{region} (from main page)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -960,7 +1190,18 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
};
|
||||
}
|
||||
|
||||
let region = region_code.unwrap();
|
||||
let region = match region_code {
|
||||
Some(code) => code,
|
||||
None => {
|
||||
logging!(error, Type::Network, "No region code found for Disney+");
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 判断日本地区
|
||||
if region == "JP" {
|
||||
@@ -968,7 +1209,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, region)),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -990,7 +1231,7 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Soon".to_string(),
|
||||
region: Some(format!("{}{}(即将上线)", emoji, region)),
|
||||
region: Some(format!("{emoji}{region}(即将上线)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
@@ -999,13 +1240,13 @@ async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, region)),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
None => UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: format!("Failed (Error: Unknown region status for {})", region),
|
||||
status: format!("Failed (Error: Unknown region status for {region})"),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
@@ -1030,13 +1271,47 @@ async fn check_prime_video(client: &Client) -> UnlockItem {
|
||||
}
|
||||
|
||||
// 解析响应内容
|
||||
match result.unwrap().text().await {
|
||||
let response = match result {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to get Prime Video response: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
match response.text().await {
|
||||
Ok(body) => {
|
||||
// 检查是否被地区限制
|
||||
let is_blocked = body.contains("isServiceRestricted");
|
||||
|
||||
// 提取地区信息
|
||||
let region_re = Regex::new(r#""currentTerritory":"([^"]+)"#).unwrap();
|
||||
let region_re = match Regex::new(r#""currentTerritory":"([^"]+)"#) {
|
||||
Ok(re) => re,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Prime Video region regex: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Regex Error)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let region_code = region_re
|
||||
.captures(&body)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
|
||||
@@ -1056,7 +1331,7 @@ async fn check_prime_video(client: &Client) -> UnlockItem {
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, region)),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
@@ -1170,7 +1445,7 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
.connection_verbose(true) // 详细连接信息
|
||||
.build() {
|
||||
Ok(client) => client,
|
||||
Err(e) => return Err(format!("创建HTTP客户端失败: {}", e)),
|
||||
Err(e) => return Err(format!("创建HTTP客户端失败: {e}")),
|
||||
};
|
||||
|
||||
// 创建共享的结果集
|
||||
@@ -1184,8 +1459,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
|
||||
// 添加哔哩哔哩大陆检测任务
|
||||
{
|
||||
let client = client_arc.clone();
|
||||
let results = results.clone();
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_bilibili_china_mainland(&client).await;
|
||||
let mut results = results.lock().await;
|
||||
@@ -1195,8 +1470,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
|
||||
// 添加哔哩哔哩港澳台检测任务
|
||||
{
|
||||
let client = client_arc.clone();
|
||||
let results = results.clone();
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_bilibili_hk_mc_tw(&client).await;
|
||||
let mut results = results.lock().await;
|
||||
@@ -1206,8 +1481,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
|
||||
// 添加合并的ChatGPT检测任务
|
||||
{
|
||||
let client = client_arc.clone();
|
||||
let results = results.clone();
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let chatgpt_results = check_chatgpt_combined(&client).await;
|
||||
let mut results = results.lock().await;
|
||||
@@ -1217,8 +1492,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
|
||||
// 添加Gemini检测任务
|
||||
{
|
||||
let client = client_arc.clone();
|
||||
let results = results.clone();
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_gemini(&client).await;
|
||||
let mut results = results.lock().await;
|
||||
@@ -1228,8 +1503,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
|
||||
// 添加YouTube Premium检测任务
|
||||
{
|
||||
let client = client_arc.clone();
|
||||
let results = results.clone();
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_youtube_premium(&client).await;
|
||||
let mut results = results.lock().await;
|
||||
@@ -1239,8 +1514,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
|
||||
// 添加动画疯检测任务
|
||||
{
|
||||
let client = client_arc.clone();
|
||||
let results = results.clone();
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_bahamut_anime(&client).await;
|
||||
let mut results = results.lock().await;
|
||||
@@ -1250,8 +1525,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
|
||||
// 添加 Netflix 检测任务
|
||||
{
|
||||
let client = client_arc.clone();
|
||||
let results = results.clone();
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_netflix(&client).await;
|
||||
let mut results = results.lock().await;
|
||||
@@ -1261,8 +1536,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
|
||||
// 添加 Disney+ 检测任务
|
||||
{
|
||||
let client = client_arc.clone();
|
||||
let results = results.clone();
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_disney_plus(&client).await;
|
||||
let mut results = results.lock().await;
|
||||
@@ -1272,8 +1547,8 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
|
||||
// 添加 Prime Video 检测任务
|
||||
{
|
||||
let client = client_arc.clone();
|
||||
let results = results.clone();
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_prime_video(&client).await;
|
||||
let mut results = results.lock().await;
|
||||
@@ -1284,14 +1559,22 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
// 等待所有任务完成
|
||||
while let Some(res) = tasks.join_next().await {
|
||||
if let Err(e) = res {
|
||||
eprintln!("任务执行失败: {}", e);
|
||||
eprintln!("任务执行失败: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有结果
|
||||
let results = Arc::try_unwrap(results)
|
||||
.expect("无法获取结果,可能仍有引用存在")
|
||||
.into_inner();
|
||||
let results = match Arc::try_unwrap(results) {
|
||||
Ok(mutex) => mutex.into_inner(),
|
||||
Err(_) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to unwrap results Arc, references still exist"
|
||||
);
|
||||
return Err("Failed to collect results".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use anyhow::Result;
|
||||
|
||||
// Common result type used by command functions
|
||||
pub type CmdResult<T = ()> = Result<T, String>;
|
||||
|
||||
// Command modules
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
use super::CmdResult;
|
||||
use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager};
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::wrap_err;
|
||||
use network_interface::NetworkInterface;
|
||||
use serde_yaml::Mapping;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
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());
|
||||
@@ -21,21 +20,28 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
);
|
||||
map.insert("bypass".into(), current.bypass.into());
|
||||
|
||||
log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// get the system proxy
|
||||
/// 获取自动代理配置
|
||||
#[tauri::command]
|
||||
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
let current = spawn_blocking(Autoproxy::get_auto_proxy)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to spawn blocking task for autoproxy: {}", e))?
|
||||
.map_err(|e| format!("Failed to get auto proxy: {}", e))?;
|
||||
log::debug!(target: "app", "开始获取自动代理配置(事件驱动)");
|
||||
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
|
||||
let current = proxy_manager.get_auto_proxy_cached();
|
||||
// 异步请求更新,立即返回缓存数据
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let _ = proxy_manager.get_auto_proxy_async().await;
|
||||
});
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert("url".into(), current.url.into());
|
||||
map.insert("url".into(), current.url.clone().into());
|
||||
|
||||
log::debug!(target: "app", "返回自动代理配置(缓存): enable={}, url={}", current.enable, current.url);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
@@ -49,7 +55,7 @@ pub fn get_system_hostname() -> CmdResult<String> {
|
||||
Ok(name) => name,
|
||||
Err(os_string) => {
|
||||
// 对于包含非UTF-8的主机名,使用调试格式化
|
||||
let fallback = format!("{:?}", os_string);
|
||||
let fallback = format!("{os_string:?}");
|
||||
// 去掉可能存在的引号
|
||||
fallback.trim_matches('"').to_string()
|
||||
}
|
||||
|
||||
+399
-63
@@ -2,55 +2,120 @@ use super::CmdResult;
|
||||
use crate::{
|
||||
config::{Config, IProfiles, PrfItem, PrfOption},
|
||||
core::{handle, timer::Timer, tray::Tray, CoreManager},
|
||||
feat, logging, ret_err,
|
||||
feat, logging,
|
||||
process::AsyncHandler,
|
||||
ret_err,
|
||||
utils::{dirs, help, logging::Type},
|
||||
wrap_err,
|
||||
};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
|
||||
// 添加全局互斥锁防止并发配置更新
|
||||
// 全局互斥锁防止并发配置更新
|
||||
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
|
||||
|
||||
/// 获取配置文件列表
|
||||
// 全局请求序列号跟踪,用于避免队列化执行
|
||||
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
static CURRENT_PROCESSING_PROFILE: RwLock<Option<String>> = RwLock::const_new(None);
|
||||
|
||||
/// 清理配置处理状态
|
||||
async fn cleanup_processing_state(sequence: u64, reason: &str) {
|
||||
*CURRENT_PROCESSING_PROFILE.write().await = None;
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"{},清理状态,序列号: {}",
|
||||
reason,
|
||||
sequence
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取配置文件避免锁竞争
|
||||
#[tauri::command]
|
||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||
let profiles_result = tokio::time::timeout(
|
||||
Duration::from_secs(3), // 3秒超时
|
||||
tokio::task::spawn_blocking(move || Config::profiles().data().clone()),
|
||||
// 策略1: 尝试快速获取latest数据
|
||||
let latest_result = tokio::time::timeout(
|
||||
Duration::from_millis(500),
|
||||
AsyncHandler::spawn_blocking(move || {
|
||||
let profiles = Config::profiles();
|
||||
let latest = profiles.latest_ref();
|
||||
IProfiles {
|
||||
current: latest.current.clone(),
|
||||
items: latest.items.clone(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
match profiles_result {
|
||||
Ok(Ok(profiles)) => Ok(profiles),
|
||||
match latest_result {
|
||||
Ok(Ok(profiles)) => {
|
||||
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
|
||||
return Ok(profiles);
|
||||
}
|
||||
Ok(Err(join_err)) => {
|
||||
logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err);
|
||||
Ok(IProfiles {
|
||||
current: None,
|
||||
items: Some(vec![]),
|
||||
})
|
||||
logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err);
|
||||
}
|
||||
Err(_) => {
|
||||
// 超时情况
|
||||
logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
|
||||
}
|
||||
}
|
||||
|
||||
// 策略2: 如果快速获取失败,尝试获取data()
|
||||
let data_result = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
AsyncHandler::spawn_blocking(move || {
|
||||
let profiles = Config::profiles();
|
||||
let data = profiles.latest_ref();
|
||||
IProfiles {
|
||||
current: data.current.clone(),
|
||||
items: data.items.clone(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
match data_result {
|
||||
Ok(Ok(profiles)) => {
|
||||
logging!(info, Type::Cmd, false, "获取draft配置列表成功");
|
||||
return Ok(profiles);
|
||||
}
|
||||
Ok(Err(join_err)) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"获取配置列表超时(3秒),可能存在锁竞争"
|
||||
"获取draft配置任务失败: {}",
|
||||
join_err
|
||||
);
|
||||
match tokio::task::spawn_blocking(move || Config::profiles().latest().clone()).await {
|
||||
Ok(profiles) => {
|
||||
logging!(info, Type::Cmd, true, "使用latest()成功获取配置");
|
||||
Ok(profiles)
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(error, Type::Cmd, true, "fallback获取配置也失败,返回空配置");
|
||||
Ok(IProfiles {
|
||||
current: None,
|
||||
items: Some(vec![]),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)");
|
||||
}
|
||||
}
|
||||
|
||||
// 策略3: fallback,尝试重新创建配置
|
||||
logging!(
|
||||
warn,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"所有获取配置策略都失败,尝试fallback"
|
||||
);
|
||||
|
||||
match AsyncHandler::spawn_blocking(IProfiles::new).await {
|
||||
Ok(profiles) => {
|
||||
logging!(info, Type::Cmd, true, "使用fallback配置成功");
|
||||
Ok(profiles)
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Cmd, true, "fallback配置也失败: {}", err);
|
||||
// 返回空配置避免崩溃
|
||||
Ok(IProfiles {
|
||||
current: None,
|
||||
items: Some(vec![]),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,21 +131,113 @@ pub async fn enhance_profiles() -> CmdResult {
|
||||
/// 导入配置文件
|
||||
#[tauri::command]
|
||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
logging!(info, Type::Cmd, true, "[导入订阅] 开始导入: {}", url);
|
||||
|
||||
// 使用超时保护避免长时间阻塞
|
||||
let import_result = tokio::time::timeout(
|
||||
Duration::from_secs(60), // 60秒超时
|
||||
async {
|
||||
let item = PrfItem::from_url(&url, None, None, option).await?;
|
||||
logging!(info, Type::Cmd, true, "[导入订阅] 下载完成,开始保存配置");
|
||||
|
||||
// 获取导入前的配置数量用于验证
|
||||
let pre_count = Config::profiles()
|
||||
.latest_ref()
|
||||
.items
|
||||
.as_ref()
|
||||
.map_or(0, |items| items.len());
|
||||
|
||||
Config::profiles().data_mut().append_item(item.clone())?;
|
||||
|
||||
// 验证导入是否成功
|
||||
let post_count = Config::profiles()
|
||||
.latest_ref()
|
||||
.items
|
||||
.as_ref()
|
||||
.map_or(0, |items| items.len());
|
||||
if post_count <= pre_count {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"[导入订阅] 配置未增加,导入可能失败"
|
||||
);
|
||||
return Err(anyhow::anyhow!("配置导入后数量未增加"));
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"[导入订阅] 配置保存成功,数量: {} -> {}",
|
||||
pre_count,
|
||||
post_count
|
||||
);
|
||||
|
||||
// 立即发送配置变更通知
|
||||
if let Some(uid) = &item.uid {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"[导入订阅] 发送配置变更通知: {}",
|
||||
uid
|
||||
);
|
||||
handle::Handle::notify_profile_changed(uid.clone());
|
||||
}
|
||||
|
||||
// 异步保存配置文件并发送全局通知
|
||||
let uid_clone = item.uid.clone();
|
||||
crate::process::AsyncHandler::spawn(move || async move {
|
||||
// 在异步块中重新获取锁,避免跨await问题
|
||||
let save_result = { Config::profiles().data_mut().save_file() };
|
||||
|
||||
if let Err(e) = save_result {
|
||||
logging!(error, Type::Cmd, true, "[导入订阅] 保存配置文件失败: {}", e);
|
||||
} else {
|
||||
logging!(info, Type::Cmd, true, "[导入订阅] 配置文件保存成功");
|
||||
|
||||
// 发送全局配置更新通知
|
||||
if let Some(uid) = uid_clone {
|
||||
// 延迟发送,确保文件已完全写入
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
handle::Handle::notify_profile_changed(uid);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
match import_result {
|
||||
Ok(Ok(())) => {
|
||||
logging!(info, Type::Cmd, true, "[导入订阅] 导入完成: {}", url);
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging!(error, Type::Cmd, true, "[导入订阅] 导入失败: {}", e);
|
||||
Err(format!("导入订阅失败: {e}"))
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(error, Type::Cmd, true, "[导入订阅] 导入超时(60秒): {}", url);
|
||||
Err("导入订阅超时,请检查网络连接".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 重新排序配置文件
|
||||
#[tauri::command]
|
||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
wrap_err!(Config::profiles().data().reorder(active_id, over_id))
|
||||
wrap_err!(Config::profiles().data_mut().reorder(active_id, over_id))
|
||||
}
|
||||
|
||||
/// 创建配置文件
|
||||
#[tauri::command]
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
wrap_err!(Config::profiles().data_mut().append_item(item))
|
||||
}
|
||||
|
||||
/// 更新配置文件
|
||||
@@ -92,7 +249,11 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
|
||||
/// 删除配置文件
|
||||
#[tauri::command]
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
||||
let should_update = wrap_err!({ Config::profiles().data_mut().delete_item(index) })?;
|
||||
|
||||
// 删除后自动清理冗余文件
|
||||
let _ = Config::profiles().latest_ref().auto_cleanup();
|
||||
|
||||
if should_update {
|
||||
wrap_err!(CoreManager::global().update_config().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
@@ -103,13 +264,63 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
||||
/// 修改profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
// 获取互斥锁,防止并发执行
|
||||
let _guard = PROFILE_UPDATE_MUTEX.lock().await;
|
||||
// 为当前请求分配序列号
|
||||
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
let target_profile = profiles.current.clone();
|
||||
|
||||
logging!(info, Type::Cmd, true, "开始修改配置文件");
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
|
||||
current_sequence,
|
||||
target_profile
|
||||
);
|
||||
|
||||
let mutex_result =
|
||||
tokio::time::timeout(Duration::from_millis(100), PROFILE_UPDATE_MUTEX.lock()).await;
|
||||
|
||||
let _guard = match mutex_result {
|
||||
Ok(guard) => guard,
|
||||
Err(_) => {
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"检测到更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"强制获取锁以处理最新请求: {}",
|
||||
current_sequence
|
||||
);
|
||||
PROFILE_UPDATE_MUTEX.lock().await
|
||||
}
|
||||
};
|
||||
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 保存当前配置,以便在验证失败时恢复
|
||||
let current_profile = Config::profiles().latest().current.clone();
|
||||
let current_profile = Config::profiles().latest_ref().current.clone();
|
||||
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
|
||||
|
||||
// 如果要切换配置,先检查目标配置文件是否有语法错误
|
||||
@@ -120,7 +331,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
// 获取目标配置文件路径
|
||||
let config_file_result = {
|
||||
let profiles_config = Config::profiles();
|
||||
let profiles_data = profiles_config.latest();
|
||||
let profiles_data = profiles_config.latest_ref();
|
||||
match profiles_data.get_item(new_profile) {
|
||||
Ok(item) => {
|
||||
if let Some(file) = &item.file {
|
||||
@@ -163,7 +374,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
|
||||
match file_read_result {
|
||||
Ok(Ok(content)) => {
|
||||
let yaml_parse_result = tokio::task::spawn_blocking(move || {
|
||||
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
|
||||
serde_yaml::from_str::<serde_yaml::Value>(&content)
|
||||
})
|
||||
.await;
|
||||
@@ -173,7 +384,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!(" {}", err);
|
||||
let error_msg = format!(" {err}");
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
@@ -188,7 +399,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
return Ok(false);
|
||||
}
|
||||
Err(join_err) => {
|
||||
let error_msg = format!("YAML解析任务失败: {}", join_err);
|
||||
let error_msg = format!("YAML解析任务失败: {join_err}");
|
||||
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_parse_error",
|
||||
@@ -199,7 +410,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!("无法读取目标配置文件: {}", err);
|
||||
let error_msg = format!("无法读取目标配置文件: {err}");
|
||||
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_read_error",
|
||||
@@ -221,14 +432,68 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查请求有效性
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if let Some(ref profile) = target_profile {
|
||||
*CURRENT_PROCESSING_PROFILE.write().await = Some(profile.clone());
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"设置当前处理profile: {}, 序列号: {}",
|
||||
profile,
|
||||
current_sequence
|
||||
);
|
||||
}
|
||||
|
||||
// 更新profiles配置
|
||||
logging!(info, Type::Cmd, true, "正在更新配置草稿");
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"正在更新配置草稿,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
|
||||
let current_value = profiles.current.clone();
|
||||
|
||||
let _ = Config::profiles().draft().patch_config(profiles);
|
||||
let _ = Config::profiles().draft_mut().patch_config(profiles);
|
||||
|
||||
// 在调用内核前再次验证请求有效性
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 为配置更新添加超时保护
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"开始内核配置更新,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
let update_result = tokio::time::timeout(
|
||||
Duration::from_secs(30), // 30秒超时
|
||||
CoreManager::global().update_config(),
|
||||
@@ -238,31 +503,68 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
// 更新配置并进行验证
|
||||
match update_result {
|
||||
Ok(Ok((true, _))) => {
|
||||
logging!(info, Type::Cmd, true, "配置更新成功");
|
||||
// 内核操作完成后再次检查请求有效性
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"配置更新成功,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = super::proxy::force_refresh_proxies().await {
|
||||
log::warn!(target: "app", "强制刷新代理缓存失败: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = Tray::global().update_tooltip() {
|
||||
log::warn!(target: "app", "异步更新托盘提示失败: {}", e);
|
||||
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = Tray::global().update_menu() {
|
||||
log::warn!(target: "app", "异步更新托盘菜单失败: {}", e);
|
||||
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
|
||||
}
|
||||
|
||||
// 保存配置文件
|
||||
if let Err(e) = Config::profiles().data().save_file() {
|
||||
log::warn!(target: "app", "异步保存配置文件失败: {}", e);
|
||||
if let Err(e) = Config::profiles().data_mut().save_file() {
|
||||
log::warn!(target: "app", "异步保存配置文件失败: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
// 立即通知前端配置变更
|
||||
if let Some(current) = ¤t_value {
|
||||
logging!(info, Type::Cmd, true, "向前端发送配置变更事件: {}", current);
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"向前端发送配置变更事件: {}, 序列号: {}",
|
||||
current,
|
||||
current_sequence
|
||||
);
|
||||
handle::Handle::notify_profile_changed(current.clone());
|
||||
}
|
||||
|
||||
cleanup_processing_state(current_sequence, "配置切换完成").await;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
Ok(Ok((false, error_msg))) => {
|
||||
@@ -282,12 +584,16 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
items: None,
|
||||
};
|
||||
// 静默恢复,不触发验证
|
||||
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||
wrap_err!({
|
||||
Config::profiles()
|
||||
.draft_mut()
|
||||
.patch_config(restore_profiles)
|
||||
})?;
|
||||
Config::profiles().apply();
|
||||
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = Config::profiles().data().save_file() {
|
||||
log::warn!(target: "app", "异步保存恢复配置文件失败: {}", e);
|
||||
if let Err(e) = Config::profiles().data_mut().save_file() {
|
||||
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -296,18 +602,38 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
|
||||
// 发送验证错误通知
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
|
||||
cleanup_processing_state(current_sequence, "配置验证失败").await;
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging!(warn, Type::Cmd, true, "更新过程发生错误: {}", e);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"更新过程发生错误: {}, 序列号: {}",
|
||||
e,
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||
|
||||
cleanup_processing_state(current_sequence, "更新过程错误").await;
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
Err(_) => {
|
||||
// 超时处理
|
||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||
logging!(error, Type::Cmd, true, "{}", timeout_msg);
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"{}, 序列号: {}",
|
||||
timeout_msg,
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
|
||||
if let Some(prev_profile) = current_profile {
|
||||
@@ -315,18 +641,26 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"超时后尝试恢复到之前的配置: {}",
|
||||
prev_profile
|
||||
"超时后尝试恢复到之前的配置: {}, 序列号: {}",
|
||||
prev_profile,
|
||||
current_sequence
|
||||
);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
items: None,
|
||||
};
|
||||
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||
wrap_err!({
|
||||
Config::profiles()
|
||||
.draft_mut()
|
||||
.patch_config(restore_profiles)
|
||||
})?;
|
||||
Config::profiles().apply();
|
||||
}
|
||||
|
||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
||||
|
||||
cleanup_processing_state(current_sequence, "配置更新超时").await;
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
@@ -352,7 +686,7 @@ pub async fn patch_profiles_config_by_profile_index(
|
||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
// 保存修改前检查是否有更新 update_interval
|
||||
let update_interval_changed =
|
||||
if let Ok(old_profile) = Config::profiles().latest().get_item(&index) {
|
||||
if let Ok(old_profile) = Config::profiles().latest_ref().get_item(&index) {
|
||||
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
|
||||
let new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
|
||||
old_interval != new_interval
|
||||
@@ -361,7 +695,9 @@ pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
};
|
||||
|
||||
// 保存修改
|
||||
wrap_err!(Config::profiles().data().patch_item(index.clone(), profile))?;
|
||||
wrap_err!(Config::profiles()
|
||||
.data_mut()
|
||||
.patch_item(index.clone(), profile))?;
|
||||
|
||||
// 如果更新间隔变更,异步刷新定时器
|
||||
if update_interval_changed {
|
||||
@@ -384,7 +720,7 @@ pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
#[tauri::command]
|
||||
pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
|
||||
let file = {
|
||||
wrap_err!(Config::profiles().latest().get_item(&index))?
|
||||
wrap_err!(Config::profiles().latest_ref().get_item(&index))?
|
||||
.file
|
||||
.clone()
|
||||
.ok_or("the file field is null")
|
||||
@@ -402,7 +738,7 @@ pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
|
||||
#[tauri::command]
|
||||
pub fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let profiles = profiles.latest_ref();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
let data = wrap_err!(item.read_file())?;
|
||||
Ok(data)
|
||||
|
||||
+35
-59
@@ -1,71 +1,47 @@
|
||||
use super::CmdResult;
|
||||
use crate::module::mihomo::MihomoManager;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use crate::{ipc::IpcManager, logging, state::proxy::ProxyRequestCache, utils::logging::Type};
|
||||
use std::time::Duration;
|
||||
|
||||
static LAST_REFRESH_TIME: Lazy<Mutex<Option<Instant>>> = Lazy::new(|| Mutex::new(None));
|
||||
static IS_REFRESHING: AtomicBool = AtomicBool::new(false);
|
||||
const REFRESH_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
let manager = MihomoManager::global();
|
||||
let manager = IpcManager::global();
|
||||
let cache = ProxyRequestCache::global();
|
||||
let key = ProxyRequestCache::make_key("proxies", "default");
|
||||
let value = cache
|
||||
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
|
||||
manager.get_proxies().await.unwrap_or_else(|e| {
|
||||
logging!(error, Type::Cmd, "Failed to fetch proxies: {e}");
|
||||
serde_json::Value::Object(serde_json::Map::new())
|
||||
})
|
||||
})
|
||||
.await;
|
||||
Ok((*value).clone())
|
||||
}
|
||||
|
||||
manager
|
||||
.refresh_proxies()
|
||||
.await
|
||||
.map(|_| manager.get_proxies())
|
||||
.or_else(|_| Ok(manager.get_proxies()))
|
||||
/// 强制刷新代理缓存用于profile切换
|
||||
#[tauri::command]
|
||||
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
||||
let cache = ProxyRequestCache::global();
|
||||
let key = ProxyRequestCache::make_key("proxies", "default");
|
||||
cache.map.remove(&key);
|
||||
get_proxies().await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||
let manager = MihomoManager::global();
|
||||
let cached_data = manager.get_providers_proxies();
|
||||
|
||||
let safe_data = if cached_data.is_null() {
|
||||
serde_json::json!({
|
||||
"providers": {}
|
||||
let manager = IpcManager::global();
|
||||
let cache = ProxyRequestCache::global();
|
||||
let key = ProxyRequestCache::make_key("providers", "default");
|
||||
let value = cache
|
||||
.get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
|
||||
manager.get_providers_proxies().await.unwrap_or_else(|e| {
|
||||
logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}");
|
||||
serde_json::Value::Object(serde_json::Map::new())
|
||||
})
|
||||
})
|
||||
} else {
|
||||
cached_data
|
||||
};
|
||||
|
||||
// 检查是否需要刷新
|
||||
let should_refresh = {
|
||||
let last_refresh = LAST_REFRESH_TIME.lock();
|
||||
match *last_refresh {
|
||||
Some(last_time) => last_time.elapsed() > REFRESH_INTERVAL,
|
||||
None => true,
|
||||
}
|
||||
};
|
||||
|
||||
if should_refresh && !IS_REFRESHING.load(Ordering::Acquire) {
|
||||
IS_REFRESHING.store(true, Ordering::Release);
|
||||
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
let manager = MihomoManager::global();
|
||||
match manager.refresh_providers_proxies().await {
|
||||
Ok(_) => {
|
||||
log::debug!(target: "app", "providers_proxies静默后台刷新成功");
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "providers_proxies后台刷新失败: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut last_refresh = LAST_REFRESH_TIME.lock();
|
||||
*last_refresh = Some(Instant::now());
|
||||
}
|
||||
|
||||
IS_REFRESHING.store(false, Ordering::Release);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(safe_data)
|
||||
.await;
|
||||
Ok((*value).clone())
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@ use std::collections::HashMap;
|
||||
/// 获取运行时配置
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
Ok(Config::runtime().latest().config.clone())
|
||||
Ok(Config::runtime().latest_ref().config.clone())
|
||||
}
|
||||
|
||||
/// 获取运行时YAML配置
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime();
|
||||
let runtime = runtime.latest();
|
||||
let runtime = runtime.latest_ref();
|
||||
let config = runtime.config.as_ref();
|
||||
wrap_err!(config
|
||||
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||
@@ -26,11 +26,11 @@ pub fn get_runtime_yaml() -> CmdResult<String> {
|
||||
/// 获取运行时存在的键
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
Ok(Config::runtime().latest().exists_keys.clone())
|
||||
Ok(Config::runtime().latest_ref().exists_keys.clone())
|
||||
}
|
||||
|
||||
/// 获取运行时日志
|
||||
#[tauri::command]
|
||||
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
Ok(Config::runtime().latest().chain_logs.clone())
|
||||
Ok(Config::runtime().latest_ref().chain_logs.clone())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
use super::CmdResult;
|
||||
use crate::{config::*, core::*, utils::dirs, wrap_err};
|
||||
use crate::{
|
||||
config::*,
|
||||
core::*,
|
||||
logging,
|
||||
utils::{dirs, logging::Type},
|
||||
wrap_err,
|
||||
};
|
||||
use std::fs;
|
||||
|
||||
/// 保存profiles的配置
|
||||
@@ -12,7 +18,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
// 在异步操作前完成所有文件操作
|
||||
let (file_path, original_content, is_merge_file) = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles_guard = profiles.latest();
|
||||
let profiles_guard = profiles.latest_ref();
|
||||
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||
// 确定是否为merge类型文件
|
||||
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
|
||||
@@ -23,32 +29,64 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
};
|
||||
|
||||
// 保存新的配置文件
|
||||
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
||||
let file_data = file_data.ok_or("file_data is None")?;
|
||||
wrap_err!(fs::write(&file_path, &file_data))?;
|
||||
|
||||
let file_path_str = file_path.to_string_lossy().to_string();
|
||||
println!(
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}",
|
||||
file_path_str, is_merge_file
|
||||
file_path_str,
|
||||
is_merge_file
|
||||
);
|
||||
|
||||
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
|
||||
if is_merge_file {
|
||||
println!("[cmd配置save] 检测到merge文件,只进行语法验证");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] 检测到merge文件,只进行语法验证"
|
||||
);
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path_str, Some(true))
|
||||
.await
|
||||
{
|
||||
Ok((true, _)) => {
|
||||
println!("[cmd配置save] merge文件语法验证通过");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] merge文件语法验证通过"
|
||||
);
|
||||
// 成功后尝试更新整体配置
|
||||
if let Err(e) = CoreManager::global().update_config().await {
|
||||
println!("[cmd配置save] 更新整体配置时发生错误: {}", e);
|
||||
log::warn!(target: "app", "更新整体配置时发生错误: {}", e);
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
// 配置更新成功,刷新前端
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] 更新整体配置时发生错误: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[cmd配置save] merge文件语法验证失败: {}", error_msg);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] merge文件语法验证失败: {}",
|
||||
error_msg
|
||||
);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
// 发送合并文件专用错误通知
|
||||
@@ -57,7 +95,13 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] 验证过程发生错误: {}",
|
||||
e
|
||||
);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
return Err(e.to_string());
|
||||
@@ -71,11 +115,17 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
.await
|
||||
{
|
||||
Ok((true, _)) => {
|
||||
println!("[cmd配置save] 验证成功");
|
||||
logging!(info, Type::Config, true, "[cmd配置save] 验证成功");
|
||||
Ok(())
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[cmd配置save] 验证失败: {}", error_msg);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] 验证失败: {}",
|
||||
error_msg
|
||||
);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
|
||||
@@ -90,24 +140,42 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|
||||
|| (!file_path_str.ends_with(".js") && !is_script_error)
|
||||
{
|
||||
// 普通YAML错误使用YAML通知处理
|
||||
println!("[cmd配置save] YAML配置文件验证失败,发送通知");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] YAML配置文件验证失败,发送通知"
|
||||
);
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
||||
} else if is_script_error {
|
||||
// 脚本错误使用专门的通知处理
|
||||
println!("[cmd配置save] 脚本文件验证失败,发送通知");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] 脚本文件验证失败,发送通知"
|
||||
);
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
||||
} else {
|
||||
// 普通配置错误使用一般通知
|
||||
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] 其他类型验证失败,发送一般通知"
|
||||
);
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"[cmd配置save] 验证过程发生错误: {}",
|
||||
e
|
||||
);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
Err(e.to_string())
|
||||
|
||||
@@ -3,13 +3,15 @@ use crate::{
|
||||
core::{service, CoreManager},
|
||||
utils::i18n::t,
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
||||
async fn execute_service_operation(
|
||||
service_op: impl std::future::Future<Output = Result<(), impl ToString + std::fmt::Debug>>,
|
||||
op_type: &str,
|
||||
) -> CmdResult {
|
||||
if service_op.await.is_err() {
|
||||
let emsg = format!("{} {} failed", op_type, "Service");
|
||||
async fn execute_service_operation_sync<F, E>(service_op: F, op_type: &str) -> CmdResult
|
||||
where
|
||||
F: FnOnce() -> Result<(), E>,
|
||||
E: ToString + std::fmt::Debug,
|
||||
{
|
||||
if let Err(e) = service_op() {
|
||||
let emsg = format!("{} {} failed: {}", op_type, "Service", e.to_string());
|
||||
return Err(t(emsg.as_str()));
|
||||
}
|
||||
if CoreManager::global().restart_core().await.is_err() {
|
||||
@@ -21,22 +23,22 @@ async fn execute_service_operation(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_service() -> CmdResult {
|
||||
execute_service_operation(service::install_service(), "Install").await
|
||||
execute_service_operation_sync(service::install_service, "Install").await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_service() -> CmdResult {
|
||||
execute_service_operation(service::uninstall_service(), "Uninstall").await
|
||||
execute_service_operation_sync(service::uninstall_service, "Uninstall").await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reinstall_service() -> CmdResult {
|
||||
execute_service_operation(service::reinstall_service(), "Reinstall").await
|
||||
execute_service_operation_sync(service::reinstall_service, "Reinstall").await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn repair_service() -> CmdResult {
|
||||
execute_service_operation(service::force_reinstall_service(), "Repair").await
|
||||
execute_service_operation_sync(service::force_reinstall_service, "Repair").await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
core::{handle, CoreManager},
|
||||
logging,
|
||||
module::sysinfo::PlatformSpecification,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
@@ -23,28 +25,30 @@ static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||
let sysinfo = PlatformSpecification::new_async().await;
|
||||
let info = format!("{:?}", sysinfo);
|
||||
let sysinfo = PlatformSpecification::new_sync();
|
||||
let info = format!("{sysinfo:?}");
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or("Failed to get app handle")?;
|
||||
let cliboard = app_handle.clipboard();
|
||||
if cliboard.write_text(info).is_err() {
|
||||
log::error!(target: "app", "Failed to write to clipboard");
|
||||
logging!(error, Type::System, "Failed to write to clipboard");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_system_info() -> CmdResult<String> {
|
||||
let sysinfo = PlatformSpecification::new_async().await;
|
||||
let info = format!("{:?}", sysinfo);
|
||||
let sysinfo = PlatformSpecification::new_sync();
|
||||
let info = format!("{sysinfo:?}");
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// 获取当前内核运行模式
|
||||
#[tauri::command]
|
||||
pub async fn get_running_mode() -> Result<String, String> {
|
||||
Ok(CoreManager::global().get_running_mode().await.to_string())
|
||||
Ok(CoreManager::global().get_running_mode().to_string())
|
||||
}
|
||||
|
||||
/// 获取应用的运行时间(毫秒)
|
||||
|
||||
@@ -6,8 +6,8 @@ mod platform {
|
||||
use super::CmdResult;
|
||||
use crate::{core::win_uwp, wrap_err};
|
||||
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
wrap_err!(win_uwp::invoke_uwptools().await)
|
||||
pub fn invoke_uwp_tool() -> CmdResult {
|
||||
wrap_err!(win_uwp::invoke_uwptools())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ mod platform {
|
||||
mod platform {
|
||||
use super::CmdResult;
|
||||
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
pub fn invoke_uwp_tool() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -24,5 +24,5 @@ mod platform {
|
||||
/// Command exposed to Tauri
|
||||
#[tauri::command]
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
platform::invoke_uwp_tool().await
|
||||
platform::invoke_uwp_tool()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::CmdResult;
|
||||
use crate::core::*;
|
||||
use crate::{core::*, logging, utils::logging::Type};
|
||||
|
||||
/// 发送脚本验证通知消息
|
||||
#[tauri::command]
|
||||
@@ -28,7 +28,14 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
||||
"config_validate::script_error"
|
||||
};
|
||||
|
||||
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"{} 验证失败: {}",
|
||||
file_type,
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message(status, error_msg);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +43,7 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
||||
/// 验证指定脚本文件
|
||||
#[tauri::command]
|
||||
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||
log::info!(target: "app", "验证脚本文件: {}", file_path);
|
||||
logging!(info, Type::Config, true, "验证脚本文件: {}", file_path);
|
||||
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path, None)
|
||||
@@ -48,7 +55,13 @@ pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg);
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"验证脚本文件过程发生错误: {}",
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
|
||||
Ok(false)
|
||||
}
|
||||
@@ -60,7 +73,14 @@ pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||
pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
||||
if !result.0 {
|
||||
let error_msg = &result.1;
|
||||
println!("[通知] 处理{}验证错误: {}", file_type, error_msg);
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[通知] 处理{}验证错误: {}",
|
||||
file_type,
|
||||
error_msg
|
||||
);
|
||||
|
||||
// 检查是否为merge文件
|
||||
let is_merge_file = file_type.contains("合并");
|
||||
@@ -97,8 +117,22 @@ pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
||||
}
|
||||
};
|
||||
|
||||
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||
println!("[通知] 发送通知: status={}, msg={}", status, error_msg);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"{} 验证失败: {}",
|
||||
file_type,
|
||||
error_msg
|
||||
);
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"[通知] 发送通知: status={}, msg={}",
|
||||
status,
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message(status, error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ use crate::{config::*, feat, wrap_err};
|
||||
#[tauri::command]
|
||||
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||
let verge = Config::verge();
|
||||
let verge_data = verge.data().clone();
|
||||
Ok(IVergeResponse::from(verge_data))
|
||||
let verge_data = verge.latest_ref().clone();
|
||||
Ok(IVergeResponse::from(*verge_data))
|
||||
}
|
||||
|
||||
/// 修改Verge配置
|
||||
|
||||
@@ -11,10 +11,10 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
|
||||
webdav_password: Some(password),
|
||||
..IVerge::default()
|
||||
};
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
Config::verge().draft_mut().patch_config(patch.clone());
|
||||
Config::verge().apply();
|
||||
Config::verge()
|
||||
.data()
|
||||
.latest_ref()
|
||||
.save_file()
|
||||
.map_err(|err| err.to_string())?;
|
||||
core::backup::WebDavClient::global().reset();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::config::Config;
|
||||
use crate::utils::dirs::{ipc_path, path_to_str};
|
||||
use crate::utils::{dirs, help};
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -17,7 +19,9 @@ impl IClashTemp {
|
||||
Ok(mut map) => {
|
||||
template.0.keys().for_each(|key| {
|
||||
if !map.contains_key(key) {
|
||||
map.insert(key.clone(), template.0.get(key).unwrap().clone());
|
||||
if let Some(value) = template.0.get(key) {
|
||||
map.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
// 确保 secret 字段存在且不为空
|
||||
@@ -57,12 +61,24 @@ impl IClashTemp {
|
||||
map.insert("ipv6".into(), true.into());
|
||||
map.insert("mode".into(), "rule".into());
|
||||
map.insert("external-controller".into(), "127.0.0.1:9097".into());
|
||||
#[cfg(unix)]
|
||||
map.insert(
|
||||
"external-controller-unix".into(),
|
||||
Self::guard_external_controller_ipc().into(),
|
||||
);
|
||||
#[cfg(windows)]
|
||||
map.insert(
|
||||
"external-controller-pipe".into(),
|
||||
Self::guard_external_controller_ipc().into(),
|
||||
);
|
||||
cors_map.insert("allow-private-network".into(), true.into());
|
||||
cors_map.insert(
|
||||
"allow-origins".into(),
|
||||
vec![
|
||||
"tauri://localhost",
|
||||
"http://tauri.localhost",
|
||||
// Only enable this in dev mode
|
||||
#[cfg(feature = "verge-dev")]
|
||||
"http://localhost:3000",
|
||||
"https://yacd.metacubex.one",
|
||||
"https://metacubex.github.io",
|
||||
@@ -85,7 +101,12 @@ impl IClashTemp {
|
||||
let mixed_port = Self::guard_mixed_port(&config);
|
||||
let socks_port = Self::guard_socks_port(&config);
|
||||
let port = Self::guard_port(&config);
|
||||
let ctrl = Self::guard_server_ctrl(&config);
|
||||
let ctrl = Self::guard_external_controller(&config);
|
||||
#[cfg(unix)]
|
||||
let external_controller_unix = Self::guard_external_controller_ipc();
|
||||
#[cfg(windows)]
|
||||
let external_controller_pipe = Self::guard_external_controller_ipc();
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
config.insert("redir-port".into(), redir_port.into());
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -95,23 +116,16 @@ impl IClashTemp {
|
||||
config.insert("port".into(), port.into());
|
||||
config.insert("external-controller".into(), ctrl.into());
|
||||
|
||||
// 强制覆盖 external-controller-cors 字段,允许本地和 tauri 前端
|
||||
let mut cors_map = Mapping::new();
|
||||
cors_map.insert("allow-private-network".into(), true.into());
|
||||
cors_map.insert(
|
||||
"allow-origins".into(),
|
||||
vec![
|
||||
"tauri://localhost",
|
||||
"http://tauri.localhost",
|
||||
"http://localhost:3000",
|
||||
"https://yacd.metacubex.one",
|
||||
"https://metacubex.github.io",
|
||||
"https://board.zash.run.place",
|
||||
]
|
||||
.into(),
|
||||
#[cfg(unix)]
|
||||
config.insert(
|
||||
"external-controller-unix".into(),
|
||||
external_controller_unix.into(),
|
||||
);
|
||||
#[cfg(windows)]
|
||||
config.insert(
|
||||
"external-controller-pipe".into(),
|
||||
external_controller_pipe.into(),
|
||||
);
|
||||
config.insert("external-controller-cors".into(), cors_map.into());
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
@@ -260,6 +274,26 @@ impl IClashTemp {
|
||||
.unwrap_or("127.0.0.1:9097".into())
|
||||
}
|
||||
|
||||
pub fn guard_external_controller(config: &Mapping) -> String {
|
||||
// 在初始化阶段,直接返回配置中的值,不进行额外检查
|
||||
// 这样可以避免在配置加载期间的循环依赖
|
||||
Self::guard_server_ctrl(config)
|
||||
}
|
||||
|
||||
pub fn guard_external_controller_with_setting(config: &Mapping) -> String {
|
||||
// 检查 enable_external_controller 设置,用于运行时配置生成
|
||||
let enable_external_controller = Config::verge()
|
||||
.latest_ref()
|
||||
.enable_external_controller
|
||||
.unwrap_or(false);
|
||||
|
||||
if enable_external_controller {
|
||||
Self::guard_server_ctrl(config)
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn guard_client_ctrl(config: &Mapping) -> String {
|
||||
let value = Self::guard_server_ctrl(config);
|
||||
match SocketAddr::from_str(value.as_str()) {
|
||||
@@ -272,6 +306,17 @@ impl IClashTemp {
|
||||
Err(_) => "127.0.0.1:9097".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn guard_external_controller_ipc() -> String {
|
||||
// 总是使用当前的 IPC 路径,确保配置文件与运行时路径一致
|
||||
ipc_path()
|
||||
.ok()
|
||||
.and_then(|path| path_to_str(&path).ok().map(|s| s.to_string()))
|
||||
.unwrap_or_else(|| {
|
||||
log::error!(target: "app", "Failed to get IPC path, using default");
|
||||
"127.0.0.1:9090".to_string()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
|
||||
@@ -15,10 +15,10 @@ pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||
|
||||
pub struct Config {
|
||||
clash_config: Draft<IClashTemp>,
|
||||
verge_config: Draft<IVerge>,
|
||||
profiles_config: Draft<IProfiles>,
|
||||
runtime_config: Draft<IRuntime>,
|
||||
clash_config: Draft<Box<IClashTemp>>,
|
||||
verge_config: Draft<Box<IVerge>>,
|
||||
profiles_config: Draft<Box<IProfiles>>,
|
||||
runtime_config: Draft<Box<IRuntime>>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -26,49 +26,53 @@ impl Config {
|
||||
static CONFIG: OnceCell<Config> = OnceCell::new();
|
||||
|
||||
CONFIG.get_or_init(|| Config {
|
||||
clash_config: Draft::from(IClashTemp::new()),
|
||||
verge_config: Draft::from(IVerge::new()),
|
||||
profiles_config: Draft::from(IProfiles::new()),
|
||||
runtime_config: Draft::from(IRuntime::new()),
|
||||
clash_config: Draft::from(Box::new(IClashTemp::new())),
|
||||
verge_config: Draft::from(Box::new(IVerge::new())),
|
||||
profiles_config: Draft::from(Box::new(IProfiles::new())),
|
||||
runtime_config: Draft::from(Box::new(IRuntime::new())),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clash() -> Draft<IClashTemp> {
|
||||
pub fn clash() -> Draft<Box<IClashTemp>> {
|
||||
Self::global().clash_config.clone()
|
||||
}
|
||||
|
||||
pub fn verge() -> Draft<IVerge> {
|
||||
pub fn verge() -> Draft<Box<IVerge>> {
|
||||
Self::global().verge_config.clone()
|
||||
}
|
||||
|
||||
pub fn profiles() -> Draft<IProfiles> {
|
||||
pub fn profiles() -> Draft<Box<IProfiles>> {
|
||||
Self::global().profiles_config.clone()
|
||||
}
|
||||
|
||||
pub fn runtime() -> Draft<IRuntime> {
|
||||
pub fn runtime() -> Draft<Box<IRuntime>> {
|
||||
Self::global().runtime_config.clone()
|
||||
}
|
||||
|
||||
/// 初始化订阅
|
||||
pub async fn init_config() -> Result<()> {
|
||||
if Self::profiles()
|
||||
.data()
|
||||
.latest_ref()
|
||||
.get_item(&"Merge".to_string())
|
||||
.is_err()
|
||||
{
|
||||
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?;
|
||||
Self::profiles().data().append_item(merge_item.clone())?;
|
||||
Self::profiles()
|
||||
.data_mut()
|
||||
.append_item(merge_item.clone())?;
|
||||
}
|
||||
if Self::profiles()
|
||||
.data()
|
||||
.latest_ref()
|
||||
.get_item(&"Script".to_string())
|
||||
.is_err()
|
||||
{
|
||||
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
|
||||
Self::profiles().data().append_item(script_item.clone())?;
|
||||
Self::profiles()
|
||||
.data_mut()
|
||||
.append_item(script_item.clone())?;
|
||||
}
|
||||
// 生成运行时配置
|
||||
if let Err(err) = Self::generate().await {
|
||||
if let Err(err) = Self::generate() {
|
||||
logging!(error, Type::Config, true, "生成运行时配置失败: {}", err);
|
||||
} else {
|
||||
logging!(info, Type::Config, true, "生成运行时配置成功");
|
||||
@@ -92,8 +96,7 @@ impl Config {
|
||||
error_msg
|
||||
);
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::boot_error", &error_msg)
|
||||
.await?;
|
||||
.use_default_config("config_validate::boot_error", &error_msg)?;
|
||||
Some(("config_validate::boot_error", error_msg))
|
||||
} else {
|
||||
logging!(info, Type::Config, true, "配置验证成功");
|
||||
@@ -103,16 +106,13 @@ impl Config {
|
||||
Err(err) => {
|
||||
logging!(warn, Type::Config, true, "验证进程执行失败: {}", err);
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::process_terminated", "")
|
||||
.await?;
|
||||
.use_default_config("config_validate::process_terminated", "")?;
|
||||
Some(("config_validate::process_terminated", String::new()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::error", "")
|
||||
.await?;
|
||||
CoreManager::global().use_default_config("config_validate::error", "")?;
|
||||
Some(("config_validate::error", String::new()))
|
||||
};
|
||||
|
||||
@@ -135,7 +135,7 @@ impl Config {
|
||||
};
|
||||
|
||||
let runtime = Config::runtime();
|
||||
let runtime = runtime.latest();
|
||||
let runtime = runtime.latest_ref();
|
||||
let config = runtime
|
||||
.config
|
||||
.as_ref()
|
||||
@@ -146,14 +146,14 @@ impl Config {
|
||||
}
|
||||
|
||||
/// 生成订阅存好
|
||||
pub async fn generate() -> Result<()> {
|
||||
let (config, exists_keys, logs) = enhance::enhance().await;
|
||||
pub fn generate() -> Result<()> {
|
||||
let (config, exists_keys, logs) = enhance::enhance();
|
||||
|
||||
*Config::runtime().draft() = IRuntime {
|
||||
*Config::runtime().draft_mut() = Box::new(IRuntime {
|
||||
config: Some(config),
|
||||
exists_keys,
|
||||
chain_logs: logs,
|
||||
};
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -164,3 +164,42 @@ pub enum ConfigType {
|
||||
Run,
|
||||
Check,
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::mem;
|
||||
|
||||
#[test]
|
||||
#[allow(unused_variables)]
|
||||
#[allow(clippy::expect_used)]
|
||||
fn test_prfitem_from_merge_size() {
|
||||
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))
|
||||
.expect("Failed to create merge item in test");
|
||||
let prfitem_size = mem::size_of_val(&merge_item);
|
||||
// Boxed version
|
||||
let boxed_merge_item = Box::new(merge_item);
|
||||
let box_prfitem_size = mem::size_of_val(&boxed_merge_item);
|
||||
// The size of Box<T> is always pointer-sized (usually 8 bytes on 64-bit)
|
||||
// assert_eq!(box_prfitem_size, mem::size_of::<Box<PrfItem>>());
|
||||
assert!(box_prfitem_size < prfitem_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(unused_variables)]
|
||||
fn test_draft_size_non_boxed() {
|
||||
let draft = Draft::from(IRuntime::new());
|
||||
let iruntime_size = std::mem::size_of_val(&draft);
|
||||
assert_eq!(iruntime_size, std::mem::size_of::<Draft<IRuntime>>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(unused_variables)]
|
||||
fn test_draft_size_boxed() {
|
||||
let draft = Draft::from(Box::new(IRuntime::new()));
|
||||
let box_iruntime_size = std::mem::size_of_val(&draft);
|
||||
assert_eq!(
|
||||
box_iruntime_size,
|
||||
std::mem::size_of::<Draft<Box<IRuntime>>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+128
-101
@@ -1,127 +1,154 @@
|
||||
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
|
||||
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::{
|
||||
MappedRwLockReadGuard, MappedRwLockWriteGuard, RwLock, RwLockReadGuard,
|
||||
RwLockUpgradableReadGuard, RwLockWriteGuard,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Draft<T: Clone + ToOwned> {
|
||||
inner: Arc<Mutex<(T, Option<T>)>>,
|
||||
inner: Arc<RwLock<(T, Option<T>)>>,
|
||||
}
|
||||
|
||||
macro_rules! draft_define {
|
||||
($id: ident) => {
|
||||
impl Draft<$id> {
|
||||
#[allow(unused)]
|
||||
pub fn data(&self) -> MappedMutexGuard<$id> {
|
||||
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
|
||||
}
|
||||
|
||||
pub fn latest(&self) -> MappedMutexGuard<$id> {
|
||||
MutexGuard::map(self.inner.lock(), |inner| {
|
||||
if inner.1.is_none() {
|
||||
&mut inner.0
|
||||
} else {
|
||||
inner.1.as_mut().unwrap()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn draft(&self) -> MappedMutexGuard<$id> {
|
||||
MutexGuard::map(self.inner.lock(), |inner| {
|
||||
if inner.1.is_none() {
|
||||
inner.1 = Some(inner.0.clone());
|
||||
}
|
||||
|
||||
inner.1.as_mut().unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply(&self) -> Option<$id> {
|
||||
let mut inner = self.inner.lock();
|
||||
|
||||
match inner.1.take() {
|
||||
Some(draft) => {
|
||||
let old_value = inner.0.to_owned();
|
||||
inner.0 = draft.to_owned();
|
||||
Some(old_value)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn discard(&self) -> Option<$id> {
|
||||
let mut inner = self.inner.lock();
|
||||
inner.1.take()
|
||||
}
|
||||
impl<T: Clone + ToOwned> From<T> for Draft<T> {
|
||||
fn from(data: T) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new((data, None))),
|
||||
}
|
||||
|
||||
impl From<$id> for Draft<$id> {
|
||||
fn from(data: $id) -> Self {
|
||||
Draft {
|
||||
inner: Arc::new(Mutex::new((data, None))),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// draft_define!(IClash);
|
||||
draft_define!(IClashTemp);
|
||||
draft_define!(IProfiles);
|
||||
draft_define!(IRuntime);
|
||||
draft_define!(IVerge);
|
||||
/// Implements draft management for `Box<T>`, allowing for safe concurrent editing and committing of draft data.
|
||||
/// # Type Parameters
|
||||
/// - `T`: The underlying data type, which must implement `Clone` and `ToOwned`.
|
||||
///
|
||||
/// # Methods
|
||||
/// - `data_mut`: Returns a mutable reference to the committed data.
|
||||
/// - `data_ref`: Returns an immutable reference to the committed data.
|
||||
/// - `draft_mut`: Creates or retrieves a mutable reference to the draft data, cloning the committed data if no draft exists.
|
||||
/// - `latest_ref`: Returns an immutable reference to the draft data if it exists, otherwise to the committed data.
|
||||
/// - `apply`: Commits the draft data, replacing the committed data and returning the old committed value if a draft existed.
|
||||
/// - `discard`: Discards the draft data and returns it if it existed.
|
||||
impl<T: Clone + ToOwned> Draft<Box<T>> {
|
||||
/// 可写正式数据
|
||||
pub fn data_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
|
||||
RwLockWriteGuard::map(self.inner.write(), |inner| &mut inner.0)
|
||||
}
|
||||
|
||||
/// 返回正式数据的只读视图(不包含草稿)
|
||||
pub fn data_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
|
||||
RwLockReadGuard::map(self.inner.read(), |inner| &inner.0)
|
||||
}
|
||||
|
||||
/// 创建或获取草稿并返回可写引用
|
||||
pub fn draft_mut(&self) -> MappedRwLockWriteGuard<'_, Box<T>> {
|
||||
let guard = self.inner.upgradable_read();
|
||||
if guard.1.is_none() {
|
||||
let mut guard = RwLockUpgradableReadGuard::upgrade(guard);
|
||||
guard.1 = Some(guard.0.clone());
|
||||
return RwLockWriteGuard::map(guard, |inner| {
|
||||
inner.1.as_mut().unwrap_or_else(|| {
|
||||
unreachable!("Draft was just created above, this should never fail")
|
||||
})
|
||||
});
|
||||
}
|
||||
// 已存在草稿,升级为写锁映射
|
||||
RwLockWriteGuard::map(RwLockUpgradableReadGuard::upgrade(guard), |inner| {
|
||||
inner
|
||||
.1
|
||||
.as_mut()
|
||||
.unwrap_or_else(|| unreachable!("Draft should exist when guard.1.is_some()"))
|
||||
})
|
||||
}
|
||||
|
||||
/// 零拷贝只读视图:返回草稿(若存在)或正式值
|
||||
pub fn latest_ref(&self) -> MappedRwLockReadGuard<'_, Box<T>> {
|
||||
RwLockReadGuard::map(self.inner.read(), |inner| {
|
||||
inner.1.as_ref().unwrap_or(&inner.0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 提交草稿,返回旧正式数据
|
||||
pub fn apply(&self) -> Option<Box<T>> {
|
||||
let mut inner = self.inner.write();
|
||||
inner
|
||||
.1
|
||||
.take()
|
||||
.map(|draft| std::mem::replace(&mut inner.0, draft))
|
||||
}
|
||||
|
||||
/// 丢弃草稿,返回被丢弃的草稿
|
||||
pub fn discard(&self) -> Option<Box<T>> {
|
||||
self.inner.write().1.take()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_draft() {
|
||||
let verge = IVerge {
|
||||
fn test_draft_box() {
|
||||
use super::IVerge;
|
||||
|
||||
// 1. 创建 Draft<Box<IVerge>>
|
||||
let verge = Box::new(IVerge {
|
||||
enable_auto_launch: Some(true),
|
||||
enable_tun_mode: Some(false),
|
||||
..IVerge::default()
|
||||
};
|
||||
|
||||
});
|
||||
let draft = Draft::from(verge);
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||
// 2. 读取正式数据(data_mut)
|
||||
{
|
||||
let data = draft.data_mut();
|
||||
assert_eq!(data.enable_auto_launch, Some(true));
|
||||
assert_eq!(data.enable_tun_mode, Some(false));
|
||||
}
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.draft().enable_tun_mode, Some(false));
|
||||
// 3. 初次获取草稿(draft_mut 会自动 clone 一份)
|
||||
{
|
||||
let draft_view = draft.draft_mut();
|
||||
assert_eq!(draft_view.enable_auto_launch, Some(true));
|
||||
assert_eq!(draft_view.enable_tun_mode, Some(false));
|
||||
}
|
||||
|
||||
let mut d = draft.draft();
|
||||
d.enable_auto_launch = Some(false);
|
||||
d.enable_tun_mode = Some(true);
|
||||
drop(d);
|
||||
// 4. 修改草稿
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(false);
|
||||
d.enable_tun_mode = Some(true);
|
||||
}
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||
// 正式数据未变
|
||||
assert_eq!(draft.data_mut().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.data_mut().enable_tun_mode, Some(false));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||
// 草稿已变
|
||||
{
|
||||
let latest = draft.latest_ref();
|
||||
assert_eq!(latest.enable_auto_launch, Some(false));
|
||||
assert_eq!(latest.enable_tun_mode, Some(true));
|
||||
}
|
||||
|
||||
assert_eq!(draft.latest().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.latest().enable_tun_mode, Some(true));
|
||||
// 5. 提交草稿
|
||||
assert!(draft.apply().is_some()); // 第一次提交应有返回
|
||||
assert!(draft.apply().is_none()); // 第二次提交返回 None
|
||||
|
||||
assert!(draft.apply().is_some());
|
||||
assert!(draft.apply().is_none());
|
||||
// 正式数据已更新
|
||||
{
|
||||
let data = draft.data_mut();
|
||||
assert_eq!(data.enable_auto_launch, Some(false));
|
||||
assert_eq!(data.enable_tun_mode, Some(true));
|
||||
}
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.data().enable_tun_mode, Some(true));
|
||||
// 6. 新建并修改下一轮草稿
|
||||
{
|
||||
let mut d = draft.draft_mut();
|
||||
d.enable_auto_launch = Some(true);
|
||||
}
|
||||
assert_eq!(draft.draft_mut().enable_auto_launch, Some(true));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||
// 7. 丢弃草稿
|
||||
assert!(draft.discard().is_some()); // 第一次丢弃返回 Some
|
||||
assert!(draft.discard().is_none()); // 再次丢弃返回 None
|
||||
|
||||
let mut d = draft.draft();
|
||||
d.enable_auto_launch = Some(true);
|
||||
drop(d);
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||
|
||||
assert!(draft.discard().is_some());
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||
|
||||
assert!(draft.discard().is_none());
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||
// 8. 草稿已被丢弃,新的 draft_mut() 会重新 clone
|
||||
assert_eq!(draft.draft_mut().enable_auto_launch, Some(false));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Encrypt data
|
||||
let ciphertext = cipher
|
||||
.encrypt(nonce.as_slice().into(), data.as_bytes())
|
||||
.map_err(|e| format!("Encryption failed: {}", e))?;
|
||||
.map_err(|e| format!("Encryption failed: {e}"))?;
|
||||
|
||||
// Concatenate nonce and ciphertext and encode them in base64
|
||||
let mut combined = nonce;
|
||||
@@ -46,7 +46,7 @@ pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error
|
||||
// Decrypt data
|
||||
let plaintext = cipher
|
||||
.decrypt(nonce.into(), ciphertext)
|
||||
.map_err(|e| format!("Decryption failed: {}", e))?;
|
||||
.map_err(|e| format!("Decryption failed: {e}"))?;
|
||||
|
||||
String::from_utf8(plaintext).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
@@ -147,12 +147,15 @@ impl PrfItem {
|
||||
bail!("type should not be null");
|
||||
}
|
||||
|
||||
match item.itype.unwrap().as_str() {
|
||||
let itype = item
|
||||
.itype
|
||||
.ok_or_else(|| anyhow::anyhow!("type should not be null"))?;
|
||||
match itype.as_str() {
|
||||
"remote" => {
|
||||
if item.url.is_none() {
|
||||
bail!("url should not be null");
|
||||
}
|
||||
let url = item.url.as_ref().unwrap().as_str();
|
||||
let url = item
|
||||
.url
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("url should not be null"))?;
|
||||
let name = item.name;
|
||||
let desc = item.desc;
|
||||
PrfItem::from_url(url, name, desc, item.option).await
|
||||
@@ -186,29 +189,37 @@ impl PrfItem {
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = PrfItem::from_merge(None)?;
|
||||
Config::profiles().data().append_item(merge_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(merge_item.clone())?;
|
||||
merge = merge_item.uid;
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = PrfItem::from_script(None)?;
|
||||
Config::profiles().data().append_item(script_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(script_item.clone())?;
|
||||
script = script_item.uid;
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = PrfItem::from_rules()?;
|
||||
Config::profiles().data().append_item(rules_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(rules_item.clone())?;
|
||||
rules = rules_item.uid;
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = PrfItem::from_proxies()?;
|
||||
Config::profiles()
|
||||
.data()
|
||||
.data_mut()
|
||||
.append_item(proxies_item.clone())?;
|
||||
proxies = proxies_item.uid;
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = PrfItem::from_groups()?;
|
||||
Config::profiles().data().append_item(groups_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(groups_item.clone())?;
|
||||
groups = groups_item.uid;
|
||||
}
|
||||
Ok(PrfItem {
|
||||
@@ -366,29 +377,37 @@ impl PrfItem {
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = PrfItem::from_merge(None)?;
|
||||
Config::profiles().data().append_item(merge_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(merge_item.clone())?;
|
||||
merge = merge_item.uid;
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = PrfItem::from_script(None)?;
|
||||
Config::profiles().data().append_item(script_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(script_item.clone())?;
|
||||
script = script_item.uid;
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = PrfItem::from_rules()?;
|
||||
Config::profiles().data().append_item(rules_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(rules_item.clone())?;
|
||||
rules = rules_item.uid;
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = PrfItem::from_proxies()?;
|
||||
Config::profiles()
|
||||
.data()
|
||||
.data_mut()
|
||||
.append_item(proxies_item.clone())?;
|
||||
proxies = proxies_item.uid;
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = PrfItem::from_groups()?;
|
||||
Config::profiles().data().append_item(groups_item.clone())?;
|
||||
Config::profiles()
|
||||
.data_mut()
|
||||
.append_item(groups_item.clone())?;
|
||||
groups = groups_item.uid;
|
||||
}
|
||||
|
||||
@@ -533,22 +552,20 @@ impl PrfItem {
|
||||
|
||||
/// get the file data
|
||||
pub fn read_file(&self) -> Result<String> {
|
||||
if self.file.is_none() {
|
||||
bail!("could not find the file");
|
||||
}
|
||||
|
||||
let file = self.file.clone().unwrap();
|
||||
let file = self
|
||||
.file
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
|
||||
let path = dirs::app_profiles_dir()?.join(file);
|
||||
fs::read_to_string(path).context("failed to read the file")
|
||||
}
|
||||
|
||||
/// save the file data
|
||||
pub fn save_file(&self, data: String) -> Result<()> {
|
||||
if self.file.is_none() {
|
||||
bail!("could not find the file");
|
||||
}
|
||||
|
||||
let file = self.file.clone().unwrap();
|
||||
let file = self
|
||||
.file
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
|
||||
let path = dirs::app_profiles_dir()?.join(file);
|
||||
fs::write(path, data.as_bytes()).context("failed to save the file")
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::utils::{dirs, help};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use std::{fs, io::Write};
|
||||
use std::{collections::HashSet, fs, io::Write};
|
||||
|
||||
/// Define the `profiles.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -15,6 +15,14 @@ pub struct IProfiles {
|
||||
pub items: Option<Vec<PrfItem>>,
|
||||
}
|
||||
|
||||
/// 清理结果
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CleanupResult {
|
||||
pub total_files: usize,
|
||||
pub deleted_files: Vec<String>,
|
||||
pub failed_deletions: Vec<String>,
|
||||
}
|
||||
|
||||
macro_rules! patch {
|
||||
($lv: expr, $rv: expr, $key: tt) => {
|
||||
if ($rv.$key).is_some() {
|
||||
@@ -69,10 +77,11 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
if let Some(current) = patch.current {
|
||||
let items = self.items.as_ref().unwrap();
|
||||
let some_uid = Some(current);
|
||||
if items.iter().any(|e| e.uid == some_uid) {
|
||||
self.current = some_uid;
|
||||
if let Some(items) = self.items.as_ref() {
|
||||
let some_uid = Some(current);
|
||||
if items.iter().any(|e| e.uid == some_uid) {
|
||||
self.current = some_uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,13 +128,15 @@ impl IProfiles {
|
||||
bail!("the file should not be null");
|
||||
}
|
||||
|
||||
let file = item.file.clone().unwrap();
|
||||
let file = item.file.clone().ok_or_else(|| {
|
||||
anyhow::anyhow!("file field is required when file_data is provided")
|
||||
})?;
|
||||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||
.with_context(|| format!("failed to create file \"{file}\""))?
|
||||
.write(file_data.as_bytes())
|
||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||
}
|
||||
|
||||
if self.current.is_none()
|
||||
@@ -160,11 +171,12 @@ impl IProfiles {
|
||||
}
|
||||
}
|
||||
|
||||
if old_index.is_none() || new_index.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let item = items.remove(old_index.unwrap());
|
||||
items.insert(new_index.unwrap(), item);
|
||||
let (old_idx, new_idx) = match (old_index, new_index) {
|
||||
(Some(old), Some(new)) => (old, new),
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let item = items.remove(old_idx);
|
||||
items.insert(new_idx, item);
|
||||
self.items = Some(items);
|
||||
self.save_file()
|
||||
}
|
||||
@@ -226,9 +238,9 @@ impl IProfiles {
|
||||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||
.with_context(|| format!("failed to create file \"{file}\""))?
|
||||
.write(file_data.as_bytes())
|
||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -485,4 +497,197 @@ impl IProfiles {
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// 以 app 中的 profile 列表为准,删除不再需要的文件
|
||||
pub fn cleanup_orphaned_files(&self) -> Result<CleanupResult> {
|
||||
let profiles_dir = dirs::app_profiles_dir()?;
|
||||
|
||||
if !profiles_dir.exists() {
|
||||
return Ok(CleanupResult {
|
||||
total_files: 0,
|
||||
deleted_files: vec![],
|
||||
failed_deletions: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有 active profile 的文件名集合
|
||||
let active_files = self.get_all_active_files();
|
||||
|
||||
// 添加全局扩展配置文件到保护列表
|
||||
let protected_files = self.get_protected_global_files();
|
||||
|
||||
// 扫描 profiles 目录下的所有文件
|
||||
let mut total_files = 0;
|
||||
let mut deleted_files = vec![];
|
||||
let mut failed_deletions = vec![];
|
||||
|
||||
for entry in std::fs::read_dir(&profiles_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
total_files += 1;
|
||||
|
||||
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if Self::is_profile_file(file_name) {
|
||||
// 检查是否为全局扩展文件
|
||||
if protected_files.contains(file_name) {
|
||||
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否为活跃文件
|
||||
if !active_files.contains(file_name) {
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(_) => {
|
||||
deleted_files.push(file_name.to_string());
|
||||
log::info!(target: "app", "已清理冗余文件: {file_name}");
|
||||
}
|
||||
Err(e) => {
|
||||
failed_deletions.push(format!("{file_name}: {e}"));
|
||||
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = CleanupResult {
|
||||
total_files,
|
||||
deleted_files,
|
||||
failed_deletions,
|
||||
};
|
||||
|
||||
log::info!(
|
||||
target: "app",
|
||||
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
|
||||
result.total_files,
|
||||
result.deleted_files.len(),
|
||||
result.failed_deletions.len()
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 不删除全局扩展配置
|
||||
fn get_protected_global_files(&self) -> HashSet<String> {
|
||||
let mut protected_files = HashSet::new();
|
||||
|
||||
protected_files.insert("Merge.yaml".to_string());
|
||||
protected_files.insert("Script.js".to_string());
|
||||
|
||||
protected_files
|
||||
}
|
||||
|
||||
/// 获取所有 active profile 关联的文件名
|
||||
fn get_all_active_files(&self) -> HashSet<String> {
|
||||
let mut active_files = HashSet::new();
|
||||
|
||||
if let Some(items) = &self.items {
|
||||
for item in items {
|
||||
// 收集所有类型 profile 的文件
|
||||
if let Some(file) = &item.file {
|
||||
active_files.insert(file.clone());
|
||||
}
|
||||
|
||||
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
|
||||
if let Some(itype) = &item.itype {
|
||||
if itype == "remote" || itype == "local" {
|
||||
if let Some(option) = &item.option {
|
||||
// 收集关联的扩展文件
|
||||
if let Some(merge_uid) = &option.merge {
|
||||
if let Ok(merge_item) = self.get_item(merge_uid) {
|
||||
if let Some(file) = &merge_item.file {
|
||||
active_files.insert(file.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(script_uid) = &option.script {
|
||||
if let Ok(script_item) = self.get_item(script_uid) {
|
||||
if let Some(file) = &script_item.file {
|
||||
active_files.insert(file.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rules_uid) = &option.rules {
|
||||
if let Ok(rules_item) = self.get_item(rules_uid) {
|
||||
if let Some(file) = &rules_item.file {
|
||||
active_files.insert(file.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(proxies_uid) = &option.proxies {
|
||||
if let Ok(proxies_item) = self.get_item(proxies_uid) {
|
||||
if let Some(file) = &proxies_item.file {
|
||||
active_files.insert(file.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(groups_uid) = &option.groups {
|
||||
if let Ok(groups_item) = self.get_item(groups_uid) {
|
||||
if let Some(file) = &groups_item.file {
|
||||
active_files.insert(file.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
active_files
|
||||
}
|
||||
|
||||
/// 检查文件名是否符合 profile 文件的命名规则
|
||||
fn is_profile_file(filename: &str) -> bool {
|
||||
// 匹配各种 profile 文件格式
|
||||
// R12345678.yaml (remote)
|
||||
// L12345678.yaml (local)
|
||||
// m12345678.yaml (merge)
|
||||
// s12345678.js (script)
|
||||
// r12345678.yaml (rules)
|
||||
// p12345678.yaml (proxies)
|
||||
// g12345678.yaml (groups)
|
||||
|
||||
let patterns = [
|
||||
r"^[RL][a-zA-Z0-9]+\.yaml$", // Remote/Local profiles
|
||||
r"^m[a-zA-Z0-9]+\.yaml$", // Merge files
|
||||
r"^s[a-zA-Z0-9]+\.js$", // Script files
|
||||
r"^[rpg][a-zA-Z0-9]+\.yaml$", // Rules/Proxies/Groups files
|
||||
];
|
||||
|
||||
patterns.iter().any(|pattern| {
|
||||
regex::Regex::new(pattern)
|
||||
.map(|re| re.is_match(filename))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn auto_cleanup(&self) -> Result<()> {
|
||||
match self.cleanup_orphaned_files() {
|
||||
Ok(result) => {
|
||||
if !result.deleted_files.is_empty() {
|
||||
log::info!(
|
||||
target: "app",
|
||||
"自动清理完成,删除了 {} 个冗余文件",
|
||||
result.deleted_files.len()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "自动清理失败: {e}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+107
-10
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC},
|
||||
utils::{dirs, help, i18n},
|
||||
logging,
|
||||
utils::{dirs, help, i18n, logging::Type},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use log::LevelFilter;
|
||||
@@ -137,9 +138,6 @@ pub struct IVerge {
|
||||
/// 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天
|
||||
pub auto_log_clean: Option<i32>,
|
||||
|
||||
/// 是否启用随机端口
|
||||
pub enable_random_port: Option<bool>,
|
||||
|
||||
/// verge 的各种 port 用于覆盖 clash 的各种 port
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub verge_redir_port: Option<u16>,
|
||||
@@ -200,6 +198,12 @@ pub struct IVerge {
|
||||
/// 自动进入轻量模式的延迟(分钟)
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
|
||||
/// 启用代理页面自动滚动
|
||||
pub enable_hover_jump_navigator: Option<bool>,
|
||||
|
||||
/// 启用外部控制器
|
||||
pub enable_external_controller: Option<bool>,
|
||||
|
||||
/// 服务状态跟踪
|
||||
pub service_state: Option<crate::core::service::ServiceState>,
|
||||
}
|
||||
@@ -229,6 +233,93 @@ pub struct IVergeTheme {
|
||||
}
|
||||
|
||||
impl IVerge {
|
||||
/// 有效的clash核心名称
|
||||
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
|
||||
|
||||
/// 验证并修正配置文件中的clash_core值
|
||||
pub fn validate_and_fix_config() -> Result<()> {
|
||||
let config_path = dirs::verge_path()?;
|
||||
let mut config = match help::read_yaml::<IVerge>(&config_path) {
|
||||
Ok(config) => config,
|
||||
Err(_) => Self::template(),
|
||||
};
|
||||
|
||||
let mut needs_fix = false;
|
||||
|
||||
if let Some(ref core) = config.clash_core {
|
||||
let core_str = core.trim();
|
||||
if core_str.is_empty() || !Self::VALID_CLASH_CORES.contains(&core_str) {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'verge-mihomo'",
|
||||
core
|
||||
);
|
||||
config.clash_core = Some("verge-mihomo".to_string());
|
||||
needs_fix = true;
|
||||
}
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"启动时发现未配置clash_core, 将设置为默认值 'verge-mihomo'"
|
||||
);
|
||||
config.clash_core = Some("verge-mihomo".to_string());
|
||||
needs_fix = true;
|
||||
}
|
||||
|
||||
// 修正后保存配置
|
||||
if needs_fix {
|
||||
logging!(info, Type::Config, true, "正在保存修正后的配置文件...");
|
||||
help::save_yaml(&config_path, &config, Some("# Clash Verge Config"))?;
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"配置文件修正完成,需要重新加载配置"
|
||||
);
|
||||
|
||||
Self::reload_config_after_fix(config)?;
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"clash_core配置验证通过: {:?}",
|
||||
config.clash_core
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 配置修正后重新加载配置
|
||||
fn reload_config_after_fix(updated_config: IVerge) -> Result<()> {
|
||||
use crate::config::Config;
|
||||
|
||||
let config_draft = Config::verge();
|
||||
*config_draft.draft_mut() = Box::new(updated_config.clone());
|
||||
config_draft.apply();
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
true,
|
||||
"内存配置已强制更新,新的clash_core: {:?}",
|
||||
updated_config.clash_core
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_valid_clash_core(&self) -> String {
|
||||
self.clash_core
|
||||
.clone()
|
||||
.unwrap_or_else(|| "verge-mihomo".to_string())
|
||||
}
|
||||
|
||||
fn get_system_language() -> String {
|
||||
let sys_lang = sys_locale::get_locale()
|
||||
.unwrap_or_else(|| String::from("en"))
|
||||
@@ -275,11 +366,11 @@ impl IVerge {
|
||||
tun_tray_icon: Some(false),
|
||||
enable_auto_launch: Some(false),
|
||||
enable_silent_start: Some(false),
|
||||
enable_hover_jump_navigator: Some(true),
|
||||
enable_system_proxy: Some(false),
|
||||
proxy_auto_config: Some(false),
|
||||
pac_file_content: Some(DEFAULT_PAC.into()),
|
||||
proxy_host: Some("127.0.0.1".into()),
|
||||
enable_random_port: Some(false),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
verge_redir_port: Some(7895),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -299,7 +390,7 @@ impl IVerge {
|
||||
auto_close_connection: Some(true),
|
||||
auto_check_update: Some(true),
|
||||
enable_builtin_enhanced: Some(true),
|
||||
auto_log_clean: Some(3),
|
||||
auto_log_clean: Some(2), // 1: 1天, 2: 7天, 3: 30天, 4: 90天
|
||||
webdav_url: None,
|
||||
webdav_username: None,
|
||||
webdav_password: None,
|
||||
@@ -311,6 +402,7 @@ impl IVerge {
|
||||
enable_dns_settings: Some(false),
|
||||
home_cards: None,
|
||||
service_state: None,
|
||||
enable_external_controller: Some(false),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
@@ -351,7 +443,7 @@ impl IVerge {
|
||||
patch!(enable_tun_mode);
|
||||
patch!(enable_auto_launch);
|
||||
patch!(enable_silent_start);
|
||||
patch!(enable_random_port);
|
||||
patch!(enable_hover_jump_navigator);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
patch!(verge_redir_port);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -398,6 +490,7 @@ impl IVerge {
|
||||
patch!(enable_dns_settings);
|
||||
patch!(home_cards);
|
||||
patch!(service_state);
|
||||
patch!(enable_external_controller);
|
||||
}
|
||||
|
||||
/// 在初始化前尝试拿到单例端口的值
|
||||
@@ -469,7 +562,6 @@ pub struct IVergeResponse {
|
||||
pub proxy_layout_column: Option<i32>,
|
||||
pub test_list: Option<Vec<IVergeTestItem>>,
|
||||
pub auto_log_clean: Option<i32>,
|
||||
pub enable_random_port: Option<bool>,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub verge_redir_port: Option<u16>,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -492,11 +584,15 @@ pub struct IVergeResponse {
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
pub home_cards: Option<serde_json::Value>,
|
||||
pub enable_hover_jump_navigator: Option<bool>,
|
||||
pub enable_external_controller: Option<bool>,
|
||||
pub service_state: Option<crate::core::service::ServiceState>,
|
||||
}
|
||||
|
||||
impl From<IVerge> for IVergeResponse {
|
||||
fn from(verge: IVerge) -> Self {
|
||||
// 先获取验证后的clash_core值,避免后续借用冲突
|
||||
let valid_clash_core = verge.get_valid_clash_core();
|
||||
Self {
|
||||
app_log_level: verge.app_log_level,
|
||||
language: verge.language,
|
||||
@@ -528,7 +624,7 @@ impl From<IVerge> for IVergeResponse {
|
||||
proxy_host: verge.proxy_host,
|
||||
theme_setting: verge.theme_setting,
|
||||
web_ui_list: verge.web_ui_list,
|
||||
clash_core: verge.clash_core,
|
||||
clash_core: Some(valid_clash_core),
|
||||
hotkeys: verge.hotkeys,
|
||||
auto_close_connection: verge.auto_close_connection,
|
||||
auto_check_update: verge.auto_check_update,
|
||||
@@ -538,7 +634,6 @@ impl From<IVerge> for IVergeResponse {
|
||||
proxy_layout_column: verge.proxy_layout_column,
|
||||
test_list: verge.test_list,
|
||||
auto_log_clean: verge.auto_log_clean,
|
||||
enable_random_port: verge.enable_random_port,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
verge_redir_port: verge.verge_redir_port,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -561,6 +656,8 @@ impl From<IVerge> for IVergeResponse {
|
||||
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
||||
enable_dns_settings: verge.enable_dns_settings,
|
||||
home_cards: verge.home_cards,
|
||||
enable_hover_jump_navigator: verge.enable_hover_jump_navigator,
|
||||
enable_external_controller: verge.enable_external_controller,
|
||||
service_state: verge.service_state,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::process::AsyncHandler;
|
||||
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: 7897,
|
||||
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<AsyncAutoproxy> {
|
||||
// Windows: 从注册表读取PAC配置
|
||||
AsyncHandler::spawn_blocking(move || -> Result<AsyncAutoproxy> {
|
||||
Self::get_pac_config_from_registry()
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_pac_config_from_registry() -> Result<AsyncAutoproxy> {
|
||||
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::<Vec<u16>>();
|
||||
|
||||
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::<Vec<u16>>();
|
||||
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::<Vec<u16>>();
|
||||
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={pac_url}, AutoDetect={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<AsyncAutoproxy> {
|
||||
// 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_enabled}, pac_url={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<AsyncAutoproxy> {
|
||||
// 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<AsyncSysproxy> {
|
||||
// Windows: 使用注册表直接读取代理设置
|
||||
AsyncHandler::spawn_blocking(move || -> Result<AsyncSysproxy> {
|
||||
Self::get_system_proxy_from_registry()
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_system_proxy_from_registry() -> Result<AsyncSysproxy> {
|
||||
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::<Vec<u16>>();
|
||||
|
||||
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::<Vec<u16>>();
|
||||
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::<Vec<u16>>();
|
||||
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::<Vec<u16>>();
|
||||
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::<u16>().unwrap_or(8080);
|
||||
(host, port)
|
||||
} else {
|
||||
(proxy_server, 8080)
|
||||
};
|
||||
|
||||
log::debug!(target: "app", "从注册表读取到代理设置: {host}:{port}, bypass: {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<AsyncSysproxy> {
|
||||
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::<u16>() {
|
||||
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<AsyncSysproxy> {
|
||||
// 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::<u16>()
|
||||
.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<AsyncSysproxy> {
|
||||
// 解析形如 "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::<u16>().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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ impl WebDavClient {
|
||||
if let Some(cfg) = lock.as_ref() {
|
||||
cfg.clone()
|
||||
} else {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
if verge.webdav_url.is_none()
|
||||
|| verge.webdav_username.is_none()
|
||||
|| verge.webdav_password.is_none()
|
||||
@@ -108,10 +108,7 @@ impl WebDavClient {
|
||||
reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(op.timeout()))
|
||||
.user_agent(format!(
|
||||
"clash-verge/{} ({} WebDAV-Client)",
|
||||
APP_VERSION, OS
|
||||
))
|
||||
.user_agent(format!("clash-verge/{APP_VERSION} ({OS} WebDAV-Client)"))
|
||||
.redirect(reqwest::redirect::Policy::custom(|attempt| {
|
||||
// 允许所有请求类型的重定向,包括PUT
|
||||
if attempt.previous().len() >= 5 {
|
||||
@@ -120,8 +117,7 @@ impl WebDavClient {
|
||||
attempt.follow()
|
||||
}
|
||||
}))
|
||||
.build()
|
||||
.unwrap(),
|
||||
.build()?,
|
||||
)
|
||||
.set_host(config.url)
|
||||
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
|
||||
@@ -177,7 +173,7 @@ impl WebDavClient {
|
||||
}
|
||||
|
||||
Ok(Err(e)) => {
|
||||
log::warn!("Upload failed, retrying once: {}", e);
|
||||
log::warn!("Upload failed, retrying once: {e}");
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
timeout(
|
||||
Duration::from_secs(TIMEOUT_UPLOAD),
|
||||
@@ -237,7 +233,7 @@ impl WebDavClient {
|
||||
|
||||
pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
||||
let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
|
||||
let zip_file_name = format!("{}-backup-{}.zip", OS, now);
|
||||
let zip_file_name = format!("{OS}-backup-{now}.zip");
|
||||
let zip_path = temp_dir().join(&zip_file_name);
|
||||
|
||||
let file = fs::File::create(&zip_path)?;
|
||||
@@ -246,12 +242,17 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> {
|
||||
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) {
|
||||
for entry in entries {
|
||||
let entry = entry.unwrap();
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let backup_path = format!("profiles/{}", entry.file_name().to_str().unwrap());
|
||||
let file_name_os = entry.file_name();
|
||||
let file_name = file_name_os
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::Error::msg("Invalid file name encoding"))?;
|
||||
let backup_path = format!("profiles/{}", file_name);
|
||||
zip.start_file(backup_path, options)?;
|
||||
zip.write_all(fs::read(path).unwrap().as_slice())?;
|
||||
let file_content = fs::read(&path)?;
|
||||
zip.write_all(&file_content)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+389
-74
@@ -1,13 +1,13 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::core::tray::Tray;
|
||||
use crate::{
|
||||
config::*,
|
||||
core::{
|
||||
handle,
|
||||
service::{self},
|
||||
},
|
||||
ipc::IpcManager,
|
||||
logging, logging_error,
|
||||
module::mihomo::MihomoManager,
|
||||
process::AsyncHandler,
|
||||
singleton_lazy,
|
||||
utils::{
|
||||
dirs,
|
||||
help::{self},
|
||||
@@ -16,7 +16,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
fmt,
|
||||
fs::{create_dir_all, File},
|
||||
@@ -25,7 +25,6 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use tauri_plugin_shell::{process::CommandChild, ShellExt};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CoreManager {
|
||||
@@ -54,7 +53,7 @@ impl fmt::Display for RunningMode {
|
||||
}
|
||||
}
|
||||
|
||||
const CLASH_CORES: [&str; 2] = ["verge-mihomo", "verge-mihomo-alpha"];
|
||||
use crate::config::IVerge;
|
||||
|
||||
impl CoreManager {
|
||||
/// 检查文件是否为脚本文件
|
||||
@@ -138,16 +137,16 @@ impl CoreManager {
|
||||
Ok(false)
|
||||
}
|
||||
/// 使用默认配置
|
||||
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
|
||||
pub fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
|
||||
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
||||
*Config::runtime().draft() = IRuntime {
|
||||
config: Some(Config::clash().latest().0.clone()),
|
||||
*Config::runtime().draft_mut() = Box::new(IRuntime {
|
||||
config: Some(Config::clash().latest_ref().0.clone()),
|
||||
exists_keys: vec![],
|
||||
chain_logs: Default::default(),
|
||||
};
|
||||
});
|
||||
help::save_yaml(
|
||||
&runtime_path,
|
||||
&Config::clash().latest().0,
|
||||
&Config::clash().latest_ref().0,
|
||||
Some("# Clash Verge Runtime"),
|
||||
)?;
|
||||
handle::Handle::notice_message(msg_type, msg_content);
|
||||
@@ -174,7 +173,7 @@ impl CoreManager {
|
||||
|
||||
// 检查文件是否存在
|
||||
if !std::path::Path::new(config_path).exists() {
|
||||
let error_msg = format!("File not found: {}", config_path);
|
||||
let error_msg = format!("File not found: {config_path}");
|
||||
//handle::Handle::notice_message("config_validate::file_not_found", &error_msg);
|
||||
return Ok((false, error_msg));
|
||||
}
|
||||
@@ -188,7 +187,7 @@ impl CoreManager {
|
||||
"检测到Merge文件,仅进行语法检查: {}",
|
||||
config_path
|
||||
);
|
||||
return self.validate_file_syntax(config_path).await;
|
||||
return self.validate_file_syntax(config_path);
|
||||
}
|
||||
|
||||
// 检查是否为脚本文件
|
||||
@@ -220,7 +219,7 @@ impl CoreManager {
|
||||
"检测到脚本文件,使用JavaScript验证: {}",
|
||||
config_path
|
||||
);
|
||||
return self.validate_script_file(config_path).await;
|
||||
return self.validate_script_file(config_path);
|
||||
}
|
||||
|
||||
// 对YAML配置文件使用Clash内核验证
|
||||
@@ -249,11 +248,14 @@ impl CoreManager {
|
||||
config_path
|
||||
);
|
||||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||
let clash_core = Config::verge().latest_ref().get_valid_clash_core();
|
||||
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let app_handle = handle::Handle::global().app_handle().ok_or_else(|| {
|
||||
let msg = "Failed to get app handle";
|
||||
logging!(error, Type::Core, true, "{}", msg);
|
||||
anyhow::anyhow!(msg)
|
||||
})?;
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let app_dir_str = dirs::path_to_str(&app_dir)?;
|
||||
logging!(info, Type::Config, true, "验证目录: {}", app_dir_str);
|
||||
@@ -287,7 +289,7 @@ impl CoreManager {
|
||||
} else if !stderr.is_empty() {
|
||||
stderr.to_string()
|
||||
} else if let Some(code) = output.status.code() {
|
||||
format!("验证进程异常退出,退出码: {}", code)
|
||||
format!("验证进程异常退出,退出码: {code}")
|
||||
} else {
|
||||
"验证进程被终止".to_string()
|
||||
};
|
||||
@@ -301,14 +303,14 @@ impl CoreManager {
|
||||
}
|
||||
}
|
||||
/// 只进行文件语法检查,不进行完整验证
|
||||
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
|
||||
fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
|
||||
logging!(info, Type::Config, true, "开始检查文件: {}", config_path);
|
||||
|
||||
// 读取文件内容
|
||||
let content = match std::fs::read_to_string(config_path) {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read file: {}", err);
|
||||
let error_msg = format!("Failed to read file: {err}");
|
||||
logging!(error, Type::Config, true, "无法读取文件: {}", error_msg);
|
||||
return Ok((false, error_msg));
|
||||
}
|
||||
@@ -322,19 +324,19 @@ impl CoreManager {
|
||||
}
|
||||
Err(err) => {
|
||||
// 使用标准化的前缀,以便错误处理函数能正确识别
|
||||
let error_msg = format!("YAML syntax error: {}", err);
|
||||
let error_msg = format!("YAML syntax error: {err}");
|
||||
logging!(error, Type::Config, true, "YAML语法错误: {}", error_msg);
|
||||
Ok((false, error_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 验证脚本文件语法
|
||||
async fn validate_script_file(&self, path: &str) -> Result<(bool, String)> {
|
||||
fn validate_script_file(&self, path: &str) -> Result<(bool, String)> {
|
||||
// 读取脚本内容
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
let error_msg = format!("Failed to read script file: {}", err);
|
||||
let error_msg = format!("Failed to read script file: {err}");
|
||||
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
|
||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||
return Ok((false, error_msg));
|
||||
@@ -367,7 +369,7 @@ impl CoreManager {
|
||||
Ok((true, String::new()))
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = format!("Script syntax error: {}", err);
|
||||
let error_msg = format!("Script syntax error: {err}");
|
||||
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
|
||||
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
|
||||
Ok((false, error_msg))
|
||||
@@ -386,7 +388,7 @@ impl CoreManager {
|
||||
|
||||
// 1. 先生成新的配置内容
|
||||
logging!(info, Type::Config, true, "生成新的配置内容");
|
||||
Config::generate().await?;
|
||||
Config::generate()?;
|
||||
|
||||
// 2. 验证配置
|
||||
match self.validate_config().await {
|
||||
@@ -416,10 +418,7 @@ impl CoreManager {
|
||||
logging_error!(Type::Core, true, "{}", msg);
|
||||
msg
|
||||
});
|
||||
match MihomoManager::global()
|
||||
.put_configs_force(run_path_str?)
|
||||
.await
|
||||
{
|
||||
match IpcManager::global().put_configs_force(run_path_str?).await {
|
||||
Ok(_) => {
|
||||
Config::runtime().apply();
|
||||
logging!(info, Type::Core, true, "Configuration updated successfully");
|
||||
@@ -436,17 +435,313 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
async fn start_core_by_sidecar(&self) -> Result<()> {
|
||||
/// 清理多余的 mihomo 进程
|
||||
async fn cleanup_orphaned_mihomo_processes(&self) -> Result<()> {
|
||||
logging!(info, Type::Core, true, "开始清理多余的 mihomo 进程");
|
||||
|
||||
// 获取当前管理的进程 PID
|
||||
let current_pid = {
|
||||
let child_guard = self.child_sidecar.lock();
|
||||
child_guard.as_ref().map(|child| child.pid())
|
||||
};
|
||||
|
||||
let target_processes = ["verge-mihomo", "verge-mihomo-alpha"];
|
||||
|
||||
// 并行查找所有目标进程
|
||||
let mut process_futures = Vec::new();
|
||||
for &target in &target_processes {
|
||||
let process_name = if cfg!(windows) {
|
||||
format!("{target}.exe")
|
||||
} else {
|
||||
target.to_string()
|
||||
};
|
||||
process_futures.push(self.find_processes_by_name(process_name, target));
|
||||
}
|
||||
|
||||
let process_results = futures::future::join_all(process_futures).await;
|
||||
|
||||
// 收集所有需要终止的进程PID
|
||||
let mut pids_to_kill = Vec::new();
|
||||
for result in process_results {
|
||||
match result {
|
||||
Ok((pids, process_name)) => {
|
||||
for pid in pids {
|
||||
// 跳过当前管理的进程
|
||||
if let Some(current) = current_pid {
|
||||
if pid == current {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Core,
|
||||
true,
|
||||
"跳过当前管理的进程: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
pids_to_kill.push((pid, process_name.clone()));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(debug, Type::Core, true, "查找进程时发生错误: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pids_to_kill.is_empty() {
|
||||
logging!(debug, Type::Core, true, "未发现多余的 mihomo 进程");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut kill_futures = Vec::new();
|
||||
for (pid, process_name) in &pids_to_kill {
|
||||
kill_futures.push(self.kill_process_with_verification(*pid, process_name.clone()));
|
||||
}
|
||||
|
||||
let kill_results = futures::future::join_all(kill_futures).await;
|
||||
|
||||
let killed_count = kill_results.into_iter().filter(|&success| success).count();
|
||||
|
||||
if killed_count > 0 {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"清理完成,共终止了 {} 个多余的 mihomo 进程",
|
||||
killed_count
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 根据进程名查找进程PID列
|
||||
async fn find_processes_by_name(
|
||||
&self,
|
||||
process_name: String,
|
||||
_target: &str,
|
||||
) -> Result<(Vec<u32>, String)> {
|
||||
#[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;
|
||||
|
||||
let process_name_clone = process_name.clone();
|
||||
let pids = AsyncHandler::spawn_blocking(move || -> Result<Vec<u32>> {
|
||||
let mut pids = Vec::new();
|
||||
|
||||
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::<PROCESSENTRY32W>() 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));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let mut pids = Vec::new();
|
||||
|
||||
// Unix系统直接解析PID列表
|
||||
for pid_str in stdout.split_whitespace() {
|
||||
if let Ok(pid) = pid_str.parse::<u32>() {
|
||||
pids.push(pid);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((pids, process_name))
|
||||
}
|
||||
}
|
||||
|
||||
/// 终止进程并验证结果 - 使用Windows API直接终止,更优雅高效
|
||||
async fn kill_process_with_verification(&self, pid: u32, process_name: String) -> bool {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"尝试终止进程: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
|
||||
#[cfg(windows)]
|
||||
let success = {
|
||||
use winapi::um::handleapi::CloseHandle;
|
||||
use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess};
|
||||
use winapi::um::winnt::{HANDLE, PROCESS_TERMINATE};
|
||||
|
||||
AsyncHandler::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()])
|
||||
.output()
|
||||
.await
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
if success {
|
||||
// 短暂等待并验证进程是否真正终止
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
let still_running = self.is_process_running(pid).await.unwrap_or(false);
|
||||
if still_running {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"进程 {} (PID: {}) 终止命令成功但进程仍在运行",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
false
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
true,
|
||||
"成功终止进程: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
true
|
||||
}
|
||||
} else {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"无法终止进程: {} (PID: {})",
|
||||
process_name,
|
||||
pid
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows API检查进程
|
||||
async fn is_process_running(&self, pid: u32) -> Result<bool> {
|
||||
#[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 = AsyncHandler::spawn_blocking(move || -> Result<bool> {
|
||||
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?;
|
||||
|
||||
Ok(output.status.success() && !output.stdout.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
fn start_core_by_sidecar(&self) -> Result<()> {
|
||||
logging!(trace, Type::Core, true, "Running core by sidecar");
|
||||
let config_file = &Config::generate_file(ConfigType::Run)?;
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or(anyhow::anyhow!("failed to get app handle"))?;
|
||||
let clash_core = Config::verge()
|
||||
.latest()
|
||||
.clash_core
|
||||
.clone()
|
||||
.unwrap_or("verge-mihomo".to_string());
|
||||
let clash_core = Config::verge().latest_ref().get_valid_clash_core();
|
||||
let config_dir = dirs::app_home_dir()?;
|
||||
|
||||
let service_log_dir = dirs::app_home_dir()?.join("logs").join("service");
|
||||
@@ -455,7 +750,7 @@ impl CoreManager {
|
||||
let now = Local::now();
|
||||
let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
|
||||
|
||||
let log_path = service_log_dir.join(format!("sidecar_{}.log", timestamp));
|
||||
let log_path = service_log_dir.join(format!("sidecar_{timestamp}.log"));
|
||||
|
||||
let mut log_file = File::create(log_path)?;
|
||||
|
||||
@@ -470,7 +765,7 @@ impl CoreManager {
|
||||
])
|
||||
.spawn()?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event {
|
||||
if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) {
|
||||
@@ -494,14 +789,14 @@ impl CoreManager {
|
||||
"Started core by sidecar pid: {}",
|
||||
pid
|
||||
);
|
||||
*self.child_sidecar.lock().await = Some(child);
|
||||
self.set_running_mode(RunningMode::Sidecar).await;
|
||||
*self.child_sidecar.lock() = Some(child);
|
||||
self.set_running_mode(RunningMode::Sidecar);
|
||||
Ok(())
|
||||
}
|
||||
async fn stop_core_by_sidecar(&self) -> Result<()> {
|
||||
fn stop_core_by_sidecar(&self) -> Result<()> {
|
||||
logging!(trace, Type::Core, true, "Stopping core by sidecar");
|
||||
|
||||
if let Some(child) = self.child_sidecar.lock().await.take() {
|
||||
if let Some(child) = self.child_sidecar.lock().take() {
|
||||
let pid = child.pid();
|
||||
child.kill()?;
|
||||
logging!(
|
||||
@@ -512,7 +807,7 @@ impl CoreManager {
|
||||
pid
|
||||
);
|
||||
}
|
||||
self.set_running_mode(RunningMode::NotRunning).await;
|
||||
self.set_running_mode(RunningMode::NotRunning);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -522,30 +817,35 @@ impl CoreManager {
|
||||
logging!(trace, Type::Core, true, "Running core by service");
|
||||
let config_file = &Config::generate_file(ConfigType::Run)?;
|
||||
service::run_core_by_service(config_file).await?;
|
||||
self.set_running_mode(RunningMode::Service).await;
|
||||
self.set_running_mode(RunningMode::Service);
|
||||
Ok(())
|
||||
}
|
||||
async fn stop_core_by_service(&self) -> Result<()> {
|
||||
logging!(trace, Type::Core, true, "Stopping core by service");
|
||||
service::stop_core_by_service().await?;
|
||||
self.set_running_mode(RunningMode::NotRunning).await;
|
||||
self.set_running_mode(RunningMode::NotRunning);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
pub fn global() -> &'static CoreManager {
|
||||
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
|
||||
CORE_MANAGER.get_or_init(|| CoreManager {
|
||||
impl Default for CoreManager {
|
||||
fn default() -> Self {
|
||||
CoreManager {
|
||||
running: Arc::new(Mutex::new(RunningMode::NotRunning)),
|
||||
child_sidecar: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use simplified singleton_lazy macro
|
||||
singleton_lazy!(CoreManager, CORE_MANAGER, CoreManager::default);
|
||||
|
||||
impl CoreManager {
|
||||
// 当服务安装失败时的回退逻辑
|
||||
async fn attempt_service_init(&self) -> Result<()> {
|
||||
if service::check_service_needs_reinstall().await {
|
||||
logging!(info, Type::Core, true, "服务版本不匹配或状态异常,执行重装");
|
||||
if let Err(e) = service::reinstall_service().await {
|
||||
if let Err(e) = service::reinstall_service() {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
@@ -571,7 +871,7 @@ impl CoreManager {
|
||||
let mut state = service::ServiceState::get();
|
||||
if !state.prefer_sidecar {
|
||||
state.prefer_sidecar = true;
|
||||
state.last_error = Some(format!("通过服务启动核心失败: {}", e));
|
||||
state.last_error = Some(format!("通过服务启动核心失败: {e}"));
|
||||
if let Err(save_err) = state.save() {
|
||||
logging!(
|
||||
error,
|
||||
@@ -590,6 +890,17 @@ impl CoreManager {
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
logging!(trace, Type::Core, "Initializing core");
|
||||
|
||||
// 应用启动时先清理任何遗留的 mihomo 进程
|
||||
if let Err(e) = self.cleanup_orphaned_mihomo_processes().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
true,
|
||||
"应用初始化时清理多余 mihomo 进程失败: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
let mut core_started_successfully = false;
|
||||
|
||||
if service::is_service_available().await.is_ok() {
|
||||
@@ -639,7 +950,7 @@ impl CoreManager {
|
||||
true,
|
||||
"用户偏好Sidecar模式或先前服务启动失败,使用Sidecar模式启动"
|
||||
);
|
||||
self.start_core_by_sidecar().await?;
|
||||
self.start_core_by_sidecar()?;
|
||||
// 如果 sidecar 启动成功,我们可以认为核心初始化流程到此结束
|
||||
// 后续的 Tray::global().subscribe_traffic().await 仍然会执行
|
||||
} else {
|
||||
@@ -651,7 +962,7 @@ impl CoreManager {
|
||||
true,
|
||||
"无服务安装记录 (首次运行或状态重置),尝试安装服务"
|
||||
);
|
||||
match service::install_service().await {
|
||||
match service::install_service() {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Core, true, "服务安装成功(首次尝试)");
|
||||
let mut new_state = service::ServiceState::default();
|
||||
@@ -675,7 +986,7 @@ impl CoreManager {
|
||||
final_state.last_error =
|
||||
Some("Newly installed service failed to start".to_string());
|
||||
final_state.save()?;
|
||||
self.start_core_by_sidecar().await?;
|
||||
self.start_core_by_sidecar()?;
|
||||
}
|
||||
} else {
|
||||
logging!(
|
||||
@@ -691,7 +1002,7 @@ impl CoreManager {
|
||||
.to_string(),
|
||||
);
|
||||
final_state.save()?;
|
||||
self.start_core_by_sidecar().await?;
|
||||
self.start_core_by_sidecar()?;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -702,7 +1013,7 @@ impl CoreManager {
|
||||
..Default::default()
|
||||
};
|
||||
new_state.save()?;
|
||||
self.start_core_by_sidecar().await?;
|
||||
self.start_core_by_sidecar()?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -731,25 +1042,24 @@ impl CoreManager {
|
||||
}));
|
||||
final_state.save()?;
|
||||
}
|
||||
self.start_core_by_sidecar().await?;
|
||||
self.start_core_by_sidecar()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
pub async fn set_running_mode(&self, mode: RunningMode) {
|
||||
let mut guard = self.running.lock().await;
|
||||
pub fn set_running_mode(&self, mode: RunningMode) {
|
||||
let mut guard = self.running.lock();
|
||||
*guard = mode;
|
||||
}
|
||||
|
||||
pub async fn get_running_mode(&self) -> RunningMode {
|
||||
let guard = self.running.lock().await;
|
||||
pub fn get_running_mode(&self) -> RunningMode {
|
||||
let guard = self.running.lock();
|
||||
(*guard).clone()
|
||||
}
|
||||
|
||||
@@ -757,7 +1067,7 @@ impl CoreManager {
|
||||
pub async fn start_core(&self) -> Result<()> {
|
||||
if service::is_service_available().await.is_ok() {
|
||||
if service::check_service_needs_reinstall().await {
|
||||
service::reinstall_service().await?;
|
||||
service::reinstall_service()?;
|
||||
}
|
||||
logging!(info, Type::Core, true, "服务可用,使用服务模式启动");
|
||||
self.start_core_by_service().await?;
|
||||
@@ -771,10 +1081,10 @@ impl CoreManager {
|
||||
true,
|
||||
"服务不可用,根据用户偏好使用Sidecar模式"
|
||||
);
|
||||
self.start_core_by_sidecar().await?;
|
||||
self.start_core_by_sidecar()?;
|
||||
} else {
|
||||
logging!(info, Type::Core, true, "服务不可用,使用Sidecar模式");
|
||||
self.start_core_by_sidecar().await?;
|
||||
self.start_core_by_sidecar()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -782,9 +1092,9 @@ impl CoreManager {
|
||||
|
||||
/// 停止核心运行
|
||||
pub async fn stop_core(&self) -> Result<()> {
|
||||
match self.get_running_mode().await {
|
||||
match self.get_running_mode() {
|
||||
RunningMode::Service => self.stop_core_by_service().await,
|
||||
RunningMode::Sidecar => self.stop_core_by_sidecar().await,
|
||||
RunningMode::Sidecar => self.stop_core_by_sidecar(),
|
||||
RunningMode::NotRunning => Ok(()),
|
||||
}
|
||||
}
|
||||
@@ -792,6 +1102,7 @@ impl CoreManager {
|
||||
/// 重启内核
|
||||
pub async fn restart_core(&self) -> Result<()> {
|
||||
self.stop_core().await?;
|
||||
|
||||
self.start_core().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -803,16 +1114,20 @@ impl CoreManager {
|
||||
logging!(error, Type::Core, true, "{}", error_message);
|
||||
return Err(error_message.to_string());
|
||||
}
|
||||
let core: &str = &clash_core.clone().unwrap();
|
||||
if !CLASH_CORES.contains(&core) {
|
||||
let error_message = format!("Clash core invalid name: {}", core);
|
||||
let core = clash_core.as_ref().ok_or_else(|| {
|
||||
let msg = "Clash core should not be None";
|
||||
logging!(error, Type::Core, true, "{}", msg);
|
||||
msg.to_string()
|
||||
})?;
|
||||
if !IVerge::VALID_CLASH_CORES.contains(&core.as_str()) {
|
||||
let error_message = format!("Clash core invalid name: {core}");
|
||||
logging!(error, Type::Core, true, "{}", error_message);
|
||||
return Err(error_message);
|
||||
}
|
||||
|
||||
Config::verge().draft().clash_core = clash_core.clone();
|
||||
Config::verge().draft_mut().clash_core = clash_core.clone();
|
||||
Config::verge().apply();
|
||||
logging_error!(Type::Core, true, Config::verge().latest().save_file());
|
||||
logging_error!(Type::Core, true, Config::verge().latest_ref().save_file());
|
||||
|
||||
let run_path = Config::generate_file(ConfigType::Run).map_err(|e| {
|
||||
let msg = e.to_string();
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio::time::{sleep, timeout, Duration};
|
||||
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
|
||||
|
||||
use crate::config::{Config, IVerge};
|
||||
use crate::core::async_proxy_query::AsyncProxyQuery;
|
||||
use crate::logging_error;
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::utils::logging::Type;
|
||||
use once_cell::sync::Lazy;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProxyEvent {
|
||||
/// 配置变更事件
|
||||
ConfigChanged,
|
||||
/// 强制检查代理状态
|
||||
#[allow(dead_code)]
|
||||
ForceCheck,
|
||||
/// 启用系统代理
|
||||
#[allow(dead_code)]
|
||||
EnableProxy,
|
||||
/// 禁用系统代理
|
||||
#[allow(dead_code)]
|
||||
DisableProxy,
|
||||
/// 切换到PAC模式
|
||||
#[allow(dead_code)]
|
||||
SwitchToPac,
|
||||
/// 切换到HTTP代理模式
|
||||
#[allow(dead_code)]
|
||||
SwitchToHttp,
|
||||
/// 应用启动事件
|
||||
AppStarted,
|
||||
/// 应用关闭事件
|
||||
#[allow(dead_code)]
|
||||
AppStopping,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyState {
|
||||
pub sys_enabled: bool,
|
||||
pub pac_enabled: bool,
|
||||
pub auto_proxy: Autoproxy,
|
||||
pub sys_proxy: Sysproxy,
|
||||
pub last_updated: std::time::Instant,
|
||||
pub is_healthy: bool,
|
||||
}
|
||||
|
||||
impl Default for ProxyState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sys_enabled: false,
|
||||
pac_enabled: false,
|
||||
auto_proxy: Autoproxy {
|
||||
enable: false,
|
||||
url: "".to_string(),
|
||||
},
|
||||
sys_proxy: Sysproxy {
|
||||
enable: false,
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 7897,
|
||||
bypass: "".to_string(),
|
||||
},
|
||||
last_updated: std::time::Instant::now(),
|
||||
is_healthy: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventDrivenProxyManager {
|
||||
state: Arc<RwLock<ProxyState>>,
|
||||
event_sender: mpsc::UnboundedSender<ProxyEvent>,
|
||||
query_sender: mpsc::UnboundedSender<QueryRequest>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QueryRequest {
|
||||
response_tx: oneshot::Sender<Autoproxy>,
|
||||
}
|
||||
|
||||
// 配置结构体移到外部
|
||||
struct ProxyConfig {
|
||||
sys_enabled: bool,
|
||||
pac_enabled: bool,
|
||||
guard_enabled: bool,
|
||||
}
|
||||
|
||||
static PROXY_MANAGER: Lazy<EventDrivenProxyManager> = Lazy::new(EventDrivenProxyManager::new);
|
||||
|
||||
impl EventDrivenProxyManager {
|
||||
pub fn global() -> &'static EventDrivenProxyManager {
|
||||
&PROXY_MANAGER
|
||||
}
|
||||
|
||||
fn new() -> Self {
|
||||
let state = Arc::new(RwLock::new(ProxyState::default()));
|
||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||
let (query_tx, query_rx) = mpsc::unbounded_channel();
|
||||
|
||||
Self::start_event_loop(Arc::clone(&state), event_rx, query_rx);
|
||||
|
||||
Self {
|
||||
state,
|
||||
event_sender: event_tx,
|
||||
query_sender: query_tx,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取自动代理配置(缓存)
|
||||
pub fn get_auto_proxy_cached(&self) -> Autoproxy {
|
||||
self.state.read().auto_proxy.clone()
|
||||
}
|
||||
|
||||
/// 异步获取最新的自动代理配置
|
||||
pub async fn get_auto_proxy_async(&self) -> Autoproxy {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let query = QueryRequest { response_tx: tx };
|
||||
|
||||
if self.query_sender.send(query).is_err() {
|
||||
log::error!(target: "app", "发送查询请求失败,返回缓存数据");
|
||||
return self.get_auto_proxy_cached();
|
||||
}
|
||||
|
||||
match timeout(Duration::from_secs(5), rx).await {
|
||||
Ok(Ok(result)) => result,
|
||||
_ => {
|
||||
log::warn!(target: "app", "查询超时,返回缓存数据");
|
||||
self.get_auto_proxy_cached()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 通知配置变更
|
||||
pub fn notify_config_changed(&self) {
|
||||
self.send_event(ProxyEvent::ConfigChanged);
|
||||
}
|
||||
|
||||
/// 通知应用启动
|
||||
pub fn notify_app_started(&self) {
|
||||
self.send_event(ProxyEvent::AppStarted);
|
||||
}
|
||||
|
||||
/// 通知应用即将关闭
|
||||
#[allow(dead_code)]
|
||||
pub fn notify_app_stopping(&self) {
|
||||
self.send_event(ProxyEvent::AppStopping);
|
||||
}
|
||||
|
||||
/// 启用系统代理
|
||||
#[allow(dead_code)]
|
||||
pub fn enable_proxy(&self) {
|
||||
self.send_event(ProxyEvent::EnableProxy);
|
||||
}
|
||||
|
||||
/// 禁用系统代理
|
||||
#[allow(dead_code)]
|
||||
pub fn disable_proxy(&self) {
|
||||
self.send_event(ProxyEvent::DisableProxy);
|
||||
}
|
||||
|
||||
/// 强制检查代理状态
|
||||
#[allow(dead_code)]
|
||||
pub fn force_check(&self) {
|
||||
self.send_event(ProxyEvent::ForceCheck);
|
||||
}
|
||||
|
||||
fn send_event(&self, event: ProxyEvent) {
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
log::error!(target: "app", "发送代理事件失败: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_event_loop(
|
||||
state: Arc<RwLock<ProxyState>>,
|
||||
event_rx: mpsc::UnboundedReceiver<ProxyEvent>,
|
||||
query_rx: mpsc::UnboundedReceiver<QueryRequest>,
|
||||
) {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
log::info!(target: "app", "事件驱动代理管理器启动");
|
||||
|
||||
// 将 mpsc 接收器包装成 Stream,避免每次循环创建 future
|
||||
let mut event_stream = UnboundedReceiverStream::new(event_rx);
|
||||
let mut query_stream = UnboundedReceiverStream::new(query_rx);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(event) = event_stream.next() => {
|
||||
log::debug!(target: "app", "处理代理事件: {event:?}");
|
||||
Self::handle_event(&state, event).await;
|
||||
}
|
||||
Some(query) = query_stream.next() => {
|
||||
let result = Self::handle_query(&state).await;
|
||||
let _ = query.response_tx.send(result);
|
||||
}
|
||||
else => {
|
||||
// 两个通道都关闭时退出
|
||||
log::info!(target: "app", "事件或查询通道关闭,代理管理器停止");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn handle_event(state: &Arc<RwLock<ProxyState>>, event: ProxyEvent) {
|
||||
match event {
|
||||
ProxyEvent::ConfigChanged | ProxyEvent::ForceCheck => {
|
||||
Self::update_proxy_config(state).await;
|
||||
}
|
||||
ProxyEvent::EnableProxy => {
|
||||
Self::enable_system_proxy(state).await;
|
||||
}
|
||||
ProxyEvent::DisableProxy => {
|
||||
Self::disable_system_proxy(state);
|
||||
}
|
||||
ProxyEvent::SwitchToPac => {
|
||||
Self::switch_proxy_mode(state, true).await;
|
||||
}
|
||||
ProxyEvent::SwitchToHttp => {
|
||||
Self::switch_proxy_mode(state, false).await;
|
||||
}
|
||||
ProxyEvent::AppStarted => {
|
||||
Self::initialize_proxy_state(state).await;
|
||||
}
|
||||
ProxyEvent::AppStopping => {
|
||||
log::info!(target: "app", "清理代理状态");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_query(state: &Arc<RwLock<ProxyState>>) -> Autoproxy {
|
||||
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.auto_proxy = auto_proxy.clone();
|
||||
});
|
||||
|
||||
auto_proxy
|
||||
}
|
||||
|
||||
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::info!(target: "app", "初始化代理状态");
|
||||
|
||||
let config = Self::get_proxy_config();
|
||||
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
|
||||
let sys_proxy = Self::get_sys_proxy_with_timeout().await;
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.sys_enabled = config.sys_enabled;
|
||||
s.pac_enabled = config.pac_enabled;
|
||||
s.auto_proxy = auto_proxy;
|
||||
s.sys_proxy = sys_proxy;
|
||||
s.is_healthy = true;
|
||||
});
|
||||
|
||||
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
|
||||
}
|
||||
|
||||
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::debug!(target: "app", "更新代理配置");
|
||||
|
||||
let config = Self::get_proxy_config();
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.sys_enabled = config.sys_enabled;
|
||||
s.pac_enabled = config.pac_enabled;
|
||||
});
|
||||
|
||||
if config.guard_enabled && config.sys_enabled {
|
||||
Self::check_and_restore_proxy(state).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_and_restore_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
let (sys_enabled, pac_enabled) = {
|
||||
let s = state.read();
|
||||
(s.sys_enabled, s.pac_enabled)
|
||||
};
|
||||
|
||||
if !sys_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
log::debug!(target: "app", "检查代理状态");
|
||||
|
||||
if pac_enabled {
|
||||
Self::check_and_restore_pac_proxy(state).await;
|
||||
} else {
|
||||
Self::check_and_restore_sys_proxy(state).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_and_restore_pac_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
let current = Self::get_auto_proxy_with_timeout().await;
|
||||
let expected = Self::get_expected_pac_config();
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.auto_proxy = current.clone();
|
||||
});
|
||||
|
||||
if !current.enable || current.url != expected.url {
|
||||
log::info!(target: "app", "PAC代理设置异常,正在恢复...");
|
||||
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
|
||||
log::error!(target: "app", "恢复PAC代理失败: {}", e);
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
let restored = Self::get_auto_proxy_with_timeout().await;
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.is_healthy = restored.enable && restored.url == expected.url;
|
||||
s.auto_proxy = restored;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_and_restore_sys_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
let current = Self::get_sys_proxy_with_timeout().await;
|
||||
let expected = Self::get_expected_sys_proxy();
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.sys_proxy = current.clone();
|
||||
});
|
||||
|
||||
if !current.enable || current.host != expected.host || current.port != expected.port {
|
||||
log::info!(target: "app", "系统代理设置异常,正在恢复...");
|
||||
if let Err(e) = Self::restore_sys_proxy(&expected).await {
|
||||
log::error!(target: "app", "恢复系统代理失败: {}", e);
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
let restored = Self::get_sys_proxy_with_timeout().await;
|
||||
|
||||
Self::update_state_timestamp(state, |s| {
|
||||
s.is_healthy = restored.enable
|
||||
&& restored.host == expected.host
|
||||
&& restored.port == expected.port;
|
||||
s.sys_proxy = restored;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) {
|
||||
log::info!(target: "app", "启用系统代理");
|
||||
|
||||
let pac_enabled = state.read().pac_enabled;
|
||||
|
||||
if pac_enabled {
|
||||
let expected = Self::get_expected_pac_config();
|
||||
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
|
||||
log::error!(target: "app", "启用PAC代理失败: {}", e);
|
||||
}
|
||||
} else {
|
||||
let expected = Self::get_expected_sys_proxy();
|
||||
if let Err(e) = Self::restore_sys_proxy(&expected).await {
|
||||
log::error!(target: "app", "启用系统代理失败: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Self::check_and_restore_proxy(state).await;
|
||||
}
|
||||
|
||||
fn disable_system_proxy(_state: &Arc<RwLock<ProxyState>>) {
|
||||
log::info!(target: "app", "禁用系统代理");
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let disabled_sys = Sysproxy::default();
|
||||
let disabled_auto = Autoproxy::default();
|
||||
|
||||
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
|
||||
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
|
||||
}
|
||||
}
|
||||
|
||||
async fn switch_proxy_mode(state: &Arc<RwLock<ProxyState>>, to_pac: bool) {
|
||||
log::info!(target: "app", "切换到{}模式", if to_pac { "PAC" } else { "HTTP代理" });
|
||||
|
||||
if to_pac {
|
||||
let disabled_sys = Sysproxy::default();
|
||||
logging_error!(Type::System, true, disabled_sys.set_system_proxy());
|
||||
|
||||
let expected = Self::get_expected_pac_config();
|
||||
if let Err(e) = Self::restore_pac_proxy(&expected.url).await {
|
||||
log::error!(target: "app", "切换到PAC模式失败: {}", e);
|
||||
}
|
||||
} else {
|
||||
let disabled_auto = Autoproxy::default();
|
||||
logging_error!(Type::System, true, disabled_auto.set_auto_proxy());
|
||||
|
||||
let expected = Self::get_expected_sys_proxy();
|
||||
if let Err(e) = Self::restore_sys_proxy(&expected).await {
|
||||
log::error!(target: "app", "切换到HTTP代理模式失败: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Self::update_state_timestamp(state, |s| s.pac_enabled = to_pac);
|
||||
Self::check_and_restore_proxy(state).await;
|
||||
}
|
||||
|
||||
async fn get_auto_proxy_with_timeout() -> Autoproxy {
|
||||
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 async_proxy = AsyncProxyQuery::get_system_proxy().await;
|
||||
|
||||
// 转换为兼容的结构
|
||||
Sysproxy {
|
||||
enable: async_proxy.enable,
|
||||
host: async_proxy.host,
|
||||
port: async_proxy.port,
|
||||
bypass: async_proxy.bypass,
|
||||
}
|
||||
}
|
||||
|
||||
// 统一的状态更新方法
|
||||
fn update_state_timestamp<F>(state: &Arc<RwLock<ProxyState>>, update_fn: F)
|
||||
where
|
||||
F: FnOnce(&mut ProxyState),
|
||||
{
|
||||
let mut state_guard = state.write();
|
||||
update_fn(&mut state_guard);
|
||||
state_guard.last_updated = std::time::Instant::now();
|
||||
}
|
||||
|
||||
fn get_proxy_config() -> ProxyConfig {
|
||||
let (sys_enabled, pac_enabled, guard_enabled) = {
|
||||
let verge_config = Config::verge();
|
||||
let verge = verge_config.latest_ref();
|
||||
(
|
||||
verge.enable_system_proxy.unwrap_or(false),
|
||||
verge.proxy_auto_config.unwrap_or(false),
|
||||
verge.enable_proxy_guard.unwrap_or(false),
|
||||
)
|
||||
};
|
||||
ProxyConfig {
|
||||
sys_enabled,
|
||||
pac_enabled,
|
||||
guard_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_expected_pac_config() -> Autoproxy {
|
||||
let proxy_host = {
|
||||
let verge_config = Config::verge();
|
||||
let verge = verge_config.latest_ref();
|
||||
verge
|
||||
.proxy_host
|
||||
.clone()
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string())
|
||||
};
|
||||
let pac_port = IVerge::get_singleton_port();
|
||||
Autoproxy {
|
||||
enable: true,
|
||||
url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_expected_sys_proxy() -> Sysproxy {
|
||||
let verge_config = Config::verge();
|
||||
let verge = verge_config.latest_ref();
|
||||
let port = verge
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
|
||||
let proxy_host = verge
|
||||
.proxy_host
|
||||
.clone()
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string());
|
||||
|
||||
Sysproxy {
|
||||
enable: true,
|
||||
host: proxy_host,
|
||||
port,
|
||||
bypass: Self::get_bypass_config(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_bypass_config() -> String {
|
||||
let (use_default, custom_bypass) = {
|
||||
let verge_config = Config::verge();
|
||||
let verge = verge_config.latest_ref();
|
||||
(
|
||||
verge.use_default_bypass.unwrap_or(true),
|
||||
verge.system_proxy_bypass.clone().unwrap_or_default(),
|
||||
)
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let default_bypass = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let default_bypass =
|
||||
"localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let default_bypass = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
|
||||
|
||||
if custom_bypass.is_empty() {
|
||||
default_bypass.to_string()
|
||||
} else if use_default {
|
||||
format!("{default_bypass},{custom_bypass}")
|
||||
} else {
|
||||
custom_bypass
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> {
|
||||
Self::execute_sysproxy_command(&["pac", expected_url]).await
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
async fn restore_pac_proxy(expected_url: &str) -> Result<(), anyhow::Error> {
|
||||
{
|
||||
let new_autoproxy = Autoproxy {
|
||||
enable: true,
|
||||
url: expected_url.to_string(),
|
||||
};
|
||||
// logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
|
||||
new_autoproxy
|
||||
.set_auto_proxy()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to set auto proxy: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> {
|
||||
let address = format!("{}:{}", expected.host, expected.port);
|
||||
Self::execute_sysproxy_command(&["global", &address, &expected.bypass]).await
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
async fn restore_sys_proxy(expected: &Sysproxy) -> Result<(), anyhow::Error> {
|
||||
{
|
||||
// logging_error!(Type::System, true, expected.set_system_proxy());
|
||||
expected
|
||||
.set_system_proxy()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to set system proxy: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn execute_sysproxy_command(args: &[&str]) -> Result<(), anyhow::Error> {
|
||||
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,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "获取服务路径失败: {e}");
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||
if !sysproxy_exe.exists() {
|
||||
log::error!(target: "app", "sysproxy.exe 不存在");
|
||||
}
|
||||
anyhow::ensure!(sysproxy_exe.exists(), "sysproxy.exe does not exist");
|
||||
|
||||
let _output = Command::new(sysproxy_exe)
|
||||
.args(args)
|
||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+125
-103
@@ -1,4 +1,4 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use crate::singleton;
|
||||
use parking_lot::RwLock;
|
||||
use std::{
|
||||
sync::{
|
||||
@@ -82,118 +82,123 @@ impl NotificationSystem {
|
||||
|
||||
*self.last_emit_time.write() = Instant::now();
|
||||
|
||||
self.worker_handle = Some(
|
||||
thread::Builder::new()
|
||||
.name("frontend-notifier".into())
|
||||
.spawn(move || {
|
||||
let handle = Handle::global();
|
||||
match thread::Builder::new()
|
||||
.name("frontend-notifier".into())
|
||||
.spawn(move || {
|
||||
let handle = Handle::global();
|
||||
|
||||
while !handle.is_exiting() {
|
||||
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(event) => {
|
||||
let system_guard = handle.notification_system.read();
|
||||
if system_guard.as_ref().is_none() {
|
||||
log::warn!("NotificationSystem not found in handle while processing event.");
|
||||
continue;
|
||||
}
|
||||
let system = system_guard.as_ref().unwrap();
|
||||
while !handle.is_exiting() {
|
||||
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(event) => {
|
||||
let system_guard = handle.notification_system.read();
|
||||
let Some(system) = system_guard.as_ref() else {
|
||||
log::warn!("NotificationSystem not found in handle while processing event.");
|
||||
continue;
|
||||
};
|
||||
|
||||
let is_emergency = *system.emergency_mode.read();
|
||||
let is_emergency = *system.emergency_mode.read();
|
||||
|
||||
if is_emergency {
|
||||
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
|
||||
if status == "info" {
|
||||
log::warn!(
|
||||
"Emergency mode active, skipping info message"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if is_emergency {
|
||||
if let FrontendEvent::NoticeMessage { ref status, .. } = event {
|
||||
if status == "info" {
|
||||
log::warn!(
|
||||
"Emergency mode active, skipping info message"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window) = handle.get_window() {
|
||||
*system.last_emit_time.write() = Instant::now();
|
||||
if let Some(window) = handle.get_window() {
|
||||
*system.last_emit_time.write() = Instant::now();
|
||||
|
||||
let (event_name_str, payload_result) = match event {
|
||||
FrontendEvent::RefreshClash => {
|
||||
("verge://refresh-clash-config", Ok(serde_json::json!("yes")))
|
||||
}
|
||||
FrontendEvent::RefreshVerge => {
|
||||
("verge://refresh-verge-config", Ok(serde_json::json!("yes")))
|
||||
}
|
||||
FrontendEvent::NoticeMessage { status, message } => {
|
||||
match serde_json::to_value((status, message)) {
|
||||
Ok(p) => ("verge://notice-message", Ok(p)),
|
||||
Err(e) => {
|
||||
log::error!("Failed to serialize NoticeMessage payload: {}", e);
|
||||
("verge://notice-message", Err(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
FrontendEvent::ProfileChanged { current_profile_id } => {
|
||||
("profile-changed", Ok(serde_json::json!(current_profile_id)))
|
||||
}
|
||||
FrontendEvent::TimerUpdated { profile_index } => {
|
||||
("verge://timer-updated", Ok(serde_json::json!(profile_index)))
|
||||
}
|
||||
FrontendEvent::StartupCompleted => {
|
||||
("verge://startup-completed", Ok(serde_json::json!(null)))
|
||||
}
|
||||
FrontendEvent::ProfileUpdateStarted { uid } => {
|
||||
("profile-update-started", Ok(serde_json::json!({ "uid": uid })))
|
||||
}
|
||||
FrontendEvent::ProfileUpdateCompleted { uid } => {
|
||||
("profile-update-completed", Ok(serde_json::json!({ "uid": uid })))
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(payload) = payload_result {
|
||||
match window.emit(event_name_str, payload) {
|
||||
Ok(_) => {
|
||||
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
let (event_name_str, payload_result) = match event {
|
||||
FrontendEvent::RefreshClash => {
|
||||
("verge://refresh-clash-config", Ok(serde_json::json!("yes")))
|
||||
}
|
||||
FrontendEvent::RefreshVerge => {
|
||||
("verge://refresh-verge-config", Ok(serde_json::json!("yes")))
|
||||
}
|
||||
FrontendEvent::NoticeMessage { status, message } => {
|
||||
match serde_json::to_value((status, message)) {
|
||||
Ok(p) => ("verge://notice-message", Ok(p)),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to emit event {}: {}", event_name_str, e);
|
||||
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||
log::error!("Failed to serialize NoticeMessage payload: {e}");
|
||||
("verge://notice-message", Err(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
FrontendEvent::ProfileChanged { current_profile_id } => {
|
||||
("profile-changed", Ok(serde_json::json!(current_profile_id)))
|
||||
}
|
||||
FrontendEvent::TimerUpdated { profile_index } => {
|
||||
("verge://timer-updated", Ok(serde_json::json!(profile_index)))
|
||||
}
|
||||
FrontendEvent::StartupCompleted => {
|
||||
("verge://startup-completed", Ok(serde_json::json!(null)))
|
||||
}
|
||||
FrontendEvent::ProfileUpdateStarted { uid } => {
|
||||
("profile-update-started", Ok(serde_json::json!({ "uid": uid })))
|
||||
}
|
||||
FrontendEvent::ProfileUpdateCompleted { uid } => {
|
||||
("profile-update-completed", Ok(serde_json::json!({ "uid": uid })))
|
||||
}
|
||||
};
|
||||
|
||||
let errors = system.stats.total_errors.load(Ordering::SeqCst);
|
||||
const EMIT_ERROR_THRESHOLD: u64 = 10;
|
||||
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
|
||||
log::warn!(
|
||||
"Reached {} emit errors, entering emergency mode",
|
||||
EMIT_ERROR_THRESHOLD
|
||||
);
|
||||
*system.emergency_mode.write() = true;
|
||||
}
|
||||
if let Ok(payload) = payload_result {
|
||||
match window.emit(event_name_str, payload) {
|
||||
Ok(_) => {
|
||||
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
|
||||
// 记录成功发送的事件
|
||||
if log::log_enabled!(log::Level::Debug) {
|
||||
log::debug!("Successfully emitted event: {event_name_str}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to emit event {event_name_str}: {e}");
|
||||
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||
|
||||
let errors = system.stats.total_errors.load(Ordering::SeqCst);
|
||||
const EMIT_ERROR_THRESHOLD: u64 = 10;
|
||||
if errors > EMIT_ERROR_THRESHOLD && !*system.emergency_mode.read() {
|
||||
log::warn!(
|
||||
"Reached {EMIT_ERROR_THRESHOLD} emit errors, entering emergency mode"
|
||||
);
|
||||
*system.emergency_mode.write() = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||
log::warn!("Skipped emitting event due to payload serialization error for {}", event_name_str);
|
||||
}
|
||||
} else {
|
||||
log::warn!("No window found, skipping event emit.");
|
||||
system.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||
*system.stats.last_error_time.write() = Some(Instant::now());
|
||||
log::warn!("Skipped emitting event due to payload serialization error for {event_name_str}");
|
||||
}
|
||||
thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||
continue;
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||
log::info!(
|
||||
"Notification channel disconnected, exiting worker thread"
|
||||
);
|
||||
break;
|
||||
} else {
|
||||
log::warn!("No window found, skipping event emit.");
|
||||
}
|
||||
thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||
log::info!(
|
||||
"Notification channel disconnected, exiting worker thread"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Notification worker thread exiting");
|
||||
})
|
||||
.expect("Failed to start notification worker thread"),
|
||||
);
|
||||
log::info!("Notification worker thread exiting");
|
||||
}) {
|
||||
Ok(handle) => {
|
||||
self.worker_handle = Some(handle);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to start notification worker thread: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送事件到队列
|
||||
@@ -211,7 +216,7 @@ impl NotificationSystem {
|
||||
match sender.send(event) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to send event to notification queue: {:?}", e);
|
||||
log::warn!("Failed to send event to notification queue: {e:?}");
|
||||
self.stats.total_errors.fetch_add(1, Ordering::SeqCst);
|
||||
*self.stats.last_error_time.write() = Some(Instant::now());
|
||||
false
|
||||
@@ -224,12 +229,27 @@ impl NotificationSystem {
|
||||
}
|
||||
|
||||
fn shutdown(&mut self) {
|
||||
log::info!("NotificationSystem shutdown initiated");
|
||||
self.is_running = false;
|
||||
self.sender = None;
|
||||
|
||||
if let Some(handle) = self.worker_handle.take() {
|
||||
let _ = handle.join();
|
||||
// 先关闭发送端,让接收端知道不会再有新消息
|
||||
if let Some(sender) = self.sender.take() {
|
||||
drop(sender);
|
||||
}
|
||||
|
||||
// 设置超时避免无限等待
|
||||
if let Some(handle) = self.worker_handle.take() {
|
||||
match handle.join() {
|
||||
Ok(_) => {
|
||||
log::info!("NotificationSystem worker thread joined successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("NotificationSystem worker thread join failed: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("NotificationSystem shutdown completed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,10 +274,12 @@ impl Default for Handle {
|
||||
}
|
||||
}
|
||||
|
||||
// Use singleton macro
|
||||
singleton!(Handle, HANDLE);
|
||||
|
||||
impl Handle {
|
||||
pub fn global() -> &'static Handle {
|
||||
static HANDLE: OnceCell<Handle> = OnceCell::new();
|
||||
HANDLE.get_or_init(Handle::default)
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn init(&self, app_handle: &AppHandle) {
|
||||
@@ -481,7 +503,7 @@ impl Handle {
|
||||
});
|
||||
|
||||
if let Err(e) = thread_result {
|
||||
log::error!("Failed to spawn startup errors thread: {}", e);
|
||||
log::error!("Failed to spawn startup errors thread: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+283
-158
@@ -1,34 +1,278 @@
|
||||
use crate::utils::notification::{notify_event, NotificationEvent};
|
||||
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, singleton_with_logging, utils::logging::Type,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{collections::HashMap, fmt, str::FromStr, sync::Arc};
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
|
||||
|
||||
/// Enum representing all available hotkey functions
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum HotkeyFunction {
|
||||
OpenOrCloseDashboard,
|
||||
ClashModeRule,
|
||||
ClashModeGlobal,
|
||||
ClashModeDirect,
|
||||
ToggleSystemProxy,
|
||||
ToggleTunMode,
|
||||
EntryLightweightMode,
|
||||
Quit,
|
||||
#[cfg(target_os = "macos")]
|
||||
Hide,
|
||||
}
|
||||
|
||||
impl fmt::Display for HotkeyFunction {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
HotkeyFunction::OpenOrCloseDashboard => "open_or_close_dashboard",
|
||||
HotkeyFunction::ClashModeRule => "clash_mode_rule",
|
||||
HotkeyFunction::ClashModeGlobal => "clash_mode_global",
|
||||
HotkeyFunction::ClashModeDirect => "clash_mode_direct",
|
||||
HotkeyFunction::ToggleSystemProxy => "toggle_system_proxy",
|
||||
HotkeyFunction::ToggleTunMode => "toggle_tun_mode",
|
||||
HotkeyFunction::EntryLightweightMode => "entry_lightweight_mode",
|
||||
HotkeyFunction::Quit => "quit",
|
||||
#[cfg(target_os = "macos")]
|
||||
HotkeyFunction::Hide => "hide",
|
||||
};
|
||||
write!(f, "{s}")
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for HotkeyFunction {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.trim() {
|
||||
"open_or_close_dashboard" => Ok(HotkeyFunction::OpenOrCloseDashboard),
|
||||
"clash_mode_rule" => Ok(HotkeyFunction::ClashModeRule),
|
||||
"clash_mode_global" => Ok(HotkeyFunction::ClashModeGlobal),
|
||||
"clash_mode_direct" => Ok(HotkeyFunction::ClashModeDirect),
|
||||
"toggle_system_proxy" => Ok(HotkeyFunction::ToggleSystemProxy),
|
||||
"toggle_tun_mode" => Ok(HotkeyFunction::ToggleTunMode),
|
||||
"entry_lightweight_mode" => Ok(HotkeyFunction::EntryLightweightMode),
|
||||
"quit" => Ok(HotkeyFunction::Quit),
|
||||
#[cfg(target_os = "macos")]
|
||||
"hide" => Ok(HotkeyFunction::Hide),
|
||||
_ => bail!("invalid hotkey function: {}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
/// Enum representing predefined system hotkeys
|
||||
pub enum SystemHotkey {
|
||||
CmdQ,
|
||||
CmdW,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl fmt::Display for SystemHotkey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
SystemHotkey::CmdQ => "CMD+Q",
|
||||
SystemHotkey::CmdW => "CMD+W",
|
||||
};
|
||||
write!(f, "{s}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl SystemHotkey {
|
||||
pub fn function(self) -> HotkeyFunction {
|
||||
match self {
|
||||
SystemHotkey::CmdQ => HotkeyFunction::Quit,
|
||||
SystemHotkey::CmdW => HotkeyFunction::Hide,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Hotkey {
|
||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||
current: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl Hotkey {
|
||||
pub fn global() -> &'static Hotkey {
|
||||
static HOTKEY: OnceCell<Hotkey> = OnceCell::new();
|
||||
|
||||
HOTKEY.get_or_init(|| Hotkey {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current: Arc::new(Mutex::new(Vec::new())),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute the function associated with a hotkey function enum
|
||||
fn execute_function(function: HotkeyFunction, app_handle: &tauri::AppHandle) {
|
||||
match function {
|
||||
HotkeyFunction::OpenOrCloseDashboard => {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"=== Hotkey Dashboard Window Operation Start ==="
|
||||
);
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Using unified WindowManager for hotkey operation (bypass debounce)"
|
||||
);
|
||||
|
||||
crate::feat::open_or_close_dashboard_hotkey();
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
"=== Hotkey Dashboard Window Operation End ==="
|
||||
);
|
||||
notify_event(app_handle, NotificationEvent::DashboardToggled);
|
||||
}
|
||||
HotkeyFunction::ClashModeRule => {
|
||||
feat::change_clash_mode("rule".into());
|
||||
notify_event(
|
||||
app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Rule" },
|
||||
);
|
||||
}
|
||||
HotkeyFunction::ClashModeGlobal => {
|
||||
feat::change_clash_mode("global".into());
|
||||
notify_event(
|
||||
app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Global" },
|
||||
);
|
||||
}
|
||||
HotkeyFunction::ClashModeDirect => {
|
||||
feat::change_clash_mode("direct".into());
|
||||
notify_event(
|
||||
app_handle,
|
||||
NotificationEvent::ClashModeChanged { mode: "Direct" },
|
||||
);
|
||||
}
|
||||
HotkeyFunction::ToggleSystemProxy => {
|
||||
feat::toggle_system_proxy();
|
||||
notify_event(app_handle, NotificationEvent::SystemProxyToggled);
|
||||
}
|
||||
HotkeyFunction::ToggleTunMode => {
|
||||
feat::toggle_tun_mode(None);
|
||||
notify_event(app_handle, NotificationEvent::TunModeToggled);
|
||||
}
|
||||
HotkeyFunction::EntryLightweightMode => {
|
||||
entry_lightweight_mode();
|
||||
notify_event(app_handle, NotificationEvent::LightweightModeEntered);
|
||||
}
|
||||
HotkeyFunction::Quit => {
|
||||
notify_event(app_handle, NotificationEvent::AppQuit);
|
||||
feat::quit();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
HotkeyFunction::Hide => {
|
||||
feat::hide();
|
||||
notify_event(app_handle, NotificationEvent::AppHidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
/// Register a system hotkey using enum
|
||||
pub fn register_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> {
|
||||
let hotkey_str = hotkey.to_string();
|
||||
let function = hotkey.function();
|
||||
self.register_hotkey_with_function(&hotkey_str, function)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
/// Unregister a system hotkey using enum
|
||||
pub fn unregister_system_hotkey(&self, hotkey: SystemHotkey) -> Result<()> {
|
||||
let hotkey_str = hotkey.to_string();
|
||||
self.unregister(&hotkey_str)
|
||||
}
|
||||
|
||||
/// Register a hotkey with function enum
|
||||
pub fn register_hotkey_with_function(
|
||||
&self,
|
||||
hotkey: &str,
|
||||
function: HotkeyFunction,
|
||||
) -> Result<()> {
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
|
||||
let manager = app_handle.global_shortcut();
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
"Attempting to register hotkey: {} for function: {}",
|
||||
hotkey,
|
||||
function
|
||||
);
|
||||
|
||||
if manager.is_registered(hotkey) {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
"Hotkey {} was already registered, unregistering first",
|
||||
hotkey
|
||||
);
|
||||
manager.unregister(hotkey)?;
|
||||
}
|
||||
|
||||
let app_handle_clone = app_handle.clone();
|
||||
let is_quit = matches!(function, HotkeyFunction::Quit);
|
||||
|
||||
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey_event, event| {
|
||||
if event.state == ShortcutState::Pressed {
|
||||
logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey_event);
|
||||
|
||||
if hotkey_event.key == Code::KeyQ && is_quit {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
if window.is_focused().unwrap_or(false) {
|
||||
logging!(debug, Type::Hotkey, "Executing quit function");
|
||||
Self::execute_function(function, &app_handle_clone);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logging!(debug, Type::Hotkey, "Executing function directly");
|
||||
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest_ref()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
|
||||
if is_enable_global_hotkey {
|
||||
Self::execute_function(function, &app_handle_clone);
|
||||
} else {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
let is_visible = WindowManager::is_main_window_visible();
|
||||
let is_focused = WindowManager::is_main_window_focused();
|
||||
|
||||
if is_focused && is_visible {
|
||||
Self::execute_function(function, &app_handle_clone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
"Successfully registered hotkey {} for {}",
|
||||
hotkey,
|
||||
function
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Use unified singleton macro
|
||||
singleton_with_logging!(Hotkey, INSTANCE, "Hotkey");
|
||||
|
||||
impl Hotkey {
|
||||
pub fn init(&self) -> Result<()> {
|
||||
let verge = Config::verge();
|
||||
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
|
||||
let enable_global_hotkey = verge.latest_ref().enable_global_hotkey.unwrap_or(true);
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
@@ -38,12 +282,11 @@ impl Hotkey {
|
||||
enable_global_hotkey
|
||||
);
|
||||
|
||||
// 如果全局热键被禁用,则不注册热键
|
||||
if !enable_global_hotkey {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
||||
if let Some(hotkeys) = verge.latest_ref().hotkeys.as_ref() {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
@@ -110,152 +353,24 @@ impl Hotkey {
|
||||
}
|
||||
|
||||
pub fn reset(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
|
||||
let manager = app_handle.global_shortcut();
|
||||
manager.unregister_all()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register a hotkey with string-based function (backward compatibility)
|
||||
pub fn register(&self, hotkey: &str, func: &str) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let manager = app_handle.global_shortcut();
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
"Attempting to register hotkey: {} for function: {}",
|
||||
hotkey,
|
||||
func
|
||||
);
|
||||
|
||||
if manager.is_registered(hotkey) {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
"Hotkey {} was already registered, unregistering first",
|
||||
hotkey
|
||||
);
|
||||
manager.unregister(hotkey)?;
|
||||
}
|
||||
|
||||
let f = match func.trim() {
|
||||
"open_or_close_dashboard" => {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
"Registering open_or_close_dashboard function"
|
||||
);
|
||||
|| {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"=== Hotkey Dashboard Window Operation Start ==="
|
||||
);
|
||||
|
||||
// 使用 spawn_blocking 来确保在正确的线程上执行
|
||||
AsyncHandler::spawn_blocking(|| {
|
||||
logging!(debug, Type::Hotkey, "Toggle dashboard window visibility");
|
||||
|
||||
// 检查窗口是否存在
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
// 如果窗口可见,则隐藏它
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
logging!(info, Type::Window, "Window is visible, hiding it");
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
// 如果窗口不可见,则显示它
|
||||
logging!(info, Type::Window, "Window is hidden, showing it");
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
} else {
|
||||
// 如果窗口不存在,创建一个新窗口
|
||||
logging!(
|
||||
info,
|
||||
Type::Window,
|
||||
"Window does not exist, creating a new one"
|
||||
);
|
||||
resolve::create_window(true);
|
||||
}
|
||||
});
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
"=== Hotkey Dashboard Window Operation End ==="
|
||||
);
|
||||
}
|
||||
}
|
||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
||||
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
|
||||
"entry_lightweight_mode" => || entry_lightweight_mode(),
|
||||
"quit" => || feat::quit(),
|
||||
#[cfg(target_os = "macos")]
|
||||
"hide" => || feat::hide(),
|
||||
|
||||
_ => {
|
||||
logging!(error, Type::Hotkey, "Invalid function: {}", func);
|
||||
bail!("invalid function \"{func}\"");
|
||||
}
|
||||
};
|
||||
|
||||
let is_quit = func.trim() == "quit";
|
||||
|
||||
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
|
||||
if event.state == ShortcutState::Pressed {
|
||||
logging!(debug, Type::Hotkey, "Hotkey pressed: {:?}", hotkey);
|
||||
|
||||
if hotkey.key == Code::KeyQ && is_quit {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
if window.is_focused().unwrap_or(false) {
|
||||
logging!(debug, Type::Hotkey, "Executing quit function");
|
||||
f();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接执行函数,不做任何状态检查
|
||||
logging!(debug, Type::Hotkey, "Executing function directly");
|
||||
|
||||
// 获取全局热键状态
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
|
||||
if is_enable_global_hotkey {
|
||||
f();
|
||||
} else if let Some(window) = app_handle.get_webview_window("main") {
|
||||
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
|
||||
let is_visible = window.is_visible().unwrap_or(false);
|
||||
let is_focused = window.is_focused().unwrap_or(false);
|
||||
|
||||
if is_focused && is_visible {
|
||||
f();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Hotkey,
|
||||
"Successfully registered hotkey {} for {}",
|
||||
hotkey,
|
||||
func
|
||||
);
|
||||
Ok(())
|
||||
let function = HotkeyFunction::from_str(func)?;
|
||||
self.register_hotkey_with_function(hotkey, function)
|
||||
}
|
||||
|
||||
pub fn unregister(&self, hotkey: &str) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?;
|
||||
let manager = app_handle.global_shortcut();
|
||||
manager.unregister(hotkey)?;
|
||||
logging!(debug, Type::Hotkey, "Unregister hotkey {}", hotkey);
|
||||
@@ -289,9 +404,9 @@ impl Hotkey {
|
||||
let func = iter.next();
|
||||
let key = iter.next();
|
||||
|
||||
if func.is_some() && key.is_some() {
|
||||
let func = func.unwrap().trim();
|
||||
let key = key.unwrap().trim();
|
||||
if let (Some(func), Some(key)) = (func, key) {
|
||||
let func = func.trim();
|
||||
let key = key.trim();
|
||||
map.insert(key, func);
|
||||
}
|
||||
});
|
||||
@@ -329,7 +444,17 @@ impl Hotkey {
|
||||
|
||||
impl Drop for Hotkey {
|
||||
fn drop(&mut self) {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Hotkey,
|
||||
"Failed to get app handle during hotkey cleanup"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) = app_handle.global_shortcut().unregister_all() {
|
||||
logging!(
|
||||
error,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod async_proxy_query;
|
||||
pub mod backup;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod core;
|
||||
pub mod event_driven_proxy;
|
||||
pub mod handle;
|
||||
pub mod hotkey;
|
||||
pub mod service;
|
||||
@@ -10,4 +12,4 @@ pub mod timer;
|
||||
pub mod tray;
|
||||
pub mod win_uwp;
|
||||
|
||||
pub use self::{core::*, timer::Timer};
|
||||
pub use self::{core::*, event_driven_proxy::EventDrivenProxyManager, timer::Timer};
|
||||
|
||||
@@ -32,7 +32,7 @@ pub struct ServiceState {
|
||||
impl ServiceState {
|
||||
// 获取当前的服务状态
|
||||
pub fn get() -> Self {
|
||||
if let Some(state) = Config::verge().latest().service_state.clone() {
|
||||
if let Some(state) = Config::verge().latest_ref().service_state.clone() {
|
||||
return state;
|
||||
}
|
||||
Self::default()
|
||||
@@ -41,11 +41,11 @@ impl ServiceState {
|
||||
// 保存服务状态
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let config = Config::verge();
|
||||
let mut latest = config.latest().clone();
|
||||
let mut latest = config.latest_ref().clone();
|
||||
latest.service_state = Some(self.clone());
|
||||
*config.draft() = latest;
|
||||
*config.draft_mut() = latest;
|
||||
config.apply();
|
||||
let result = config.latest().save_file();
|
||||
let result = config.latest_ref().save_file();
|
||||
result
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ pub struct JsonResponse {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub async fn uninstall_service() -> Result<()> {
|
||||
pub fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "uninstall service");
|
||||
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
@@ -138,7 +138,7 @@ pub async fn uninstall_service() -> Result<()> {
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap()
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ pub async fn uninstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub async fn install_service() -> Result<()> {
|
||||
pub fn install_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "install service");
|
||||
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
@@ -172,7 +172,7 @@ pub async fn install_service() -> Result<()> {
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to install service with status {}",
|
||||
status.code().unwrap()
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ pub async fn install_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub async fn reinstall_service() -> Result<()> {
|
||||
pub fn reinstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "reinstall service");
|
||||
|
||||
// 获取当前服务状态
|
||||
@@ -198,7 +198,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
// 先卸载服务
|
||||
if let Err(err) = uninstall_service().await {
|
||||
if let Err(err) = uninstall_service() {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
@@ -209,7 +209,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
// 再安装服务
|
||||
match install_service().await {
|
||||
match install_service() {
|
||||
Ok(_) => {
|
||||
// 记录安装信息并保存
|
||||
service_state.record_install();
|
||||
@@ -218,7 +218,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {}", err);
|
||||
let error = format!("failed to install service: {err}");
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.prefer_sidecar = true;
|
||||
service_state.save()?;
|
||||
@@ -228,7 +228,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn uninstall_service() -> Result<()> {
|
||||
pub fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "uninstall service");
|
||||
use users::get_effective_uid;
|
||||
|
||||
@@ -254,13 +254,13 @@ pub async fn uninstall_service() -> Result<()> {
|
||||
Type::Service,
|
||||
true,
|
||||
"uninstall status code:{}",
|
||||
status.code().unwrap()
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap()
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ pub async fn uninstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn install_service() -> Result<()> {
|
||||
pub fn install_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "install service");
|
||||
use users::get_effective_uid;
|
||||
|
||||
@@ -294,13 +294,13 @@ pub async fn install_service() -> Result<()> {
|
||||
Type::Service,
|
||||
true,
|
||||
"install status code:{}",
|
||||
status.code().unwrap()
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to install service with status {}",
|
||||
status.code().unwrap()
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ pub async fn install_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn reinstall_service() -> Result<()> {
|
||||
pub fn reinstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "reinstall service");
|
||||
|
||||
// 获取当前服务状态
|
||||
@@ -326,7 +326,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
// 先卸载服务
|
||||
if let Err(err) = uninstall_service().await {
|
||||
if let Err(err) = uninstall_service() {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
@@ -337,7 +337,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
// 再安装服务
|
||||
match install_service().await {
|
||||
match install_service() {
|
||||
Ok(_) => {
|
||||
// 记录安装信息并保存
|
||||
service_state.record_install();
|
||||
@@ -346,7 +346,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {}", err);
|
||||
let error = format!("failed to install service: {err}");
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.prefer_sidecar = true;
|
||||
service_state.save()?;
|
||||
@@ -356,7 +356,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn uninstall_service() -> Result<()> {
|
||||
pub fn uninstall_service() -> Result<()> {
|
||||
use crate::utils::i18n::t;
|
||||
|
||||
logging!(info, Type::Service, true, "uninstall service");
|
||||
@@ -384,7 +384,7 @@ pub async fn uninstall_service() -> Result<()> {
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap()
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ pub async fn uninstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn install_service() -> Result<()> {
|
||||
pub fn install_service() -> Result<()> {
|
||||
use crate::utils::i18n::t;
|
||||
|
||||
logging!(info, Type::Service, true, "install service");
|
||||
@@ -420,7 +420,7 @@ pub async fn install_service() -> Result<()> {
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to install service with status {}",
|
||||
status.code().unwrap()
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -428,7 +428,7 @@ pub async fn install_service() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn reinstall_service() -> Result<()> {
|
||||
pub fn reinstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, true, "reinstall service");
|
||||
|
||||
// 获取当前服务状态
|
||||
@@ -446,7 +446,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
// 先卸载服务
|
||||
if let Err(err) = uninstall_service().await {
|
||||
if let Err(err) = uninstall_service() {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Service,
|
||||
@@ -457,7 +457,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
// 再安装服务
|
||||
match install_service().await {
|
||||
match install_service() {
|
||||
Ok(_) => {
|
||||
// 记录安装信息并保存
|
||||
service_state.record_install();
|
||||
@@ -466,7 +466,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {}", err);
|
||||
let error = format!("failed to install service: {err}");
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.prefer_sidecar = true;
|
||||
service_state.save()?;
|
||||
@@ -686,7 +686,7 @@ pub async fn check_service_needs_reinstall() -> bool {
|
||||
// 检查版本和可用性
|
||||
match check_service_version().await {
|
||||
Ok(version) => {
|
||||
log::info!(target: "app", "服务版本检测:当前={}, 要求={}", version, REQUIRED_SERVICE_VERSION);
|
||||
log::info!(target: "app", "服务版本检测:当前={version}, 要求={REQUIRED_SERVICE_VERSION}");
|
||||
/* logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
@@ -698,8 +698,7 @@ pub async fn check_service_needs_reinstall() -> bool {
|
||||
|
||||
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
|
||||
if needs_reinstall {
|
||||
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}",
|
||||
version, REQUIRED_SERVICE_VERSION);
|
||||
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={version}, 要求={REQUIRED_SERVICE_VERSION}");
|
||||
logging!(warn, Type::Service, true, "服务版本不匹配,需要重装");
|
||||
|
||||
// log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
|
||||
@@ -717,7 +716,7 @@ pub async fn check_service_needs_reinstall() -> bool {
|
||||
// 检查服务是否可用
|
||||
match is_service_available().await {
|
||||
Ok(()) => {
|
||||
log::info!(target: "app", "服务正在运行但版本检查失败: {}", err);
|
||||
log::info!(target: "app", "服务正在运行但版本检查失败: {err}");
|
||||
/* logging!(
|
||||
info,
|
||||
Type::Service,
|
||||
@@ -742,8 +741,7 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
|
||||
log::info!(target:"app", "尝试使用现有服务启动核心 (IPC)");
|
||||
// logging!(info, Type::Service, true, "尝试使用现有服务启动核心");
|
||||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||
let clash_core = Config::verge().latest_ref().get_valid_clash_core();
|
||||
|
||||
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
|
||||
let clash_bin = format!("{clash_core}{bin_ext}");
|
||||
@@ -828,8 +826,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
// 先检查服务版本,不受冷却期限制
|
||||
let version_check = match check_service_version().await {
|
||||
Ok(version) => {
|
||||
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}",
|
||||
version, REQUIRED_SERVICE_VERSION);
|
||||
log::info!(target: "app", "检测到服务版本: {version}, 要求版本: {REQUIRED_SERVICE_VERSION}");
|
||||
|
||||
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
|
||||
log::warn!(target: "app", "服务版本不匹配,需要重装");
|
||||
@@ -840,16 +837,14 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "无法获取服务版本: {}", err);
|
||||
log::warn!(target: "app", "无法获取服务版本: {err}");
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if version_check {
|
||||
if is_service_available().await.is_ok() {
|
||||
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
if version_check && is_service_available().await.is_ok() {
|
||||
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
|
||||
return start_with_existing_service(config_file).await;
|
||||
}
|
||||
|
||||
if !version_check {
|
||||
@@ -861,17 +856,14 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
if let Ok(()) = start_with_existing_service(config_file).await {
|
||||
log::info!(target: "app", "尽管版本不匹配,但成功启动了服务");
|
||||
return Ok(());
|
||||
} else {
|
||||
bail!("服务版本不匹配且无法重装,启动失败");
|
||||
}
|
||||
bail!("服务版本不匹配且无法重装,启动失败");
|
||||
}
|
||||
|
||||
log::info!(target: "app", "开始重装服务");
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
||||
|
||||
log::info!(target: "app", "尝试使用现有服务");
|
||||
return start_with_existing_service(config_file).await;
|
||||
if let Err(err) = reinstall_service() {
|
||||
log::warn!(target: "app", "服务重装失败: {err}");
|
||||
bail!("Failed to reinstall service: {}", err);
|
||||
}
|
||||
|
||||
log::info!(target: "app", "服务重装成功,尝试启动");
|
||||
@@ -887,7 +879,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(target: "app", "服务检查失败: {}", err);
|
||||
log::warn!(target: "app", "服务检查失败: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -895,8 +887,8 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
if check_service_needs_reinstall().await {
|
||||
log::info!(target: "app", "服务需要重装");
|
||||
|
||||
if let Err(err) = reinstall_service().await {
|
||||
log::warn!(target: "app", "服务重装失败: {}", err);
|
||||
if let Err(err) = reinstall_service() {
|
||||
log::warn!(target: "app", "服务重装失败: {err}");
|
||||
bail!("Failed to reinstall service: {}", err);
|
||||
}
|
||||
|
||||
@@ -975,7 +967,7 @@ pub async fn is_service_available() -> Result<()> {
|
||||
}
|
||||
|
||||
/// 强制重装服务(UI修复按钮)
|
||||
pub async fn force_reinstall_service() -> Result<()> {
|
||||
pub fn force_reinstall_service() -> Result<()> {
|
||||
log::info!(target: "app", "用户请求强制重装服务");
|
||||
|
||||
let service_state = ServiceState::default();
|
||||
@@ -983,13 +975,13 @@ pub async fn force_reinstall_service() -> Result<()> {
|
||||
|
||||
log::info!(target: "app", "已重置服务状态,开始执行重装");
|
||||
|
||||
match reinstall_service().await {
|
||||
match reinstall_service() {
|
||||
Ok(()) => {
|
||||
log::info!(target: "app", "服务重装成功");
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "强制重装服务失败: {}", err);
|
||||
log::error!(target: "app", "强制重装服务失败: {err}");
|
||||
bail!("强制重装服务失败: {}", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use hmac::{Hmac, Mac};
|
||||
@@ -131,7 +133,7 @@ pub async fn send_ipc_request(
|
||||
|
||||
logging!(info, Type::Service, true, "正在连接服务 (Windows)...");
|
||||
|
||||
let command_type = format!("{:?}", command);
|
||||
let command_type = format!("{command:?}");
|
||||
|
||||
let request = match create_signed_request(command, payload) {
|
||||
Ok(req) => req,
|
||||
@@ -143,7 +145,7 @@ pub async fn send_ipc_request(
|
||||
|
||||
let request_json = serde_json::to_string(&request)?;
|
||||
|
||||
let result = tokio::task::spawn_blocking(move || -> Result<IpcResponse> {
|
||||
let result = AsyncHandler::spawn_blocking(move || -> Result<IpcResponse> {
|
||||
let c_pipe_name = match CString::new(IPC_SOCKET_NAME) {
|
||||
Ok(name) => name,
|
||||
Err(e) => {
|
||||
@@ -257,7 +259,7 @@ pub async fn send_ipc_request(
|
||||
|
||||
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
|
||||
|
||||
let command_type = format!("{:?}", command);
|
||||
let command_type = format!("{command:?}");
|
||||
|
||||
let request = match create_signed_request(command, payload) {
|
||||
Ok(req) => req,
|
||||
|
||||
+94
-218
@@ -2,25 +2,20 @@
|
||||
use crate::utils::autostart as startup_shortcut;
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::handle::Handle,
|
||||
logging, logging_error,
|
||||
process::AsyncHandler,
|
||||
core::{handle::Handle, EventDrivenProxyManager},
|
||||
logging, logging_error, singleton_lazy,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
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;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
pub struct Sysopt {
|
||||
update_sysproxy: Arc<TokioMutex<bool>>,
|
||||
reset_sysproxy: Arc<TokioMutex<bool>>,
|
||||
/// record whether the guard async is running or not
|
||||
guard_state: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -33,10 +28,13 @@ static DEFAULT_BYPASS: &str =
|
||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
|
||||
|
||||
fn get_bypass() -> String {
|
||||
let use_default = Config::verge().latest().use_default_bypass.unwrap_or(true);
|
||||
let use_default = Config::verge()
|
||||
.latest_ref()
|
||||
.use_default_bypass
|
||||
.unwrap_or(true);
|
||||
let res = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
let verge = verge.latest_ref();
|
||||
verge.system_proxy_bypass.clone()
|
||||
};
|
||||
let custom_bypass = match res {
|
||||
@@ -47,24 +45,31 @@ fn get_bypass() -> String {
|
||||
if custom_bypass.is_empty() {
|
||||
DEFAULT_BYPASS.to_string()
|
||||
} else if use_default {
|
||||
format!("{},{}", DEFAULT_BYPASS, custom_bypass)
|
||||
format!("{DEFAULT_BYPASS},{custom_bypass}")
|
||||
} else {
|
||||
custom_bypass
|
||||
}
|
||||
}
|
||||
|
||||
impl Sysopt {
|
||||
pub fn global() -> &'static Sysopt {
|
||||
static SYSOPT: OnceCell<Sysopt> = OnceCell::new();
|
||||
SYSOPT.get_or_init(|| Sysopt {
|
||||
impl Default for Sysopt {
|
||||
fn default() -> Self {
|
||||
Sysopt {
|
||||
update_sysproxy: Arc::new(TokioMutex::new(false)),
|
||||
reset_sysproxy: Arc::new(TokioMutex::new(false)),
|
||||
guard_state: Arc::new(false.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use simplified singleton_lazy macro
|
||||
singleton_lazy!(Sysopt, SYSOPT, Sysopt::default);
|
||||
|
||||
impl Sysopt {
|
||||
pub fn init_guard_sysproxy(&self) -> Result<()> {
|
||||
self.guard_proxy();
|
||||
// 使用事件驱动代理管理器
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_app_started();
|
||||
|
||||
log::info!(target: "app", "已启用事件驱动代理守卫");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -73,14 +78,14 @@ impl Sysopt {
|
||||
let _lock = self.update_sysproxy.lock().await;
|
||||
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
.unwrap_or(Config::clash().latest_ref().get_mixed_port());
|
||||
let pac_port = IVerge::get_singleton_port();
|
||||
|
||||
let (sys_enable, pac_enable, proxy_host) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
let verge = verge.latest_ref();
|
||||
(
|
||||
verge.enable_system_proxy.unwrap_or(false),
|
||||
verge.proxy_auto_config.unwrap_or(false),
|
||||
@@ -101,12 +106,14 @@ impl Sysopt {
|
||||
};
|
||||
let mut auto = Autoproxy {
|
||||
enable: false,
|
||||
url: format!("http://{}:{}/commands/pac", proxy_host, pac_port),
|
||||
url: format!("http://{proxy_host}:{pac_port}/commands/pac"),
|
||||
};
|
||||
|
||||
if !sys_enable {
|
||||
sys.set_system_proxy()?;
|
||||
auto.set_auto_proxy()?;
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -115,6 +122,8 @@ impl Sysopt {
|
||||
auto.enable = true;
|
||||
sys.set_system_proxy()?;
|
||||
auto.set_auto_proxy()?;
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -123,19 +132,26 @@ impl Sysopt {
|
||||
sys.enable = true;
|
||||
auto.set_auto_proxy()?;
|
||||
sys.set_system_proxy()?;
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if !sys_enable {
|
||||
return self.reset_sysproxy().await;
|
||||
let result = self.reset_sysproxy().await;
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
return result;
|
||||
}
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use anyhow::bail;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let app_handle = Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
|
||||
|
||||
let binary_path = dirs::service_path()?;
|
||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||
@@ -145,30 +161,36 @@ impl Sysopt {
|
||||
|
||||
let shell = app_handle.shell();
|
||||
let output = if pac_enable {
|
||||
let address = format!("http://{}:{}/commands/pac", proxy_host, pac_port);
|
||||
let output = shell
|
||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||
let address = format!("http://{proxy_host}:{pac_port}/commands/pac");
|
||||
let sysproxy_str = sysproxy_exe
|
||||
.as_path()
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
|
||||
shell
|
||||
.command(sysproxy_str)
|
||||
.args(["pac", address.as_str()])
|
||||
.output()
|
||||
.await
|
||||
.unwrap();
|
||||
output
|
||||
.await?
|
||||
} else {
|
||||
let address = format!("{}:{}", proxy_host, port);
|
||||
let address = format!("{proxy_host}:{port}");
|
||||
let bypass = get_bypass();
|
||||
let output = shell
|
||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||
let sysproxy_str = sysproxy_exe
|
||||
.as_path()
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
|
||||
shell
|
||||
.command(sysproxy_str)
|
||||
.args(["global", address.as_str(), bypass.as_ref()])
|
||||
.output()
|
||||
.await
|
||||
.unwrap();
|
||||
output
|
||||
.await?
|
||||
};
|
||||
|
||||
if !output.status.success() {
|
||||
bail!("sysproxy exe run failed");
|
||||
}
|
||||
}
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
proxy_manager.notify_config_changed();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -180,7 +202,16 @@ impl Sysopt {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let mut sysproxy: Sysproxy = Sysproxy::get_system_proxy()?;
|
||||
let mut autoproxy = Autoproxy::get_auto_proxy()?;
|
||||
let mut autoproxy = match Autoproxy::get_auto_proxy() {
|
||||
Ok(ap) => ap,
|
||||
Err(e) => {
|
||||
log::warn!(target: "app", "重置代理时获取自动代理配置失败: {e}, 使用默认配置");
|
||||
Autoproxy {
|
||||
enable: false,
|
||||
url: "".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
sysproxy.enable = false;
|
||||
autoproxy.enable = false;
|
||||
autoproxy.set_auto_proxy()?;
|
||||
@@ -193,7 +224,9 @@ impl Sysopt {
|
||||
use anyhow::bail;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let app_handle = Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
|
||||
|
||||
let binary_path = dirs::service_path()?;
|
||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||
@@ -203,12 +236,15 @@ impl Sysopt {
|
||||
}
|
||||
|
||||
let shell = app_handle.shell();
|
||||
let sysproxy_str = sysproxy_exe
|
||||
.as_path()
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid sysproxy.exe path"))?;
|
||||
let output = shell
|
||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||
.command(sysproxy_str)
|
||||
.args(["set", "1"])
|
||||
.output()
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!("sysproxy exe run failed");
|
||||
@@ -220,7 +256,7 @@ impl Sysopt {
|
||||
|
||||
/// update the startup
|
||||
pub fn update_launch(&self) -> Result<()> {
|
||||
let enable_auto_launch = { Config::verge().latest().enable_auto_launch };
|
||||
let enable_auto_launch = { Config::verge().latest_ref().enable_auto_launch };
|
||||
let is_enable = enable_auto_launch.unwrap_or(false);
|
||||
logging!(info, true, "Setting auto-launch state to: {:?}", is_enable);
|
||||
|
||||
@@ -229,19 +265,17 @@ impl Sysopt {
|
||||
{
|
||||
if is_enable {
|
||||
if let Err(e) = startup_shortcut::create_shortcut() {
|
||||
log::error!(target: "app", "创建启动快捷方式失败: {}", e);
|
||||
log::error!(target: "app", "创建启动快捷方式失败: {e}");
|
||||
// 如果快捷方式创建失败,回退到原来的方法
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else if let Err(e) = startup_shortcut::remove_shortcut() {
|
||||
log::error!(target: "app", "删除启动快捷方式失败: {e}");
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
if let Err(e) = startup_shortcut::remove_shortcut() {
|
||||
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +290,10 @@ impl Sysopt {
|
||||
|
||||
/// 尝试使用原来的自启动方法
|
||||
fn try_original_autostart_method(&self, is_enable: bool) {
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let Some(app_handle) = Handle::global().app_handle() else {
|
||||
log::error!(target: "app", "App handle not available for autostart");
|
||||
return;
|
||||
};
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
|
||||
if is_enable {
|
||||
@@ -273,191 +310,30 @@ impl Sysopt {
|
||||
{
|
||||
match startup_shortcut::is_shortcut_enabled() {
|
||||
Ok(enabled) => {
|
||||
log::info!(target: "app", "快捷方式自启动状态: {}", enabled);
|
||||
log::info!(target: "app", "快捷方式自启动状态: {enabled}");
|
||||
return Ok(enabled);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {}", e);
|
||||
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到原来的方法
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let app_handle = Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("App handle not available"))?;
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
|
||||
match autostart_manager.is_enabled() {
|
||||
Ok(status) => {
|
||||
log::info!(target: "app", "Auto launch status: {}", status);
|
||||
log::info!(target: "app", "Auto launch status: {status}");
|
||||
Ok(status)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get auto launch status: {}", e);
|
||||
log::error!(target: "app", "Failed to get auto launch status: {e}");
|
||||
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn guard_proxy(&self) {
|
||||
let _lock = self.guard_state.lock();
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
// default duration is 10s
|
||||
let mut wait_secs = 10u64;
|
||||
|
||||
loop {
|
||||
sleep(Duration::from_secs(wait_secs)).await;
|
||||
|
||||
let (enable, guard, guard_duration, pac, proxy_host) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
verge.enable_system_proxy.unwrap_or(false),
|
||||
verge.enable_proxy_guard.unwrap_or(false),
|
||||
verge.proxy_guard_duration.unwrap_or(10),
|
||||
verge.proxy_auto_config.unwrap_or(false),
|
||||
verge
|
||||
.proxy_host
|
||||
.clone()
|
||||
.unwrap_or_else(|| String::from("127.0.0.1")),
|
||||
)
|
||||
};
|
||||
|
||||
// stop loop
|
||||
if !enable || !guard {
|
||||
continue;
|
||||
}
|
||||
|
||||
// update duration
|
||||
wait_secs = guard_duration;
|
||||
|
||||
log::debug!(target: "app", "try to guard the system proxy");
|
||||
|
||||
// 获取期望的代理端口
|
||||
let port = Config::verge()
|
||||
.latest()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(Config::clash().data().get_mixed_port());
|
||||
let pac_port = IVerge::get_singleton_port();
|
||||
let bypass = get_bypass();
|
||||
|
||||
// 检查系统代理配置
|
||||
if pac {
|
||||
// 检查 PAC 代理设置
|
||||
let expected_url = format!("http://{}:{}/commands/pac", proxy_host, pac_port);
|
||||
let autoproxy = match Autoproxy::get_auto_proxy() {
|
||||
Ok(ap) => ap,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to get the auto proxy: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查自动代理是否启用且URL是否正确
|
||||
if !autoproxy.enable || autoproxy.url != expected_url {
|
||||
log::info!(target: "app", "auto proxy settings changed, restoring...");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let new_autoproxy = Autoproxy {
|
||||
enable: true,
|
||||
url: expected_url,
|
||||
};
|
||||
logging_error!(Type::System, true, new_autoproxy.set_auto_proxy());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let binary_path = match dirs::service_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to get service path: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||
if !sysproxy_exe.exists() {
|
||||
log::error!(target: "app", "sysproxy.exe not found");
|
||||
continue;
|
||||
}
|
||||
|
||||
let shell = app_handle.shell();
|
||||
let output = shell
|
||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||
.args(["pac", expected_url.as_str()])
|
||||
.output()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if !output.status.success() {
|
||||
log::error!(target: "app", "failed to set auto proxy");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 检查常规系统代理设置
|
||||
let sysproxy = match Sysproxy::get_system_proxy() {
|
||||
Ok(sp) => sp,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to get the system proxy: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查系统代理是否启用且配置是否匹配
|
||||
if !sysproxy.enable || sysproxy.host != proxy_host || sysproxy.port != port {
|
||||
log::info!(target: "app", "system proxy settings changed, restoring...");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let new_sysproxy = Sysproxy {
|
||||
enable: true,
|
||||
host: proxy_host.clone(),
|
||||
port,
|
||||
bypass: bypass.clone(),
|
||||
};
|
||||
logging_error!(Type::System, true, new_sysproxy.set_system_proxy());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use crate::{core::handle::Handle, utils::dirs};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let binary_path = match dirs::service_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "failed to get service path: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
|
||||
if !sysproxy_exe.exists() {
|
||||
log::error!(target: "app", "sysproxy.exe not found");
|
||||
continue;
|
||||
}
|
||||
|
||||
let address = format!("{}:{}", proxy_host, port);
|
||||
let shell = app_handle.shell();
|
||||
let output = shell
|
||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||
.args(["global", address.as_str(), bypass.as_ref()])
|
||||
.output()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if !output.status.success() {
|
||||
log::error!(target: "app", "failed to set system proxy");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+30
-29
@@ -1,9 +1,14 @@
|
||||
use crate::{config::Config, feat, logging, logging_error, utils::logging::Type};
|
||||
use crate::{config::Config, feat, logging, logging_error, singleton, utils::logging::Type};
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use parking_lot::RwLock;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
type TaskID = u64;
|
||||
|
||||
@@ -22,23 +27,24 @@ pub struct Timer {
|
||||
/// save the current state - using RwLock for better read concurrency
|
||||
pub timer_map: Arc<RwLock<HashMap<String, TimerTask>>>,
|
||||
|
||||
/// increment id - kept as mutex since it's just a counter
|
||||
pub timer_count: Arc<Mutex<TaskID>>,
|
||||
/// increment id - atomic counter for better performance
|
||||
pub timer_count: AtomicU64,
|
||||
|
||||
/// Flag to mark if timer is initialized - atomic for better performance
|
||||
pub initialized: Arc<std::sync::atomic::AtomicBool>,
|
||||
pub initialized: AtomicBool,
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
pub fn global() -> &'static Timer {
|
||||
static TIMER: OnceCell<Timer> = OnceCell::new();
|
||||
// Use singleton macro
|
||||
singleton!(Timer, TIMER_INSTANCE);
|
||||
|
||||
TIMER.get_or_init(|| Timer {
|
||||
impl Timer {
|
||||
fn new() -> Self {
|
||||
Timer {
|
||||
delay_timer: Arc::new(RwLock::new(DelayTimerBuilder::default().build())),
|
||||
timer_map: Arc::new(RwLock::new(HashMap::new())),
|
||||
timer_count: Arc::new(Mutex::new(1)),
|
||||
initialized: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
})
|
||||
timer_count: AtomicU64::new(1),
|
||||
initialized: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize timer with better error handling and atomic operations
|
||||
@@ -46,12 +52,7 @@ impl Timer {
|
||||
// Use compare_exchange for thread-safe initialization check
|
||||
if self
|
||||
.initialized
|
||||
.compare_exchange(
|
||||
false,
|
||||
true,
|
||||
std::sync::atomic::Ordering::SeqCst,
|
||||
std::sync::atomic::Ordering::SeqCst,
|
||||
)
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_err()
|
||||
{
|
||||
logging!(debug, Type::Timer, "Timer already initialized, skipping...");
|
||||
@@ -63,8 +64,7 @@ impl Timer {
|
||||
// Initialize timer tasks
|
||||
if let Err(e) = self.refresh() {
|
||||
// Reset initialization flag on error
|
||||
self.initialized
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
self.initialized.store(false, Ordering::SeqCst);
|
||||
logging_error!(Type::Timer, false, "Failed to initialize timer: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
@@ -91,7 +91,7 @@ impl Timer {
|
||||
let cur_timestamp = chrono::Local::now().timestamp();
|
||||
|
||||
// Collect profiles that need immediate update
|
||||
let profiles_to_update = if let Some(items) = Config::profiles().latest().get_items() {
|
||||
let profiles_to_update = if let Some(items) = Config::profiles().latest_ref().get_items() {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
@@ -229,7 +229,7 @@ impl Timer {
|
||||
fn gen_map(&self) -> HashMap<String, u64> {
|
||||
let mut new_map = HashMap::new();
|
||||
|
||||
if let Some(items) = Config::profiles().latest().get_items() {
|
||||
if let Some(items) = Config::profiles().latest_ref().get_items() {
|
||||
for item in items.iter() {
|
||||
if let Some(option) = item.option.as_ref() {
|
||||
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) {
|
||||
@@ -299,7 +299,8 @@ impl Timer {
|
||||
}
|
||||
|
||||
// Find new tasks to add
|
||||
let mut next_id = *self.timer_count.lock();
|
||||
let mut next_id = self.timer_count.load(Ordering::Relaxed);
|
||||
let original_id = next_id;
|
||||
|
||||
for (uid, &interval) in new_map.iter() {
|
||||
if !timer_map.contains_key(uid) {
|
||||
@@ -316,8 +317,8 @@ impl Timer {
|
||||
}
|
||||
|
||||
// Update counter only if we added new tasks
|
||||
if next_id > *self.timer_count.lock() {
|
||||
*self.timer_count.lock() = next_id;
|
||||
if next_id > original_id {
|
||||
self.timer_count.store(next_id, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len());
|
||||
@@ -376,7 +377,7 @@ impl Timer {
|
||||
|
||||
// Get the profile updated timestamp
|
||||
let profiles_config = Config::profiles();
|
||||
let profiles = profiles_config.latest();
|
||||
let profiles = profiles_config.latest_ref();
|
||||
let items = match profiles.get_items() {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
@@ -438,7 +439,7 @@ impl Timer {
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(40), async {
|
||||
Self::emit_update_event(&uid, true);
|
||||
|
||||
let is_current = Config::profiles().latest().current.as_ref() == Some(&uid);
|
||||
let is_current = Config::profiles().latest_ref().current.as_ref() == Some(&uid);
|
||||
logging!(
|
||||
info,
|
||||
Type::Timer,
|
||||
|
||||
+234
-329
@@ -2,29 +2,19 @@ use once_cell::sync::OnceCell;
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod speed_rate;
|
||||
use crate::ipc::Rate;
|
||||
use crate::{
|
||||
cmd,
|
||||
config::Config,
|
||||
feat, logging,
|
||||
module::{
|
||||
lightweight::{entry_lightweight_mode, is_in_lightweight_mode},
|
||||
mihomo::Rate,
|
||||
},
|
||||
resolve,
|
||||
module::lightweight::is_in_lightweight_mode,
|
||||
singleton_lazy,
|
||||
utils::{dirs::find_target_icons, i18n::t, resolve::VERSION},
|
||||
Type,
|
||||
};
|
||||
|
||||
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},
|
||||
@@ -35,20 +25,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<Mutex<Instant>> = OnceCell::new();
|
||||
const TRAY_CLICK_DEBOUNCE_MS: u64 = 300;
|
||||
|
||||
fn get_tray_click_debounce() -> &'static Mutex<Instant> {
|
||||
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<Mutex<Option<SpeedRate>>>,
|
||||
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
||||
is_subscribed: Arc<RwLock<bool>>,
|
||||
pub rate_cache: Arc<Mutex<Option<Rate>>>,
|
||||
last_menu_update: Mutex<Option<Instant>>,
|
||||
menu_updating: AtomicBool,
|
||||
}
|
||||
@@ -61,12 +68,13 @@ pub struct Tray {
|
||||
|
||||
impl TrayState {
|
||||
pub fn get_common_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false);
|
||||
if is_common_tray_icon {
|
||||
if let Some(common_icon_path) = find_target_icons("common").unwrap() {
|
||||
let icon_data = fs::read(common_icon_path).unwrap();
|
||||
return (true, icon_data);
|
||||
if let Ok(Some(common_icon_path)) = find_target_icons("common") {
|
||||
if let Ok(icon_data) = fs::read(common_icon_path) {
|
||||
return (true, icon_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -95,12 +103,13 @@ impl TrayState {
|
||||
}
|
||||
|
||||
pub fn get_sysproxy_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false);
|
||||
if is_sysproxy_tray_icon {
|
||||
if let Some(sysproxy_icon_path) = find_target_icons("sysproxy").unwrap() {
|
||||
let icon_data = fs::read(sysproxy_icon_path).unwrap();
|
||||
return (true, icon_data);
|
||||
if let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy") {
|
||||
if let Ok(icon_data) = fs::read(sysproxy_icon_path) {
|
||||
return (true, icon_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -109,7 +118,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 {
|
||||
(
|
||||
@@ -129,12 +138,13 @@ impl TrayState {
|
||||
}
|
||||
|
||||
pub fn get_tun_tray_icon() -> (bool, Vec<u8>) {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false);
|
||||
if is_tun_tray_icon {
|
||||
if let Some(tun_icon_path) = find_target_icons("tun").unwrap() {
|
||||
let icon_data = fs::read(tun_icon_path).unwrap();
|
||||
return (true, icon_data);
|
||||
if let Ok(Some(tun_icon_path)) = find_target_icons("tun") {
|
||||
if let Ok(icon_data) = fs::read(tun_icon_path) {
|
||||
return (true, icon_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -143,7 +153,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 {
|
||||
(
|
||||
@@ -162,42 +172,33 @@ impl TrayState {
|
||||
}
|
||||
}
|
||||
|
||||
impl Tray {
|
||||
pub fn global() -> &'static Tray {
|
||||
static TRAY: OnceCell<Tray> = OnceCell::new();
|
||||
|
||||
#[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)),
|
||||
impl Default for Tray {
|
||||
fn default() -> Self {
|
||||
Tray {
|
||||
last_menu_update: Mutex::new(None),
|
||||
menu_updating: AtomicBool::new(false),
|
||||
});
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
return TRAY.get_or_init(|| Tray {
|
||||
last_menu_update: Mutex::new(None),
|
||||
menu_updating: AtomicBool::new(false),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn init(&self) -> Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mut speed_rate = self.speed_rate.lock();
|
||||
*speed_rate = Some(SpeedRate::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use simplified singleton_lazy macro
|
||||
singleton_lazy!(Tray, TRAY, Tray::default);
|
||||
|
||||
impl Tray {
|
||||
pub fn init(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘点击行为
|
||||
pub fn update_click_behavior(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray update"))?;
|
||||
let tray_event = { Config::verge().latest_ref().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
let tray = app_handle.tray_by_id("main").unwrap();
|
||||
let tray = app_handle
|
||||
.tray_by_id("main")
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?;
|
||||
match tray_event.as_str() {
|
||||
"tray_menu" => tray.set_show_menu_on_left_click(true)?,
|
||||
_ => tray.set_show_menu_on_left_click(false)?,
|
||||
@@ -205,33 +206,38 @@ impl Tray {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘菜单(带频率限制)
|
||||
/// 更新托盘菜单
|
||||
pub fn update_menu(&self) -> Result<()> {
|
||||
// 检查是否正在更新或距离上次更新太近
|
||||
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(500);
|
||||
// 调整最小更新间隔,确保状态及时刷新
|
||||
const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(100);
|
||||
|
||||
// 检查是否已有更新任务在执行
|
||||
// 检查是否正在更新
|
||||
if self.menu_updating.load(Ordering::Acquire) {
|
||||
log::debug!(target: "app", "托盘菜单正在更新中,跳过本次更新");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 检查更新频率
|
||||
{
|
||||
let last_update = self.last_menu_update.lock();
|
||||
if let Some(last_time) = *last_update {
|
||||
if last_time.elapsed() < MIN_UPDATE_INTERVAL {
|
||||
log::debug!(target: "app", "托盘菜单更新频率过高,跳过本次更新");
|
||||
return Ok(());
|
||||
// 检查更新频率,但允许重要事件跳过频率限制
|
||||
let should_force_update = match std::thread::current().name() {
|
||||
Some("main") => true,
|
||||
_ => {
|
||||
let last_update = self.last_menu_update.lock();
|
||||
if let Some(last_time) = *last_update {
|
||||
last_time.elapsed() >= MIN_UPDATE_INTERVAL
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !should_force_update {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在");
|
||||
return Ok(()); // 早期返回,避免panic
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -248,13 +254,14 @@ impl Tray {
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> {
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
let mode = {
|
||||
Config::clash()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.0
|
||||
.get("mode")
|
||||
.map(|val| val.as_str().unwrap_or("rule"))
|
||||
@@ -262,7 +269,7 @@ impl Tray {
|
||||
.to_owned()
|
||||
};
|
||||
let profile_uid_and_name = Config::profiles()
|
||||
.data()
|
||||
.data_mut()
|
||||
.all_profile_uid_and_name()
|
||||
.unwrap_or_default();
|
||||
let is_lightweight_mode = is_in_lightweight_mode();
|
||||
@@ -270,7 +277,7 @@ impl Tray {
|
||||
match app_handle.tray_by_id("main") {
|
||||
Some(tray) => {
|
||||
let _ = tray.set_menu(Some(create_tray_menu(
|
||||
&app_handle,
|
||||
app_handle,
|
||||
Some(mode.as_str()),
|
||||
*system_proxy,
|
||||
*tun_mode,
|
||||
@@ -288,7 +295,8 @@ impl Tray {
|
||||
}
|
||||
|
||||
/// 更新托盘图标
|
||||
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
@@ -305,68 +313,71 @@ impl Tray {
|
||||
}
|
||||
};
|
||||
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
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(),
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
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);
|
||||
let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
|
||||
let is_colorful = colorful == "colorful";
|
||||
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn update_icon(&self, _rate: Option<Rate>) -> Result<()> {
|
||||
let app_handle = match handle::Handle::global().app_handle() {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
|
||||
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 = match app_handle.tray_by_id("main") {
|
||||
Some(tray) => tray,
|
||||
None => {
|
||||
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
Ok(())
|
||||
}
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
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) {
|
||||
(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 _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘显示状态的函数
|
||||
pub fn update_tray_display(&self) -> Result<()> {
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray update"))?;
|
||||
let _tray = app_handle
|
||||
.tray_by_id("main")
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?;
|
||||
|
||||
// 更新菜单
|
||||
self.update_menu()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新托盘提示
|
||||
@@ -387,7 +398,7 @@ impl Tray {
|
||||
}
|
||||
};
|
||||
|
||||
let verge = Config::verge().latest().clone();
|
||||
let verge = Config::verge().latest_ref().clone();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
@@ -400,7 +411,7 @@ impl Tray {
|
||||
|
||||
let mut current_profile_name = "None".to_string();
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let profiles = profiles.latest_ref();
|
||||
if let Some(current_profile_uid) = profiles.get_current() {
|
||||
if let Ok(profile) = profiles.get_item(¤t_profile_uid) {
|
||||
current_profile_name = match &profile.name {
|
||||
@@ -431,158 +442,14 @@ impl Tray {
|
||||
self.update_menu()?;
|
||||
self.update_icon(None)?;
|
||||
self.update_tooltip()?;
|
||||
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");
|
||||
|
||||
// 更新轻量模式显示状态
|
||||
self.update_tray_display()?;
|
||||
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创建系统托盘");
|
||||
@@ -591,13 +458,19 @@ impl Tray {
|
||||
let icon_bytes = TrayState::get_common_tray_icon().1;
|
||||
let icon = tauri::image::Image::from_bytes(&icon_bytes)?;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let builder = TrayIconBuilder::with_id("main")
|
||||
.icon(icon)
|
||||
.icon_as_template(false);
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let mut builder = TrayIconBuilder::with_id("main")
|
||||
.icon(icon)
|
||||
.icon_as_template(false);
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
{
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let tray_event = { Config::verge().latest_ref().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
if tray_event.as_str() != "tray_menu" {
|
||||
builder = builder.show_menu_on_left_click(false);
|
||||
@@ -607,9 +480,9 @@ impl Tray {
|
||||
let tray = builder.build(app_handle)?;
|
||||
|
||||
tray.on_tray_icon_event(|_, event| {
|
||||
let tray_event = { Config::verge().latest().tray_event.clone() };
|
||||
let tray_event = { Config::verge().latest_ref().tray_event.clone() };
|
||||
let tray_event: String = tray_event.unwrap_or("main_window".into());
|
||||
log::debug!(target: "app","tray event: {:?}", tray_event);
|
||||
log::debug!(target: "app","tray event: {tray_event:?}");
|
||||
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
@@ -617,14 +490,23 @@ 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),
|
||||
"main_window" => {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
log::info!(target: "app", "Tray点击事件: 显示主窗口");
|
||||
if crate::module::lightweight::is_in_lightweight_mode() {
|
||||
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式");
|
||||
crate::module::lightweight::exit_lightweight_mode();
|
||||
}
|
||||
let _ = resolve::create_window(true);
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "窗口显示结果: {result:?}");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -634,6 +516,17 @@ impl Tray {
|
||||
log::info!(target: "app", "系统托盘创建成功");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 托盘统一的状态更新函数
|
||||
pub fn update_all_states(&self) -> Result<()> {
|
||||
// 确保所有状态更新完成
|
||||
self.update_menu()?;
|
||||
self.update_icon(None)?;
|
||||
self.update_tooltip()?;
|
||||
self.update_tray_display()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_tray_menu(
|
||||
@@ -650,7 +543,7 @@ fn create_tray_menu(
|
||||
let version = VERSION.get().unwrap_or(&unknown_version);
|
||||
|
||||
let hotkeys = Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.hotkeys
|
||||
.as_ref()
|
||||
.map(|h| {
|
||||
@@ -670,19 +563,18 @@ fn create_tray_menu(
|
||||
.iter()
|
||||
.map(|(profile_uid, profile_name)| {
|
||||
let is_current_profile = Config::profiles()
|
||||
.data()
|
||||
.data_mut()
|
||||
.is_current_profile_index(profile_uid.to_string());
|
||||
CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
format!("profiles_{}", profile_uid),
|
||||
format!("profiles_{profile_uid}"),
|
||||
t(profile_name),
|
||||
true,
|
||||
is_current_profile,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let profile_menu_items: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
|
||||
.iter()
|
||||
.map(|item| item as &dyn IsMenuItem<Wry>)
|
||||
@@ -694,8 +586,7 @@ fn create_tray_menu(
|
||||
t("Dashboard"),
|
||||
true,
|
||||
hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let rule_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -704,8 +595,7 @@ fn create_tray_menu(
|
||||
true,
|
||||
mode == "rule",
|
||||
hotkeys.get("clash_mode_rule").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let global_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -714,8 +604,7 @@ fn create_tray_menu(
|
||||
true,
|
||||
mode == "global",
|
||||
hotkeys.get("clash_mode_global").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let direct_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -724,8 +613,7 @@ fn create_tray_menu(
|
||||
true,
|
||||
mode == "direct",
|
||||
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let profiles = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
@@ -733,8 +621,7 @@ fn create_tray_menu(
|
||||
t("Profiles"),
|
||||
true,
|
||||
&profile_menu_items,
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let system_proxy = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -743,8 +630,7 @@ fn create_tray_menu(
|
||||
true,
|
||||
system_proxy_enabled,
|
||||
hotkeys.get("toggle_system_proxy").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let tun_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -753,8 +639,7 @@ fn create_tray_menu(
|
||||
true,
|
||||
tun_mode_enabled,
|
||||
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let lighteweight_mode = &CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -763,11 +648,9 @@ fn create_tray_menu(
|
||||
true,
|
||||
is_lightweight_mode,
|
||||
hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let copy_env =
|
||||
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
|
||||
let copy_env = &MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>)?;
|
||||
|
||||
let open_app_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -775,8 +658,7 @@ fn create_tray_menu(
|
||||
t("Conf Dir"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let open_core_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -784,8 +666,7 @@ fn create_tray_menu(
|
||||
t("Core Dir"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let open_logs_dir = &MenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -793,8 +674,7 @@ fn create_tray_menu(
|
||||
t("Logs Dir"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let open_dir = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
@@ -802,8 +682,7 @@ fn create_tray_menu(
|
||||
t("Open Dir"),
|
||||
true,
|
||||
&[open_app_dir, open_core_dir, open_logs_dir],
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let restart_clash = &MenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -811,8 +690,7 @@ fn create_tray_menu(
|
||||
t("Restart Clash Core"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let restart_app = &MenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -820,8 +698,7 @@ fn create_tray_menu(
|
||||
t("Restart App"),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let app_version = &MenuItem::with_id(
|
||||
app_handle,
|
||||
@@ -829,8 +706,7 @@ fn create_tray_menu(
|
||||
format!("{} {version}", t("Verge Version")),
|
||||
true,
|
||||
None::<&str>,
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let more = &Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
@@ -838,13 +714,11 @@ fn create_tray_menu(
|
||||
t("More"),
|
||||
true,
|
||||
&[restart_clash, restart_app, app_version],
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
let quit =
|
||||
&MenuItem::with_id(app_handle, "quit", t("Exit"), true, Some("CmdOrControl+Q")).unwrap();
|
||||
let quit = &MenuItem::with_id(app_handle, "quit", t("Exit"), true, Some("CmdOrControl+Q"))?;
|
||||
|
||||
let separator = &PredefinedMenuItem::separator(app_handle).unwrap();
|
||||
let separator = &PredefinedMenuItem::separator(app_handle)?;
|
||||
|
||||
let menu = tauri::menu::MenuBuilder::new(app_handle)
|
||||
.items(&[
|
||||
@@ -866,8 +740,7 @@ fn create_tray_menu(
|
||||
separator,
|
||||
quit,
|
||||
])
|
||||
.build()
|
||||
.unwrap();
|
||||
.build()?;
|
||||
Ok(menu)
|
||||
}
|
||||
|
||||
@@ -885,15 +758,26 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
feat::change_clash_mode(mode.into());
|
||||
}
|
||||
"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 _ = resolve::create_window(true);
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "窗口显示结果: {result:?}");
|
||||
}
|
||||
"system_proxy" => {
|
||||
feat::toggle_system_proxy();
|
||||
}
|
||||
"tun_mode" => {
|
||||
feat::toggle_tun_mode(None);
|
||||
}
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(None),
|
||||
"copy_env" => feat::copy_clash_env(),
|
||||
"open_app_dir" => {
|
||||
let _ = cmd::open_app_dir();
|
||||
@@ -906,7 +790,24 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
}
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => feat::restart_app(),
|
||||
"entry_lightweight_mode" => entry_lightweight_mode(),
|
||||
"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();
|
||||
} else {
|
||||
crate::module::lightweight::entry_lightweight_mode();
|
||||
}
|
||||
|
||||
if was_lightweight {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
let result = WindowManager::show_main_window();
|
||||
log::info!(target: "app", "退出轻量模式后显示主窗口: {result:?}");
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
feat::quit();
|
||||
}
|
||||
@@ -916,4 +817,8 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Err(e) = Tray::global().update_all_states() {
|
||||
log::warn!(target: "app", "更新托盘状态失败: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Mutex<(Rate, Rate)>>,
|
||||
last_update: Arc<Mutex<std::time::Instant>>,
|
||||
}
|
||||
|
||||
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<Rate> {
|
||||
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<Rate> {
|
||||
let rates = self.rate.lock();
|
||||
let (current, _) = &*rates;
|
||||
Some(current.clone())
|
||||
}
|
||||
|
||||
// 分离图标加载和速率渲染
|
||||
pub fn add_speed_text(
|
||||
is_custom_icon: bool,
|
||||
icon_bytes: Option<Vec<u8>>,
|
||||
rate: Option<&Rate>,
|
||||
) -> Result<Vec<u8>> {
|
||||
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<impl Stream<Item = Result<Traffic, anyhow::Error>>>
|
||||
{
|
||||
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::<serde_json::Value>(&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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use deelevate::{PrivilegeLevel, Token};
|
||||
use runas::Command as RunasCommand;
|
||||
use std::process::Command as StdCommand;
|
||||
|
||||
pub async fn invoke_uwptools() -> Result<()> {
|
||||
pub fn invoke_uwptools() -> Result<()> {
|
||||
let resource_dir = dirs::app_resources_dir()?;
|
||||
let tool_path = resource_dir.join("enableLoopback.exe");
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@ pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
|
||||
|
||||
deep_merge(&mut config, &Value::from(merge));
|
||||
|
||||
let config = config.as_mapping().unwrap().clone();
|
||||
let config = config.as_mapping().cloned().unwrap_or_else(|| {
|
||||
log::error!("Failed to convert merged config to mapping, using empty mapping");
|
||||
Mapping::new()
|
||||
});
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
@@ -14,15 +14,15 @@ type ResultLog = Vec<(String, String)>;
|
||||
|
||||
/// Enhance mode
|
||||
/// 返回最终订阅、该订阅包含的键、和script执行的结果
|
||||
pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
// config.yaml 的订阅
|
||||
let clash_config = { Config::clash().latest().0.clone() };
|
||||
let clash_config = { Config::clash().latest_ref().0.clone() };
|
||||
|
||||
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
let verge = verge.latest_ref();
|
||||
(
|
||||
verge.clash_core.clone(),
|
||||
Some(verge.get_valid_clash_core()),
|
||||
verge.enable_tun_mode.unwrap_or(false),
|
||||
verge.enable_builtin_enhanced.unwrap_or(true),
|
||||
verge.verge_socks_enabled.unwrap_or(false),
|
||||
@@ -33,13 +33,13 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let redir_enabled = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
let verge = verge.latest_ref();
|
||||
verge.verge_redir_enabled.unwrap_or(false)
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
let tproxy_enabled = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
let verge = verge.latest_ref();
|
||||
verge.verge_tproxy_enabled.unwrap_or(false)
|
||||
};
|
||||
|
||||
@@ -56,7 +56,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
profile_name,
|
||||
) = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let profiles = profiles.latest_ref();
|
||||
|
||||
let current = profiles.current_mapping().unwrap_or_default();
|
||||
let merge = profiles
|
||||
@@ -214,6 +214,12 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
config.remove("port");
|
||||
continue;
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if key.as_str() == Some("redir-port") || key.as_str() == Some("tproxy-port") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
if key.as_str() == Some("redir-port") && !redir_enabled {
|
||||
@@ -228,7 +234,22 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
config.insert(key, value);
|
||||
// 处理 external-controller 键的开关逻辑
|
||||
if key.as_str() == Some("external-controller") {
|
||||
let enable_external_controller = Config::verge()
|
||||
.latest_ref()
|
||||
.enable_external_controller
|
||||
.unwrap_or(false);
|
||||
|
||||
if enable_external_controller {
|
||||
config.insert(key, value);
|
||||
} else {
|
||||
// 如果禁用了外部控制器,设置为空字符串
|
||||
config.insert(key, "".into());
|
||||
}
|
||||
} else {
|
||||
config.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +274,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
});
|
||||
}
|
||||
|
||||
config = use_tun(config, enable_tun).await;
|
||||
config = use_tun(config, enable_tun);
|
||||
config = use_sort(config);
|
||||
|
||||
// 应用独立的DNS配置(如果启用)
|
||||
|
||||
+106
-32
@@ -7,24 +7,43 @@ pub fn use_script(
|
||||
config: Mapping,
|
||||
name: String,
|
||||
) -> Result<(Mapping, Vec<(String, String)>)> {
|
||||
use boa_engine::{native_function::NativeFunction, Context, JsValue, Source};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use boa_engine::{native_function::NativeFunction, Context, JsString, JsValue, Source};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
let mut context = Context::default();
|
||||
|
||||
let outputs = Arc::new(Mutex::new(vec![]));
|
||||
let outputs = Rc::new(RefCell::new(vec![]));
|
||||
|
||||
let copy_outputs = outputs.clone();
|
||||
let copy_outputs = Rc::clone(&outputs);
|
||||
unsafe {
|
||||
let _ = context.register_global_builtin_callable(
|
||||
"__verge_log__".into(),
|
||||
2,
|
||||
NativeFunction::from_closure(
|
||||
move |_: &JsValue, args: &[JsValue], context: &mut Context| {
|
||||
let level = args.first().unwrap().to_string(context)?;
|
||||
let level = level.to_std_string().unwrap();
|
||||
let data = args.get(1).unwrap().to_string(context)?;
|
||||
let data = data.to_std_string().unwrap();
|
||||
let mut out = copy_outputs.lock().unwrap();
|
||||
let level = args.first().ok_or_else(|| {
|
||||
boa_engine::JsError::from_opaque(
|
||||
JsString::from("Missing level argument").into(),
|
||||
)
|
||||
})?;
|
||||
let level = level.to_string(context)?;
|
||||
let level = level.to_std_string().map_err(|_| {
|
||||
boa_engine::JsError::from_opaque(
|
||||
JsString::from("Failed to convert level to string").into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let data = args.get(1).ok_or_else(|| {
|
||||
boa_engine::JsError::from_opaque(
|
||||
JsString::from("Missing data argument").into(),
|
||||
)
|
||||
})?;
|
||||
let data = data.to_string(context)?;
|
||||
let data = data.to_std_string().map_err(|_| {
|
||||
boa_engine::JsError::from_opaque(
|
||||
JsString::from("Failed to convert data to string").into(),
|
||||
)
|
||||
})?;
|
||||
let mut out = copy_outputs.borrow_mut();
|
||||
out.push((level, data));
|
||||
Ok(JsValue::undefined())
|
||||
},
|
||||
@@ -33,38 +52,45 @@ pub fn use_script(
|
||||
}
|
||||
let _ = context.eval(Source::from_bytes(
|
||||
r#"var console = Object.freeze({
|
||||
log(data){__verge_log__("log",JSON.stringify(data))},
|
||||
info(data){__verge_log__("info",JSON.stringify(data))},
|
||||
error(data){__verge_log__("error",JSON.stringify(data))},
|
||||
debug(data){__verge_log__("debug",JSON.stringify(data))},
|
||||
log(data){__verge_log__("log",JSON.stringify(data, null, 2))},
|
||||
info(data){__verge_log__("info",JSON.stringify(data, null, 2))},
|
||||
error(data){__verge_log__("error",JSON.stringify(data, null, 2))},
|
||||
debug(data){__verge_log__("debug",JSON.stringify(data, null, 2))},
|
||||
warn(data){__verge_log__("warn",JSON.stringify(data, null, 2))},
|
||||
table(data){__verge_log__("table",JSON.stringify(data, null, 2))},
|
||||
});"#,
|
||||
));
|
||||
|
||||
let config = use_lowercase(config.clone());
|
||||
let config_str = serde_json::to_string(&config)?;
|
||||
|
||||
// 仅处理 name 参数中的特殊字符
|
||||
let safe_name = escape_js_string_for_single_quote(&name);
|
||||
|
||||
let code = format!(
|
||||
r#"try{{
|
||||
r"try{{
|
||||
{script};
|
||||
JSON.stringify(main({config_str},'{name}')||'')
|
||||
JSON.stringify(main({config_str},'{safe_name}')||'')
|
||||
}} catch(err) {{
|
||||
`__error_flag__ ${{err.toString()}}`
|
||||
}}"#
|
||||
}}"
|
||||
);
|
||||
|
||||
if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
|
||||
if !result.is_string() {
|
||||
anyhow::bail!("main function should return object");
|
||||
}
|
||||
let result = result.to_string(&mut context).unwrap();
|
||||
let result = result.to_std_string().unwrap();
|
||||
if result.starts_with("__error_flag__") {
|
||||
anyhow::bail!(result[15..].to_owned());
|
||||
}
|
||||
if result == "\"\"" {
|
||||
anyhow::bail!("main function should return object");
|
||||
}
|
||||
let res: Result<Mapping, Error> = Ok(serde_json::from_str::<Mapping>(result.as_str())?);
|
||||
let mut out = outputs.lock().unwrap();
|
||||
let result = result
|
||||
.to_string(&mut context)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to convert JS result to string: {}", e))?;
|
||||
let result = result
|
||||
.to_std_string()
|
||||
.map_err(|_| anyhow::anyhow!("Failed to convert JS string to std string"))?;
|
||||
|
||||
// 直接解析JSON结果,不做其他解析
|
||||
let res: Result<Mapping, Error> = parse_json_safely(&result);
|
||||
|
||||
let mut out = outputs.borrow_mut();
|
||||
match res {
|
||||
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
||||
Err(err) => {
|
||||
@@ -77,7 +103,30 @@ pub fn use_script(
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_json_safely(json_str: &str) -> Result<Mapping, Error> {
|
||||
let json_str = strip_outer_quotes(json_str);
|
||||
|
||||
Ok(serde_json::from_str::<Mapping>(json_str)?)
|
||||
}
|
||||
|
||||
// 移除字符串外层的引号
|
||||
fn strip_outer_quotes(s: &str) -> &str {
|
||||
let s = s.trim();
|
||||
if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
|
||||
&s[1..s.len() - 1]
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
// 转义单引号和反斜杠,用于单引号包裹的JavaScript字符串
|
||||
fn escape_js_string_for_single_quote(s: &str) -> String {
|
||||
s.replace('\\', "\\\\").replace('\'', "\\'")
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(unused_variables)]
|
||||
#[allow(clippy::expect_used)]
|
||||
fn test_script() {
|
||||
let script = r#"
|
||||
function main(config) {
|
||||
@@ -90,7 +139,7 @@ fn test_script() {
|
||||
}
|
||||
"#;
|
||||
|
||||
let config = r#"
|
||||
let config = r"
|
||||
rules:
|
||||
- 111
|
||||
- 222
|
||||
@@ -98,12 +147,37 @@ fn test_script() {
|
||||
enable: false
|
||||
dns:
|
||||
enable: false
|
||||
"#;
|
||||
";
|
||||
|
||||
let config = serde_yaml::from_str(config).unwrap();
|
||||
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
|
||||
let config = serde_yaml::from_str(config).expect("Failed to parse test config YAML");
|
||||
let (config, results) = use_script(script.into(), config, "".to_string())
|
||||
.expect("Script execution should succeed in test");
|
||||
|
||||
let _ = serde_yaml::to_string(&config).unwrap();
|
||||
let _ = serde_yaml::to_string(&config).expect("Failed to serialize config to YAML");
|
||||
let yaml_config_size = std::mem::size_of_val(&config);
|
||||
let box_yaml_config_size = std::mem::size_of_val(&Box::new(config));
|
||||
assert!(box_yaml_config_size < yaml_config_size);
|
||||
}
|
||||
|
||||
dbg!(results);
|
||||
// 测试特殊字符转义功能
|
||||
#[test]
|
||||
#[allow(clippy::expect_used)]
|
||||
fn test_escape_unescape() {
|
||||
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
|
||||
let escaped = escape_js_string_for_single_quote(test_string);
|
||||
println!("Original: {test_string}");
|
||||
println!("Escaped: {escaped}");
|
||||
|
||||
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
|
||||
let parsed = parse_json_safely(json_str).expect("Failed to parse test JSON safely");
|
||||
|
||||
assert!(parsed.contains_key("key"));
|
||||
assert!(parsed.contains_key("nested"));
|
||||
|
||||
let quoted_json_str = r#""{"key":"value","nested":{"key":"value"}}""#;
|
||||
let parsed_quoted =
|
||||
parse_json_safely(quoted_json_str).expect("Failed to parse quoted test JSON safely");
|
||||
|
||||
assert!(parsed_quoted.contains_key("key"));
|
||||
assert!(parsed_quoted.contains_key("nested"));
|
||||
}
|
||||
|
||||
@@ -89,6 +89,8 @@ mod tests {
|
||||
use serde_yaml::Value;
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[allow(clippy::expect_used)]
|
||||
fn test_delete_proxy_and_references() {
|
||||
let config_str = r#"
|
||||
proxies:
|
||||
@@ -107,7 +109,8 @@ proxy-groups:
|
||||
proxies:
|
||||
- "proxy1"
|
||||
"#;
|
||||
let mut config: Mapping = serde_yaml::from_str(config_str).unwrap();
|
||||
let mut config: Mapping =
|
||||
serde_yaml::from_str(config_str).expect("Failed to parse test config YAML");
|
||||
|
||||
let seq = SeqMap {
|
||||
prepend: Sequence::new(),
|
||||
@@ -118,38 +121,51 @@ proxy-groups:
|
||||
config = use_seq(seq, config, "proxies");
|
||||
|
||||
// Check if proxy1 is removed from proxies
|
||||
let proxies = config.get("proxies").unwrap().as_sequence().unwrap();
|
||||
let proxies = config
|
||||
.get("proxies")
|
||||
.expect("proxies field should exist")
|
||||
.as_sequence()
|
||||
.expect("proxies should be a sequence");
|
||||
assert_eq!(proxies.len(), 1);
|
||||
assert_eq!(
|
||||
proxies[0]
|
||||
.as_mapping()
|
||||
.unwrap()
|
||||
.expect("proxy should be a mapping")
|
||||
.get("name")
|
||||
.unwrap()
|
||||
.expect("proxy should have name")
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
.expect("name should be string"),
|
||||
"proxy2"
|
||||
);
|
||||
|
||||
// Check if proxy1 is removed from all groups
|
||||
let groups = config.get("proxy-groups").unwrap().as_sequence().unwrap();
|
||||
let groups = config
|
||||
.get("proxy-groups")
|
||||
.expect("proxy-groups field should exist")
|
||||
.as_sequence()
|
||||
.expect("proxy-groups should be a sequence");
|
||||
let group1_proxies = groups[0]
|
||||
.as_mapping()
|
||||
.unwrap()
|
||||
.expect("group should be a mapping")
|
||||
.get("proxies")
|
||||
.unwrap()
|
||||
.expect("group should have proxies")
|
||||
.as_sequence()
|
||||
.unwrap();
|
||||
.expect("group proxies should be a sequence");
|
||||
let group2_proxies = groups[1]
|
||||
.as_mapping()
|
||||
.unwrap()
|
||||
.expect("group should be a mapping")
|
||||
.get("proxies")
|
||||
.unwrap()
|
||||
.expect("group should have proxies")
|
||||
.as_sequence()
|
||||
.unwrap();
|
||||
.expect("group proxies should be a sequence");
|
||||
|
||||
assert_eq!(group1_proxies.len(), 1);
|
||||
assert_eq!(group1_proxies[0].as_str().unwrap(), "proxy2");
|
||||
assert_eq!(
|
||||
group1_proxies[0]
|
||||
.as_str()
|
||||
.expect("proxy name should be string"),
|
||||
"proxy2"
|
||||
);
|
||||
assert_eq!(group2_proxies.len(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use serde_yaml::{Mapping, Value};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::process::AsyncHandler;
|
||||
|
||||
macro_rules! revise {
|
||||
($map: expr, $key: expr, $val: expr) => {
|
||||
let ret_key = Value::String($key.into());
|
||||
@@ -18,7 +21,7 @@ macro_rules! append {
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
let tun_key = Value::from("tun");
|
||||
let tun_val = config.get(&tun_key);
|
||||
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
|
||||
@@ -59,8 +62,10 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
crate::utils::resolve::restore_public_dns().await;
|
||||
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
|
||||
AsyncHandler::spawn(move || async move {
|
||||
crate::utils::resolve::restore_public_dns().await;
|
||||
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +74,9 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
} else {
|
||||
// TUN未启用时,仅恢复系统DNS,不修改配置文件中的DNS设置
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::utils::resolve::restore_public_dns().await;
|
||||
AsyncHandler::spawn(move || async move {
|
||||
crate::utils::resolve::restore_public_dns().await;
|
||||
});
|
||||
}
|
||||
|
||||
// 更新TUN配置
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod service;
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::fs;
|
||||
/// Create a backup and upload to WebDAV
|
||||
pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
||||
let (file_name, temp_file_path) = backup::create_backup().map_err(|err| {
|
||||
log::error!(target: "app", "Failed to create backup: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to create backup: {err:#?}");
|
||||
err
|
||||
})?;
|
||||
|
||||
@@ -19,12 +19,12 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
||||
.upload(temp_file_path.clone(), file_name)
|
||||
.await
|
||||
{
|
||||
log::error!(target: "app", "Failed to upload to WebDAV: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to upload to WebDAV: {err:#?}");
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if let Err(err) = std::fs::remove_file(&temp_file_path) {
|
||||
log::warn!(target: "app", "Failed to remove temp file: {:#?}", err);
|
||||
log::warn!(target: "app", "Failed to remove temp file: {err:#?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -33,7 +33,7 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
||||
/// List WebDAV backups
|
||||
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
|
||||
backup::WebDavClient::global().list().await.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to list WebDAV backup files: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to list WebDAV backup files: {err:#?}");
|
||||
err
|
||||
})
|
||||
}
|
||||
@@ -44,7 +44,7 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
||||
.delete(filename)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to delete WebDAV backup file: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to delete WebDAV backup file: {err:#?}");
|
||||
err
|
||||
})
|
||||
}
|
||||
@@ -52,17 +52,19 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
||||
/// Restore WebDAV backup
|
||||
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
let verge = Config::verge();
|
||||
let verge_data = verge.data().clone();
|
||||
let verge_data = verge.latest_ref().clone();
|
||||
let webdav_url = verge_data.webdav_url.clone();
|
||||
let webdav_username = verge_data.webdav_username.clone();
|
||||
let webdav_password = verge_data.webdav_password.clone();
|
||||
|
||||
let backup_storage_path = app_home_dir().unwrap().join(&filename);
|
||||
let backup_storage_path = app_home_dir()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))?
|
||||
.join(&filename);
|
||||
backup::WebDavClient::global()
|
||||
.download(filename, backup_storage_path.clone())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!(target: "app", "Failed to download WebDAV backup file: {:#?}", err);
|
||||
log::error!(target: "app", "Failed to download WebDAV backup file: {err:#?}");
|
||||
err
|
||||
})?;
|
||||
|
||||
|
||||
+30
-16
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, tray, CoreManager},
|
||||
ipc::IpcManager,
|
||||
logging_error,
|
||||
module::mihomo::MihomoManager,
|
||||
process::AsyncHandler,
|
||||
utils::{logging::Type, resolve},
|
||||
};
|
||||
@@ -28,28 +28,39 @@ pub fn restart_clash_core() {
|
||||
/// Restart the application
|
||||
pub fn restart_app() {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
|
||||
// logging_error!(Type::Core, true, CoreManager::global().stop_core().await);
|
||||
resolve::resolve_reset_async().await;
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
tauri::process::restart(&app_handle.env());
|
||||
|
||||
handle::Handle::global()
|
||||
.app_handle()
|
||||
.map(|app_handle| {
|
||||
tauri::process::restart(&app_handle.env());
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
logging_error!(
|
||||
Type::System,
|
||||
false,
|
||||
"{}",
|
||||
"Failed to get app handle for restart"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn after_change_clash_mode() {
|
||||
AsyncHandler::spawn(move || async {
|
||||
match MihomoManager::global().get_connections().await {
|
||||
match IpcManager::global().get_connections().await {
|
||||
Ok(connections) => {
|
||||
if let Some(connections_array) = connections["connections"].as_array() {
|
||||
for connection in connections_array {
|
||||
if let Some(id) = connection["id"].as_str() {
|
||||
let _ = MihomoManager::global().delete_connection(id).await;
|
||||
let _ = IpcManager::global().delete_connection(id).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "Failed to get connections: {}", err);
|
||||
log::error!(target: "app", "Failed to get connections: {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -65,26 +76,26 @@ pub fn change_clash_mode(mode: String) {
|
||||
});
|
||||
AsyncHandler::spawn(move || async move {
|
||||
log::debug!(target: "app", "change clash mode to {mode}");
|
||||
match MihomoManager::global().patch_configs(json_value).await {
|
||||
match IpcManager::global().patch_configs(json_value).await {
|
||||
Ok(_) => {
|
||||
// 更新订阅
|
||||
Config::clash().data().patch_config(mapping);
|
||||
Config::clash().data_mut().patch_config(mapping);
|
||||
|
||||
if Config::clash().data().save_config().is_ok() {
|
||||
if Config::clash().data_mut().save_config().is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
|
||||
}
|
||||
|
||||
let is_auto_close_connection = Config::verge()
|
||||
.data()
|
||||
.data_mut()
|
||||
.auto_close_connection
|
||||
.unwrap_or(false);
|
||||
if is_auto_close_connection {
|
||||
after_change_clash_mode();
|
||||
}
|
||||
}
|
||||
Err(err) => println!("{err}"),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -94,7 +105,10 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
use crate::utils::network::{NetworkManager, ProxyType};
|
||||
use tokio::time::Instant;
|
||||
|
||||
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
|
||||
let tun_mode = Config::verge()
|
||||
.latest_ref()
|
||||
.enable_tun_mode
|
||||
.unwrap_or(false);
|
||||
|
||||
// 如果是TUN模式,不使用代理,否则使用自身代理
|
||||
let proxy_type = if !tun_mode {
|
||||
@@ -113,7 +127,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
log::trace!(target: "app", "test_delay response: {:#?}", response);
|
||||
log::trace!(target: "app", "test_delay response: {response:#?}");
|
||||
if response.status().is_success() {
|
||||
Ok(start.elapsed().as_millis() as u32)
|
||||
} else {
|
||||
@@ -121,7 +135,7 @@ pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::trace!(target: "app", "test_delay error: {:#?}", err);
|
||||
log::trace!(target: "app", "test_delay error: {err:#?}");
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,19 +10,19 @@ use serde_yaml::Mapping;
|
||||
|
||||
/// Patch Clash configuration
|
||||
pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
Config::clash().draft().patch_config(patch.clone());
|
||||
Config::clash().draft_mut().patch_config(patch.clone());
|
||||
|
||||
let res = {
|
||||
// 激活订阅
|
||||
if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
|
||||
Config::generate().await?;
|
||||
Config::generate()?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
} else {
|
||||
if patch.get("mode").is_some() {
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_menu());
|
||||
logging_error!(Type::Tray, true, tray::Tray::global().update_icon(None));
|
||||
}
|
||||
Config::runtime().latest().patch_config(patch);
|
||||
Config::runtime().draft_mut().patch_config(patch);
|
||||
CoreManager::global().update_config().await?;
|
||||
}
|
||||
handle::Handle::refresh_clash();
|
||||
@@ -31,7 +31,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
match res {
|
||||
Ok(()) => {
|
||||
Config::clash().apply();
|
||||
Config::clash().data().save_config()?;
|
||||
Config::clash().data_mut().save_config()?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -60,7 +60,7 @@ enum UpdateFlags {
|
||||
|
||||
/// Patch Verge configuration
|
||||
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
Config::verge().draft_mut().patch_config(patch.clone());
|
||||
|
||||
let tun_mode = patch.enable_tun_mode;
|
||||
let auto_launch = patch.enable_auto_launch;
|
||||
@@ -95,6 +95,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
let tray_event = patch.tray_event;
|
||||
let home_cards = patch.home_cards.clone();
|
||||
let enable_auto_light_weight = patch.enable_auto_light_weight_mode;
|
||||
let enable_external_controller = patch.enable_external_controller;
|
||||
let res: std::result::Result<(), anyhow::Error> = {
|
||||
// Initialize with no flags set
|
||||
let mut update_flags: i32 = UpdateFlags::None as i32;
|
||||
@@ -165,9 +166,14 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
update_flags |= UpdateFlags::LighteWeight as i32;
|
||||
}
|
||||
|
||||
// 处理 external-controller 的开关
|
||||
if enable_external_controller.is_some() {
|
||||
update_flags |= UpdateFlags::RestartCore as i32;
|
||||
}
|
||||
|
||||
// Process updates based on flags
|
||||
if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 {
|
||||
Config::generate().await?;
|
||||
Config::generate()?;
|
||||
CoreManager::global().restart_core().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::ClashConfig as i32)) != 0 {
|
||||
@@ -175,7 +181,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
if (update_flags & (UpdateFlags::VergeConfig as i32)) != 0 {
|
||||
Config::verge().draft().enable_global_hotkey = enable_global_hotkey;
|
||||
Config::verge().draft_mut().enable_global_hotkey = enable_global_hotkey;
|
||||
handle::Handle::refresh_verge();
|
||||
}
|
||||
if (update_flags & (UpdateFlags::Launch as i32)) != 0 {
|
||||
@@ -185,7 +191,9 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
sysopt::Sysopt::global().update_sysproxy().await?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::Hotkey as i32)) != 0 {
|
||||
hotkey::Hotkey::global().update(patch.hotkeys.unwrap())?;
|
||||
if let Some(hotkeys) = patch.hotkeys {
|
||||
hotkey::Hotkey::global().update(hotkeys)?;
|
||||
}
|
||||
}
|
||||
if (update_flags & (UpdateFlags::SystrayMenu as i32)) != 0 {
|
||||
tray::Tray::global().update_menu()?;
|
||||
@@ -200,7 +208,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
tray::Tray::global().update_click_behavior()?;
|
||||
}
|
||||
if (update_flags & (UpdateFlags::LighteWeight as i32)) != 0 {
|
||||
if enable_auto_light_weight.unwrap() {
|
||||
if enable_auto_light_weight.unwrap_or(false) {
|
||||
lightweight::enable_auto_light_weight_mode();
|
||||
} else {
|
||||
lightweight::disable_auto_light_weight_mode();
|
||||
@@ -213,7 +221,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
|
||||
Ok(()) => {
|
||||
Config::verge().apply();
|
||||
if !not_save_file {
|
||||
Config::verge().data().save_file()?;
|
||||
Config::verge().data_mut().save_file()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -2,14 +2,23 @@ use crate::{
|
||||
cmd,
|
||||
config::{Config, PrfItem, PrfOption},
|
||||
core::{handle, CoreManager, *},
|
||||
logging,
|
||||
process::AsyncHandler,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
/// Toggle proxy profile
|
||||
pub fn toggle_proxy_profile(profile_index: String) {
|
||||
AsyncHandler::spawn(|| async move {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let Some(app_handle) = handle::Handle::global().app_handle() else {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"Failed to get app handle for profile toggle"
|
||||
);
|
||||
return;
|
||||
};
|
||||
match cmd::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
|
||||
Ok(_) => {
|
||||
let _ = tray::Tray::global().update_menu();
|
||||
@@ -29,51 +38,56 @@ pub async fn update_profile(
|
||||
option: Option<PrfOption>,
|
||||
auto_refresh: Option<bool>,
|
||||
) -> Result<()> {
|
||||
println!("[订阅更新] 开始更新订阅 {}", uid);
|
||||
logging!(info, Type::Config, true, "[订阅更新] 开始更新订阅 {}", uid);
|
||||
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true,保持兼容性
|
||||
|
||||
let url_opt = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let profiles = profiles.latest_ref();
|
||||
let item = profiles.get_item(&uid)?;
|
||||
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
|
||||
|
||||
if !is_remote {
|
||||
println!("[订阅更新] {} 不是远程订阅,跳过更新", uid);
|
||||
log::info!(target: "app", "[订阅更新] {uid} 不是远程订阅,跳过更新");
|
||||
None // 非远程订阅直接更新
|
||||
} else if item.url.is_none() {
|
||||
println!("[订阅更新] {} 缺少URL,无法更新", uid);
|
||||
log::warn!(target: "app", "[订阅更新] {uid} 缺少URL,无法更新");
|
||||
bail!("failed to get the profile item url");
|
||||
} else {
|
||||
println!(
|
||||
log::info!(target: "app",
|
||||
"[订阅更新] {} 是远程订阅,URL: {}",
|
||||
uid,
|
||||
item.url.clone().unwrap()
|
||||
item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
|
||||
);
|
||||
Some((item.url.clone().unwrap(), item.option.clone()))
|
||||
Some((
|
||||
item.url
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?,
|
||||
item.option.clone(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let should_update = match url_opt {
|
||||
Some((url, opt)) => {
|
||||
println!("[订阅更新] 开始下载新的订阅内容");
|
||||
log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容");
|
||||
let merged_opt = PrfOption::merge(opt.clone(), option.clone());
|
||||
|
||||
// 尝试使用正常设置更新
|
||||
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
|
||||
Ok(item) => {
|
||||
println!("[订阅更新] 更新订阅配置成功");
|
||||
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.latest();
|
||||
let mut profiles = profiles.draft_mut();
|
||||
profiles.update_item(uid.clone(), item)?;
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
||||
is_current && auto_refresh
|
||||
}
|
||||
Err(err) => {
|
||||
// 首次更新失败,尝试使用Clash代理
|
||||
println!("[订阅更新] 正常更新失败: {},尝试使用Clash代理更新", err);
|
||||
log::warn!(target: "app", "[订阅更新] 正常更新失败: {err},尝试使用Clash代理更新");
|
||||
|
||||
// 发送通知
|
||||
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
|
||||
@@ -90,7 +104,7 @@ pub async fn update_profile(
|
||||
// 使用Clash代理重试
|
||||
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
|
||||
Ok(mut item) => {
|
||||
println!("[订阅更新] 使用Clash代理更新成功");
|
||||
log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
|
||||
|
||||
// 恢复原始代理设置到item
|
||||
if let Some(option) = item.option.as_mut() {
|
||||
@@ -100,7 +114,7 @@ pub async fn update_profile(
|
||||
|
||||
// 更新到配置
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.latest();
|
||||
let mut profiles = profiles.draft_mut();
|
||||
profiles.update_item(uid.clone(), item.clone())?;
|
||||
|
||||
// 获取配置名称用于通知
|
||||
@@ -110,14 +124,14 @@ pub async fn update_profile(
|
||||
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
|
||||
|
||||
let is_current = Some(uid.clone()) == profiles.get_current();
|
||||
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
|
||||
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
|
||||
is_current && auto_refresh
|
||||
}
|
||||
Err(retry_err) => {
|
||||
println!("[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err);
|
||||
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {retry_err}");
|
||||
handle::Handle::notice_message(
|
||||
"update_failed_even_with_clash",
|
||||
format!("{}", retry_err),
|
||||
format!("{retry_err}"),
|
||||
);
|
||||
return Err(retry_err);
|
||||
}
|
||||
@@ -129,14 +143,23 @@ pub async fn update_profile(
|
||||
};
|
||||
|
||||
if should_update {
|
||||
println!("[订阅更新] 更新内核配置");
|
||||
logging!(info, Type::Config, true, "[订阅更新] 更新内核配置");
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
println!("[订阅更新] 更新成功");
|
||||
logging!(info, Type::Config, true, "[订阅更新] 更新成功");
|
||||
handle::Handle::refresh_clash();
|
||||
if let Err(err) = cmd::proxy::force_refresh_proxies().await {
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
true,
|
||||
"[订阅更新] 代理组刷新失败: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[订阅更新] 更新失败: {}", err);
|
||||
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
|
||||
handle::Handle::notice_message("update_failed", format!("{err}"));
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
|
||||
+26
-14
@@ -1,28 +1,28 @@
|
||||
use crate::{
|
||||
config::{Config, IVerge},
|
||||
core::handle,
|
||||
ipc::IpcManager,
|
||||
logging,
|
||||
process::AsyncHandler,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use std::env;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
|
||||
/// Toggle system proxy on/off
|
||||
pub fn toggle_system_proxy() {
|
||||
let enable = Config::verge().draft().enable_system_proxy;
|
||||
let enable = Config::verge().draft_mut().enable_system_proxy;
|
||||
let enable = enable.unwrap_or(false);
|
||||
let auto_close_connection = Config::verge()
|
||||
.data()
|
||||
.data_mut()
|
||||
.auto_close_connection
|
||||
.unwrap_or(false);
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
// 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接
|
||||
if enable && auto_close_connection {
|
||||
if let Err(err) = crate::module::mihomo::MihomoManager::global()
|
||||
.close_all_connections()
|
||||
.await
|
||||
{
|
||||
log::error!(target: "app", "Failed to close all connections: {}", err);
|
||||
if let Err(err) = IpcManager::global().close_all_connections().await {
|
||||
log::error!(target: "app", "Failed to close all connections: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn toggle_system_proxy() {
|
||||
|
||||
/// Toggle TUN mode on/off
|
||||
pub fn toggle_tun_mode(not_save_file: Option<bool>) {
|
||||
let enable = Config::verge().data().enable_tun_mode;
|
||||
let enable = Config::verge().data_mut().enable_tun_mode;
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
AsyncHandler::spawn(async move || {
|
||||
@@ -67,19 +67,31 @@ pub fn copy_clash_env() {
|
||||
// 从环境变量获取IP地址,如果没有则从配置中获取 proxy_host,默认为 127.0.0.1
|
||||
let clash_verge_rev_ip = env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| {
|
||||
Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.proxy_host
|
||||
.clone()
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string())
|
||||
});
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
|
||||
let http_proxy = format!("http://{clash_verge_rev_ip}:{}", port);
|
||||
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{}", port);
|
||||
let Some(app_handle) = handle::Handle::global().app_handle() else {
|
||||
logging!(
|
||||
error,
|
||||
Type::System,
|
||||
"Failed to get app handle for proxy operation"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let port = {
|
||||
Config::verge()
|
||||
.latest_ref()
|
||||
.verge_mixed_port
|
||||
.unwrap_or(7897)
|
||||
};
|
||||
let http_proxy = format!("http://{clash_verge_rev_ip}:{port}");
|
||||
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{port}");
|
||||
|
||||
let cliboard = app_handle.clipboard();
|
||||
let env_type = { Config::verge().latest().env_type.clone() };
|
||||
let env_type = { Config::verge().latest_ref().env_type.clone() };
|
||||
let env_type = match env_type {
|
||||
Some(env_type) => env_type,
|
||||
None => {
|
||||
|
||||
+209
-79
@@ -3,112 +3,242 @@ use crate::AppHandleManager;
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, sysopt, CoreManager},
|
||||
module::mihomo::MihomoManager,
|
||||
utils::resolve,
|
||||
ipc::IpcManager,
|
||||
logging,
|
||||
utils::logging::Type,
|
||||
};
|
||||
|
||||
/// Open or close the dashboard window
|
||||
#[allow(dead_code)]
|
||||
pub fn open_or_close_dashboard() {
|
||||
println!("Attempting to open/close dashboard");
|
||||
log::info!(target: "app", "Attempting to open/close dashboard");
|
||||
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
println!("Found existing window");
|
||||
log::info!(target: "app", "Found existing window");
|
||||
|
||||
// 如果窗口存在,则切换其显示状态
|
||||
match window.is_visible() {
|
||||
Ok(visible) => {
|
||||
println!("Window visibility status: {}", visible);
|
||||
log::info!(target: "app", "Window visibility status: {}", visible);
|
||||
|
||||
if visible {
|
||||
println!("Attempting to hide window");
|
||||
log::info!(target: "app", "Attempting to hide window");
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
println!("Attempting to show and focus window");
|
||||
log::info!(target: "app", "Attempting to show and focus window");
|
||||
if window.is_minimized().unwrap_or(false) {
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get window visibility: {:?}", e);
|
||||
log::error!(target: "app", "Failed to get window visibility: {:?}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("No existing window found, creating new window");
|
||||
log::info!(target: "app", "No existing window found, creating new window");
|
||||
resolve::create_window(true);
|
||||
}
|
||||
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 (绕过防抖: {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();
|
||||
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:?}");
|
||||
}
|
||||
|
||||
/// 异步优化的应用退出函数
|
||||
pub fn quit() {
|
||||
log::debug!(target: "app", "启动退出流程");
|
||||
use crate::process::AsyncHandler;
|
||||
logging!(debug, Type::System, true, "启动退出流程");
|
||||
|
||||
// 获取应用句柄并设置退出标志
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let Some(app_handle) = handle::Handle::global().app_handle() else {
|
||||
logging!(
|
||||
error,
|
||||
Type::System,
|
||||
"Failed to get app handle for quit operation"
|
||||
);
|
||||
return;
|
||||
};
|
||||
handle::Handle::global().set_is_exiting();
|
||||
|
||||
// 优先关闭窗口,提供立即反馈
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
let _ = window.hide();
|
||||
log::info!(target: "app", "窗口已隐藏");
|
||||
}
|
||||
|
||||
// 在单独线程中处理资源清理,避免阻塞主线程
|
||||
std::thread::spawn(move || {
|
||||
let cleanup_result = clean();
|
||||
app_handle.exit(match cleanup_result {
|
||||
true => 0,
|
||||
false => 1,
|
||||
});
|
||||
// 使用异步任务处理资源清理,避免阻塞
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::System, true, "开始异步清理资源");
|
||||
let cleanup_result = clean_async().await;
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"资源清理完成,退出代码: {}",
|
||||
if cleanup_result { 0 } else { 1 }
|
||||
);
|
||||
app_handle.exit(if cleanup_result { 0 } else { 1 });
|
||||
});
|
||||
}
|
||||
|
||||
pub fn clean() -> bool {
|
||||
async fn clean_async() -> bool {
|
||||
use tokio::time::{timeout, Duration};
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let cleanup_result = rt.block_on(async {
|
||||
// 1. 处理TUN模式
|
||||
let tun_success = if Config::verge().data().enable_tun_mode.unwrap_or(false) {
|
||||
let disable_tun = serde_json::json!({
|
||||
"tun": {
|
||||
"enable": false
|
||||
}
|
||||
});
|
||||
timeout(
|
||||
Duration::from_secs(1),
|
||||
MihomoManager::global().patch_configs(disable_tun),
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
// 2. 顺序执行关键清理
|
||||
let proxy_res = timeout(
|
||||
Duration::from_secs(1),
|
||||
logging!(info, Type::System, true, "开始执行异步清理操作...");
|
||||
|
||||
// 1. 处理TUN模式
|
||||
let tun_success = if Config::verge().data_mut().enable_tun_mode.unwrap_or(false) {
|
||||
let disable_tun = serde_json::json!({"tun": {"enable": false}});
|
||||
match timeout(
|
||||
Duration::from_secs(3),
|
||||
IpcManager::global().patch_configs(disable_tun),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => {
|
||||
log::info!(target: "app", "TUN模式已禁用");
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
true
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!(target: "app", "禁用TUN模式失败: {e}");
|
||||
false
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "禁用TUN模式超时");
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
// 2. 系统代理重置
|
||||
let proxy_task = async {
|
||||
match timeout(
|
||||
Duration::from_secs(3),
|
||||
sysopt::Sysopt::global().reset_sysproxy(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "系统代理已重置");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "重置系统代理超时");
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let core_res = timeout(Duration::from_secs(1), CoreManager::global().stop_core()).await;
|
||||
// 3. 核心服务停止
|
||||
let core_task = async {
|
||||
match timeout(Duration::from_secs(3), CoreManager::global().stop_core()).await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "核心服务已停止");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "停止核心服务超时");
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 平台特定清理
|
||||
#[cfg(target_os = "macos")]
|
||||
let _dns_res = timeout(Duration::from_millis(500), resolve::restore_public_dns()).await;
|
||||
// 4. DNS恢复(仅macOS)
|
||||
#[cfg(target_os = "macos")]
|
||||
let dns_task = async {
|
||||
match timeout(
|
||||
Duration::from_millis(1000),
|
||||
crate::utils::resolve::restore_public_dns(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "DNS设置已恢复");
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(target: "app", "恢复DNS设置超时");
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tun_success && proxy_res.is_ok() && core_res.is_ok()
|
||||
// 并行执行剩余清理任务
|
||||
let (proxy_success, core_success) = tokio::join!(proxy_task, core_task);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let dns_success = dns_task.await;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let dns_success = true;
|
||||
|
||||
let all_success = tun_success && proxy_success && core_success && dns_success;
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"异步关闭操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
|
||||
tun_success,
|
||||
proxy_success,
|
||||
core_success,
|
||||
dns_success,
|
||||
all_success
|
||||
);
|
||||
|
||||
all_success
|
||||
}
|
||||
|
||||
pub fn clean() -> bool {
|
||||
use crate::process::AsyncHandler;
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::System, true, "开始执行关闭操作...");
|
||||
|
||||
// 使用已有的异步清理函数
|
||||
let cleanup_result = clean_async().await;
|
||||
|
||||
// 发送结果
|
||||
let _ = tx.send(cleanup_result);
|
||||
});
|
||||
cleanup_result
|
||||
|
||||
match rx.recv_timeout(std::time::Duration::from_secs(8)) {
|
||||
Ok(result) => {
|
||||
logging!(info, Type::System, true, "关闭操作完成,结果: {}", result);
|
||||
result
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::System,
|
||||
true,
|
||||
"清理操作超时,返回成功状态避免阻塞"
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -116,7 +246,7 @@ pub fn hide() {
|
||||
use crate::module::lightweight::add_light_weight_timer;
|
||||
|
||||
let enable_auto_light_weight_mode = Config::verge()
|
||||
.data()
|
||||
.data_mut()
|
||||
.enable_auto_light_weight_mode
|
||||
.unwrap_or(false);
|
||||
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use kode_bridge::{
|
||||
errors::{AnyError, AnyResult},
|
||||
pool::PoolConfig,
|
||||
ClientConfig, IpcHttpClient, LegacyResponse,
|
||||
};
|
||||
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
||||
|
||||
// 定义用于URL路径的编码集合,只编码真正必要的字符
|
||||
const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
|
||||
.add(b' ') // 空格
|
||||
.add(b'/') // 斜杠
|
||||
.add(b'?') // 问号
|
||||
.add(b'#') // 井号
|
||||
.add(b'&') // 和号
|
||||
.add(b'%'); // 百分号
|
||||
|
||||
use crate::{logging, singleton_with_logging, utils::dirs::ipc_path};
|
||||
|
||||
// Helper function to create AnyError from string
|
||||
fn create_error(msg: impl Into<String>) -> AnyError {
|
||||
Box::new(std::io::Error::other(msg.into()))
|
||||
}
|
||||
|
||||
pub struct IpcManager {
|
||||
ipc_path: String,
|
||||
config: ClientConfig,
|
||||
}
|
||||
|
||||
impl IpcManager {
|
||||
fn new() -> Self {
|
||||
let ipc_path_buf = ipc_path().unwrap_or_else(|e| {
|
||||
logging!(
|
||||
error,
|
||||
crate::utils::logging::Type::Ipc,
|
||||
true,
|
||||
"Failed to get IPC path: {}",
|
||||
e
|
||||
);
|
||||
std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path
|
||||
});
|
||||
let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
|
||||
Self {
|
||||
ipc_path: ipc_path.to_string(),
|
||||
config: ClientConfig {
|
||||
default_timeout: Duration::from_secs(5),
|
||||
enable_pooling: true,
|
||||
max_retries: 3,
|
||||
max_concurrent_requests: 32,
|
||||
max_requests_per_second: Some(5.0),
|
||||
pool_config: PoolConfig {
|
||||
max_size: 32,
|
||||
min_idle: 2,
|
||||
max_idle_time_ms: 10_000,
|
||||
max_retries: 3,
|
||||
max_concurrent_requests: 32,
|
||||
max_requests_per_second: Some(5.0),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use singleton macro with logging
|
||||
singleton_with_logging!(IpcManager, INSTANCE, "IpcManager");
|
||||
|
||||
impl IpcManager {
|
||||
pub async fn request(
|
||||
&self,
|
||||
method: &str,
|
||||
path: &str,
|
||||
body: Option<&serde_json::Value>,
|
||||
) -> AnyResult<LegacyResponse> {
|
||||
let client = IpcHttpClient::with_config(&self.ipc_path, self.config.clone())?;
|
||||
client.request(method, path, body).await
|
||||
}
|
||||
}
|
||||
|
||||
impl IpcManager {
|
||||
pub async fn send_request(
|
||||
&self,
|
||||
method: &str,
|
||||
path: &str,
|
||||
body: Option<&serde_json::Value>,
|
||||
) -> AnyResult<serde_json::Value> {
|
||||
let response = IpcManager::global().request(method, path, body).await?;
|
||||
match method {
|
||||
"GET" => Ok(response.json()?),
|
||||
"PATCH" => {
|
||||
if response.status == 204 {
|
||||
Ok(serde_json::json!({"code": 204}))
|
||||
} else {
|
||||
Ok(response.json()?)
|
||||
}
|
||||
}
|
||||
"PUT" | "DELETE" => {
|
||||
if response.status == 204 {
|
||||
Ok(serde_json::json!({"code": 204}))
|
||||
} else {
|
||||
match response.json() {
|
||||
Ok(json) => Ok(json),
|
||||
Err(_) => Ok(serde_json::json!({
|
||||
"code": response.status,
|
||||
"message": response.body,
|
||||
"error": "failed to parse response as JSON"
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => match response.json() {
|
||||
Ok(json) => Ok(json),
|
||||
Err(_) => Ok(serde_json::json!({
|
||||
"code": response.status,
|
||||
"message": response.body,
|
||||
"error": "failed to parse response as JSON"
|
||||
})),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 基础代理信息获取
|
||||
pub async fn get_proxies(&self) -> AnyResult<serde_json::Value> {
|
||||
let url = "/proxies";
|
||||
self.send_request("GET", url, None).await
|
||||
}
|
||||
|
||||
// 代理提供者信息获取
|
||||
pub async fn get_providers_proxies(&self) -> AnyResult<serde_json::Value> {
|
||||
let url = "/providers/proxies";
|
||||
self.send_request("GET", url, None).await
|
||||
}
|
||||
|
||||
// 连接管理
|
||||
pub async fn get_connections(&self) -> AnyResult<serde_json::Value> {
|
||||
let url = "/connections";
|
||||
self.send_request("GET", url, None).await
|
||||
}
|
||||
|
||||
pub async fn delete_connection(&self, id: &str) -> AnyResult<()> {
|
||||
let encoded_id = utf8_percent_encode(id, URL_PATH_ENCODE_SET).to_string();
|
||||
let url = format!("/connections/{encoded_id}");
|
||||
let response = self.send_request("DELETE", &url, None).await?;
|
||||
if response["code"] == 204 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error(
|
||||
response["message"].as_str().unwrap_or("unknown error"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn close_all_connections(&self) -> AnyResult<()> {
|
||||
let url = "/connections";
|
||||
let response = self.send_request("DELETE", url, None).await?;
|
||||
if response["code"] == 204 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error(
|
||||
response["message"]
|
||||
.as_str()
|
||||
.unwrap_or("unknown error")
|
||||
.to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IpcManager {
|
||||
#[allow(dead_code)]
|
||||
pub async fn is_mihomo_running(&self) -> AnyResult<()> {
|
||||
let url = "/version";
|
||||
let _response = self.send_request("GET", url, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn put_configs_force(&self, clash_config_path: &str) -> AnyResult<()> {
|
||||
let url = "/configs?force=true";
|
||||
let payload = serde_json::json!({
|
||||
"path": clash_config_path,
|
||||
});
|
||||
let _response = self.send_request("PUT", url, Some(&payload)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn patch_configs(&self, config: serde_json::Value) -> AnyResult<()> {
|
||||
let url = "/configs";
|
||||
let response = self.send_request("PATCH", url, Some(&config)).await?;
|
||||
if response["code"] == 204 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error(
|
||||
response["message"]
|
||||
.as_str()
|
||||
.unwrap_or("unknown error")
|
||||
.to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_proxy_delay(
|
||||
&self,
|
||||
name: &str,
|
||||
test_url: Option<String>,
|
||||
timeout: i32,
|
||||
) -> AnyResult<serde_json::Value> {
|
||||
let test_url =
|
||||
test_url.unwrap_or_else(|| "https://cp.cloudflare.com/generate_204".to_string());
|
||||
|
||||
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
|
||||
// 测速URL不再编码,直接传递
|
||||
let url = format!("/proxies/{encoded_name}/delay?url={test_url}&timeout={timeout}");
|
||||
|
||||
let response = self.send_request("GET", &url, None).await;
|
||||
response
|
||||
}
|
||||
|
||||
// 版本和配置相关
|
||||
pub async fn get_version(&self) -> AnyResult<serde_json::Value> {
|
||||
let url = "/version";
|
||||
self.send_request("GET", url, None).await
|
||||
}
|
||||
|
||||
pub async fn get_config(&self) -> AnyResult<serde_json::Value> {
|
||||
let url = "/configs";
|
||||
self.send_request("GET", url, None).await
|
||||
}
|
||||
|
||||
pub async fn update_geo_data(&self) -> AnyResult<()> {
|
||||
let url = "/configs/geo";
|
||||
let response = self.send_request("POST", url, None).await?;
|
||||
if response["code"] == 204 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error(
|
||||
response["message"]
|
||||
.as_str()
|
||||
.unwrap_or("unknown error")
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upgrade_core(&self) -> AnyResult<()> {
|
||||
let url = "/upgrade";
|
||||
let response = self.send_request("POST", url, None).await?;
|
||||
if response["code"] == 204 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error(
|
||||
response["message"]
|
||||
.as_str()
|
||||
.unwrap_or("unknown error")
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// 规则相关
|
||||
pub async fn get_rules(&self) -> AnyResult<serde_json::Value> {
|
||||
let url = "/rules";
|
||||
self.send_request("GET", url, None).await
|
||||
}
|
||||
|
||||
pub async fn get_rule_providers(&self) -> AnyResult<serde_json::Value> {
|
||||
let url = "/providers/rules";
|
||||
self.send_request("GET", url, None).await
|
||||
}
|
||||
|
||||
pub async fn update_rule_provider(&self, name: &str) -> AnyResult<()> {
|
||||
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
|
||||
let url = format!("/providers/rules/{encoded_name}");
|
||||
let response = self.send_request("PUT", &url, None).await?;
|
||||
if response["code"] == 204 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error(
|
||||
response["message"]
|
||||
.as_str()
|
||||
.unwrap_or("unknown error")
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// 代理相关
|
||||
pub async fn update_proxy(&self, group: &str, proxy: &str) -> AnyResult<()> {
|
||||
// 使用 percent-encoding 进行正确的 URL 编码
|
||||
let encoded_group = utf8_percent_encode(group, URL_PATH_ENCODE_SET).to_string();
|
||||
let url = format!("/proxies/{encoded_group}");
|
||||
let payload = serde_json::json!({
|
||||
"name": proxy
|
||||
});
|
||||
|
||||
match self.send_request("PUT", &url, Some(&payload)).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
crate::utils::logging::Type::Ipc,
|
||||
true,
|
||||
"IPC: updateProxy encountered error: {} (ignored, always returning true)",
|
||||
e
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn proxy_provider_health_check(&self, name: &str) -> AnyResult<()> {
|
||||
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
|
||||
let url = format!("/providers/proxies/{encoded_name}/healthcheck");
|
||||
let response = self.send_request("GET", &url, None).await?;
|
||||
if response["code"] == 204 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error(
|
||||
response["message"]
|
||||
.as_str()
|
||||
.unwrap_or("unknown error")
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_proxy_provider(&self, name: &str) -> AnyResult<()> {
|
||||
let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string();
|
||||
let url = format!("/providers/proxies/{encoded_name}");
|
||||
let response = self.send_request("PUT", &url, None).await?;
|
||||
if response["code"] == 204 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error(
|
||||
response["message"]
|
||||
.as_str()
|
||||
.unwrap_or("unknown error")
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟测试相关
|
||||
pub async fn get_group_proxy_delays(
|
||||
&self,
|
||||
group_name: &str,
|
||||
url: Option<String>,
|
||||
timeout: i32,
|
||||
) -> AnyResult<serde_json::Value> {
|
||||
let test_url = url.unwrap_or_else(|| "https://cp.cloudflare.com/generate_204".to_string());
|
||||
|
||||
let encoded_group_name = utf8_percent_encode(group_name, URL_PATH_ENCODE_SET).to_string();
|
||||
// 测速URL不再编码,直接传递
|
||||
let url = format!("/group/{encoded_group_name}/delay?url={test_url}&timeout={timeout}");
|
||||
|
||||
let response = self.send_request("GET", &url, None).await;
|
||||
response
|
||||
}
|
||||
|
||||
// 调试相关
|
||||
pub async fn is_debug_enabled(&self) -> AnyResult<bool> {
|
||||
let url = "/debug/pprof";
|
||||
match self.send_request("GET", url, None).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn gc(&self) -> AnyResult<()> {
|
||||
let url = "/debug/gc";
|
||||
let response = self.send_request("PUT", url, None).await?;
|
||||
if response["code"] == 204 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error(
|
||||
response["message"]
|
||||
.as_str()
|
||||
.unwrap_or("unknown error")
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// 日志相关功能已迁移到 logs.rs 模块,使用流式处理
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::VecDeque, sync::Arc, time::Instant};
|
||||
use tauri::async_runtime::JoinHandle;
|
||||
use tokio::{sync::RwLock, time::Duration};
|
||||
|
||||
use crate::{
|
||||
ipc::monitor::MonitorData,
|
||||
logging,
|
||||
process::AsyncHandler,
|
||||
singleton_with_logging,
|
||||
utils::{dirs::ipc_path, logging::Type},
|
||||
};
|
||||
|
||||
const MAX_LOGS: usize = 1000; // Maximum number of logs to keep in memory
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct LogData {
|
||||
#[serde(rename = "type")]
|
||||
pub log_type: String,
|
||||
pub payload: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LogItem {
|
||||
pub log_type: String,
|
||||
pub payload: String,
|
||||
pub time: String,
|
||||
}
|
||||
|
||||
impl LogItem {
|
||||
fn new(log_type: String, payload: String) -> Self {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
|
||||
.as_secs();
|
||||
|
||||
// Simple time formatting (HH:MM:SS)
|
||||
let hours = (now / 3600) % 24;
|
||||
let minutes = (now / 60) % 60;
|
||||
let seconds = now % 60;
|
||||
let time_str = format!("{hours:02}:{minutes:02}:{seconds:02}");
|
||||
|
||||
Self {
|
||||
log_type,
|
||||
payload,
|
||||
time: time_str,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CurrentLogs {
|
||||
pub logs: VecDeque<LogItem>,
|
||||
// pub level: String,
|
||||
pub last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Default for CurrentLogs {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
logs: VecDeque::with_capacity(MAX_LOGS),
|
||||
// level: "info".to_string(),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MonitorData for CurrentLogs {
|
||||
fn mark_fresh(&mut self) {
|
||||
self.last_updated = Instant::now();
|
||||
}
|
||||
|
||||
fn is_fresh_within(&self, duration: Duration) -> bool {
|
||||
self.last_updated.elapsed() < duration
|
||||
}
|
||||
}
|
||||
|
||||
// Logs monitor with streaming support
|
||||
pub struct LogsMonitor {
|
||||
current: Arc<RwLock<CurrentLogs>>,
|
||||
task_handle: Arc<RwLock<Option<JoinHandle<()>>>>,
|
||||
current_monitoring_level: Arc<RwLock<Option<String>>>,
|
||||
}
|
||||
|
||||
// Use singleton_with_logging macro
|
||||
singleton_with_logging!(LogsMonitor, INSTANCE, "LogsMonitor");
|
||||
|
||||
impl LogsMonitor {
|
||||
fn new() -> Self {
|
||||
let current = Arc::new(RwLock::new(CurrentLogs::default()));
|
||||
|
||||
Self {
|
||||
current,
|
||||
task_handle: Arc::new(RwLock::new(None)),
|
||||
current_monitoring_level: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_monitoring(&self, level: Option<String>) {
|
||||
let filter_level = level.clone().unwrap_or_else(|| "info".to_string());
|
||||
|
||||
// Check if we're already monitoring the same level
|
||||
// let level_changed = {
|
||||
// let current_level = self.current_monitoring_level.read().await;
|
||||
// if let Some(existing_level) = current_level.as_ref() {
|
||||
// if existing_level == &filter_level {
|
||||
// logging!(
|
||||
// info,
|
||||
// Type::Ipc,
|
||||
// true,
|
||||
// "LogsMonitor: Already monitoring level '{}', skipping duplicate request",
|
||||
// filter_level
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
// true // Level changed
|
||||
// } else {
|
||||
// true // First time or was stopped
|
||||
// }
|
||||
// };
|
||||
|
||||
// Stop existing monitoring task if level changed or first time
|
||||
{
|
||||
let mut handle = self.task_handle.write().await;
|
||||
if let Some(task) = handle.take() {
|
||||
task.abort();
|
||||
logging!(
|
||||
info,
|
||||
Type::Ipc,
|
||||
true,
|
||||
"LogsMonitor: Stopped previous monitoring task (level changed)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// We want to keep the logs cache even if the level changes,
|
||||
// so we don't clear it here. The cache will be cleared only when the level changes
|
||||
// and a new task is started. This allows us to keep logs from previous levels
|
||||
// even if the level changes during monitoring.
|
||||
// Clear logs cache when level changes to ensure fresh data
|
||||
// if level_changed {
|
||||
// let mut current = self.current.write().await;
|
||||
// current.logs.clear();
|
||||
// current.level = filter_level.clone();
|
||||
// current.mark_fresh();
|
||||
// logging!(
|
||||
// info,
|
||||
// Type::Ipc,
|
||||
// true,
|
||||
// "LogsMonitor: Cleared logs cache due to level change to '{}'",
|
||||
// filter_level
|
||||
// );
|
||||
// }
|
||||
|
||||
// Update current monitoring level
|
||||
{
|
||||
let mut current_level = self.current_monitoring_level.write().await;
|
||||
*current_level = Some(filter_level.clone());
|
||||
}
|
||||
|
||||
let monitor_current = Arc::clone(&self.current);
|
||||
|
||||
let task = AsyncHandler::spawn(move || async move {
|
||||
loop {
|
||||
// Get fresh IPC path and client for each connection attempt
|
||||
let (_ipc_path_buf, client) = match Self::create_ipc_client() {
|
||||
Ok((path, client)) => (path, client),
|
||||
Err(e) => {
|
||||
logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e);
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let url = if filter_level == "all" {
|
||||
"/logs".to_string()
|
||||
} else {
|
||||
format!("/logs?level={filter_level}")
|
||||
};
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Ipc,
|
||||
true,
|
||||
"LogsMonitor: Starting stream for {}",
|
||||
url
|
||||
);
|
||||
|
||||
let _ = client
|
||||
.get(&url)
|
||||
.timeout(Duration::from_secs(30))
|
||||
.process_lines(|line| {
|
||||
Self::process_log_line(line, Arc::clone(&monitor_current))
|
||||
})
|
||||
.await;
|
||||
|
||||
// Wait before retrying
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
});
|
||||
|
||||
// Store the task handle
|
||||
{
|
||||
let mut handle = self.task_handle.write().await;
|
||||
*handle = Some(task);
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Ipc,
|
||||
true,
|
||||
"LogsMonitor: Started new monitoring task for level: {:?}",
|
||||
level
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn stop_monitoring(&self) {
|
||||
// Stop monitoring task but keep logs
|
||||
{
|
||||
let mut handle = self.task_handle.write().await;
|
||||
if let Some(task) = handle.take() {
|
||||
task.abort();
|
||||
logging!(
|
||||
info,
|
||||
Type::Ipc,
|
||||
true,
|
||||
"LogsMonitor: Stopped monitoring task"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset monitoring level
|
||||
{
|
||||
let mut monitoring_level = self.current_monitoring_level.write().await;
|
||||
*monitoring_level = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn create_ipc_client() -> Result<
|
||||
(std::path::PathBuf, kode_bridge::IpcStreamClient),
|
||||
Box<dyn std::error::Error + Send + Sync>,
|
||||
> {
|
||||
use kode_bridge::IpcStreamClient;
|
||||
|
||||
let ipc_path_buf = ipc_path()?;
|
||||
let ipc_path = ipc_path_buf.to_str().ok_or("Invalid IPC path")?;
|
||||
let client = IpcStreamClient::new(ipc_path)?;
|
||||
Ok((ipc_path_buf, client))
|
||||
}
|
||||
|
||||
fn process_log_line(
|
||||
line: &str,
|
||||
current: Arc<RwLock<CurrentLogs>>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if let Ok(log_data) = serde_json::from_str::<LogData>(line.trim()) {
|
||||
// Server-side filtering via query parameters handles the level filtering
|
||||
// We only need to accept all logs since filtering is done at the endpoint level
|
||||
let log_item = LogItem::new(log_data.log_type, log_data.payload);
|
||||
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let mut logs = current.write().await;
|
||||
|
||||
// Add new log
|
||||
logs.logs.push_back(log_item);
|
||||
|
||||
// Keep only the last 1000 logs
|
||||
if logs.logs.len() > 1000 {
|
||||
logs.logs.pop_front();
|
||||
}
|
||||
|
||||
logs.mark_fresh();
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn current(&self) -> CurrentLogs {
|
||||
self.current.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn clear_logs(&self) {
|
||||
let mut current = self.current.write().await;
|
||||
current.logs.clear();
|
||||
current.mark_fresh();
|
||||
logging!(
|
||||
info,
|
||||
Type::Ipc,
|
||||
true,
|
||||
"LogsMonitor: Cleared frontend logs (monitoring continues)"
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn get_logs_as_json(&self) -> serde_json::Value {
|
||||
let current = self.current().await;
|
||||
|
||||
// Simply return all cached logs since filtering is handled by start_monitoring
|
||||
// and the cache is cleared when level changes
|
||||
let logs: Vec<serde_json::Value> = current
|
||||
.logs
|
||||
.iter()
|
||||
.map(|log| {
|
||||
serde_json::json!({
|
||||
"type": log.log_type,
|
||||
"payload": log.payload,
|
||||
"time": log.time
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
serde_json::Value::Array(logs)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_logs_monitoring(level: Option<String>) {
|
||||
LogsMonitor::global().start_monitoring(level).await;
|
||||
}
|
||||
|
||||
pub async fn stop_logs_monitoring() {
|
||||
LogsMonitor::global().stop_monitoring().await;
|
||||
}
|
||||
|
||||
pub async fn clear_logs() {
|
||||
LogsMonitor::global().clear_logs().await;
|
||||
}
|
||||
|
||||
pub async fn get_logs_json() -> serde_json::Value {
|
||||
LogsMonitor::global().get_logs_as_json().await
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use tokio::{sync::RwLock, time::Duration};
|
||||
|
||||
use crate::{
|
||||
ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser},
|
||||
process::AsyncHandler,
|
||||
singleton_lazy_with_logging,
|
||||
utils::format::fmt_bytes,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct MemoryData {
|
||||
pub inuse: u64,
|
||||
pub oslimit: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CurrentMemory {
|
||||
pub inuse: u64,
|
||||
pub oslimit: u64,
|
||||
pub last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Default for CurrentMemory {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inuse: 0,
|
||||
oslimit: 0,
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MonitorData for CurrentMemory {
|
||||
fn mark_fresh(&mut self) {
|
||||
self.last_updated = Instant::now();
|
||||
}
|
||||
|
||||
fn is_fresh_within(&self, duration: Duration) -> bool {
|
||||
self.last_updated.elapsed() < duration
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamingParser for CurrentMemory {
|
||||
fn parse_and_update(
|
||||
line: &str,
|
||||
current: Arc<RwLock<Self>>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if let Ok(memory) = serde_json::from_str::<MemoryData>(line.trim()) {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let mut current_guard = current.write().await;
|
||||
current_guard.inuse = memory.inuse;
|
||||
current_guard.oslimit = memory.oslimit;
|
||||
current_guard.mark_fresh();
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal memory monitor using the new architecture
|
||||
pub struct MemoryMonitor {
|
||||
monitor: IpcStreamMonitor<CurrentMemory>,
|
||||
}
|
||||
|
||||
impl Default for MemoryMonitor {
|
||||
fn default() -> Self {
|
||||
MemoryMonitor {
|
||||
monitor: IpcStreamMonitor::new(
|
||||
"/memory".to_string(),
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(2),
|
||||
Duration::from_secs(10),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use simplified singleton_lazy_with_logging macro
|
||||
singleton_lazy_with_logging!(
|
||||
MemoryMonitor,
|
||||
INSTANCE,
|
||||
"MemoryMonitor",
|
||||
MemoryMonitor::default
|
||||
);
|
||||
|
||||
impl MemoryMonitor {
|
||||
pub async fn current(&self) -> CurrentMemory {
|
||||
self.monitor.current().await
|
||||
}
|
||||
|
||||
pub async fn is_fresh(&self) -> bool {
|
||||
self.monitor.is_fresh().await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_current_memory() -> CurrentMemory {
|
||||
MemoryMonitor::global().current().await
|
||||
}
|
||||
|
||||
pub async fn get_formatted_memory() -> (String, String, f64, bool) {
|
||||
let monitor = MemoryMonitor::global();
|
||||
let memory = monitor.current().await;
|
||||
let is_fresh = monitor.is_fresh().await;
|
||||
|
||||
let usage_percent = if memory.oslimit > 0 {
|
||||
(memory.inuse as f64 / memory.oslimit as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
(
|
||||
fmt_bytes(memory.inuse),
|
||||
fmt_bytes(memory.oslimit),
|
||||
usage_percent,
|
||||
is_fresh,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
pub mod general;
|
||||
pub mod logs;
|
||||
pub mod memory;
|
||||
pub mod monitor;
|
||||
pub mod traffic;
|
||||
|
||||
pub use general::IpcManager;
|
||||
pub use logs::{clear_logs, get_logs_json, start_logs_monitoring, stop_logs_monitoring};
|
||||
pub use memory::{get_current_memory, get_formatted_memory};
|
||||
pub use traffic::{get_current_traffic, get_formatted_traffic};
|
||||
|
||||
pub struct Rate {
|
||||
// pub up: usize,
|
||||
// pub down: usize,
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
use kode_bridge::IpcStreamClient;
|
||||
use std::sync::Arc;
|
||||
use tokio::{sync::RwLock, time::Duration};
|
||||
|
||||
use crate::{
|
||||
logging,
|
||||
process::AsyncHandler,
|
||||
utils::{dirs::ipc_path, logging::Type},
|
||||
};
|
||||
|
||||
/// Generic base structure for IPC monitoring data with freshness tracking
|
||||
pub trait MonitorData: Clone + Send + Sync + 'static {
|
||||
/// Update the last_updated timestamp to now
|
||||
fn mark_fresh(&mut self);
|
||||
|
||||
/// Check if data is fresh based on the given duration
|
||||
fn is_fresh_within(&self, duration: Duration) -> bool;
|
||||
}
|
||||
|
||||
/// Trait for parsing streaming data and updating monitor state
|
||||
pub trait StreamingParser: MonitorData {
|
||||
/// Parse a line of streaming data and update the current state
|
||||
fn parse_and_update(
|
||||
line: &str,
|
||||
current: Arc<RwLock<Self>>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
}
|
||||
|
||||
/// Generic IPC stream monitor that handles the common streaming pattern
|
||||
pub struct IpcStreamMonitor<T>
|
||||
where
|
||||
T: MonitorData + StreamingParser + Default,
|
||||
{
|
||||
current: Arc<RwLock<T>>,
|
||||
#[allow(dead_code)]
|
||||
endpoint: String,
|
||||
#[allow(dead_code)]
|
||||
timeout: Duration,
|
||||
#[allow(dead_code)]
|
||||
retry_interval: Duration,
|
||||
freshness_duration: Duration,
|
||||
}
|
||||
|
||||
impl<T> IpcStreamMonitor<T>
|
||||
where
|
||||
T: MonitorData + StreamingParser + Default,
|
||||
{
|
||||
pub fn new(
|
||||
endpoint: String,
|
||||
timeout: Duration,
|
||||
retry_interval: Duration,
|
||||
freshness_duration: Duration,
|
||||
) -> Self {
|
||||
let current = Arc::new(RwLock::new(T::default()));
|
||||
let monitor_current = Arc::clone(¤t);
|
||||
let endpoint_clone = endpoint.clone();
|
||||
|
||||
// Start the monitoring task
|
||||
AsyncHandler::spawn(move || async move {
|
||||
Self::streaming_task(monitor_current, endpoint_clone, timeout, retry_interval).await;
|
||||
});
|
||||
|
||||
Self {
|
||||
current,
|
||||
endpoint,
|
||||
timeout,
|
||||
retry_interval,
|
||||
freshness_duration,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn current(&self) -> T {
|
||||
self.current.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn is_fresh(&self) -> bool {
|
||||
self.current
|
||||
.read()
|
||||
.await
|
||||
.is_fresh_within(self.freshness_duration)
|
||||
}
|
||||
|
||||
/// The core streaming task that can be specialized per monitor type
|
||||
async fn streaming_task(
|
||||
current: Arc<RwLock<T>>,
|
||||
endpoint: String,
|
||||
timeout: Duration,
|
||||
retry_interval: Duration,
|
||||
) {
|
||||
loop {
|
||||
let ipc_path_buf = match ipc_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e);
|
||||
tokio::time::sleep(retry_interval).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let ipc_path = ipc_path_buf.to_str().unwrap_or_default();
|
||||
|
||||
let client = match IpcStreamClient::new(ipc_path) {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e);
|
||||
tokio::time::sleep(retry_interval).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let _ = client
|
||||
.get(&endpoint)
|
||||
.timeout(timeout)
|
||||
.process_lines(|line| T::parse_and_update(line, Arc::clone(¤t)))
|
||||
.await;
|
||||
|
||||
tokio::time::sleep(retry_interval).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use tokio::{sync::RwLock, time::Duration};
|
||||
|
||||
use crate::{
|
||||
ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser},
|
||||
process::AsyncHandler,
|
||||
singleton_lazy_with_logging,
|
||||
utils::format::fmt_bytes,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct TrafficData {
|
||||
pub up: u64,
|
||||
pub down: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CurrentTraffic {
|
||||
pub up_rate: u64,
|
||||
pub down_rate: u64,
|
||||
pub total_up: u64,
|
||||
pub total_down: u64,
|
||||
pub last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Default for CurrentTraffic {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
up_rate: 0,
|
||||
down_rate: 0,
|
||||
total_up: 0,
|
||||
total_down: 0,
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MonitorData for CurrentTraffic {
|
||||
fn mark_fresh(&mut self) {
|
||||
self.last_updated = Instant::now();
|
||||
}
|
||||
|
||||
fn is_fresh_within(&self, duration: Duration) -> bool {
|
||||
self.last_updated.elapsed() < duration
|
||||
}
|
||||
}
|
||||
|
||||
// Traffic monitoring state for calculating rates
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TrafficMonitorState {
|
||||
pub current: CurrentTraffic,
|
||||
pub last_traffic: Option<TrafficData>,
|
||||
}
|
||||
|
||||
impl MonitorData for TrafficMonitorState {
|
||||
fn mark_fresh(&mut self) {
|
||||
self.current.mark_fresh();
|
||||
}
|
||||
|
||||
fn is_fresh_within(&self, duration: Duration) -> bool {
|
||||
self.current.is_fresh_within(duration)
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamingParser for TrafficMonitorState {
|
||||
fn parse_and_update(
|
||||
line: &str,
|
||||
current: Arc<RwLock<Self>>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if let Ok(traffic) = serde_json::from_str::<TrafficData>(line.trim()) {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let mut state_guard = current.write().await;
|
||||
|
||||
let (up_rate, down_rate) = state_guard
|
||||
.last_traffic
|
||||
.as_ref()
|
||||
.map(|l| {
|
||||
(
|
||||
traffic.up.saturating_sub(l.up),
|
||||
traffic.down.saturating_sub(l.down),
|
||||
)
|
||||
})
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
state_guard.current = CurrentTraffic {
|
||||
up_rate,
|
||||
down_rate,
|
||||
total_up: traffic.up,
|
||||
total_down: traffic.down,
|
||||
last_updated: Instant::now(),
|
||||
};
|
||||
|
||||
state_guard.last_traffic = Some(traffic);
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal traffic monitor using the new architecture
|
||||
pub struct TrafficMonitor {
|
||||
monitor: IpcStreamMonitor<TrafficMonitorState>,
|
||||
}
|
||||
|
||||
impl Default for TrafficMonitor {
|
||||
fn default() -> Self {
|
||||
TrafficMonitor {
|
||||
monitor: IpcStreamMonitor::new(
|
||||
"/traffic".to_string(),
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(1),
|
||||
Duration::from_secs(5),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use simplified singleton_lazy_with_logging macro
|
||||
singleton_lazy_with_logging!(
|
||||
TrafficMonitor,
|
||||
INSTANCE,
|
||||
"TrafficMonitor",
|
||||
TrafficMonitor::default
|
||||
);
|
||||
|
||||
impl TrafficMonitor {
|
||||
pub async fn current(&self) -> CurrentTraffic {
|
||||
self.monitor.current().await.current
|
||||
}
|
||||
|
||||
pub async fn is_fresh(&self) -> bool {
|
||||
self.monitor.is_fresh().await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_current_traffic() -> CurrentTraffic {
|
||||
TrafficMonitor::global().current().await
|
||||
}
|
||||
|
||||
pub async fn get_formatted_traffic() -> (String, String, String, String, bool) {
|
||||
let monitor = TrafficMonitor::global();
|
||||
let traffic = monitor.current().await;
|
||||
let is_fresh = monitor.is_fresh().await;
|
||||
|
||||
(
|
||||
fmt_bytes(traffic.up_rate),
|
||||
fmt_bytes(traffic.down_rate),
|
||||
fmt_bytes(traffic.total_up),
|
||||
fmt_bytes(traffic.total_down),
|
||||
is_fresh,
|
||||
)
|
||||
}
|
||||
+525
-242
@@ -1,22 +1,26 @@
|
||||
mod cmd;
|
||||
mod config;
|
||||
pub mod config;
|
||||
mod core;
|
||||
mod enhance;
|
||||
mod error;
|
||||
mod feat;
|
||||
mod ipc;
|
||||
mod module;
|
||||
mod process;
|
||||
mod state;
|
||||
mod utils;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
use crate::{
|
||||
core::hotkey,
|
||||
process::AsyncHandler,
|
||||
utils::{resolve, resolve::resolve_scheme, server},
|
||||
};
|
||||
use config::Config;
|
||||
use std::sync::{Mutex, Once};
|
||||
use parking_lot::Mutex;
|
||||
use tauri::AppHandle;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::Manager;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tokio::time::{timeout, Duration};
|
||||
@@ -24,192 +28,271 @@ use utils::logging::Type;
|
||||
|
||||
/// A global singleton handle to the application.
|
||||
pub struct AppHandleManager {
|
||||
inner: Mutex<Option<AppHandle>>,
|
||||
init: Once,
|
||||
handle: Mutex<Option<AppHandle>>,
|
||||
}
|
||||
|
||||
impl AppHandleManager {
|
||||
/// Get the global instance of the app handle manager.
|
||||
pub fn global() -> &'static Self {
|
||||
static INSTANCE: AppHandleManager = AppHandleManager {
|
||||
inner: Mutex::new(None),
|
||||
init: Once::new(),
|
||||
};
|
||||
&INSTANCE
|
||||
/// Create a new AppHandleManager instance
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
handle: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the app handle manager with an app handle.
|
||||
pub fn init(&self, handle: AppHandle) {
|
||||
self.init.call_once(|| {
|
||||
let mut app_handle = self.inner.lock().unwrap();
|
||||
let mut app_handle = self.handle.lock();
|
||||
if app_handle.is_none() {
|
||||
*app_handle = Some(handle);
|
||||
});
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"AppHandleManager initialized with handle"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the app handle if it has been initialized.
|
||||
pub fn get(&self) -> Option<AppHandle> {
|
||||
self.inner.lock().unwrap().clone()
|
||||
self.handle.lock().clone()
|
||||
}
|
||||
|
||||
/// Get the app handle, panics if it hasn't been initialized.
|
||||
pub fn get_handle(&self) -> AppHandle {
|
||||
self.get().expect("AppHandle not initialized")
|
||||
if let Some(handle) = self.get() {
|
||||
handle
|
||||
} else {
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
"AppHandle not initialized - ensure init() was called first"
|
||||
);
|
||||
std::process::exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the app handle has been initialized.
|
||||
pub fn is_initialized(&self) -> bool {
|
||||
self.handle.lock().is_some()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn set_activation_policy(&self, policy: tauri::ActivationPolicy) -> Result<(), String> {
|
||||
let app_handle = self.handle.lock();
|
||||
if let Some(app_handle) = app_handle.as_ref() {
|
||||
app_handle
|
||||
.set_activation_policy(policy)
|
||||
.map_err(|e| e.to_string())
|
||||
} else {
|
||||
Err("AppHandle not initialized".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_activation_policy_regular(&self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_handle = self.inner.lock().unwrap();
|
||||
let app_handle = app_handle.as_ref().unwrap();
|
||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Regular);
|
||||
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Regular) {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Failed to set regular activation policy: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_activation_policy_accessory(&self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_handle = self.inner.lock().unwrap();
|
||||
let app_handle = app_handle.as_ref().unwrap();
|
||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Accessory) {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Failed to set accessory activation policy: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_activation_policy_prohibited(&self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_handle = self.inner.lock().unwrap();
|
||||
let app_handle = app_handle.as_ref().unwrap();
|
||||
let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Prohibited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
pub fn run() {
|
||||
utils::network::NetworkManager::global().init();
|
||||
|
||||
let _ = utils::dirs::init_portable_flag();
|
||||
|
||||
// 单例检测
|
||||
let app_exists: bool = AsyncHandler::block_on(move || async move {
|
||||
logging!(info, Type::Setup, true, "开始检查单例实例...");
|
||||
match timeout(Duration::from_secs(3), server::check_singleton()).await {
|
||||
Ok(result) => {
|
||||
if result.is_err() {
|
||||
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
|
||||
true
|
||||
} else {
|
||||
logging!(info, Type::Setup, true, "未检测到其他应用实例");
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
if let Err(e) = self.set_activation_policy(tauri::ActivationPolicy::Prohibited) {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Setup,
|
||||
true,
|
||||
"单例检查超时,假定没有其他实例运行"
|
||||
"Failed to set prohibited activation policy: {}",
|
||||
e
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
});
|
||||
if app_exists {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Use unified singleton macro
|
||||
singleton_with_logging!(AppHandleManager, INSTANCE, "AppHandleManager");
|
||||
|
||||
/// Application initialization helper functions
|
||||
mod app_init {
|
||||
use super::*;
|
||||
|
||||
/// Initialize singleton monitoring for other instances
|
||||
pub fn init_singleton_check() {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::Setup, true, "开始检查单例实例...");
|
||||
match timeout(Duration::from_secs(3), server::check_singleton()).await {
|
||||
Ok(result) => {
|
||||
if result.is_err() {
|
||||
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
|
||||
if let Some(app_handle) = AppHandleManager::global().get() {
|
||||
app_handle.exit(0);
|
||||
} else {
|
||||
std::process::exit(0);
|
||||
}
|
||||
} else {
|
||||
logging!(info, Type::Setup, true, "未检测到其他应用实例");
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Setup,
|
||||
true,
|
||||
"单例检查超时,假定没有其他实例运行"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||
/// Setup plugins for the Tauri builder
|
||||
pub fn setup_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = builder
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_deep_link::init());
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let devtools = tauri_plugin_devtools::init();
|
||||
#[cfg(all(debug_assertions, not(feature = "tokio-trace")))]
|
||||
{
|
||||
builder = builder.plugin(tauri_plugin_devtools::init());
|
||||
}
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.setup(|app| {
|
||||
logging!(info, Type::Setup, true, "开始应用初始化...");
|
||||
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
auto_start_plugin_builder = auto_start_plugin_builder
|
||||
.macos_launcher(MacosLauncher::LaunchAgent)
|
||||
.app_name(app.config().identifier.clone());
|
||||
}
|
||||
let _ = app.handle().plugin(auto_start_plugin_builder.build());
|
||||
builder.manage(Mutex::new(state::lightweight::LightWeightState::default()))
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
logging!(info, Type::Setup, true, "注册深层链接...");
|
||||
logging_error!(Type::System, true, app.deep_link().register_all());
|
||||
}
|
||||
/// Setup deep link handling
|
||||
pub fn setup_deep_links(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
||||
{
|
||||
logging!(info, Type::Setup, true, "注册深层链接...");
|
||||
app.deep_link().register_all()?;
|
||||
}
|
||||
|
||||
app.deep_link().on_open_url(|event| {
|
||||
AsyncHandler::spawn(move || {
|
||||
let url = event.urls().first().map(|u| u.to_string());
|
||||
async move {
|
||||
if let Some(url) = url {
|
||||
logging_error!(Type::Setup, true, resolve_scheme(url).await);
|
||||
app.deep_link().on_open_url(|event| {
|
||||
AsyncHandler::spawn(move || {
|
||||
let url = event.urls().first().map(|u| u.to_string());
|
||||
async move {
|
||||
if let Some(url) = url {
|
||||
if let Err(e) = resolve_scheme(url).await {
|
||||
logging!(error, Type::Setup, true, "Failed to resolve scheme: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 异步处理
|
||||
let app_handle = app.handle().clone();
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::Setup, true, "异步执行应用设置...");
|
||||
match timeout(
|
||||
Duration::from_secs(30),
|
||||
resolve::resolve_setup_async(&app_handle),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Setup, true, "应用设置成功完成");
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
true,
|
||||
"应用设置超时(30秒),继续执行后续流程"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
logging!(info, Type::Setup, true, "执行主要设置操作...");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化AppHandleManager...");
|
||||
AppHandleManager::global().init(app.handle().clone());
|
||||
/// Setup autostart plugin
|
||||
pub fn setup_autostart(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化核心句柄...");
|
||||
core::handle::Handle::global().init(app.handle());
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
auto_start_plugin_builder = auto_start_plugin_builder
|
||||
.macos_launcher(MacosLauncher::LaunchAgent)
|
||||
.app_name(app.config().identifier.clone());
|
||||
}
|
||||
app.handle().plugin(auto_start_plugin_builder.build())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化配置...");
|
||||
if let Err(e) = utils::init::init_config() {
|
||||
logging!(error, Type::Setup, true, "初始化配置失败: {}", e);
|
||||
/// Setup window state management
|
||||
pub fn setup_window_state(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
|
||||
let window_state_plugin = tauri_plugin_window_state::Builder::new()
|
||||
.with_filename("window_state.json")
|
||||
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
|
||||
.build();
|
||||
app.handle().plugin(window_state_plugin)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize core components asynchronously
|
||||
pub fn init_core_async(app_handle: tauri::AppHandle) {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::Setup, true, "异步执行应用设置...");
|
||||
match timeout(
|
||||
Duration::from_secs(30),
|
||||
resolve::resolve_setup_async(&app_handle),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Setup, true, "应用设置成功完成");
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
true,
|
||||
"应用设置超时(30秒),继续执行后续流程"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化资源...");
|
||||
if let Err(e) = utils::init::init_resources() {
|
||||
logging!(error, Type::Setup, true, "初始化资源失败: {}", e);
|
||||
}
|
||||
/// Initialize core components synchronously
|
||||
pub fn init_core_sync(app_handle: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
logging!(info, Type::Setup, true, "初始化AppHandleManager...");
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化完成,继续执行");
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// common
|
||||
logging!(info, Type::Setup, true, "初始化核心句柄...");
|
||||
core::handle::Handle::global().init(app_handle);
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化配置...");
|
||||
utils::init::init_config()?;
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化资源...");
|
||||
utils::init::init_resources()?;
|
||||
|
||||
logging!(info, Type::Setup, true, "核心组件初始化完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate all command handlers for the application
|
||||
pub fn generate_handlers(
|
||||
) -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
|
||||
tauri::generate_handler![
|
||||
// Common commands
|
||||
cmd::get_sys_proxy,
|
||||
cmd::get_auto_proxy,
|
||||
cmd::open_app_dir,
|
||||
@@ -220,11 +303,11 @@ pub fn run() {
|
||||
cmd::get_network_interfaces,
|
||||
cmd::get_system_hostname,
|
||||
cmd::restart_app,
|
||||
// 内核管理
|
||||
// Core management
|
||||
cmd::start_core,
|
||||
cmd::stop_core,
|
||||
cmd::restart_core,
|
||||
// 启动命令
|
||||
// Application lifecycle
|
||||
cmd::notify_ui_ready,
|
||||
cmd::update_ui_stage,
|
||||
cmd::reset_ui_ready_state,
|
||||
@@ -232,16 +315,16 @@ pub fn run() {
|
||||
cmd::get_app_uptime,
|
||||
cmd::get_auto_launch_status,
|
||||
cmd::is_admin,
|
||||
// 添加轻量模式相关命令
|
||||
// Lightweight mode
|
||||
cmd::entry_lightweight_mode,
|
||||
cmd::exit_lightweight_mode,
|
||||
// service 管理
|
||||
// Service management
|
||||
cmd::install_service,
|
||||
cmd::uninstall_service,
|
||||
cmd::reinstall_service,
|
||||
cmd::repair_service,
|
||||
cmd::is_service_available,
|
||||
// clash
|
||||
// Clash core commands
|
||||
cmd::get_clash_info,
|
||||
cmd::patch_clash_config,
|
||||
cmd::patch_clash_mode,
|
||||
@@ -253,13 +336,44 @@ pub fn run() {
|
||||
cmd::invoke_uwp_tool,
|
||||
cmd::copy_clash_env,
|
||||
cmd::get_proxies,
|
||||
cmd::force_refresh_proxies,
|
||||
cmd::get_providers_proxies,
|
||||
cmd::save_dns_config,
|
||||
cmd::apply_dns_config,
|
||||
cmd::check_dns_config_exists,
|
||||
cmd::get_dns_config_content,
|
||||
cmd::validate_dns_config,
|
||||
// verge
|
||||
cmd::get_clash_version,
|
||||
cmd::get_clash_config,
|
||||
cmd::force_refresh_clash_config,
|
||||
cmd::update_geo_data,
|
||||
cmd::upgrade_clash_core,
|
||||
cmd::get_clash_rules,
|
||||
cmd::update_proxy_choice,
|
||||
cmd::get_proxy_providers,
|
||||
cmd::get_rule_providers,
|
||||
cmd::proxy_provider_health_check,
|
||||
cmd::update_proxy_provider,
|
||||
cmd::update_rule_provider,
|
||||
cmd::get_clash_connections,
|
||||
cmd::delete_clash_connection,
|
||||
cmd::close_all_clash_connections,
|
||||
cmd::get_group_proxy_delays,
|
||||
cmd::is_clash_debug_enabled,
|
||||
cmd::clash_gc,
|
||||
// Logging and monitoring
|
||||
cmd::get_clash_logs,
|
||||
cmd::start_logs_monitoring,
|
||||
cmd::stop_logs_monitoring,
|
||||
cmd::clear_logs,
|
||||
cmd::get_traffic_data,
|
||||
cmd::get_memory_data,
|
||||
cmd::get_formatted_traffic_data,
|
||||
cmd::get_formatted_memory_data,
|
||||
cmd::get_system_monitor_overview,
|
||||
cmd::start_traffic_service,
|
||||
cmd::stop_traffic_service,
|
||||
// Verge configuration
|
||||
cmd::get_verge_config,
|
||||
cmd::patch_verge_config,
|
||||
cmd::test_delay,
|
||||
@@ -269,7 +383,7 @@ pub fn run() {
|
||||
cmd::open_devtools,
|
||||
cmd::exit_app,
|
||||
cmd::get_network_interfaces_info,
|
||||
// profile
|
||||
// Profile management
|
||||
cmd::get_profiles,
|
||||
cmd::enhance_profiles,
|
||||
cmd::patch_profiles_config,
|
||||
@@ -283,67 +397,304 @@ pub fn run() {
|
||||
cmd::read_profile_file,
|
||||
cmd::save_profile_file,
|
||||
cmd::get_next_update_time,
|
||||
// script validation
|
||||
// Script validation
|
||||
cmd::script_validate_notice,
|
||||
cmd::validate_script_file,
|
||||
// clash api
|
||||
// Clash API
|
||||
cmd::clash_api_get_proxy_delay,
|
||||
// backup
|
||||
// Backup and WebDAV
|
||||
cmd::create_webdav_backup,
|
||||
cmd::save_webdav_config,
|
||||
cmd::list_webdav_backup,
|
||||
cmd::delete_webdav_backup,
|
||||
cmd::restore_webdav_backup,
|
||||
// export diagnostic info for issue reporting
|
||||
// Diagnostics and system info
|
||||
cmd::export_diagnostic_info,
|
||||
// get system info for display
|
||||
cmd::get_system_info,
|
||||
// media unlock checker
|
||||
// Media unlock checker
|
||||
cmd::get_unlock_items,
|
||||
cmd::check_media_unlock,
|
||||
// light-weight model
|
||||
cmd::entry_lightweight_mode,
|
||||
]);
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn run() {
|
||||
// Setup singleton check
|
||||
app_init::init_singleton_check();
|
||||
|
||||
// Initialize network manager
|
||||
utils::network::NetworkManager::global().init();
|
||||
|
||||
// Initialize portable flag
|
||||
let _ = utils::dirs::init_portable_flag();
|
||||
|
||||
// Set Linux environment variable
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
builder = builder.plugin(devtools);
|
||||
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||
|
||||
let desktop_env = std::env::var("XDG_CURRENT_DESKTOP")
|
||||
.unwrap_or_default()
|
||||
.to_uppercase();
|
||||
let is_kde_desktop = desktop_env.contains("KDE");
|
||||
let is_plasma_desktop = desktop_env.contains("PLASMA");
|
||||
|
||||
if is_kde_desktop || is_plasma_desktop {
|
||||
std::env::set_var("GTK_CSD", "0");
|
||||
logging!(
|
||||
info,
|
||||
Type::Setup,
|
||||
true,
|
||||
"KDE detected: Disabled GTK CSD for better titlebar stability."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Macos Application Menu
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Temporary Achived due to cannot CMD+C/V/A
|
||||
}
|
||||
// Create and configure the Tauri builder
|
||||
let builder = app_init::setup_plugins(tauri::Builder::default())
|
||||
.setup(|app| {
|
||||
logging!(info, Type::Setup, true, "开始应用初始化...");
|
||||
|
||||
let app = builder
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
// Setup autostart plugin
|
||||
if let Err(e) = app_init::setup_autostart(app) {
|
||||
logging!(error, Type::Setup, true, "Failed to setup autostart: {}", e);
|
||||
}
|
||||
|
||||
app.run(|app_handle, e| match e {
|
||||
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
|
||||
// Setup deep links
|
||||
if let Err(e) = app_init::setup_deep_links(app) {
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Failed to setup deep links: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
// Setup window state management
|
||||
if let Err(e) = app_init::setup_window_state(app) {
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Failed to setup window state: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize core components asynchronously
|
||||
app_init::init_core_async(app.handle().clone());
|
||||
|
||||
logging!(info, Type::Setup, true, "执行主要设置操作...");
|
||||
|
||||
// Initialize core components synchronously
|
||||
if let Err(e) = app_init::init_core_sync(app.handle()) {
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Failed to initialize core components: {}",
|
||||
e
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
logging!(info, Type::Setup, true, "初始化完成,继续执行");
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(app_init::generate_handlers());
|
||||
|
||||
/// Event handling helper functions
|
||||
mod event_handlers {
|
||||
use super::*;
|
||||
|
||||
/// Handle application ready/resumed events
|
||||
pub fn handle_ready_resumed(app_handle: &tauri::AppHandle) {
|
||||
logging!(info, Type::System, true, "应用就绪或恢复");
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Some(window) = AppHandleManager::global()
|
||||
.get_handle()
|
||||
.get_webview_window("main")
|
||||
{
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
logging!(info, Type::Window, true, "设置macOS窗口标题");
|
||||
let _ = window.set_title("Clash Verge");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle application reopen events (macOS)
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn handle_reopen(app_handle: &tauri::AppHandle, has_visible_windows: bool) {
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"处理 macOS 应用重新打开事件: has_visible_windows={}",
|
||||
has_visible_windows
|
||||
);
|
||||
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
|
||||
if !has_visible_windows {
|
||||
// 当没有可见窗口时,设置为 regular 模式并显示主窗口
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
|
||||
logging!(info, Type::System, true, "没有可见窗口,尝试显示主窗口");
|
||||
|
||||
let result = WindowManager::show_main_window();
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
true,
|
||||
"窗口显示操作完成,结果: {:?}",
|
||||
result
|
||||
);
|
||||
} else {
|
||||
logging!(info, Type::System, true, "已有可见窗口,无需额外操作");
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle window close requests
|
||||
pub fn handle_window_close(api: &tauri::WindowEvent) {
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
|
||||
if core::handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
|
||||
log::info!(target: "app", "closing window...");
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = api {
|
||||
api.prevent_close();
|
||||
if let Some(window) = core::handle::Handle::global().get_window() {
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle window focus events
|
||||
pub fn handle_window_focus(focused: bool) {
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest_ref()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
|
||||
if focused {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use crate::core::hotkey::SystemHotkey;
|
||||
if let Err(e) =
|
||||
hotkey::Hotkey::global().register_system_hotkey(SystemHotkey::CmdQ)
|
||||
{
|
||||
logging!(error, Type::Hotkey, true, "Failed to register CMD+Q: {}", e);
|
||||
}
|
||||
if let Err(e) =
|
||||
hotkey::Hotkey::global().register_system_hotkey(SystemHotkey::CmdW)
|
||||
{
|
||||
logging!(error, Type::Hotkey, true, "Failed to register CMD+W: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
if !is_enable_global_hotkey {
|
||||
if let Err(e) = hotkey::Hotkey::global().init() {
|
||||
logging!(error, Type::Hotkey, true, "Failed to init hotkeys: {}", e);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle unfocused state
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use crate::core::hotkey::SystemHotkey;
|
||||
if let Err(e) =
|
||||
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ)
|
||||
{
|
||||
logging!(
|
||||
error,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Failed to unregister CMD+Q: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
if let Err(e) =
|
||||
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW)
|
||||
{
|
||||
logging!(
|
||||
error,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Failed to unregister CMD+W: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !is_enable_global_hotkey {
|
||||
if let Err(e) = hotkey::Hotkey::global().reset() {
|
||||
logging!(error, Type::Hotkey, true, "Failed to reset hotkeys: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle window destroyed events
|
||||
pub fn handle_window_destroyed() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use crate::core::hotkey::SystemHotkey;
|
||||
if let Err(e) =
|
||||
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdQ)
|
||||
{
|
||||
logging!(
|
||||
error,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Failed to unregister CMD+Q on destroy: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
if let Err(e) =
|
||||
hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW)
|
||||
{
|
||||
logging!(
|
||||
error,
|
||||
Type::Hotkey,
|
||||
true,
|
||||
"Failed to unregister CMD+W on destroy: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the application
|
||||
let app = builder
|
||||
.build(tauri::generate_context!())
|
||||
.unwrap_or_else(|e| {
|
||||
logging!(
|
||||
error,
|
||||
Type::Setup,
|
||||
true,
|
||||
"Failed to build Tauri application: {}",
|
||||
e
|
||||
);
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
app.run(|app_handle, e| match e {
|
||||
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
|
||||
event_handlers::handle_ready_resumed(app_handle);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
tauri::RunEvent::Reopen {
|
||||
has_visible_windows,
|
||||
..
|
||||
} => {
|
||||
if !has_visible_windows {
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
}
|
||||
AppHandleManager::global().init(app_handle.clone());
|
||||
event_handlers::handle_reopen(app_handle, has_visible_windows);
|
||||
}
|
||||
tauri::RunEvent::ExitRequested { api, code, .. } => {
|
||||
if code.is_none() {
|
||||
@@ -351,7 +702,7 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
tauri::RunEvent::Exit => {
|
||||
// avoid duplicate cleanup
|
||||
// Avoid duplicate cleanup
|
||||
if core::handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
@@ -360,82 +711,14 @@ pub fn run() {
|
||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||
if label == "main" {
|
||||
match event {
|
||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
if core::handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
println!("closing window...");
|
||||
api.prevent_close();
|
||||
if let Some(window) = core::handle::Handle::global().get_window() {
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
|
||||
}
|
||||
tauri::WindowEvent::CloseRequested { .. } => {
|
||||
event_handlers::handle_window_close(&event);
|
||||
}
|
||||
tauri::WindowEvent::Focused(true) => {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().register("CMD+Q", "quit")
|
||||
);
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().register("CMD+W", "hide")
|
||||
);
|
||||
}
|
||||
{
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().init())
|
||||
}
|
||||
}
|
||||
}
|
||||
tauri::WindowEvent::Focused(false) => {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().unregister("CMD+Q")
|
||||
);
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().unregister("CMD+W")
|
||||
);
|
||||
}
|
||||
{
|
||||
let is_enable_global_hotkey = Config::verge()
|
||||
.latest()
|
||||
.enable_global_hotkey
|
||||
.unwrap_or(true);
|
||||
if !is_enable_global_hotkey {
|
||||
logging_error!(Type::Hotkey, true, hotkey::Hotkey::global().reset())
|
||||
}
|
||||
}
|
||||
tauri::WindowEvent::Focused(focused) => {
|
||||
event_handlers::handle_window_focus(focused);
|
||||
}
|
||||
tauri::WindowEvent::Destroyed => {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().unregister("CMD+Q")
|
||||
);
|
||||
logging_error!(
|
||||
Type::Hotkey,
|
||||
true,
|
||||
hotkey::Hotkey::global().unregister("CMD+W")
|
||||
);
|
||||
}
|
||||
event_handlers::handle_window_destroyed();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
fn main() {
|
||||
#[cfg(feature = "tokio-trace")]
|
||||
console_subscriber::init();
|
||||
app_lib::run();
|
||||
}
|
||||
|
||||
@@ -1,51 +1,133 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, timer::Timer},
|
||||
log_err, logging, logging_error,
|
||||
core::{handle, timer::Timer, tray::Tray},
|
||||
log_err, logging,
|
||||
state::lightweight::LightWeightState,
|
||||
utils::logging::Type,
|
||||
AppHandleManager,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::logging_error;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::AppHandleManager;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::TaskBuilder;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::{Listener, Manager};
|
||||
|
||||
const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task";
|
||||
|
||||
// 轻量模式状态标志
|
||||
static IS_LIGHTWEIGHT_MODE: OnceCell<Arc<RwLock<bool>>> = OnceCell::new();
|
||||
// 添加退出轻量模式的锁,防止并发调用
|
||||
static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
// 添加一个锁来防止并发退出轻量模式
|
||||
static EXIT_LOCK: OnceCell<Mutex<(bool, Instant)>> = OnceCell::new();
|
||||
|
||||
fn get_lightweight_mode() -> &'static Arc<RwLock<bool>> {
|
||||
IS_LIGHTWEIGHT_MODE.get_or_init(|| Arc::new(RwLock::new(false)))
|
||||
fn with_lightweight_status<F, R>(f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&mut LightWeightState) -> R,
|
||||
{
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
// Try to get state, but don't panic if it's not managed yet
|
||||
if let Some(state) = app_handle.try_state::<Mutex<LightWeightState>>() {
|
||||
let mut guard = state.lock();
|
||||
Some(f(&mut guard))
|
||||
} else {
|
||||
// State not managed yet, return None
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// App handle not available yet
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_exit_lock() -> &'static Mutex<(bool, Instant)> {
|
||||
EXIT_LOCK.get_or_init(|| Mutex::new((false, Instant::now())))
|
||||
pub fn run_once_auto_lightweight() {
|
||||
LightWeightState::default().run_once_time(|| {
|
||||
let is_silent_start = Config::verge()
|
||||
.latest_ref()
|
||||
.enable_silent_start
|
||||
.unwrap_or(false);
|
||||
let enable_auto = Config::verge()
|
||||
.data_mut()
|
||||
.enable_auto_light_weight_mode
|
||||
.unwrap_or(false);
|
||||
if enable_auto && is_silent_start {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
|
||||
);
|
||||
set_lightweight_mode(false);
|
||||
enable_auto_light_weight_mode();
|
||||
|
||||
// 触发托盘更新
|
||||
if let Err(e) = Tray::global().update_part() {
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn auto_lightweight_mode_init() {
|
||||
if let Some(app_handle) = handle::Handle::global().app_handle() {
|
||||
// Check if state is available before accessing it
|
||||
if app_handle.try_state::<Mutex<LightWeightState>>().is_none() {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"LightWeightState 尚未初始化,跳过自动轻量模式初始化"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let is_silent_start = { Config::verge().latest_ref().enable_silent_start }.unwrap_or(false);
|
||||
let enable_auto =
|
||||
{ Config::verge().latest_ref().enable_auto_light_weight_mode }.unwrap_or(false);
|
||||
|
||||
if enable_auto && !is_silent_start {
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"非静默启动直接挂载自动进入轻量模式监听器!"
|
||||
);
|
||||
set_lightweight_mode(true);
|
||||
enable_auto_light_weight_mode();
|
||||
|
||||
// 确保托盘状态更新
|
||||
if let Err(e) = Tray::global().update_part() {
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否处于轻量模式
|
||||
pub fn is_in_lightweight_mode() -> bool {
|
||||
*get_lightweight_mode().read()
|
||||
with_lightweight_status(|state| state.is_lightweight).unwrap_or(false)
|
||||
}
|
||||
|
||||
// 设置轻量模式状态
|
||||
fn set_lightweight_mode(value: bool) {
|
||||
let mut mode = get_lightweight_mode().write();
|
||||
*mode = value;
|
||||
logging!(info, Type::Lightweight, true, "轻量模式状态: {}", value);
|
||||
pub fn set_lightweight_mode(value: bool) {
|
||||
if with_lightweight_status(|state| {
|
||||
state.set_lightweight_mode(value);
|
||||
})
|
||||
.is_some()
|
||||
{
|
||||
// 只有在状态可用时才触发托盘更新
|
||||
if let Err(e) = Tray::global().update_part() {
|
||||
log::warn!("Failed to update tray: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable_auto_light_weight_mode() {
|
||||
Timer::global().init().unwrap();
|
||||
if let Err(e) = Timer::global().init() {
|
||||
logging!(error, Type::Lightweight, "Failed to initialize timer: {e}");
|
||||
return;
|
||||
}
|
||||
logging!(info, Type::Lightweight, true, "开启自动轻量模式");
|
||||
setup_window_close_listener();
|
||||
setup_webview_focus_listener();
|
||||
@@ -58,60 +140,72 @@ pub fn disable_auto_light_weight_mode() {
|
||||
}
|
||||
|
||||
pub fn entry_lightweight_mode() {
|
||||
use crate::utils::window_manager::WindowManager;
|
||||
|
||||
let result = WindowManager::hide_main_window();
|
||||
logging!(
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"轻量模式隐藏窗口结果: {:?}",
|
||||
result
|
||||
);
|
||||
|
||||
if let Some(window) = handle::Handle::global().get_window() {
|
||||
if window.is_visible().unwrap_or(false) {
|
||||
let _ = window.hide();
|
||||
}
|
||||
if let Some(webview) = window.get_webview_window("main") {
|
||||
let _ = webview.destroy();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_accessory();
|
||||
logging!(info, Type::Lightweight, true, "轻量模式已开启");
|
||||
}
|
||||
// 标记已进入轻量模式
|
||||
set_lightweight_mode(true);
|
||||
let _ = cancel_light_weight_timer();
|
||||
|
||||
// 更新托盘显示
|
||||
let _tray = crate::core::tray::Tray::global();
|
||||
}
|
||||
|
||||
// 添加从轻量模式恢复的函数
|
||||
pub fn exit_lightweight_mode() {
|
||||
// 获取锁,检查是否已经有退出操作在进行中
|
||||
let mut exit_lock = get_exit_lock().lock();
|
||||
let (is_exiting, last_exit_time) = *exit_lock;
|
||||
let now = Instant::now();
|
||||
|
||||
// 如果已经有一个退出操作在进行,并且距离上次退出时间不超过2秒,跳过本次退出
|
||||
if is_exiting && now.duration_since(last_exit_time) < Duration::from_secs(2) {
|
||||
// 使用原子操作检查是否已经在退出过程中,防止并发调用
|
||||
if EXITING_LIGHTWEIGHT
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_err()
|
||||
{
|
||||
logging!(
|
||||
warn,
|
||||
info,
|
||||
Type::Lightweight,
|
||||
true,
|
||||
"已有退出轻量模式操作正在进行中,跳过本次请求"
|
||||
"轻量模式退出操作已在进行中,跳过重复调用"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
*exit_lock = (true, now);
|
||||
// 使用defer确保无论如何都会重置标志
|
||||
let _guard = scopeguard::guard((), |_| {
|
||||
EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
// 确保当前确实处于轻量模式才执行退出操作
|
||||
if !is_in_lightweight_mode() {
|
||||
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
|
||||
exit_lock.0 = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记退出轻量模式
|
||||
set_lightweight_mode(false);
|
||||
logging!(info, Type::Lightweight, true, "正在退出轻量模式");
|
||||
|
||||
// macOS激活策略
|
||||
#[cfg(target_os = "macos")]
|
||||
AppHandleManager::global().set_activation_policy_regular();
|
||||
|
||||
// 重置UI就绪状态
|
||||
crate::utils::resolve::reset_ui_ready();
|
||||
|
||||
// 释放锁
|
||||
exit_lock.0 = false;
|
||||
// 更新托盘显示
|
||||
let _tray = crate::core::tray::Tray::global();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn add_light_weight_timer() {
|
||||
logging_error!(Type::Lightweight, setup_light_weight_timer());
|
||||
}
|
||||
@@ -156,19 +250,19 @@ fn cancel_window_close_listener() {
|
||||
|
||||
fn setup_light_weight_timer() -> Result<()> {
|
||||
Timer::global().init()?;
|
||||
|
||||
let mut timer_map = Timer::global().timer_map.write();
|
||||
let delay_timer = Timer::global().delay_timer.write();
|
||||
let mut timer_count = Timer::global().timer_count.lock();
|
||||
|
||||
let task_id = *timer_count;
|
||||
*timer_count += 1;
|
||||
|
||||
let once_by_minutes = Config::verge()
|
||||
.latest()
|
||||
.latest_ref()
|
||||
.auto_light_weight_minutes
|
||||
.unwrap_or(10);
|
||||
|
||||
// 获取task_id
|
||||
let task_id = {
|
||||
Timer::global()
|
||||
.timer_count
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||
};
|
||||
|
||||
// 创建任务
|
||||
let task = TaskBuilder::default()
|
||||
.set_task_id(task_id)
|
||||
.set_maximum_parallel_runnable_num(1)
|
||||
@@ -179,17 +273,24 @@ fn setup_light_weight_timer() -> Result<()> {
|
||||
})
|
||||
.context("failed to create timer task")?;
|
||||
|
||||
delay_timer
|
||||
.add_task(task)
|
||||
.context("failed to add timer task")?;
|
||||
// 添加任务到定时器
|
||||
{
|
||||
let delay_timer = Timer::global().delay_timer.write();
|
||||
delay_timer
|
||||
.add_task(task)
|
||||
.context("failed to add timer task")?;
|
||||
}
|
||||
|
||||
let timer_task = crate::core::timer::TimerTask {
|
||||
task_id,
|
||||
interval_minutes: once_by_minutes,
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
|
||||
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task);
|
||||
// 更新任务映射
|
||||
{
|
||||
let mut timer_map = Timer::global().timer_map.write();
|
||||
let timer_task = crate::core::timer::TimerTask {
|
||||
task_id,
|
||||
interval_minutes: once_by_minutes,
|
||||
last_run: chrono::Local::now().timestamp(),
|
||||
};
|
||||
timer_map.insert(LIGHT_WEIGHT_TASK_UID.to_string(), timer_task);
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
use crate::config::Config;
|
||||
use mihomo_api;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
use tauri::http::{HeaderMap, HeaderValue};
|
||||
|
||||
// 缓存的最大有效期(5秒)
|
||||
const CACHE_TTL: Duration = Duration::from_secs(5);
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct Rate {
|
||||
pub up: u64,
|
||||
pub down: u64,
|
||||
}
|
||||
// 缓存MihomoManager实例
|
||||
struct MihomoCache {
|
||||
manager: mihomo_api::MihomoManager,
|
||||
created_at: Instant,
|
||||
server: String,
|
||||
}
|
||||
// 使用RwLock替代Mutex,允许多个读取操作并发进行
|
||||
pub struct MihomoManager {
|
||||
mihomo_cache: RwLock<Option<MihomoCache>>,
|
||||
create_lock: Mutex<()>,
|
||||
}
|
||||
|
||||
impl MihomoManager {
|
||||
fn __global() -> &'static MihomoManager {
|
||||
static INSTANCE: Lazy<MihomoManager> = Lazy::new(|| MihomoManager {
|
||||
mihomo_cache: RwLock::new(None),
|
||||
create_lock: Mutex::new(()),
|
||||
});
|
||||
&INSTANCE
|
||||
}
|
||||
|
||||
pub fn global() -> mihomo_api::MihomoManager {
|
||||
let instance = MihomoManager::__global();
|
||||
|
||||
// 尝试从缓存读取(只需读锁)
|
||||
{
|
||||
let cache = instance.mihomo_cache.read();
|
||||
if let Some(cache_entry) = &*cache {
|
||||
let (current_server, _) = MihomoManager::get_clash_client_info()
|
||||
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
|
||||
|
||||
// 检查缓存是否有效
|
||||
if cache_entry.server == current_server
|
||||
&& cache_entry.created_at.elapsed() < CACHE_TTL
|
||||
{
|
||||
return cache_entry.manager.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存无效,获取创建锁
|
||||
let _create_guard = instance.create_lock.lock();
|
||||
|
||||
// 再次检查缓存(双重检查锁定模式)
|
||||
{
|
||||
let cache = instance.mihomo_cache.read();
|
||||
if let Some(cache_entry) = &*cache {
|
||||
let (current_server, _) = MihomoManager::get_clash_client_info()
|
||||
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
|
||||
|
||||
if cache_entry.server == current_server
|
||||
&& cache_entry.created_at.elapsed() < CACHE_TTL
|
||||
{
|
||||
return cache_entry.manager.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新实例
|
||||
let (current_server, headers) = MihomoManager::get_clash_client_info()
|
||||
.unwrap_or_else(|| (String::new(), HeaderMap::new()));
|
||||
let manager = mihomo_api::MihomoManager::new(current_server.clone(), headers);
|
||||
|
||||
// 更新缓存
|
||||
{
|
||||
let mut cache = instance.mihomo_cache.write();
|
||||
*cache = Some(MihomoCache {
|
||||
manager: manager.clone(),
|
||||
created_at: Instant::now(),
|
||||
server: current_server,
|
||||
});
|
||||
}
|
||||
|
||||
manager
|
||||
}
|
||||
}
|
||||
|
||||
impl MihomoManager {
|
||||
pub fn get_clash_client_info() -> Option<(String, HeaderMap)> {
|
||||
let client = { Config::clash().data().get_client_info() };
|
||||
let server = format!("http://{}", client.server);
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", "application/json".parse().unwrap());
|
||||
if let Some(secret) = client.secret {
|
||||
let secret = format!("Bearer {}", secret).parse().unwrap();
|
||||
headers.insert("Authorization", secret);
|
||||
}
|
||||
|
||||
Some((server, headers))
|
||||
}
|
||||
|
||||
// 提供默认值的版本,避免在connection_info为None时panic
|
||||
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)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user