mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-15 13:50:47 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a918afb653 |
@@ -3,7 +3,3 @@ linker = "aarch64-linux-gnu-gcc"
|
||||
|
||||
[target.armv7-unknown-linux-gnueabihf]
|
||||
linker = "arm-linux-gnueabihf-gcc"
|
||||
|
||||
[alias]
|
||||
clippy-all = "clippy --all-targets --all-features -- -D warnings"
|
||||
clippy-only = "clippy --all-targets --features clippy -- -D warnings"
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
github: clash-verge-rev
|
||||
@@ -9,21 +9,18 @@ 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. 请 **务必** 查看 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本更新日志
|
||||
5. 请 **务必** 尝试 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本,确定问题是否仍然存在
|
||||
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
|
||||
4. Please be sure to check out [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) version update log
|
||||
5. Please be sure to try the [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) version to ensure that the problem still exists
|
||||
4. Please be sure to check out [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version update log
|
||||
5. Please be sure to try the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version to ensure that the problem still exists
|
||||
6. Please describe the problem in detail according to the template specification and try to update the Alpha version, otherwise the issue will be closed
|
||||
|
||||
- type: textarea
|
||||
@@ -35,9 +32,8 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 软件版本 / CVR Version
|
||||
description: 请提供 CVR 的具体版本,如果是 AutoBuild 版本,请注明下载时间(精确到小时分钟) / Please provide the specific version of CVR. If it is an AutoBuild version, please indicate the download time (accurate to hours and minutes)
|
||||
render: text
|
||||
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)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -46,13 +42,13 @@ body:
|
||||
description: 请提供复现问题的步骤 / Steps to reproduce the behavior
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: 操作系统 / OS
|
||||
options:
|
||||
- label: Windows
|
||||
- label: Linux
|
||||
- label: MacOS
|
||||
- Windows
|
||||
- Linux
|
||||
- MacOS
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -65,9 +61,5 @@ 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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 讨论交流 / Communication
|
||||
url: https://t.me/clash_verge_rev
|
||||
|
||||
@@ -12,13 +12,13 @@ body:
|
||||
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide/term.html) 确认软件不存在类似的功能
|
||||
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
|
||||
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
|
||||
4. 请 **务必** 先下载 [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) 版本测试,确保该功能还未实现
|
||||
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保该功能还未实现
|
||||
5. 请 **务必** 按照模板规范详细描述问题,否则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) to confirm that the software does not have similar functions
|
||||
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
|
||||
4. Please be sure to download the [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) version for testing to ensure that the function has not been implemented
|
||||
4. Please be sure to download the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version for testing to ensure that the function has not been implemented
|
||||
5. Please describe the problem in detail according to the template specification, otherwise the issue will be closed
|
||||
|
||||
- type: textarea
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
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: 软件版本 / CVR Version
|
||||
description: 请提供你使用的 CVR 具体版本 / Please provide the specific version of CVR you are using
|
||||
validations:
|
||||
required: true
|
||||
+12
-60
@@ -4,63 +4,22 @@ on:
|
||||
# 因为 alpha 不再负责频繁构建,且需要相对于 autobuild 更稳定使用环境
|
||||
# 所以不再使用 workflow_dispatch 触发
|
||||
# 应当通过 git tag 来触发构建
|
||||
# TODO 手动控制版本号
|
||||
workflow_dispatch:
|
||||
# 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*"
|
||||
# workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*-alpha*"
|
||||
permissions: write-all
|
||||
env:
|
||||
TAG_NAME: alpha
|
||||
TAG_CHANNEL: Alpha
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
HUSKY: 0
|
||||
concurrency:
|
||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||
|
||||
jobs:
|
||||
check_alpha_tag:
|
||||
name: Check Alpha Tag package.json Version Consistency
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check tag and package.json version
|
||||
id: check_tag
|
||||
run: |
|
||||
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
|
||||
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
|
||||
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:
|
||||
name: Delete Old Alpha Release Assets and Tags
|
||||
needs: check_alpha_tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete Old Alpha Tags Except Latest
|
||||
@@ -287,7 +246,8 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
save-if: false
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
@@ -295,15 +255,6 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install x86 OpenSSL (macOS only)
|
||||
if: matrix.target == 'x86_64-apple-darwin'
|
||||
run: |
|
||||
arch -x86_64 brew install openssl@3
|
||||
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
|
||||
echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV
|
||||
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -317,7 +268,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
# run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
@@ -372,7 +323,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
save-if: false
|
||||
cache-all-crates: true
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -387,7 +338,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
# run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
@@ -500,7 +451,8 @@ jobs:
|
||||
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
|
||||
@@ -515,7 +467,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
# - name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
# run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
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 }}"
|
||||
+92
-228
@@ -3,15 +3,14 @@ name: Auto Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# UTC+8 12:00, 18:00 -> UTC 4:00, 10:00
|
||||
- cron: "0 4,10 * * *"
|
||||
# UTC+8 0,6,12,18
|
||||
- cron: "0 16,22,4,10 * * *"
|
||||
permissions: write-all
|
||||
env:
|
||||
TAG_NAME: autobuild
|
||||
TAG_CHANNEL: AutoBuild
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
HUSKY: 0
|
||||
concurrency:
|
||||
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
@@ -19,10 +18,55 @@ concurrency:
|
||||
jobs:
|
||||
check_commit:
|
||||
name: Check Commit Needs Build
|
||||
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' }}
|
||||
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
|
||||
|
||||
update_tag:
|
||||
name: Update tag
|
||||
@@ -51,28 +95,9 @@ 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: |
|
||||
@@ -86,24 +111,25 @@ 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位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe)
|
||||
|
||||
- 64位: x64-setup.exe
|
||||
- arm64架构: 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_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg)
|
||||
|
||||
### Linux
|
||||
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
|
||||
- 64位: x64_fixed_webview2-setup.exe
|
||||
- arm64架构: arm64_fixed_webview2-setup.exe
|
||||
|
||||
### FAQ
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
@@ -124,20 +150,9 @@ jobs:
|
||||
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: [check_commit, update_tag]
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -168,9 +183,7 @@ jobs:
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
@@ -178,43 +191,25 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install x86 OpenSSL (macOS only)
|
||||
if: matrix.target == 'x86_64-apple-darwin'
|
||||
run: |
|
||||
arch -x86_64 brew install openssl@3
|
||||
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
|
||||
echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV
|
||||
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
|
||||
- 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: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: pnpm release-version autobuild-latest
|
||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
|
||||
- name: Tauri build for Windows-macOS-Linux
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
@@ -235,12 +230,10 @@ jobs:
|
||||
prerelease: true
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
# includeUpdaterJson: true
|
||||
|
||||
autobuild-arm-linux:
|
||||
name: Autobuild ARM Linux
|
||||
needs: [check_commit, update_tag]
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -267,36 +260,24 @@ jobs:
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- 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: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: pnpm release-version autobuild-latest
|
||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
|
||||
- name: Setup for linux
|
||||
run: |
|
||||
@@ -347,7 +328,7 @@ jobs:
|
||||
gcc-arm-linux-gnueabihf \
|
||||
g++-arm-linux-gnueabihf
|
||||
|
||||
- name: Tauri Build for Linux
|
||||
- name: Build for Linux
|
||||
run: |
|
||||
export PKG_CONFIG_ALLOW_CROSS=1
|
||||
if [ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]; then
|
||||
@@ -383,8 +364,7 @@ jobs:
|
||||
|
||||
autobuild-x86-arm-windows_webview2:
|
||||
name: Autobuild x86 and ARM Windows with WebView2
|
||||
needs: [check_commit, update_tag]
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||
needs: update_tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -408,36 +388,25 @@ jobs:
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
run_install: false
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: pnpm release-version autobuild-latest
|
||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
|
||||
- name: Download WebView2 Runtime
|
||||
run: |
|
||||
@@ -446,7 +415,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 for Windows
|
||||
- name: Tauri build
|
||||
id: build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
@@ -457,7 +426,6 @@ jobs:
|
||||
with:
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
# includeUpdaterJson: true
|
||||
|
||||
- name: Rename
|
||||
run: |
|
||||
@@ -492,107 +460,3 @@ jobs:
|
||||
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_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.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_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg)
|
||||
|
||||
### Linux
|
||||
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.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 }}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
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=$(TZ=Asia/Shanghai date +%m)
|
||||
DAY=$(TZ=Asia/Shanghai 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
|
||||
@@ -1,220 +0,0 @@
|
||||
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=$(TZ=Asia/Shanghai date +%m)
|
||||
DAY=$(TZ=Asia/Shanghai 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
|
||||
@@ -1,67 +0,0 @@
|
||||
name: Cross Platform Cargo Check
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# pull_request:
|
||||
# push:
|
||||
# branches: [main, dev]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
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
|
||||
+15
-82
@@ -2,36 +2,12 @@ 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
|
||||
run_windows_arm64:
|
||||
description: "运行 Windows ARM64"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
run_linux_amd64:
|
||||
description: "运行 Linux amd64"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions: write-all
|
||||
env:
|
||||
TAG_NAME: deploytest
|
||||
TAG_CHANNEL: DeployTest
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
HUSKY: 0
|
||||
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' }}
|
||||
|
||||
@@ -44,82 +20,47 @@ 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
|
||||
id: macos-aarch64
|
||||
input: run_macos_aarch64
|
||||
- os: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
bundle: nsis
|
||||
id: windows-arm64
|
||||
input: run_windows_arm64
|
||||
- os: ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
bundle: deb
|
||||
id: linux-amd64
|
||||
input: run_linux_amd64
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
bundle: dmg
|
||||
|
||||
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
|
||||
shared-key: autobuild-shared
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04' && github.event.inputs[matrix.input] == 'true'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
with:
|
||||
run_install: false
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install Node
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Pnpm install and check
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
run: |
|
||||
pnpm i
|
||||
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 }}
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Tauri build
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
@@ -136,26 +77,18 @@ jobs:
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }} -b ${{ matrix.bundle }}
|
||||
|
||||
- name: Upload Artifacts (macOS)
|
||||
if: matrix.os == 'macos-latest' && github.event.inputs[matrix.input] == 'true'
|
||||
- name: Upload Artifacts
|
||||
if: matrix.os == 'macos-latest'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Artifacts (Windows)
|
||||
if: matrix.os == 'windows-latest' && github.event.inputs[matrix.input] == 'true'
|
||||
- name: Upload Artifacts
|
||||
if: matrix.os == 'windows-latest'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Artifacts (Linux)
|
||||
if: matrix.os == 'ubuntu-22.04' && github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
# 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:
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check Rust changes
|
||||
id: check_rust
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
rust:
|
||||
- 'src-tauri/**'
|
||||
- '**/*.rs'
|
||||
|
||||
- name: Skip if no Rust changes
|
||||
if: steps.check_rust.outputs.rust != 'true'
|
||||
run: echo "No Rust changes, skipping rustfmt."
|
||||
|
||||
- name: install Rust stable and rustfmt
|
||||
if: steps.check_rust.outputs.rust == 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
|
||||
- name: run cargo fmt
|
||||
if: steps.check_rust.outputs.rust == 'true'
|
||||
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
|
||||
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check Web changes
|
||||
id: check_web
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
web:
|
||||
- 'src/**'
|
||||
- '**/*.js'
|
||||
- '**/*.ts'
|
||||
- '**/*.tsx'
|
||||
- '**/*.css'
|
||||
- '**/*.scss'
|
||||
- '**/*.json'
|
||||
- '**/*.md'
|
||||
- '**/*.json'
|
||||
|
||||
- name: Skip if no Web changes
|
||||
if: steps.check_web.outputs.web != 'true'
|
||||
run: echo "No web changes, skipping prettier."
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
if: steps.check_web.outputs.web == 'true'
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
- run: corepack enable
|
||||
if: steps.check_web.outputs.web == 'true'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
if: steps.check_web.outputs.web == 'true'
|
||||
- run: pnpm format:check
|
||||
if: steps.check_web.outputs.web == 'true'
|
||||
|
||||
# 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
|
||||
@@ -1,82 +0,0 @@
|
||||
name: Clippy Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
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: Check src-tauri changes
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
id: check_changes
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
rust:
|
||||
- 'src-tauri/**'
|
||||
|
||||
- name: Skip if src-tauri not changed
|
||||
if: github.event_name != 'workflow_dispatch' && steps.check_changes.outputs.rust != 'true'
|
||||
run: echo "No src-tauri changes, skipping clippy lint."
|
||||
|
||||
- name: Continue if src-tauri changed
|
||||
if: github.event_name != 'workflow_dispatch' && steps.check_changes.outputs.rust == 'true'
|
||||
run: echo "src-tauri changed, running clippy lint."
|
||||
|
||||
- name: Manual trigger - always run
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "Manual trigger detected: skipping changes check and running clippy."
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: false
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
|
||||
- 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: Run Clippy
|
||||
working-directory: ./src-tauri
|
||||
run: cargo clippy-all
|
||||
|
||||
- name: Run Logging Check
|
||||
working-directory: ./src-tauri
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
|
||||
clash-verge-logging-check
|
||||
+21
-270
@@ -5,152 +5,23 @@ on:
|
||||
# ! 不再使用 workflow_dispatch 触发。
|
||||
# workflow_dispatch:
|
||||
push:
|
||||
# -rc tag 时预览发布, 跳过 telegram 通知、跳过 winget 提交、跳过 latest.json 文件更新
|
||||
# ? 应当限制在 main 分支上触发发布。
|
||||
# branches:
|
||||
# - main
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
permissions: write-all
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
HUSKY: 0
|
||||
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_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 }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.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
|
||||
draft: false
|
||||
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# generate_release_notes: true
|
||||
|
||||
release:
|
||||
name: Release Build
|
||||
needs: [check_tag_version]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -181,7 +52,8 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
save-if: false
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
@@ -189,15 +61,6 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install x86 OpenSSL (macOS only)
|
||||
if: matrix.target == 'x86_64-apple-darwin'
|
||||
run: |
|
||||
arch -x86_64 brew install openssl@3
|
||||
echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
|
||||
echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV
|
||||
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -211,11 +74,10 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Tauri build
|
||||
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
|
||||
uses: tauri-apps/tauri-action@v0.5.23
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -228,18 +90,14 @@ jobs:
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: "Clash Verge Rev ${{ github.ref_name }}"
|
||||
releaseBody: "Draft release, will be updated later."
|
||||
releaseDraft: true
|
||||
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||
tagName: v__VERSION__
|
||||
releaseName: "Clash Verge Rev v__VERSION__"
|
||||
releaseBody: "More new features are now supported."
|
||||
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:
|
||||
@@ -265,7 +123,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
save-if: false
|
||||
cache-all-crates: true
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -280,7 +138,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: "Setup for linux"
|
||||
run: |-
|
||||
@@ -354,16 +212,14 @@ jobs:
|
||||
with:
|
||||
tag_name: v${{env.VERSION}}
|
||||
name: "Clash Verge Rev v${{env.VERSION}}"
|
||||
body: "See release notes for detailed changelog."
|
||||
body: "More new features are now supported."
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||
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:
|
||||
@@ -386,7 +242,8 @@ jobs:
|
||||
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
|
||||
@@ -401,7 +258,7 @@ jobs:
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Download WebView2 Runtime
|
||||
run: |
|
||||
@@ -412,8 +269,7 @@ jobs:
|
||||
|
||||
- name: Tauri build
|
||||
id: build
|
||||
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
|
||||
uses: tauri-apps/tauri-action@v0.5.23
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -448,9 +304,8 @@ jobs:
|
||||
with:
|
||||
tag_name: v${{steps.build.outputs.appVersion}}
|
||||
name: "Clash Verge Rev v${{steps.build.outputs.appVersion}}"
|
||||
body: "See release notes for detailed changelog."
|
||||
body: "More new features are now supported."
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||
|
||||
- name: Portable Bundle
|
||||
@@ -459,10 +314,9 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release-update:
|
||||
if: ${{ !contains(github.ref_name, '-rc') }}
|
||||
name: Release Update
|
||||
runs-on: ubuntu-latest
|
||||
needs: [update_tag]
|
||||
needs: [release, release-for-linux-arm]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -486,9 +340,8 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release-update-for-fixed-webview2:
|
||||
if: ${{ !contains(github.ref_name, '-rc') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [update_tag]
|
||||
needs: [release-for-fixed-webview2]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -512,10 +365,9 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
submit-to-winget:
|
||||
if: ${{ !contains(github.ref_name, '-rc') }}
|
||||
name: Submit to Winget
|
||||
runs-on: ubuntu-latest
|
||||
needs: [update_tag, release-update]
|
||||
needs: [release-update]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -534,104 +386,3 @@ jobs:
|
||||
release-tag: v${{env.VERSION}}
|
||||
installers-regex: '_(arm64|x64|x86)-setup\.exe$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
notify-telegram:
|
||||
if: ${{ !contains(github.ref_name, '-rc') }}
|
||||
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 }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.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 }}
|
||||
|
||||
@@ -2,9 +2,6 @@ name: Updater CI
|
||||
|
||||
on: workflow_dispatch
|
||||
permissions: write-all
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
jobs:
|
||||
release-update:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -10,5 +10,3 @@ scripts/_env.sh
|
||||
.tool-versions
|
||||
.idea
|
||||
.old
|
||||
.eslintcache
|
||||
target
|
||||
+12
-38
@@ -1,44 +1,18 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(git rev-parse --show-toplevel)"
|
||||
cd "$ROOT_DIR"
|
||||
#pnpm pretty-quick --staged
|
||||
|
||||
if ! command -v pnpm >/dev/null 2>&1; then
|
||||
echo "❌ pnpm is required for pre-commit checks."
|
||||
exit 1
|
||||
# 运行 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
|
||||
fi
|
||||
cd ..
|
||||
|
||||
echo "[pre-commit] Running lint-staged for JS/TS files..."
|
||||
pnpm exec lint-staged
|
||||
git add .
|
||||
|
||||
RUST_FILES="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '^src-tauri/.*\.rs$' || true)"
|
||||
if [ -n "$RUST_FILES" ]; then
|
||||
echo "[pre-commit] Formatting Rust changes with cargo fmt..."
|
||||
(
|
||||
cd src-tauri
|
||||
cargo fmt
|
||||
)
|
||||
while IFS= read -r file; do
|
||||
[ -n "$file" ] && git add "$file"
|
||||
done <<< "$RUST_FILES"
|
||||
|
||||
echo "[pre-commit] Linting Rust changes with cargo clippy..."
|
||||
(
|
||||
cd src-tauri
|
||||
cargo clippy-all
|
||||
if ! command -v clash-verge-logging-check >/dev/null 2>&1; then
|
||||
echo "[pre-commit] Installing clash-verge-logging-check..."
|
||||
cargo install --git https://github.com/clash-verge-rev/clash-verge-logging-check.git
|
||||
fi
|
||||
clash-verge-logging-check
|
||||
)
|
||||
fi
|
||||
|
||||
TS_FILES="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(ts|tsx)$' || true)"
|
||||
if [ -n "$TS_FILES" ]; then
|
||||
echo "[pre-commit] Running TypeScript type check..."
|
||||
pnpm typecheck
|
||||
fi
|
||||
|
||||
echo "[pre-commit] All checks completed successfully."
|
||||
# 允许提交
|
||||
exit 0
|
||||
|
||||
+8
-38
@@ -1,43 +1,13 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
remote_name="${1:-origin}"
|
||||
remote_url="${2:-unknown}"
|
||||
# 运行 clippy
|
||||
# cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix
|
||||
|
||||
ROOT_DIR="$(git rev-parse --show-toplevel)"
|
||||
cd "$ROOT_DIR"
|
||||
# 如果 clippy 失败,阻止 push
|
||||
# if [ $? -ne 0 ]; then
|
||||
# echo "Clippy found issues in sub_crate. Please fix them before pushing."
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
if ! command -v pnpm >/dev/null 2>&1; then
|
||||
echo "❌ pnpm is required for pre-push checks."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[pre-push] Preparing to push to '$remote_name' ($remote_url). Running full validation..."
|
||||
|
||||
echo "[pre-push] Checking Prettier formatting..."
|
||||
pnpm format:check
|
||||
|
||||
echo "[pre-push] Running ESLint..."
|
||||
pnpm lint
|
||||
|
||||
echo "[pre-push] Running TypeScript type checking..."
|
||||
pnpm typecheck
|
||||
|
||||
if command -v cargo >/dev/null 2>&1; then
|
||||
echo "[pre-push] Verifying Rust formatting..."
|
||||
(
|
||||
cd src-tauri
|
||||
cargo fmt --check
|
||||
)
|
||||
|
||||
echo "[pre-push] Running cargo clippy..."
|
||||
(
|
||||
cd src-tauri
|
||||
cargo clippy-all
|
||||
)
|
||||
else
|
||||
echo "[pre-push] ⚠️ cargo not found; skipping Rust checks."
|
||||
fi
|
||||
|
||||
echo "[pre-push] All checks passed."
|
||||
# 允许 push
|
||||
exit 0
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# README.md
|
||||
# UPDATELOG.md
|
||||
# CONTRIBUTING.md
|
||||
|
||||
pnpm-lock.yaml
|
||||
|
||||
src-tauri/target/
|
||||
src-tauri/gen/
|
||||
|
||||
target
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"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": "auto",
|
||||
"embeddedLanguageFormatting": "auto"
|
||||
}
|
||||
+12
-34
@@ -2,25 +2,16 @@
|
||||
|
||||
Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing.
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
We welcome translations and improvements to existing locales. Please follow the detailed guidelines in [CONTRIBUTING_i18n.md](docs/CONTRIBUTING_i18n.md) for instructions on extracting strings, file naming conventions, testing translations, and submitting translation PRs.
|
||||
|
||||
## Development Setup
|
||||
|
||||
Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### 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.
|
||||
@@ -42,15 +33,12 @@ 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
|
||||
```
|
||||
@@ -60,14 +48,11 @@ 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 prebuild
|
||||
# Use '--force' or '-f' to update both the Mihomo core version
|
||||
# and the Clash Verge Rev service version to the latest available.
|
||||
pnpm run prebuild --force
|
||||
pnpm run check
|
||||
# Use '--force' to force update to the latest version
|
||||
# pnpm run check --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
|
||||
@@ -78,8 +63,6 @@ To run the development server, use the following command:
|
||||
pnpm dev
|
||||
# If an app instance already exists, use a different command
|
||||
pnpm dev:diff
|
||||
# To using tauri built-in dev tool
|
||||
pnpm dev:tauri
|
||||
```
|
||||
|
||||
### Build the Project
|
||||
@@ -122,25 +105,20 @@ pnpm portable
|
||||
|
||||
If you changed the rust code, it's recommanded to execute code style formatting and quailty checks.
|
||||
|
||||
1. Code quailty checks
|
||||
1. Code style formatting
|
||||
|
||||
```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.
|
||||
|
||||
@@ -9,16 +9,6 @@
|
||||
A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
|
||||
</h3>
|
||||
|
||||
<p align="center">
|
||||
Languages:
|
||||
<a href="./README.md">简体中文</a> ·
|
||||
<a href="./docs/README_en.md">English</a> ·
|
||||
<a href="./docs/README_es.md">Español</a> ·
|
||||
<a href="./docs/README_ru.md">Русский</a> ·
|
||||
<a href="./docs/README_ja.md">日本語</a> ·
|
||||
<a href="./docs/README_ko.md">한국어</a>
|
||||
</p>
|
||||
|
||||
## Preview
|
||||
|
||||
| Dark | Light |
|
||||
@@ -33,13 +23,13 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
|
||||
|
||||
#### 我应当怎样选择发行版
|
||||
|
||||
| 版本 | 特征 | 链接 |
|
||||
| :---------- | :--------------------------------------- | :------------------------------------------------------------------------------------- |
|
||||
| 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) |
|
||||
| 版本 | 特征 | 链接 |
|
||||
|:-----|:-----|:-----|
|
||||
|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/) 查看
|
||||
#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看
|
||||
|
||||
---
|
||||
|
||||
@@ -59,12 +49,11 @@ 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](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持,
|
||||
感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器!
|
||||
|
||||
🧩 YXVM 独立服务器优势:
|
||||
|
||||
- 🌎 优质网络,回程优化,下载快到飞起
|
||||
- 🔧 物理机独享资源,非VPS可比,性能拉满
|
||||
- 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用
|
||||
@@ -98,7 +87,7 @@ To run the development server, execute the following commands after all prerequi
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
pnpm run prebuild
|
||||
pnpm run check
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
|
||||
+136
-452
@@ -1,449 +1,143 @@
|
||||
## v2.4.3
|
||||
|
||||
**发行代号:澜**
|
||||
代号释义:澜象征平稳与融合,本次版本聚焦稳定性、兼容性、性能与体验优化,全面提升整体可靠性。
|
||||
|
||||
特别感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 优化服务模式重装逻辑,避免不必要的重复检查
|
||||
- 修复轻量模式退出无响应的问题
|
||||
- 修复托盘轻量模式支持退出/进入
|
||||
- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程
|
||||
- macOS Tun/系统代理 模式下图标大小不统一
|
||||
- 托盘节点切换不再显示隐藏组
|
||||
- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商
|
||||
- 修复MacOS 下 Tun开启后 系统代理无法打开的问题
|
||||
- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题
|
||||
- 修复 Webdav 恢复备份不重启
|
||||
- 修复 Linux 开机后无法正常代理需要手动设置
|
||||
- 修复增加订阅或导入订阅文件时订阅页面无更新
|
||||
- 修复系统代理守卫功能不工作
|
||||
- 修复 KDE + Wayland 下多屏显示 UI 异常
|
||||
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
|
||||
- 修复静默启动不加载完整 WebView 的问题
|
||||
- 修复 Linux WebKit 网络进程的崩溃
|
||||
- 修复无法导入订阅
|
||||
- 修复实际导入成功但显示导入失败的问题
|
||||
- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题
|
||||
- 修复删除订阅时未能实际删除相关文件
|
||||
- 修复 macOS 连接界面显示异常
|
||||
- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题
|
||||
- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题
|
||||
- 修复自动更新使版本回退的问题
|
||||
- 修复首页自定义卡片在切换轻量模式时失效
|
||||
- 修复悬浮跳转导航失效
|
||||
- 修复小键盘热键映射错误
|
||||
- 修复前端无法及时刷新操作状态
|
||||
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
|
||||
- 修复 Linux 系统主题切换不生效
|
||||
- 修复 `允许自动更新` 字段使手动订阅刷新失效
|
||||
- 修复轻量模式托盘状态不同步
|
||||
- 修复一键导入订阅导致应用卡死崩溃的问题
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.15**
|
||||
- 支持前端修改日志(最大文件大小、最大保留数量)
|
||||
- 新增链式代理图形化设置功能
|
||||
- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏)
|
||||
- 监听关机事件,自动关闭系统代理
|
||||
- 主界面“当前节点”卡片新增“延迟测试”按钮
|
||||
- 新增批量选择配置文件功能
|
||||
- Windows / Linux / MacOS 监听关机信号,优雅恢复网络设置
|
||||
- 新增本地备份功能
|
||||
- 主界面“当前节点”卡片新增自动延迟检测开关(默认关闭)
|
||||
- 允许独立控制订阅自动更新
|
||||
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
|
||||
- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏)
|
||||
- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志`
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 重构并简化服务模式启动检测流程,消除重复检测
|
||||
- 重构并简化窗口创建流程
|
||||
- 重构日志系统,单个日志默认最大 10 MB
|
||||
- 优化前端资源占用
|
||||
- 改进 macos 下系统代理设置的方法
|
||||
- 优化 TUN 模式可用性的判断
|
||||
- 移除流媒体检测的系统级提示(使用软件内通知)
|
||||
- 优化后端 i18n 资源占用
|
||||
- 改进 Linux 托盘支持并添加 `--no-tray` 选项
|
||||
- Linux 现在在新生成的配置中默认将 TUN 栈恢复为 mixed 模式
|
||||
- 为代理延迟测试的 URL 设置增加了保护以及添加了安全的备用 URL
|
||||
- 更新了 Wayland 合成器检测逻辑,从而在 Hyprland 会话中保留原生 Wayland 后端
|
||||
- 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核
|
||||
- 修改内核默认日志级别为 Info
|
||||
- 支持通过桌面快捷方式重新打开应用
|
||||
- 支持订阅界面输入链接后回车导入
|
||||
- 选择按延迟排序时每次延迟测试自动刷新节点顺序
|
||||
- 配置重载失败时自动重启核心
|
||||
- 启用 TUN 前等待服务就绪
|
||||
- 卸载 TUN 时会先关闭
|
||||
- 优化应用启动页
|
||||
- 优化首页当前节点对MATCH规则的支持
|
||||
- 允许在 `界面设置` 修改 `悬浮跳转导航延迟`
|
||||
- 添加热键绑定错误的提示信息
|
||||
- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题
|
||||
- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单
|
||||
- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书`
|
||||
- 允许设置 Mihomo 端口范围 1000(含) - 65536(含)
|
||||
|
||||
</details>
|
||||
|
||||
## v2.4.2
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- 增加托盘节点选择
|
||||
|
||||
### 🚀 性能优化
|
||||
|
||||
- 优化前端首页加载速度
|
||||
- 优化前端未使用 i18n 文件缓存
|
||||
- 优化后端内存占用
|
||||
- 优化后端启动速度
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复首页节点切换失效的问题
|
||||
- 修复和优化服务检查流程
|
||||
- 修复2.4.1引入的订阅地址重定向报错问题
|
||||
- 修复 rpm/deb 包名称问题
|
||||
- 修复托盘轻量模式状态检测异常
|
||||
- 修复通过 scheme 导入订阅崩溃
|
||||
- 修复单例检测实效
|
||||
- 修复启动阶段可能导致的无法连接内核
|
||||
- 修复导入订阅无法 Auth Basic
|
||||
|
||||
### 👙 界面样式
|
||||
|
||||
- 简化和改进代理设置样式
|
||||
|
||||
## v2.4.1
|
||||
|
||||
### 🏆 重大改进
|
||||
|
||||
- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.13**
|
||||
|
||||
### 🚀 性能优化
|
||||
|
||||
- 优化热键响应速度,提升快捷键操作体验
|
||||
- 改进服务管理响应性,减少系统服务操作等待时间
|
||||
- 提升文件和配置处理性能
|
||||
- 优化任务管理和日志记录效率
|
||||
- 优化异步内存管理,减少内存占用并提升多任务处理效率
|
||||
- 优化启动阶段初始化性能
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复应用在某些操作中可能出现的响应延迟问题
|
||||
- 修复任务管理中的潜在并发问题
|
||||
- 修复通过托盘重启应用无法恢复
|
||||
- 修复订阅在某些情况下无法导入
|
||||
- 修复无法新建订阅时使用远程链接
|
||||
- 修复卸载服务后的 tun 开关状态问题
|
||||
- 修复页面快速切换订阅时导致崩溃
|
||||
- 修复丢失工作目录时无法恢复环境
|
||||
- 修复从轻量模式恢复导致崩溃
|
||||
|
||||
### 👙 界面样式
|
||||
|
||||
- 统一代理设置样式
|
||||
|
||||
### 🗑️ 移除内容
|
||||
|
||||
- 移除启动阶段自动清理过期订阅
|
||||
|
||||
## 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
|
||||
|
||||
**发行代号:御**
|
||||
代号释义: 「御」,象征掌控与守护,寓意本次版本对系统稳定性、安全性与用户体验的全面驾驭与提升。
|
||||
尽管外部控制密钥已自动补全默认值且不允许为空。仍然推荐自行修改外部控制密钥。
|
||||
|
||||
尽管 `external-controller` 密钥现已自动补全默认值且不允许为空,**仍建议手动修改密钥以提高安全性**。
|
||||
#### ⚠️ 已知问题
|
||||
- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
|
||||
- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡
|
||||
- 窗口状态管理器已确定上游存在缺陷,暂时移除。当前不再内置窗口大小和位置记忆。
|
||||
- MacOS 下卸载服务后需手动重启软件才能与内核通信。
|
||||
|
||||
### ⚠️ 已知问题
|
||||
### 2.3.0 相对于 2.2.3
|
||||
#### 🐞 修复问题
|
||||
- 首页"代理模式"快速切换导致的卡死问题
|
||||
- 解锁测试报错信息
|
||||
- Macos 快捷键关闭窗口无法启用自动轻量模式
|
||||
- 静默启动异常窗口创建和关闭流程
|
||||
- Windows 错误的全局快捷键 `Ctrl+Q` 注册
|
||||
- Vless URL 解码时网络类型错误
|
||||
- 切换自定义代理地址导致系统代理状态异常
|
||||
- Macos TUN 默认无效网卡名称
|
||||
- 托盘更改订阅后 UI 不同步的问题
|
||||
- 修复提权漏洞,改用带认证的 IPC 通信
|
||||
- 编辑器中连字符问题
|
||||
- 安装服务模式后无法立即开启 TUN 模式
|
||||
- 同步更新多语言翻译
|
||||
- 修复 .window-state.json 无法删除的问题
|
||||
- 无法修改配置更新 HTTP 请求超时
|
||||
- 修复 getDelayFix 钩子问题
|
||||
- 使用外部扩展脚本覆写代理组时首页无法显示代理组
|
||||
- 导出诊断 Verge 版本与设置页面不同步
|
||||
- 切换语言时可能造成设置页面无法加载
|
||||
|
||||
- 仅在 Ubuntu 22.04/24.04、Fedora 41 的 **GNOME 桌面环境** 做过简单测试,不保证其他 Linux 发行版兼容,后续将逐步适配和优化。
|
||||
- macOS:
|
||||
- MacOS 下自动升级成功后请关闭程序等待 30 秒重启,因为 MacOS 的端口释放特性,卸载服务后需重启应用等 30 秒才能恢复内核通信。立即启动可能无法正常启动内核。
|
||||
- 墙贴主要为浅色,深色 Tray 图标存在闪烁问题;
|
||||
- 彩色 Tray 图标颜色偏淡;
|
||||
#### ✨ 新增功能
|
||||
- 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 字段。
|
||||
|
||||
- 已确认窗口状态管理器存在上游缺陷,已暂时移除窗口大小与位置记忆功能。
|
||||
#### 🚀 优化改进
|
||||
- 系统代理 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
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 修复首页“代理模式”快速切换导致的卡死问题
|
||||
- 修复 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 跨平台备份恢复限制
|
||||
|
||||
---
|
||||
#### 🗑️ 移除内容
|
||||
- 窗口状态管理器
|
||||
- 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
|
||||
|
||||
@@ -454,29 +148,23 @@
|
||||
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,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 从托盘退出
|
||||
@@ -493,7 +181,6 @@
|
||||
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
|
||||
|
||||
#### 新增了:
|
||||
|
||||
1. **首页**
|
||||
- 首页文本过长自动截断
|
||||
2. **轻量模式**
|
||||
@@ -510,7 +197,6 @@
|
||||
## 2.2.0(已下架不再提供)
|
||||
|
||||
#### 新增功能
|
||||
|
||||
1. **首页**
|
||||
- 新增首页功能,默认启动页面改为首页。
|
||||
- 首页流量图卡片显示上传/下载名称。
|
||||
@@ -542,7 +228,6 @@
|
||||
- 新增代理命令。
|
||||
|
||||
#### 修复
|
||||
|
||||
1. **系统**
|
||||
- 修复 Windows 热键崩溃。
|
||||
- 修复 macOS 无框标题。
|
||||
@@ -556,7 +241,6 @@
|
||||
- 修复构建失败问题。
|
||||
|
||||
#### 优化
|
||||
|
||||
1. **性能**
|
||||
- 重构后端,巨幅性能优化。
|
||||
- 优化首页组件性能。
|
||||
@@ -612,7 +296,7 @@
|
||||
|
||||
- 新增窗口状态实时监控与自动保存功能
|
||||
- 增强核心配置变更时的验证与错误处理机制
|
||||
- 支持通过环境变量 `CLASH_VERGE_REV_IP`自定义复制IP地址
|
||||
- 支持通过环境变量`CLASH_VERGE_REV_IP`自定义复制IP地址
|
||||
- 添加连接表列宽持久化设置与进程过滤功能
|
||||
- 新增代理组首字母导航与动态滚动定位功能
|
||||
- 实现连接追踪暂停/恢复功能
|
||||
@@ -933,7 +617,7 @@
|
||||
- 禁用部分 Webview2 快捷键
|
||||
- 热键配置新增连接符 + 号
|
||||
- 新增部分悬浮提示按钮,用于解释说明
|
||||
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||
- 设置页面右上角新增 TG 频道链接
|
||||
- 各种细节优化和界面性能优化
|
||||
|
||||
@@ -973,7 +657,7 @@
|
||||
- 禁用部分 Webview2 快捷键
|
||||
- 热键配置新增连接符 + 号
|
||||
- 新增部分悬浮提示按钮,用于解释说明
|
||||
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||
- 设置页面右上角新增 TG 频道链接
|
||||
- 各种细节优化和界面性能优化
|
||||
|
||||
@@ -1009,7 +693,7 @@
|
||||
- 禁用部分 Webview2 快捷键
|
||||
- 热键配置新增连接符 + 号
|
||||
- 新增部分悬浮提示按钮,用于解释说明
|
||||
- 当日志等级为 `Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
|
||||
- 设置页面右上角新增 TG 频道链接
|
||||
|
||||
### Bugs Fixes
|
||||
@@ -1183,7 +867,7 @@
|
||||
### Features
|
||||
|
||||
- 缓存代理组图标
|
||||
- 使用 `boa_engine` 代替 `rquickjs`
|
||||
- 使用`boa_engine` 代替 `rquickjs`
|
||||
- 支持 Linux armv7
|
||||
|
||||
### Bugs Fixes
|
||||
@@ -1248,7 +932,7 @@
|
||||
- 支持自定义托盘图标
|
||||
- 支持禁用代理组图标
|
||||
- 代理组显示当前代理
|
||||
- 修改 `打开面板` 快捷键为 `打开/关闭面板`
|
||||
- 修改 `打开面板` 快捷键为`打开/关闭面板`
|
||||
|
||||
---
|
||||
|
||||
@@ -1412,7 +1096,7 @@
|
||||
|
||||
### Bugs Fixes
|
||||
|
||||
- Windows 下更新时无法覆盖 `clash-verge-service.exe`的问题(需要卸载重装一次服务,下次更新生效)
|
||||
- Windows 下更新时无法覆盖`clash-verge-service.exe`的问题(需要卸载重装一次服务,下次更新生效)
|
||||
- 窗口最大化按钮变化问题
|
||||
- 窗口尺寸保存错误问题
|
||||
- 复制环境变量类型无法切换问题
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
files:
|
||||
- source: /src/locales/en.json
|
||||
translation: /src/locales
|
||||
multilingual: 1
|
||||
@@ -1,81 +0,0 @@
|
||||
# CONTRIBUTING — i18n
|
||||
|
||||
Thank you for considering contributing to our localization work — your help is appreciated.
|
||||
|
||||
Quick overview
|
||||
|
||||
- cvr-i18 is a CLI that helps manage simple top-level JSON locale files:
|
||||
- Detect duplicated top-level keys
|
||||
- Find keys missing versus a base file (default: en.json)
|
||||
- Export missing entries for translators
|
||||
- Reorder keys to match the base file for predictable diffs
|
||||
- Operate on a directory or a single file
|
||||
|
||||
Get the CLI (No binary provided yet)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
|
||||
cd clash-verge-rev-i18n-cli
|
||||
cargo install --path .
|
||||
# or
|
||||
cargo install --git https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
|
||||
```
|
||||
|
||||
Common commands
|
||||
|
||||
- Show help: `cvr-i18`
|
||||
- Directory (auto-detects `./locales` or `./src/locales`): `cvr-i18 -d /path/to/locales`
|
||||
- Check duplicates: `cvr-i18 -k`
|
||||
- Check missing keys: `cvr-i18 -m`
|
||||
- Export missing keys: `cvr-i18 -m -e ./exports`
|
||||
- Sort keys to base file: `cvr-i18 -s`
|
||||
- Use a base file: `cvr-i18 -b base.json`
|
||||
- Single file: `cvr-i18 -f locales/zh.json`
|
||||
|
||||
Options (short)
|
||||
|
||||
- `-d, --directory <DIR>`
|
||||
- `-f, --file <FILE>`
|
||||
- `-k, --duplicated-key`
|
||||
- `-m, --missing-key`
|
||||
- `-e, --export <DIR>`
|
||||
- `-s, --sort`
|
||||
- `-b, --base <FILE>`
|
||||
|
||||
Exit codes
|
||||
|
||||
- `0` — success (no issues)
|
||||
- `1` — issues found (duplicates/missing)
|
||||
- `2` — error (IO/parse/runtime)
|
||||
|
||||
How to contribute (recommended steps)
|
||||
|
||||
- Start small: fix typos, improve phrasing, or refine tone and consistency.
|
||||
- Run the CLI against your locale files to detect duplicates or missing keys.
|
||||
- Export starter JSONs for translators with `-m -e <DIR>`.
|
||||
- Prefer incremental PRs or draft PRs; leave a comment on the issue if you want guidance.
|
||||
- Open an issue to report missing strings, UI context, or localization bugs.
|
||||
- Add or improve docs and tests to make future contributions easier.
|
||||
|
||||
PR checklist
|
||||
|
||||
- Keep JSON files UTF-8 encoded.
|
||||
- Follow the repo’s locale file structure and naming conventions.
|
||||
- Reorder keys to match the base file (`-s`) for minimal diffs.
|
||||
- Test translations in a local dev build before opening a PR.
|
||||
- Reference related issues and explain any context for translations or changes.
|
||||
|
||||
Notes
|
||||
|
||||
- The tool expects simple top-level JSON key/value maps.
|
||||
- Exported JSONs are starter files for translators (fill in values, keep keys).
|
||||
- Sorting keeps diffs consistent and reviewable.
|
||||
|
||||
Repository
|
||||
https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
|
||||
|
||||
## Feedback & Contributions
|
||||
|
||||
- For tool usage issues or feedback: please open an Issue in the [repository](https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli) so it can be tracked and addressed.
|
||||
- For localization contributions (translations, fixes, context notes, etc.): submit a PR or Issue in this repository and include examples, context, and testing instructions when possible.
|
||||
- If you need help or a review, leave a comment on your submission requesting assistance.
|
||||
@@ -1,125 +0,0 @@
|
||||
<h1 align="center">
|
||||
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
|
||||
<br>
|
||||
Continuation of <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<h3 align="center">
|
||||
A Clash Meta GUI built with <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
|
||||
</h3>
|
||||
|
||||
<p align="center">
|
||||
Languages:
|
||||
<a href="../README.md">简体中文</a> ·
|
||||
<a href="./README_en.md">English</a> ·
|
||||
<a href="./README_es.md">Español</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="./README_ja.md">日本語</a> ·
|
||||
<a href="./README_ko.md">한국어</a>
|
||||
</p>
|
||||
|
||||
## Preview
|
||||
|
||||
| Dark | Light |
|
||||
| ----------------------------------- | ------------------------------------- |
|
||||
|  |  |
|
||||
|
||||
## Install
|
||||
|
||||
Visit the [Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the installer that matches your platform.<br>
|
||||
We provide packages for Windows (x64/x86), Linux (x64/arm64), and macOS 10.15+ (Intel/Apple).
|
||||
|
||||
#### Choosing a Release Channel
|
||||
|
||||
| Channel | Description | Link |
|
||||
| :---------- | :-------------------------------------------------------------------- | :------------------------------------------------------------------------------------- |
|
||||
| Stable | Official builds with high reliability, ideal for daily use. | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
|
||||
| Alpha (EOL) | Legacy builds used to validate the publish pipeline. | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
|
||||
| AutoBuild | Rolling builds for testing and feedback. Expect experimental changes. | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
|
||||
|
||||
#### Installation Guides & FAQ
|
||||
|
||||
Read the [project documentation](https://clash-verge-rev.github.io/) for install steps, troubleshooting, and frequently asked questions.
|
||||
|
||||
---
|
||||
|
||||
### Telegram Channel
|
||||
|
||||
Join [@clash_verge_rev](https://t.me/clash_verge_re) for update announcements.
|
||||
|
||||
## Promotion
|
||||
|
||||
#### [Doggygo VPN — Performance-oriented global accelerator](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
- High-performance overseas network service with free trials, discounted plans, streaming unlocks, and first-class Hysteria protocol support.
|
||||
- Register through the exclusive Clash Verge link to get a 3-day trial with 1 GB of traffic per day: [Sign up](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
- Exclusive 20% off coupon for Clash Verge users: `verge20` (limited to 500 uses)
|
||||
- Discounted bundle from ¥15.8 per month for 160 GB, plus an additional 20% off for yearly billing
|
||||
- Operated by an overseas team with reliable service and up to 50% revenue share
|
||||
- Load-balanced clusters with high-speed dedicated routes (compatible with legacy clients), exceptionally low latency, smooth 4K playback
|
||||
- First global provider to support the `Hysteria2` protocol—perfect fit for the Clash Verge client
|
||||
- Supports streaming services and ChatGPT access
|
||||
- Official site: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
#### Build Infrastructure Sponsor — [YXVM Dedicated Servers](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
Our builds and releases run on YXVM dedicated servers that deliver premium resources, strong performance, and high-speed networking. If downloads feel fast and usage feels snappy, it is thanks to robust hardware.
|
||||
|
||||
🧩 Highlights of YXVM Dedicated Servers:
|
||||
|
||||
- 🌎 Optimized global routes for dramatically faster downloads
|
||||
- 🔧 Bare-metal resources instead of shared VPS capacity for maximum performance
|
||||
- 🧠 Great for proxy workloads, hosting web/CDN services, CI/CD pipelines, or any high-load tasks
|
||||
- 💡 Ready to use instantly with multiple datacenter options, including CN2 and IEPL
|
||||
- 📦 The configuration used by this project is on sale—feel free to get the same setup
|
||||
- 🎯 Want the same build environment? [Order a YXVM server today](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
## Features
|
||||
|
||||
- Built on high-performance Rust with the Tauri 2 framework
|
||||
- Ships with the embedded [Clash.Meta (mihomo)](https://github.com/MetaCubeX/mihomo) core and supports switching to the `Alpha` channel
|
||||
- Clean, polished UI with theme color controls, proxy group/tray icons, and `CSS Injection`
|
||||
- Enhanced profile management (Merge and Script helpers) with configuration syntax hints
|
||||
- System proxy controls, guard mode, and `TUN` (virtual network adapter) support
|
||||
- Visual editors for nodes and rules
|
||||
- WebDAV-based backup and sync for configurations
|
||||
|
||||
### FAQ
|
||||
|
||||
See the [FAQ page](https://clash-verge-rev.github.io/faq/windows.html) for platform-specific guidance.
|
||||
|
||||
### Donation
|
||||
|
||||
[Support Clash Verge Rev development](https://github.com/sponsors/clash-verge-rev)
|
||||
|
||||
## Development
|
||||
|
||||
See [CONTRIBUTING.md](../CONTRIBUTING.md) for detailed contribution guidelines.
|
||||
|
||||
After installing all **Tauri** prerequisites, run the development shell with:
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
pnpm run prebuild
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Issues and pull requests are welcome!
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
Clash Verge Rev builds on or draws inspiration from these projects:
|
||||
|
||||
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): A Tauri-based Clash GUI for Windows, macOS, and Linux.
|
||||
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, more secure desktop apps with a web frontend.
|
||||
- [Dreamacro/clash](https://github.com/Dreamacro/clash): A rule-based tunnel written in Go.
|
||||
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): A rule-based tunnel written in Go.
|
||||
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): A Clash GUI for Windows and macOS.
|
||||
- [vitejs/vite](https://github.com/vitejs/vite): Next-generation frontend tooling with blazing-fast DX.
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0 License. See the [license file](../LICENSE) for details.
|
||||
@@ -1,125 +0,0 @@
|
||||
<h1 align="center">
|
||||
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
|
||||
<br>
|
||||
Continuación de <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<h3 align="center">
|
||||
Una interfaz gráfica para Clash Meta construida con <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
|
||||
</h3>
|
||||
|
||||
<p align="center">
|
||||
Idiomas:
|
||||
<a href="../README.md">简体中文</a> ·
|
||||
<a href="./README_en.md">English</a> ·
|
||||
<a href="./README_es.md">Español</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="./README_ja.md">日本語</a> ·
|
||||
<a href="./README_ko.md">한국어</a>
|
||||
</p>
|
||||
|
||||
## Vista previa
|
||||
|
||||
| Oscuro | Claro |
|
||||
| ----------------------------------- | ----------------------------------- |
|
||||
|  |  |
|
||||
|
||||
## Instalación
|
||||
|
||||
Visita la [página de lanzamientos](https://github.com/clash-verge-rev/clash-verge-rev/releases) y descarga el instalador que corresponda a tu plataforma.<br>
|
||||
Ofrecemos paquetes para Windows (x64/x86), Linux (x64/arm64) y macOS 10.15+ (Intel/Apple).
|
||||
|
||||
#### Cómo elegir el canal de lanzamiento
|
||||
|
||||
| Canal | Descripción | Enlace |
|
||||
| :---------- | :----------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- |
|
||||
| Stable | Compilaciones oficiales de alta fiabilidad; ideales para el uso diario. | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
|
||||
| Alpha (EOL) | Compilaciones heredadas usadas para validar el flujo de publicación. | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
|
||||
| AutoBuild | Compilaciones continuas para pruebas y retroalimentación. Espera cambios beta. | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
|
||||
|
||||
#### Guías de instalación y preguntas frecuentes
|
||||
|
||||
Consulta la [documentación del proyecto](https://clash-verge-rev.github.io/) para encontrar los pasos de instalación, solución de problemas y preguntas frecuentes.
|
||||
|
||||
---
|
||||
|
||||
### Canal de Telegram
|
||||
|
||||
Únete a [@clash_verge_rev](https://t.me/clash_verge_re) para enterarte de las novedades.
|
||||
|
||||
## Promociones
|
||||
|
||||
#### [Doggygo VPN — Acelerador global orientado al rendimiento](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
- Servicio internacional de alto rendimiento con prueba gratuita, planes con descuento, desbloqueo de streaming y soporte de protocolo Hysteria de primera clase.
|
||||
- Regístrate mediante el enlace exclusivo de Clash Verge y obtén una prueba de 3 días con 1 GB de tráfico diario: [Regístrate](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
- Cupón exclusivo de 20% de descuento para usuarios de Clash Verge: `verge20` (limitado a 500 usos)
|
||||
- Plan promocional desde ¥15.8 al mes con 160 GB, más 20% de descuento adicional por pago anual
|
||||
- Equipo ubicado en el extranjero para un servicio confiable, con hasta 50% de comisión compartida
|
||||
- Clústeres balanceados con rutas dedicadas de alta velocidad (compatibles con clientes antiguos), latencia extremadamente baja, reproducción 4K sin interrupciones
|
||||
- Primer proveedor global que soporta el protocolo `Hysteria2`, ideal para el cliente Clash Verge
|
||||
- Desbloquea servicios de streaming y acceso a ChatGPT
|
||||
- Sitio oficial: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
#### Patrocinador de la infraestructura de compilación — [Servidores dedicados YXVM](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
Las compilaciones y lanzamientos del proyecto se ejecutan en servidores dedicados de YXVM, que proporcionan recursos premium, alto rendimiento y redes de alta velocidad. Si las descargas son rápidas y el uso es fluido, es gracias a este hardware robusto.
|
||||
|
||||
🧩 Ventajas de los servidores dedicados YXVM:
|
||||
|
||||
- 🌎 Rutas globales optimizadas para descargas significativamente más rápidas
|
||||
- 🔧 Recursos bare-metal, en lugar de VPS compartidos, para obtener el máximo rendimiento
|
||||
- 🧠 Ideales para proxys, alojamiento de sitios web/CDN, pipelines de CI/CD o cualquier carga elevada
|
||||
- 💡 Listos para usar al instante, con múltiples centros de datos disponibles (incluidos CN2 e IEPL)
|
||||
- 📦 La misma configuración utilizada por este proyecto está disponible para su compra
|
||||
- 🎯 ¿Quieres el mismo entorno de compilación? [Solicita un servidor YXVM hoy](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
## Funciones
|
||||
|
||||
- Basado en Rust de alto rendimiento y en el framework Tauri 2
|
||||
- Incluye el núcleo integrado [Clash.Meta (mihomo)](https://github.com/MetaCubeX/mihomo) y permite cambiar al canal `Alpha`
|
||||
- Interfaz limpia y elegante con controles de color de tema, iconos de grupos proxy/bandeja y `CSS Injection`
|
||||
- Gestión avanzada de perfiles (herramientas Merge y Script) con sugerencias de sintaxis para configuraciones
|
||||
- Control del proxy del sistema, modo guardián y soporte para `TUN` (adaptador de red virtual)
|
||||
- Editores visuales para nodos y reglas
|
||||
- Copias de seguridad y sincronización mediante WebDAV
|
||||
|
||||
### Preguntas frecuentes
|
||||
|
||||
Visita la [página de FAQ](https://clash-verge-rev.github.io/faq/windows.html) para obtener instrucciones específicas por plataforma.
|
||||
|
||||
### Donaciones
|
||||
|
||||
[Apoya el desarrollo de Clash Verge Rev](https://github.com/sponsors/clash-verge-rev)
|
||||
|
||||
## Desarrollo
|
||||
|
||||
Consulta [CONTRIBUTING.md](../CONTRIBUTING.md) para conocer las pautas de contribución.
|
||||
|
||||
Después de instalar todos los requisitos de **Tauri**, ejecuta el entorno de desarrollo con:
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
pnpm run prebuild
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Contribuciones
|
||||
|
||||
Se agradecen los issues y pull requests.
|
||||
|
||||
## Agradecimientos
|
||||
|
||||
Clash Verge Rev se basa en, o se inspira en, los siguientes proyectos:
|
||||
|
||||
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): Interfaz gráfica para Clash basada en Tauri. Compatible con Windows, macOS y Linux.
|
||||
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Construye aplicaciones de escritorio más pequeñas, rápidas y seguras con un frontend web.
|
||||
- [Dreamacro/clash](https://github.com/Dreamacro/clash): Túnel basado en reglas escrito en Go.
|
||||
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): Túnel basado en reglas escrito en Go.
|
||||
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): Interfaz de Clash para Windows y macOS.
|
||||
- [vitejs/vite](https://github.com/vitejs/vite): Herramientas de frontend de nueva generación con una experiencia rapidísima.
|
||||
|
||||
## Licencia
|
||||
|
||||
Licencia GPL-3.0. Consulta el [archivo de licencia](../LICENSE) para más detalles.
|
||||
@@ -1,125 +0,0 @@
|
||||
<h1 align="center">
|
||||
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
|
||||
<br>
|
||||
<a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a> の継続プロジェクト
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<h3 align="center">
|
||||
<a href="https://github.com/tauri-apps/tauri">Tauri</a> で構築された Clash Meta GUI。
|
||||
</h3>
|
||||
|
||||
<p align="center">
|
||||
言語:
|
||||
<a href="../README.md">简体中文</a> ·
|
||||
<a href="./README_en.md">English</a> ·
|
||||
<a href="./README_es.md">Español</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="./README_ja.md">日本語</a> ·
|
||||
<a href="./README_ko.md">한국어</a>
|
||||
</p>
|
||||
|
||||
## プレビュー
|
||||
|
||||
| ダーク | ライト |
|
||||
| --------------------------------------- | ---------------------------------------- |
|
||||
|  |  |
|
||||
|
||||
## インストール
|
||||
|
||||
[リリースページ](https://github.com/clash-verge-rev/clash-verge-rev/releases) から、ご利用のプラットフォームに対応したインストーラーをダウンロードしてください。<br>
|
||||
Windows (x64/x86)、Linux (x64/arm64)、macOS 10.15+ (Intel/Apple) をサポートしています。
|
||||
|
||||
#### リリースチャンネルの選び方
|
||||
|
||||
| チャンネル | 説明 | リンク |
|
||||
| :---------- | :--------------------------------------------------------------- | :------------------------------------------------------------------------------------- |
|
||||
| Stable | 安定版。信頼性が高く、日常利用に最適です。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
|
||||
| Alpha (EOL) | 公開フローの検証に使用した旧テスト版。 | [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) |
|
||||
|
||||
#### インストール手順と FAQ
|
||||
|
||||
詳しい導入手順やトラブルシュートは [ドキュメントサイト](https://clash-verge-rev.github.io/) を参照してください。
|
||||
|
||||
---
|
||||
|
||||
### Telegram チャンネル
|
||||
|
||||
更新情報は [@clash_verge_rev](https://t.me/clash_verge_re) をフォローしてください。
|
||||
|
||||
## プロモーション
|
||||
|
||||
#### [Doggygo VPN — 高性能グローバルアクセラレータ](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
- 無料トライアル、割引プラン、ストリーミング解放、世界初の Hysteria プロトコル対応を備えた高性能海外ネットワークサービス。
|
||||
- Clash Verge 専用リンクから登録すると、3 日間・1 日 1 GB の無料体験が利用できます。 [登録はこちら](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
- Clash Verge 利用者限定 20% オフクーポン: `verge20`(先着 500 名)
|
||||
- 月額 15.8 元で 160 GB を利用できるプラン、年額契約ならさらに 20% オフ
|
||||
- 海外チーム運営による高信頼サービス、収益シェアは最大 50%
|
||||
- 負荷分散クラスタと高速専用回線(旧クライアント互換)、極低レイテンシで 4K も快適
|
||||
- 世界初の `Hysteria2` プロトコル対応。Clash Verge クライアントとの相性抜群
|
||||
- ストリーミングおよび ChatGPT の利用にも対応
|
||||
- 公式サイト: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
#### ビルド環境スポンサー — [YXVM 専用サーバー](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
本プロジェクトのビルドとリリースは、YXVM の専用サーバーによって支えられています。高速ダウンロードや快適な操作性は、強力なハードウェアがあってこそです。
|
||||
|
||||
🧩 YXVM 専用サーバーの特長:
|
||||
|
||||
- 🌎 最適化されたグローバル回線で圧倒的なダウンロード速度
|
||||
- 🔧 VPS とは異なるベアメタル資源で最高性能を発揮
|
||||
- 🧠 プロキシ運用、Web/CDN ホスティング、CI/CD など高負荷ワークロードに最適
|
||||
- 💡 複数データセンターから即時利用可能。CN2 や IEPL も選択可
|
||||
- 📦 本プロジェクトが使用している構成も販売中。同じ環境を入手できます
|
||||
- 🎯 同じビルド体験をしたい方は [今すぐ YXVM サーバーを注文](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
## 機能
|
||||
|
||||
- 高性能な Rust と Tauri 2 フレームワークに基づくデスクトップアプリ
|
||||
- 組み込みの [Clash.Meta (mihomo)](https://github.com/MetaCubeX/mihomo) コアを搭載し、`Alpha` チャンネルへの切り替えも可能
|
||||
- テーマカラーやプロキシグループ/トレイアイコン、`CSS Injection` をカスタマイズできる洗練された UI
|
||||
- 設定ファイルの管理および拡張(Merge・Script 支援)、構成シンタックスヒントを提供
|
||||
- システムプロキシ制御、ガード機能、`TUN`(仮想ネットワークアダプタ)モード
|
||||
- ノードとルールのビジュアルエディタ
|
||||
- WebDAV による設定のバックアップと同期
|
||||
|
||||
### FAQ
|
||||
|
||||
プラットフォーム別の案内は [FAQ ページ](https://clash-verge-rev.github.io/faq/windows.html) を参照してください。
|
||||
|
||||
### 寄付
|
||||
|
||||
[Clash Verge Rev の開発を支援する](https://github.com/sponsors/clash-verge-rev)
|
||||
|
||||
## 開発
|
||||
|
||||
詳細な貢献ガイドは [CONTRIBUTING.md](../CONTRIBUTING.md) をご覧ください。
|
||||
|
||||
**Tauri** の前提条件を整えたら、以下のコマンドで開発サーバーを起動できます:
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
pnpm run prebuild
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## コントリビューション
|
||||
|
||||
Issue や Pull Request を歓迎します。
|
||||
|
||||
## 謝辞
|
||||
|
||||
Clash Verge Rev は、以下のプロジェクトに影響を受けています。
|
||||
|
||||
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): Tauri ベースの Clash GUI。Windows / macOS / Linux に対応。
|
||||
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Web フロントエンドで小型・高速・安全なデスクトップアプリを構築するためのフレームワーク。
|
||||
- [Dreamacro/clash](https://github.com/Dreamacro/clash): Go 製のルールベーストンネル。
|
||||
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): Go 製のルールベーストンネル。
|
||||
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): Windows / macOS 向けの Clash GUI。
|
||||
- [vitejs/vite](https://github.com/vitejs/vite): 次世代のフロントエンドツール群。高速な開発体験を提供。
|
||||
|
||||
## ライセンス
|
||||
|
||||
GPL-3.0 ライセンス。詳細は [LICENSE](../LICENSE) を参照してください。
|
||||
@@ -1,125 +0,0 @@
|
||||
<h1 align="center">
|
||||
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
|
||||
<br>
|
||||
<a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>의 후속 프로젝트
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<h3 align="center">
|
||||
<a href="https://github.com/tauri-apps/tauri">Tauri</a>로 제작된 Clash Meta GUI.
|
||||
</h3>
|
||||
|
||||
<p align="center">
|
||||
언어:
|
||||
<a href="../README.md">简体中文</a> ·
|
||||
<a href="./README_en.md">English</a> ·
|
||||
<a href="./README_es.md">Español</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="./README_ja.md">日本語</a> ·
|
||||
<a href="./README_ko.md">한국어</a>
|
||||
</p>
|
||||
|
||||
## 미리보기
|
||||
|
||||
| 다크 | 라이트 |
|
||||
| ------------------------------------ | --------------------------------------- |
|
||||
|  |  |
|
||||
|
||||
## 설치
|
||||
|
||||
[릴리스 페이지](https://github.com/clash-verge-rev/clash-verge-rev/releases)에서 사용 중인 플랫폼에 맞는 설치 프로그램을 다운로드하세요.<br>
|
||||
Windows (x64/x86), Linux (x64/arm64), macOS 10.15+ (Intel/Apple)을 지원합니다.
|
||||
|
||||
#### 릴리스 채널 선택
|
||||
|
||||
| 채널 | 설명 | 링크 |
|
||||
| :---------- | :----------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- |
|
||||
| Stable | 안정 릴리스. 신뢰성이 높아 일상 사용에 적합합니다. | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
|
||||
| Alpha (EOL) | 퍼블리시 파이프라인 검증에 사용되었던 구 테스트 채널입니다. | [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) |
|
||||
|
||||
#### 설치 가이드 및 FAQ
|
||||
|
||||
설치 방법, 트러블슈팅, 자주 묻는 질문은 [프로젝트 문서](https://clash-verge-rev.github.io/)를 참고하세요.
|
||||
|
||||
---
|
||||
|
||||
### 텔레그램 채널
|
||||
|
||||
업데이트 공지는 [@clash_verge_rev](https://t.me/clash_verge_re)에서 확인하세요.
|
||||
|
||||
## 프로모션
|
||||
|
||||
#### [Doggygo VPN — 고성능 글로벌 가속기](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
- 무료 체험, 할인 요금제, 스트리밍 해제, 선도적인 Hysteria 프로토콜 지원을 갖춘 고성능 해외 네트워크 서비스
|
||||
- Clash Verge 전용 초대 링크로 가입 시 3일간 매일 1GB 무료 체험 제공: [가입하기](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
- Clash Verge 전용 20% 할인 코드: `verge20` (선착순 500회)
|
||||
- 월 15.8위안부터 160GB 제공, 연간 결제 시 추가 20% 할인
|
||||
- 해외 팀 운영, 높은 신뢰성, 최대 50% 커미션
|
||||
- 로드밸런싱 클러스터, 고속 전용 회선(구 클라이언트 호환), 매우 낮은 지연, 4K도 쾌적
|
||||
- 세계 최초 `Hysteria2` 프로토콜 지원 — Clash Verge 클라이언트와 최적의 궁합
|
||||
- 스트리밍 및 ChatGPT 접근 지원
|
||||
- 공식 사이트: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
#### 빌드 인프라 스폰서 — [YXVM 전용 서버](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
본 프로젝트의 빌드 및 릴리스는 YXVM 전용 서버에서 구동됩니다. 빠른 다운로드와 경쾌한 사용감은 탄탄한 하드웨어 덕분입니다.
|
||||
|
||||
🧩 YXVM 전용 서버 하이라이트:
|
||||
|
||||
- 🌎 최적화된 글로벌 라우팅으로 대폭 빨라진 다운로드
|
||||
- 🔧 공유 VPS가 아닌 베어메탈 자원으로 최대 성능 제공
|
||||
- 🧠 프록시 워크로드, Web/CDN 호스팅, CI/CD, 고부하 작업에 적합
|
||||
- 💡 CN2 / IEPL 등 다양한 데이터센터 옵션, 즉시 사용 가능
|
||||
- 📦 본 프로젝트가 사용하는 구성도 판매 중 — 동일한 환경을 사용할 수 있습니다
|
||||
- 🎯 동일한 빌드 환경이 필요하다면 [지금 YXVM 서버 주문](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
## 기능
|
||||
|
||||
- 고성능 Rust와 Tauri 2 프레임워크 기반 데스크톱 앱
|
||||
- 내장 [Clash.Meta (mihomo)](https://github.com/MetaCubeX/mihomo) 코어, `Alpha` 채널 전환 지원
|
||||
- 테마 색상, 프록시 그룹/트레이 아이콘, `CSS Injection` 등 세련된 UI 커스터마이징
|
||||
- 프로필 관리(병합 및 스크립트 보조), 구성 문법 힌트 제공
|
||||
- 시스템 프록시 제어, 가드 모드, `TUN`(가상 네트워크 어댑터) 지원
|
||||
- 노드/규칙 시각 편집기
|
||||
- WebDAV 기반 설정 백업 및 동기화
|
||||
|
||||
### FAQ
|
||||
|
||||
플랫폼별 가이드는 [FAQ 페이지](https://clash-verge-rev.github.io/faq/windows.html)에서 확인하세요.
|
||||
|
||||
### 후원
|
||||
|
||||
[Clash Verge Rev 개발 후원](https://github.com/sponsors/clash-verge-rev)
|
||||
|
||||
## 개발
|
||||
|
||||
자세한 기여 가이드는 [CONTRIBUTING.md](../CONTRIBUTING.md)를 참고하세요.
|
||||
|
||||
**Tauri** 필수 구성 요소를 설치한 뒤 아래 명령으로 개발 서버를 실행합니다:
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
pnpm run prebuild
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 기여
|
||||
|
||||
Issue와 Pull Request를 환영합니다!
|
||||
|
||||
## 감사의 말
|
||||
|
||||
Clash Verge Rev는 다음 프로젝트에 기반하거나 영향을 받았습니다:
|
||||
|
||||
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): Windows / macOS / Linux용 Tauri 기반 Clash GUI
|
||||
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): 웹 프론트엔드로 더 작고 빠르고 안전한 데스크톱 앱을 빌드
|
||||
- [Dreamacro/clash](https://github.com/Dreamacro/clash): Go로 작성된 규칙 기반 터널
|
||||
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): Go로 작성된 규칙 기반 터널
|
||||
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): Windows / macOS용 Clash GUI
|
||||
- [vitejs/vite](https://github.com/vitejs/vite): 차세대 프론트엔드 툴링, 매우 빠른 DX
|
||||
|
||||
## 라이선스
|
||||
|
||||
GPL-3.0 라이선스. 자세한 내용은 [LICENSE](../LICENSE)를 참고하세요.
|
||||
@@ -1,121 +0,0 @@
|
||||
<h1 align="center">
|
||||
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
|
||||
<br>
|
||||
Continuation of <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<h3 align="center">
|
||||
Clash Meta GUI базируется на <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
|
||||
</h3>
|
||||
|
||||
<p align="center">
|
||||
Языки:
|
||||
<a href="../README.md">简体中文</a> ·
|
||||
<a href="./README_en.md">English</a> ·
|
||||
<a href="./README_es.md">Español</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="./README_ja.md">日本語</a> ·
|
||||
<a href="./README_ko.md">한국어</a>
|
||||
</p>
|
||||
## Предпросмотр
|
||||
|
||||
| Тёмная тема | Светлая тема |
|
||||
| ---------------------------------- | ------------------------------------ |
|
||||
|  |  |
|
||||
|
||||
## Установка
|
||||
|
||||
Пожалуйста, перейдите на страницу релизов, чтобы скачать соответствующий установочный пакет: [Страница релизов](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
|
||||
Перейти на [Страницу релизов](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
|
||||
Поддержка Windows (x64/x86), Linux (x64/arm64) и macOS 10.15+ (intel/apple).
|
||||
|
||||
#### Как выбрать дистрибутив?
|
||||
|
||||
| Версия | Характеристики | Ссылка |
|
||||
| :-------------------- | :------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------- |
|
||||
| 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/)
|
||||
|
||||
---
|
||||
|
||||
### TG канал: [@clash_verge_rev](https://t.me/clash_verge_re)
|
||||
|
||||
## Продвижение
|
||||
|
||||
#### [Doggygo VPN —— технический VPN-сервис (айрпорт)](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
- Высокопроизводительный иностранный VPN-сервис (айрпорт) с бесплатным пробным периодом, выгодными тарифами, возможностью разблокировки потокового ТВ и первым в мире поддержкой протокола Hysteria.
|
||||
- Зарегистрируйтесь по эксклюзивной ссылке Clash Verge и получите 3 дня бесплатного использования, 1 Гб трафика в день: [регистрация](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
- Эксклюзивный промо-код на скидку 20% для Clash Verge: verge20 (только 500 штук)
|
||||
- Специальный тарифный план всего за 15,8 юаней в месяц, 160 Гб трафика, скидка 20% при оплате за год
|
||||
- Команда за рубежом, без риска побега, до 50% кэшбэка
|
||||
- Архитектура с балансировкойнагрузки, высокоскоростная выделенная линия (совместима со старыми клиентами), чрезвычайно низкая задержка, без проблем в часы пик, 4K видео загружается мгновенно
|
||||
- Первый в мире VPN-сервис (айрпорт), поддерживающий протокол Hysteria, теперь доступен более быстрый протокол `Hysteria2` (лучшее сочетание с клиентом Clash Verge)
|
||||
- Разблокировка потоковые сервисы и ChatGPT
|
||||
- Официальный сайт: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
#### Среда сборки и публикации этого проекта полностью поддерживается выделенным сервером [YXVM](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
Благодарим вас за предоставление надежной бэкэнд-среды с эксклюзивными ресурсами, высокой производительностью и высокоскоростной сетью. Если вы считаете, что загрузка файлов происходит достаточно быстро, а использование — достаточно плавно, то это потому, что мы используем серверы высшего уровня!
|
||||
|
||||
🧩 Преимущества выделенного сервера YXVM:
|
||||
|
||||
- 🌎 Премиум-сеть с оптимизацией обратного пути для молниеносной скорости загрузки
|
||||
- 🔧 Выделенные физические серверные ресурсы, не имеющие аналогов среди VPS, обеспечивающие максимальную производительность
|
||||
- 🧠 Идеально подходит для прокси, хостинга веб-сайтов/CDN-сайтов, рабочих процессов CI/CD или любых приложений с высокой нагрузкой
|
||||
- 💡 Поддержка использования сразу после включения, выбор нескольких дата-центров, CN2 / IEPL на выбор
|
||||
- 📦 Эта конфигурация в настоящее время доступна для покупки — не стесняйтесь заказывать ту же модель!
|
||||
- 🎯 Хотите попробовать такую же сборку? [Закажите выделенный сервер YXVM прямо сейчас!](https://yxvm.com/aff.php?aff=827)
|
||||
|
||||
## Фичи
|
||||
|
||||
- Основан на произвоительном Rust и фреймворке Tauri 2
|
||||
- Имеет встроенное ядро [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) и поддерживает переключение на ядро версии `Alpha`.
|
||||
- Чистый и эстетичный пользовательский интерфейс, поддержка настраиваемых цветов темы, значков прокси-группы/системного трея и `CSS Injection`。
|
||||
- Управление и расширение конфигурационными файлами (Merge и Script), подсказки по синтаксису конфигурационных файлов.
|
||||
- Режим системного прокси и защита, `TUN (Tunneled Network Interface)` режим.
|
||||
- Визуальное редактирование узлов и правил
|
||||
- Резервное копирование и синхронизация конфигурации WebDAV
|
||||
|
||||
### FAQ
|
||||
|
||||
Смотрите [Страница часто задаваемых вопросов](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
### Донат
|
||||
|
||||
[Поддержите развитие Clash Verge Rev](https://github.com/sponsors/clash-verge-rev)
|
||||
|
||||
## Разработка
|
||||
|
||||
Дополнительные сведения смотреть в файле [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
|
||||
Для запуска сервера разработки выполните следующие команды после установки всех необходимых компонентов для **Tauri**:
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
pnpm run prebuild
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Вклад
|
||||
|
||||
Обращения и запросы на PR приветствуются!
|
||||
|
||||
## Благодарность
|
||||
|
||||
Clash Verge rev был основан на этих проектах или вдохновлен ими, и так далее:
|
||||
|
||||
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): Графический интерфейс Clash на основе tauri. Поддерживает Windows, macOS и Linux.
|
||||
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Создавайте более компактные, быстрые и безопасные настольные приложения с веб-интерфейсом.
|
||||
- [Dreamacro/clash](https://github.com/Dreamacro/clash): Правило-ориентированный туннель на Go.
|
||||
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): Правило-ориентированный туннель на Go.
|
||||
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): Графический интерфейс пользователя для Windows/macOS на основе Clash.
|
||||
- [vitejs/vite](https://github.com/vitejs/vite): Инструменты нового поколения для фронтенда. Они быстрые!
|
||||
|
||||
## Лицензия
|
||||
|
||||
GPL-3.0 License. Подробности смотрите в [Лицензии](../LICENSE).
|
||||
@@ -1,135 +0,0 @@
|
||||
import eslintJS from "@eslint/js";
|
||||
import eslintReact from "@eslint-react/eslint-plugin";
|
||||
import { defineConfig } from "eslint/config";
|
||||
import configPrettier from "eslint-config-prettier";
|
||||
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
|
||||
import pluginImportX from "eslint-plugin-import-x";
|
||||
import pluginPrettier from "eslint-plugin-prettier";
|
||||
import pluginReactHooks from "eslint-plugin-react-hooks";
|
||||
import pluginReactRefresh from "eslint-plugin-react-refresh";
|
||||
import pluginUnusedImports from "eslint-plugin-unused-imports";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||
|
||||
plugins: {
|
||||
js: eslintJS,
|
||||
"react-hooks": pluginReactHooks,
|
||||
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
|
||||
"import-x": pluginImportX,
|
||||
"react-refresh": pluginReactRefresh,
|
||||
"unused-imports": pluginUnusedImports,
|
||||
prettier: pluginPrettier,
|
||||
},
|
||||
|
||||
extends: [
|
||||
eslintJS.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
eslintReact.configs["recommended-typescript"],
|
||||
configPrettier,
|
||||
],
|
||||
|
||||
languageOptions: {
|
||||
globals: globals.browser,
|
||||
},
|
||||
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
"import-x/resolver-next": [
|
||||
createTypeScriptImportResolver({
|
||||
project: "./tsconfig.json",
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
rules: {
|
||||
// React
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
|
||||
"@eslint-react/no-forward-ref": "off",
|
||||
|
||||
// React performance and production quality rules
|
||||
"@eslint-react/no-array-index-key": "warn",
|
||||
"@eslint-react/no-children-count": "error",
|
||||
"@eslint-react/no-children-for-each": "error",
|
||||
"@eslint-react/no-children-map": "error",
|
||||
"@eslint-react/no-children-only": "error",
|
||||
"@eslint-react/no-children-prop": "error",
|
||||
"@eslint-react/no-children-to-array": "error",
|
||||
"@eslint-react/no-class-component": "error",
|
||||
"@eslint-react/no-clone-element": "error",
|
||||
"@eslint-react/no-create-ref": "error",
|
||||
"@eslint-react/no-default-props": "error",
|
||||
"@eslint-react/no-direct-mutation-state": "error",
|
||||
"@eslint-react/no-implicit-key": "error",
|
||||
"@eslint-react/no-prop-types": "error",
|
||||
"@eslint-react/no-set-state-in-component-did-mount": "error",
|
||||
"@eslint-react/no-set-state-in-component-did-update": "error",
|
||||
"@eslint-react/no-set-state-in-component-will-update": "error",
|
||||
"@eslint-react/no-string-refs": "error",
|
||||
"@eslint-react/no-unstable-context-value": "warn",
|
||||
"@eslint-react/no-unstable-default-props": "warn",
|
||||
"@eslint-react/no-unused-class-component-members": "error",
|
||||
"@eslint-react/no-unused-state": "error",
|
||||
"@eslint-react/no-useless-fragment": "warn",
|
||||
"@eslint-react/prefer-destructuring-assignment": "warn",
|
||||
|
||||
// TypeScript
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
// unused-imports 代替 no-unused-vars
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
vars: "all",
|
||||
varsIgnorePattern: "^_",
|
||||
args: "after-used",
|
||||
argsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^ignore",
|
||||
},
|
||||
],
|
||||
|
||||
// Import
|
||||
"import-x/no-unresolved": "error",
|
||||
"import-x/order": [
|
||||
"warn",
|
||||
{
|
||||
groups: [
|
||||
"builtin",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index",
|
||||
],
|
||||
"newlines-between": "always",
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
caseInsensitive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// 其他常见
|
||||
"prefer-const": "warn",
|
||||
"no-case-declarations": "error",
|
||||
"no-fallthrough": "error",
|
||||
"no-empty": ["warn", { allowEmptyCatch: true }],
|
||||
|
||||
// Prettier 格式化问题
|
||||
"prettier/prettier": "warn",
|
||||
},
|
||||
},
|
||||
]);
|
||||
+71
-92
@@ -1,136 +1,115 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.4.3",
|
||||
"version": "2.3.0-alpha.5",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"prepare": "husky || true",
|
||||
"dev": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
|
||||
"dev:diff": "cross-env RUST_BACKTRACE=full tauri dev -f verge-dev",
|
||||
"dev:trace": "cross-env RUST_BACKTRACE=full RUSTFLAGS=\"--cfg tokio_unstable\" tauri dev -f verge-dev tokio-trace",
|
||||
"dev:tauri": "cross-env RUST_BACKTRACE=full tauri dev -f tauri-dev",
|
||||
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||
"dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
|
||||
"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",
|
||||
"prebuild": "node scripts/prebuild.mjs",
|
||||
"check": "node scripts/check.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:autobuild": "pnpm release-version autobuild",
|
||||
"release:deploytest": "pnpm release-version deploytest",
|
||||
"publish-version": "node scripts/publish-version.mjs",
|
||||
"prepare": "husky",
|
||||
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
|
||||
"clippy": "cargo clippy --all-features --all-targets --manifest-path ./src-tauri/Cargo.toml",
|
||||
"lint": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache src",
|
||||
"lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run"
|
||||
"clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml"
|
||||
},
|
||||
"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.1",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^7.3.5",
|
||||
"@mui/lab": "7.0.0-beta.17",
|
||||
"@mui/material": "^7.3.5",
|
||||
"@mui/x-data-grid": "^8.17.0",
|
||||
"@tauri-apps/api": "2.9.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||
"@tauri-apps/plugin-http": "~2.5.4",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "2.3.3",
|
||||
"@tauri-apps/plugin-updater": "2.9.0",
|
||||
"@mui/icons-material": "^7.1.1",
|
||||
"@mui/lab": "7.0.0-beta.13",
|
||||
"@mui/material": "^7.1.1",
|
||||
"@mui/x-data-grid": "^8.5.0",
|
||||
"@tauri-apps/api": "2.5.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||
"@tauri-apps/plugin-fs": "^2.3.0",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.2.1",
|
||||
"@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",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.9.6",
|
||||
"axios": "^1.13.2",
|
||||
"dayjs": "1.11.19",
|
||||
"foxact": "^0.2.49",
|
||||
"i18next": "^25.6.1",
|
||||
"ahooks": "^3.8.5",
|
||||
"axios": "^1.9.0",
|
||||
"chart.js": "^4.4.9",
|
||||
"cli-color": "^2.0.4",
|
||||
"d3-shape": "^3.2.0",
|
||||
"dayjs": "1.11.13",
|
||||
"foxact": "^0.2.45",
|
||||
"glob": "^11.0.2",
|
||||
"i18next": "^25.2.1",
|
||||
"js-base64": "^3.7.7",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-schema": "^0.4.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.54.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-yaml": "^5.4.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"nanoid": "^5.1.5",
|
||||
"peggy": "^5.0.3",
|
||||
"react": "19.1.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "^7.66.0",
|
||||
"react-i18next": "16.2.4",
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-i18next": "15.5.2",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-monaco-editor": "0.59.0",
|
||||
"react-router": "^7.9.5",
|
||||
"react-virtuoso": "^4.14.1",
|
||||
"swr": "^2.3.6",
|
||||
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo",
|
||||
"types-pac": "^1.0.3"
|
||||
"react-monaco-editor": "0.58.0",
|
||||
"react-router-dom": "7.6.2",
|
||||
"react-virtuoso": "^4.12.8",
|
||||
"sockette": "^2.0.6",
|
||||
"swr": "^2.3.3",
|
||||
"tar": "^7.4.3",
|
||||
"types-pac": "^1.0.3",
|
||||
"zustand": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.1",
|
||||
"@eslint-react/eslint-plugin": "^2.3.1",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tauri-apps/cli": "2.9.3",
|
||||
"@tauri-apps/cli": "2.5.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-react-swc": "^4.2.1",
|
||||
"@types/react": "19.1.6",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@vitejs/plugin-legacy": "^6.1.1",
|
||||
"@vitejs/plugin-react": "4.5.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"cli-color": "^2.0.4",
|
||||
"commander": "^14.0.2",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import-x": "^4.16.1",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"eslint-plugin-unused-imports": "^4.3.0",
|
||||
"glob": "^11.0.3",
|
||||
"globals": "^16.5.0",
|
||||
"commander": "^14.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.6.1",
|
||||
"lint-staged": "^16.2.6",
|
||||
"meta-json-schema": "^1.19.14",
|
||||
"meta-json-schema": "^1.19.10",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.93.3",
|
||||
"tar": "^7.5.2",
|
||||
"terser": "^5.44.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.3",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-monaco-editor-esm": "^2.0.2",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vitest": "^4.0.8"
|
||||
"prettier": "^3.5.3",
|
||||
"pretty-quick": "^4.2.2",
|
||||
"sass": "^1.89.1",
|
||||
"terser": "^5.40.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx}": [
|
||||
"eslint --fix --max-warnings=0",
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
"*.{css,scss,json,md}": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
]
|
||||
"prettier": {
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"endOfLine": "lf"
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.13.2"
|
||||
}
|
||||
}
|
||||
Generated
+1369
-4386
File diff suppressed because it is too large
Load Diff
+3
-8
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": ["config:recommended", ":disableDependencyDashboard"],
|
||||
"extends": ["config:recommended"],
|
||||
"baseBranches": ["dev"],
|
||||
"enabledManagers": ["cargo", "npm"],
|
||||
"labels": ["dependencies"],
|
||||
@@ -35,13 +35,8 @@
|
||||
"description": "Group all npm dependencies into a single PR",
|
||||
"matchManagers": ["npm"],
|
||||
"groupName": "npm dependencies"
|
||||
},
|
||||
{
|
||||
"description": "Group all GitHub Actions updates into a single PR",
|
||||
"matchManagers": ["github-actions"],
|
||||
"groupName": "github actions"
|
||||
}
|
||||
],
|
||||
"postUpdateOptions": ["pnpmDedupe", "updateCargoLock"],
|
||||
"ignoreDeps": ["criterion"]
|
||||
"postUpdateOptions": ["pnpmDedupe"],
|
||||
"ignoreDeps": ["serde_yaml"]
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/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,10 +65,8 @@ 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');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -76,27 +74,25 @@ 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();
|
||||
@@ -1,30 +1,18 @@
|
||||
import AdmZip from "adm-zip";
|
||||
import { execSync } from "child_process";
|
||||
import { createHash } from "crypto";
|
||||
import fs from "fs";
|
||||
import fsp from "fs/promises";
|
||||
import { glob } from "glob";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import fetch from "node-fetch";
|
||||
import path from "path";
|
||||
import { extract } from "tar";
|
||||
import zlib from "zlib";
|
||||
import { log_debug, log_error, log_info, log_success } from "./utils.mjs";
|
||||
|
||||
/**
|
||||
* Prebuild script with optimization features:
|
||||
* 1. Skip downloading mihomo core if it already exists (unless --force is used)
|
||||
* 2. Cache version information for 1 hour to avoid repeated version checks
|
||||
* 3. Use file hash to detect changes and skip unnecessary chmod/copy operations
|
||||
* 4. Use --force or -f flag to force re-download and update all resources
|
||||
*
|
||||
*/
|
||||
import { extract } from "tar";
|
||||
import path from "path";
|
||||
import AdmZip from "adm-zip";
|
||||
import fetch from "node-fetch";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import { execSync } from "child_process";
|
||||
import { log_info, log_debug, log_error, log_success } from "./utils.mjs";
|
||||
import { glob } from "glob";
|
||||
|
||||
const cwd = process.cwd();
|
||||
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
|
||||
const FORCE = process.argv.includes("--force") || process.argv.includes("-f");
|
||||
const VERSION_CACHE_FILE = path.join(TEMP_DIR, ".version_cache.json");
|
||||
const HASH_CACHE_FILE = path.join(TEMP_DIR, ".hash_cache.json");
|
||||
const FORCE = process.argv.includes("--force");
|
||||
|
||||
const PLATFORM_MAP = {
|
||||
"x86_64-pc-windows-msvc": "win32",
|
||||
@@ -55,7 +43,7 @@ const ARCH_MAP = {
|
||||
|
||||
const arg1 = process.argv.slice(2)[0];
|
||||
const arg2 = process.argv.slice(2)[1];
|
||||
let target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
|
||||
const target = arg1 === "--force" ? arg2 : arg1;
|
||||
const { platform, arch } = target
|
||||
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
|
||||
: process;
|
||||
@@ -66,120 +54,19 @@ const SIDECAR_HOST = target
|
||||
.toString()
|
||||
.match(/(?<=host: ).+(?=\s*)/g)[0];
|
||||
|
||||
// =======================
|
||||
// Version Cache
|
||||
// =======================
|
||||
async function loadVersionCache() {
|
||||
try {
|
||||
if (fs.existsSync(VERSION_CACHE_FILE)) {
|
||||
const data = await fsp.readFile(VERSION_CACHE_FILE, "utf-8");
|
||||
return JSON.parse(data);
|
||||
}
|
||||
} catch (err) {
|
||||
log_debug("Failed to load version cache:", err.message);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
async function saveVersionCache(cache) {
|
||||
try {
|
||||
await fsp.mkdir(TEMP_DIR, { recursive: true });
|
||||
await fsp.writeFile(VERSION_CACHE_FILE, JSON.stringify(cache, null, 2));
|
||||
log_debug("Version cache saved");
|
||||
} catch (err) {
|
||||
log_debug("Failed to save version cache:", err.message);
|
||||
}
|
||||
}
|
||||
async function getCachedVersion(key) {
|
||||
const cache = await loadVersionCache();
|
||||
const cached = cache[key];
|
||||
if (cached && Date.now() - cached.timestamp < 3600000) {
|
||||
log_info(`Using cached version for ${key}: ${cached.version}`);
|
||||
return cached.version;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async function setCachedVersion(key, version) {
|
||||
const cache = await loadVersionCache();
|
||||
cache[key] = { version, timestamp: Date.now() };
|
||||
await saveVersionCache(cache);
|
||||
}
|
||||
|
||||
// =======================
|
||||
// Hash Cache & File Hash
|
||||
// =======================
|
||||
async function calculateFileHash(filePath) {
|
||||
try {
|
||||
const fileBuffer = await fsp.readFile(filePath);
|
||||
const hashSum = createHash("sha256");
|
||||
hashSum.update(fileBuffer);
|
||||
return hashSum.digest("hex");
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function loadHashCache() {
|
||||
try {
|
||||
if (fs.existsSync(HASH_CACHE_FILE)) {
|
||||
const data = await fsp.readFile(HASH_CACHE_FILE, "utf-8");
|
||||
return JSON.parse(data);
|
||||
}
|
||||
} catch (err) {
|
||||
log_debug("Failed to load hash cache:", err.message);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
async function saveHashCache(cache) {
|
||||
try {
|
||||
await fsp.mkdir(TEMP_DIR, { recursive: true });
|
||||
await fsp.writeFile(HASH_CACHE_FILE, JSON.stringify(cache, null, 2));
|
||||
log_debug("Hash cache saved");
|
||||
} catch (err) {
|
||||
log_debug("Failed to save hash cache:", err.message);
|
||||
}
|
||||
}
|
||||
async function hasFileChanged(filePath, targetPath) {
|
||||
if (FORCE) return true;
|
||||
if (!fs.existsSync(targetPath)) return true;
|
||||
const hashCache = await loadHashCache();
|
||||
const sourceHash = await calculateFileHash(filePath);
|
||||
const targetHash = await calculateFileHash(targetPath);
|
||||
if (!sourceHash || !targetHash) return true;
|
||||
const cacheKey = targetPath;
|
||||
const cachedHash = hashCache[cacheKey];
|
||||
if (cachedHash === sourceHash && sourceHash === targetHash) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
async function updateHashCache(targetPath) {
|
||||
const hashCache = await loadHashCache();
|
||||
const hash = await calculateFileHash(targetPath);
|
||||
if (hash) {
|
||||
hashCache[targetPath] = hash;
|
||||
await saveHashCache(hashCache);
|
||||
}
|
||||
}
|
||||
|
||||
// =======================
|
||||
// Meta maps (stable & alpha)
|
||||
// =======================
|
||||
/* ======= clash meta alpha======= */
|
||||
const META_ALPHA_VERSION_URL =
|
||||
"https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt";
|
||||
const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`;
|
||||
let META_ALPHA_VERSION;
|
||||
|
||||
const META_VERSION_URL =
|
||||
"https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
|
||||
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
|
||||
let META_VERSION;
|
||||
|
||||
const META_ALPHA_MAP = {
|
||||
"win32-x64": "mihomo-windows-amd64-v2",
|
||||
"win32-x64": "mihomo-windows-amd64-compatible",
|
||||
"win32-ia32": "mihomo-windows-386",
|
||||
"win32-arm64": "mihomo-windows-arm64",
|
||||
"darwin-x64": "mihomo-darwin-amd64-v1-go122",
|
||||
"darwin-arm64": "mihomo-darwin-arm64-go122",
|
||||
"linux-x64": "mihomo-linux-amd64-v2",
|
||||
"darwin-x64": "mihomo-darwin-amd64-compatible",
|
||||
"darwin-arm64": "mihomo-darwin-arm64",
|
||||
"linux-x64": "mihomo-linux-amd64-compatible",
|
||||
"linux-ia32": "mihomo-linux-386",
|
||||
"linux-arm64": "mihomo-linux-arm64",
|
||||
"linux-arm": "mihomo-linux-armv7",
|
||||
@@ -187,116 +74,112 @@ const META_ALPHA_MAP = {
|
||||
"linux-loong64": "mihomo-linux-loong64",
|
||||
};
|
||||
|
||||
const META_MAP = {
|
||||
"win32-x64": "mihomo-windows-amd64-v2",
|
||||
"win32-ia32": "mihomo-windows-386",
|
||||
"win32-arm64": "mihomo-windows-arm64",
|
||||
"darwin-x64": "mihomo-darwin-amd64-v2-go122",
|
||||
"darwin-arm64": "mihomo-darwin-arm64-go122",
|
||||
"linux-x64": "mihomo-linux-amd64-v2",
|
||||
"linux-ia32": "mihomo-linux-386",
|
||||
"linux-arm64": "mihomo-linux-arm64",
|
||||
"linux-arm": "mihomo-linux-armv7",
|
||||
"linux-riscv64": "mihomo-linux-riscv64",
|
||||
"linux-loong64": "mihomo-linux-loong64",
|
||||
};
|
||||
|
||||
// =======================
|
||||
// Fetch latest versions
|
||||
// =======================
|
||||
// Fetch the latest alpha release version from the version.txt file
|
||||
async function getLatestAlphaVersion() {
|
||||
if (!FORCE) {
|
||||
const cached = await getCachedVersion("META_ALPHA_VERSION");
|
||||
if (cached) {
|
||||
META_ALPHA_VERSION = cached;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const options = {};
|
||||
|
||||
const httpProxy =
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy;
|
||||
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy);
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = new HttpsProxyAgent(httpProxy);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(META_ALPHA_VERSION_URL, {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`Failed to fetch ${META_ALPHA_VERSION_URL}: ${response.status}`,
|
||||
);
|
||||
META_ALPHA_VERSION = (await response.text()).trim();
|
||||
let v = await response.text();
|
||||
META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces
|
||||
log_info(`Latest alpha version: ${META_ALPHA_VERSION}`);
|
||||
await setCachedVersion("META_ALPHA_VERSION", META_ALPHA_VERSION);
|
||||
} catch (err) {
|
||||
log_error("Error fetching latest alpha version:", err.message);
|
||||
} catch (error) {
|
||||
log_error("Error fetching latest alpha version:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ======= clash meta stable ======= */
|
||||
const META_VERSION_URL =
|
||||
"https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
|
||||
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
|
||||
let META_VERSION;
|
||||
|
||||
const META_MAP = {
|
||||
"win32-x64": "mihomo-windows-amd64-compatible",
|
||||
"win32-ia32": "mihomo-windows-386",
|
||||
"win32-arm64": "mihomo-windows-arm64",
|
||||
"darwin-x64": "mihomo-darwin-amd64-compatible",
|
||||
"darwin-arm64": "mihomo-darwin-arm64",
|
||||
"linux-x64": "mihomo-linux-amd64-compatible",
|
||||
"linux-ia32": "mihomo-linux-386",
|
||||
"linux-arm64": "mihomo-linux-arm64",
|
||||
"linux-arm": "mihomo-linux-armv7",
|
||||
"linux-riscv64": "mihomo-linux-riscv64",
|
||||
"linux-loong64": "mihomo-linux-loong64",
|
||||
};
|
||||
|
||||
// Fetch the latest release version from the version.txt file
|
||||
async function getLatestReleaseVersion() {
|
||||
if (!FORCE) {
|
||||
const cached = await getCachedVersion("META_VERSION");
|
||||
if (cached) {
|
||||
META_VERSION = cached;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const options = {};
|
||||
|
||||
const httpProxy =
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy;
|
||||
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy);
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = new HttpsProxyAgent(httpProxy);
|
||||
}
|
||||
try {
|
||||
const response = await fetch(META_VERSION_URL, {
|
||||
...options,
|
||||
method: "GET",
|
||||
});
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`Failed to fetch ${META_VERSION_URL}: ${response.status}`,
|
||||
);
|
||||
META_VERSION = (await response.text()).trim();
|
||||
let v = await response.text();
|
||||
META_VERSION = v.trim(); // Trim to remove extra whitespaces
|
||||
log_info(`Latest release version: ${META_VERSION}`);
|
||||
await setCachedVersion("META_VERSION", META_VERSION);
|
||||
} catch (err) {
|
||||
log_error("Error fetching latest release version:", err.message);
|
||||
} catch (error) {
|
||||
log_error("Error fetching latest release version:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// =======================
|
||||
// Validate availability
|
||||
// =======================
|
||||
/*
|
||||
* check available
|
||||
*/
|
||||
if (!META_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`);
|
||||
throw new Error(
|
||||
`clash meta alpha unsupported platform "${platform}-${arch}"`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(
|
||||
`clash meta alpha unsupported platform "${platform}-${arch}"`,
|
||||
);
|
||||
}
|
||||
|
||||
// =======================
|
||||
// Build meta objects
|
||||
// =======================
|
||||
/**
|
||||
* core info
|
||||
*/
|
||||
function clashMetaAlpha() {
|
||||
const name = META_ALPHA_MAP[`${platform}-${arch}`];
|
||||
const isWin = platform === "win32";
|
||||
const urlExt = isWin ? "zip" : "gz";
|
||||
const downloadURL = `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`;
|
||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "verge-mihomo-alpha",
|
||||
targetFile: `verge-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile: `${name}${isWin ? ".exe" : ""}`,
|
||||
zipFile: `${name}-${META_ALPHA_VERSION}.${urlExt}`,
|
||||
downloadURL: `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -304,83 +187,35 @@ function clashMeta() {
|
||||
const name = META_MAP[`${platform}-${arch}`];
|
||||
const isWin = platform === "win32";
|
||||
const urlExt = isWin ? "zip" : "gz";
|
||||
const downloadURL = `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`;
|
||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipFile = `${name}-${META_VERSION}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "verge-mihomo",
|
||||
targetFile: `verge-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile: `${name}${isWin ? ".exe" : ""}`,
|
||||
zipFile: `${name}-${META_VERSION}.${urlExt}`,
|
||||
downloadURL: `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
};
|
||||
}
|
||||
|
||||
// =======================
|
||||
// download helper (增强:status + magic bytes)
|
||||
// =======================
|
||||
async function downloadFile(url, outPath) {
|
||||
const options = {};
|
||||
const httpProxy =
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy;
|
||||
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy);
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/octet-stream" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => "");
|
||||
// 将 body 写到文件以便排查(可通过临时目录查看)
|
||||
await fsp.mkdir(path.dirname(outPath), { recursive: true });
|
||||
await fsp.writeFile(outPath, body);
|
||||
throw new Error(`Failed to download ${url}: status ${response.status}`);
|
||||
}
|
||||
|
||||
const buf = Buffer.from(await response.arrayBuffer());
|
||||
await fsp.mkdir(path.dirname(outPath), { recursive: true });
|
||||
|
||||
// 简单 magic 字节检查
|
||||
if (url.endsWith(".gz") || url.endsWith(".tgz")) {
|
||||
if (!(buf[0] === 0x1f && buf[1] === 0x8b)) {
|
||||
await fsp.writeFile(outPath, buf);
|
||||
throw new Error(
|
||||
`Downloaded file for ${url} is not a valid gzip (magic mismatch).`,
|
||||
);
|
||||
}
|
||||
} else if (url.endsWith(".zip")) {
|
||||
if (!(buf[0] === 0x50 && buf[1] === 0x4b)) {
|
||||
await fsp.writeFile(outPath, buf);
|
||||
throw new Error(
|
||||
`Downloaded file for ${url} is not a valid zip (magic mismatch).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await fsp.writeFile(outPath, buf);
|
||||
log_success(`download finished: ${url}`);
|
||||
}
|
||||
|
||||
// =======================
|
||||
// resolveSidecar (支持 zip / tgz / gz)
|
||||
// =======================
|
||||
/**
|
||||
* download sidecar and rename
|
||||
*/
|
||||
async function resolveSidecar(binInfo) {
|
||||
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo;
|
||||
|
||||
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
|
||||
const sidecarPath = path.join(sidecarDir, targetFile);
|
||||
await fsp.mkdir(sidecarDir, { recursive: true });
|
||||
|
||||
if (!FORCE && fs.existsSync(sidecarPath)) {
|
||||
log_success(`"${name}" already exists, skipping download`);
|
||||
return;
|
||||
}
|
||||
await fsp.mkdir(sidecarDir, { recursive: true });
|
||||
if (!FORCE && fs.existsSync(sidecarPath)) return;
|
||||
|
||||
const tempDir = path.join(TEMP_DIR, name);
|
||||
const tempZip = path.join(tempDir, zipFile);
|
||||
const tempExe = path.join(tempDir, exeFile);
|
||||
await fsp.mkdir(tempDir, { recursive: true });
|
||||
|
||||
await fsp.mkdir(tempDir, { recursive: true });
|
||||
try {
|
||||
if (!fs.existsSync(tempZip)) {
|
||||
await downloadFile(downloadURL, tempZip);
|
||||
@@ -389,118 +224,140 @@ async function resolveSidecar(binInfo) {
|
||||
if (zipFile.endsWith(".zip")) {
|
||||
const zip = new AdmZip(tempZip);
|
||||
zip.getEntries().forEach((entry) => {
|
||||
log_debug(`"${name}" entry: ${entry.entryName}`);
|
||||
log_debug(`"${name}" entry name`, entry.entryName);
|
||||
});
|
||||
zip.extractAllTo(tempDir, true);
|
||||
// 尝试按 exeFile 重命名,否则找第一个可执行文件
|
||||
if (fs.existsSync(tempExe)) {
|
||||
await fsp.rename(tempExe, sidecarPath);
|
||||
} else {
|
||||
// 搜索候选
|
||||
const files = await fsp.readdir(tempDir);
|
||||
const candidate = files.find(
|
||||
(f) =>
|
||||
f === path.basename(exeFile) ||
|
||||
f.endsWith(".exe") ||
|
||||
!f.includes("."),
|
||||
);
|
||||
if (!candidate)
|
||||
throw new Error(`Expected binary not found in ${tempDir}`);
|
||||
await fsp.rename(path.join(tempDir, candidate), sidecarPath);
|
||||
}
|
||||
if (platform !== "win32") execSync(`chmod 755 ${sidecarPath}`);
|
||||
await fsp.rename(tempExe, sidecarPath);
|
||||
log_success(`unzip finished: "${name}"`);
|
||||
} else if (zipFile.endsWith(".tgz")) {
|
||||
await extract({ cwd: tempDir, file: tempZip });
|
||||
// tgz
|
||||
await fsp.mkdir(tempDir, { recursive: true });
|
||||
await extract({
|
||||
cwd: tempDir,
|
||||
file: tempZip,
|
||||
//strip: 1, // 可能需要根据实际的 .tgz 文件结构调整
|
||||
});
|
||||
const files = await fsp.readdir(tempDir);
|
||||
log_debug(`"${name}" extracted files:`, files);
|
||||
// 优先寻找给定 exeFile 或已知前缀
|
||||
let extracted = files.find(
|
||||
(f) =>
|
||||
f === path.basename(exeFile) ||
|
||||
f.startsWith("虚空终端-") ||
|
||||
!f.includes("."),
|
||||
);
|
||||
if (!extracted) extracted = files[0];
|
||||
if (!extracted) throw new Error(`Expected file not found in ${tempDir}`);
|
||||
await fsp.rename(path.join(tempDir, extracted), sidecarPath);
|
||||
execSync(`chmod 755 ${sidecarPath}`);
|
||||
log_success(`tgz processed: "${name}"`);
|
||||
log_debug(`"${name}" files in tempDir:`, files);
|
||||
const extractedFile = files.find((file) => file.startsWith("虚空终端-"));
|
||||
if (extractedFile) {
|
||||
const extractedFilePath = path.join(tempDir, extractedFile);
|
||||
await fsp.rename(extractedFilePath, sidecarPath);
|
||||
log_success(`"${name}" file renamed to "${sidecarPath}"`);
|
||||
execSync(`chmod 755 ${sidecarPath}`);
|
||||
log_success(`chmod binary finished: "${name}"`);
|
||||
} else {
|
||||
throw new Error(`Expected file not found in ${tempDir}`);
|
||||
}
|
||||
} else {
|
||||
// .gz
|
||||
// gz
|
||||
const readStream = fs.createReadStream(tempZip);
|
||||
const writeStream = fs.createWriteStream(sidecarPath);
|
||||
await new Promise((resolve, reject) => {
|
||||
const onError = (error) => {
|
||||
log_error(`"${name}" gz failed:`, error.message);
|
||||
reject(error);
|
||||
};
|
||||
readStream
|
||||
.pipe(zlib.createGunzip())
|
||||
.on("error", (e) => {
|
||||
log_error(`gunzip error for ${name}:`, e.message);
|
||||
reject(e);
|
||||
})
|
||||
.pipe(zlib.createGunzip().on("error", onError))
|
||||
.pipe(writeStream)
|
||||
.on("finish", () => {
|
||||
if (platform !== "win32") execSync(`chmod 755 ${sidecarPath}`);
|
||||
execSync(`chmod 755 ${sidecarPath}`);
|
||||
log_success(`chmod binary finished: "${name}"`);
|
||||
resolve();
|
||||
})
|
||||
.on("error", (e) => {
|
||||
log_error(`write stream error for ${name}:`, e.message);
|
||||
reject(e);
|
||||
});
|
||||
.on("error", onError);
|
||||
});
|
||||
log_success(`gz binary processed: "${name}"`);
|
||||
}
|
||||
} catch (err) {
|
||||
// 需要删除文件
|
||||
await fsp.rm(sidecarPath, { recursive: true, force: true });
|
||||
throw err;
|
||||
} finally {
|
||||
// delete temp dir
|
||||
await fsp.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const resolveSetDnsScript = () =>
|
||||
resolveResource({
|
||||
file: "set_dns.sh",
|
||||
localPath: path.join(cwd, "scripts/set_dns.sh"),
|
||||
});
|
||||
const resolveUnSetDnsScript = () =>
|
||||
resolveResource({
|
||||
file: "unset_dns.sh",
|
||||
localPath: path.join(cwd, "scripts/unset_dns.sh"),
|
||||
});
|
||||
|
||||
/**
|
||||
* download the file to the resources dir
|
||||
*/
|
||||
async function resolveResource(binInfo) {
|
||||
const { file, downloadURL, localPath } = binInfo;
|
||||
|
||||
const resDir = path.join(cwd, "src-tauri/resources");
|
||||
const targetPath = path.join(resDir, file);
|
||||
|
||||
if (!FORCE && fs.existsSync(targetPath) && !downloadURL && !localPath) {
|
||||
log_success(`"${file}" already exists, skipping`);
|
||||
return;
|
||||
}
|
||||
if (!FORCE && fs.existsSync(targetPath)) return;
|
||||
|
||||
if (downloadURL) {
|
||||
if (!FORCE && fs.existsSync(targetPath)) {
|
||||
log_success(`"${file}" already exists, skipping download`);
|
||||
return;
|
||||
}
|
||||
await fsp.mkdir(resDir, { recursive: true });
|
||||
await downloadFile(downloadURL, targetPath);
|
||||
await updateHashCache(targetPath);
|
||||
}
|
||||
|
||||
if (localPath) {
|
||||
if (!(await hasFileChanged(localPath, targetPath))) {
|
||||
return;
|
||||
}
|
||||
await fsp.mkdir(resDir, { recursive: true });
|
||||
await fsp.copyFile(localPath, targetPath);
|
||||
await updateHashCache(targetPath);
|
||||
log_success(`Copied file: ${file}`);
|
||||
await fs.copyFile(localPath, targetPath, (err) => {
|
||||
if (err) {
|
||||
console.error("Error copying file:", err);
|
||||
} else {
|
||||
console.log("File was copied successfully");
|
||||
}
|
||||
});
|
||||
log_debug(`copy file finished: "${localPath}"`);
|
||||
}
|
||||
|
||||
log_success(`${file} finished`);
|
||||
}
|
||||
|
||||
// SimpleSC.dll (win plugin)
|
||||
/**
|
||||
* download file and save to `path`
|
||||
*/ async function downloadFile(url, path) {
|
||||
const options = {};
|
||||
|
||||
const httpProxy =
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy;
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = new HttpsProxyAgent(httpProxy);
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/octet-stream" },
|
||||
});
|
||||
const buffer = await response.arrayBuffer();
|
||||
await fsp.writeFile(path, new Uint8Array(buffer));
|
||||
|
||||
log_success(`download finished: ${url}`);
|
||||
}
|
||||
|
||||
// SimpleSC.dll
|
||||
const resolvePlugin = async () => {
|
||||
const url =
|
||||
"https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip";
|
||||
|
||||
const tempDir = path.join(TEMP_DIR, "SimpleSC");
|
||||
const tempZip = path.join(
|
||||
tempDir,
|
||||
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip",
|
||||
);
|
||||
const tempDll = path.join(tempDir, "SimpleSC.dll");
|
||||
const pluginDir = path.join(process.env.APPDATA || "", "Local/NSIS");
|
||||
const pluginDir = path.join(process.env.APPDATA, "Local/NSIS");
|
||||
const pluginPath = path.join(pluginDir, "SimpleSC.dll");
|
||||
await fsp.mkdir(pluginDir, { recursive: true });
|
||||
await fsp.mkdir(tempDir, { recursive: true });
|
||||
@@ -510,118 +367,95 @@ const resolvePlugin = async () => {
|
||||
await downloadFile(url, tempZip);
|
||||
}
|
||||
const zip = new AdmZip(tempZip);
|
||||
zip
|
||||
.getEntries()
|
||||
.forEach((entry) => log_debug(`"SimpleSC" entry`, entry.entryName));
|
||||
zip.getEntries().forEach((entry) => {
|
||||
log_debug(`"SimpleSC" entry name`, entry.entryName);
|
||||
});
|
||||
zip.extractAllTo(tempDir, true);
|
||||
if (fs.existsSync(tempDll)) {
|
||||
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true });
|
||||
log_success(`unzip finished: "SimpleSC"`);
|
||||
} else {
|
||||
// 如果 dll 名称不同,尝试找到 dll
|
||||
const files = await fsp.readdir(tempDir);
|
||||
const dll = files.find((f) => f.toLowerCase().endsWith(".dll"));
|
||||
if (dll) {
|
||||
await fsp.cp(path.join(tempDir, dll), pluginPath, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
log_success(`unzip finished: "SimpleSC" (found ${dll})`);
|
||||
} else {
|
||||
throw new Error("SimpleSC.dll not found in zip");
|
||||
}
|
||||
}
|
||||
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true });
|
||||
log_success(`unzip finished: "SimpleSC"`);
|
||||
} finally {
|
||||
await fsp.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
|
||||
// service chmod (保留并使用 glob)
|
||||
// service chmod
|
||||
const resolveServicePermission = async () => {
|
||||
const serviceExecutables = [
|
||||
"clash-verge-service*",
|
||||
"clash-verge-service-install*",
|
||||
"clash-verge-service-uninstall*",
|
||||
"install-service*",
|
||||
"uninstall-service*",
|
||||
];
|
||||
const resDir = path.join(cwd, "src-tauri/resources");
|
||||
const hashCache = await loadHashCache();
|
||||
let hasChanges = false;
|
||||
|
||||
for (let f of serviceExecutables) {
|
||||
// 使用glob模块来处理通配符
|
||||
const files = glob.sync(path.join(resDir, f));
|
||||
for (let filePath of files) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const currentHash = await calculateFileHash(filePath);
|
||||
const cacheKey = `${filePath}_chmod`;
|
||||
if (!FORCE && hashCache[cacheKey] === currentHash) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
execSync(`chmod 755 ${filePath}`);
|
||||
log_success(`chmod finished: "${filePath}"`);
|
||||
} catch (e) {
|
||||
log_error(`chmod failed for ${filePath}:`, e.message);
|
||||
}
|
||||
hashCache[cacheKey] = currentHash;
|
||||
hasChanges = true;
|
||||
execSync(`chmod 755 ${filePath}`);
|
||||
log_success(`chmod finished: "${filePath}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
await saveHashCache(hashCache);
|
||||
}
|
||||
};
|
||||
|
||||
// resolve locales (从 src/locales 复制到 resources/locales,并使用 hash 检查)
|
||||
// 在 resolveResource 函数后添加新函数
|
||||
async function resolveLocales() {
|
||||
const srcLocalesDir = path.join(cwd, "src/locales");
|
||||
const targetLocalesDir = path.join(cwd, "src-tauri/resources/locales");
|
||||
|
||||
try {
|
||||
// 确保目标目录存在
|
||||
await fsp.mkdir(targetLocalesDir, { recursive: true });
|
||||
|
||||
// 读取所有语言文件
|
||||
const files = await fsp.readdir(srcLocalesDir);
|
||||
|
||||
// 复制每个文件
|
||||
for (const file of files) {
|
||||
const srcPath = path.join(srcLocalesDir, file);
|
||||
const targetPath = path.join(targetLocalesDir, file);
|
||||
if (!(await hasFileChanged(srcPath, targetPath))) continue;
|
||||
|
||||
await fsp.copyFile(srcPath, targetPath);
|
||||
await updateHashCache(targetPath);
|
||||
log_success(`Copied locale file: ${file}`);
|
||||
}
|
||||
log_success("All locale files processed successfully");
|
||||
|
||||
log_success("All locale files copied successfully");
|
||||
} catch (err) {
|
||||
log_error("Error copying locale files:", err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// =======================
|
||||
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback, sysproxy)
|
||||
// =======================
|
||||
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`;
|
||||
/**
|
||||
* main
|
||||
*/
|
||||
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
|
||||
|
||||
const resolveService = () => {
|
||||
let ext = platform === "win32" ? ".exe" : "";
|
||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
return resolveResource({
|
||||
resolveResource({
|
||||
file: "clash-verge-service" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
|
||||
});
|
||||
};
|
||||
|
||||
const resolveInstall = () => {
|
||||
let ext = platform === "win32" ? ".exe" : "";
|
||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
return resolveResource({
|
||||
file: "clash-verge-service-install" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/clash-verge-service-install${ext}`,
|
||||
resolveResource({
|
||||
file: "install-service" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/install-service${ext}`,
|
||||
});
|
||||
};
|
||||
|
||||
const resolveUninstall = () => {
|
||||
let ext = platform === "win32" ? ".exe" : "";
|
||||
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
|
||||
return resolveResource({
|
||||
file: "clash-verge-service-uninstall" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/clash-verge-service-uninstall${ext}`,
|
||||
|
||||
resolveResource({
|
||||
file: "uninstall-service" + suffix + ext,
|
||||
downloadURL: `${SERVICE_URL}/uninstall-service${ext}`,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -645,27 +479,15 @@ const resolveEnableLoopback = () =>
|
||||
file: "enableLoopback.exe",
|
||||
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`,
|
||||
});
|
||||
|
||||
const resolveWinSysproxy = () =>
|
||||
resolveResource({
|
||||
file: "sysproxy.exe",
|
||||
downloadURL: `https://github.com/clash-verge-rev/sysproxy/releases/download/${arch}/sysproxy.exe`,
|
||||
});
|
||||
|
||||
const resolveSetDnsScript = () =>
|
||||
resolveResource({
|
||||
file: "set_dns.sh",
|
||||
localPath: path.join(cwd, "scripts/set_dns.sh"),
|
||||
});
|
||||
const resolveUnSetDnsScript = () =>
|
||||
resolveResource({
|
||||
file: "unset_dns.sh",
|
||||
localPath: path.join(cwd, "scripts/unset_dns.sh"),
|
||||
});
|
||||
|
||||
// =======================
|
||||
// Tasks
|
||||
// =======================
|
||||
const tasks = [
|
||||
// { name: "clash", func: resolveClash, retry: 5 },
|
||||
{
|
||||
name: "verge-mihomo-alpha",
|
||||
func: () =>
|
||||
@@ -715,7 +537,11 @@ const tasks = [
|
||||
retry: 5,
|
||||
macosOnly: true,
|
||||
},
|
||||
{ name: "locales", func: resolveLocales, retry: 2 },
|
||||
{
|
||||
name: "locales",
|
||||
func: resolveLocales,
|
||||
retry: 2,
|
||||
},
|
||||
];
|
||||
|
||||
async function runTask() {
|
||||
@@ -38,9 +38,7 @@ async function run() {
|
||||
let tag = null;
|
||||
if (versionArg === "alpha") {
|
||||
// 读取 package.json 里的主版本
|
||||
const pkg = await import(path.join(rootDir, "package.json"), {
|
||||
assert: { type: "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
|
||||
@@ -54,7 +52,7 @@ async function run() {
|
||||
execSync(`git tag ${tag}`, { stdio: "inherit" });
|
||||
execSync(`git push origin ${tag}`, { stdio: "inherit" });
|
||||
console.log(`[INFO]: Git tag ${tag} created and pushed.`);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.error(`[ERROR]: Failed to create or push git tag: ${tag}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -63,4 +61,4 @@ async function run() {
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
run();
|
||||
+33
-134
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
/**
|
||||
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json.
|
||||
*
|
||||
@@ -6,19 +8,15 @@
|
||||
*
|
||||
* <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", "autobuild", "autobuild-latest", or "deploytest"
|
||||
* - A tag: "alpha", "beta", "rc", or "autobuild"
|
||||
* - "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
|
||||
@@ -29,76 +27,22 @@
|
||||
* Errors are logged and the process exits with code 1 on failure.
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import { program } from "commander";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { program } from "commander";
|
||||
|
||||
/**
|
||||
* 获取当前 git 短 commit hash
|
||||
* 生成短时间戳(格式:YYMMDDHHMM)
|
||||
* @returns {string}
|
||||
*/
|
||||
function getGitShortCommit() {
|
||||
try {
|
||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||
} catch {
|
||||
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();
|
||||
const shortHash = execSync(`git rev-parse --short ${fullHash}`)
|
||||
.toString()
|
||||
.trim();
|
||||
console.log(`[INFO]: Latest Tauri-related commit: ${shortHash}`);
|
||||
return shortHash;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[WARN]: Failed to get latest Tauri commit, fallback to current git short commit",
|
||||
);
|
||||
console.warn(`[WARN]: Error details: ${error.message}`);
|
||||
return getGitShortCommit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成短时间戳(格式:MMDD)或带 commit(格式:MMDD.cc39b27)
|
||||
* 使用 Asia/Shanghai 时区
|
||||
* @param {boolean} withCommit 是否带 commit
|
||||
* @param {boolean} useTauriCommit 是否使用 Tauri 相关的 commit(仅当 withCommit 为 true 时有效)
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateShortTimestamp(withCommit = false, useTauriCommit = false) {
|
||||
function generateShortTimestamp() {
|
||||
const now = new Date();
|
||||
|
||||
const formatter = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: "Asia/Shanghai",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
|
||||
const parts = formatter.formatToParts(now);
|
||||
const month = parts.find((part) => part.type === "month").value;
|
||||
const day = parts.find((part) => part.type === "day").value;
|
||||
|
||||
if (withCommit) {
|
||||
const gitShort = useTauriCommit
|
||||
? getLatestTauriCommit()
|
||||
: getGitShortCommit();
|
||||
return `${month}${day}.${gitShort}`;
|
||||
}
|
||||
return `${month}${day}`;
|
||||
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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,9 +51,7 @@ function generateShortTimestamp(withCommit = false, useTauriCommit = false) {
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,8 +69,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;
|
||||
}
|
||||
|
||||
@@ -143,21 +85,10 @@ 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;
|
||||
@@ -174,22 +105,18 @@ 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 versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion;
|
||||
const baseVersion = getBaseVersion(versionWithoutV);
|
||||
|
||||
const updatedLines = lines.map((line) => {
|
||||
if (line.trim().startsWith("version =")) {
|
||||
return line.replace(
|
||||
/version\s*=\s*"[^"]+"/,
|
||||
`version = "${versionWithoutV}"`,
|
||||
);
|
||||
return line.replace(/version\s*=\s*"[^"]+"/, `version = "${baseVersion}"`);
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
||||
console.log(`[INFO]: Cargo.toml version updated to: ${versionWithoutV}`);
|
||||
console.log(`[INFO]: Cargo.toml version updated to: ${baseVersion}`);
|
||||
} catch (error) {
|
||||
console.error("Error updating Cargo.toml version:", error);
|
||||
throw error;
|
||||
@@ -206,26 +133,13 @@ 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 versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion;
|
||||
const baseVersion = getBaseVersion(versionWithoutV);
|
||||
|
||||
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}`,
|
||||
);
|
||||
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}`);
|
||||
} catch (error) {
|
||||
console.error("Error updating tauri.conf.json version:", error);
|
||||
throw error;
|
||||
@@ -259,31 +173,15 @@ async function main(versionArg) {
|
||||
|
||||
try {
|
||||
let newVersion;
|
||||
const validTags = [
|
||||
"alpha",
|
||||
"beta",
|
||||
"rc",
|
||||
"autobuild",
|
||||
"autobuild-latest",
|
||||
"deploytest",
|
||||
];
|
||||
const validTags = ["alpha", "beta", "rc", "autobuild"];
|
||||
|
||||
if (validTags.includes(versionArg.toLowerCase())) {
|
||||
const currentVersion = await getCurrentVersion();
|
||||
const baseVersion = getBaseVersion(currentVersion);
|
||||
|
||||
if (versionArg.toLowerCase() === "autobuild") {
|
||||
// 格式: 2.3.0+autobuild.1004.cc39b27
|
||||
// 使用 Tauri 相关的最新 commit hash
|
||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true, true)}`;
|
||||
} else if (versionArg.toLowerCase() === "autobuild-latest") {
|
||||
// 格式: 2.3.0+autobuild.1004.a1b2c3d (使用最新 Tauri 提交)
|
||||
const latestTauriCommit = getLatestTauriCommit();
|
||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`;
|
||||
} else if (versionArg.toLowerCase() === "deploytest") {
|
||||
// 格式: 2.3.0+deploytest.1004.cc39b27
|
||||
// 使用 Tauri 相关的最新 commit hash
|
||||
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true, true)}`;
|
||||
const timestamp = generateShortTimestamp();
|
||||
newVersion = `${baseVersion}+autobuild.${timestamp}`;
|
||||
} else {
|
||||
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
||||
}
|
||||
@@ -312,3 +210,4 @@ program
|
||||
.argument("<version>", "version tag or full version")
|
||||
.action(main)
|
||||
.parse(process.argv);
|
||||
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import axios from "axios";
|
||||
import { readFileSync } from "fs";
|
||||
import { log_error, log_info, log_success } 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,使用默认发布说明", error);
|
||||
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,
|
||||
(match, text, url) => {
|
||||
const encodedUrl = encodeURI(url);
|
||||
return `<a href="${encodedUrl}">${text}</a>`;
|
||||
},
|
||||
);
|
||||
processedLine = processedLine.replace(
|
||||
/\*\*([^*]+)\*\*/g,
|
||||
"<b>$1</b>",
|
||||
);
|
||||
return processedLine;
|
||||
}
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function normalizeDetailsTags(content) {
|
||||
return content
|
||||
.replace(
|
||||
/<summary>\s*<strong>\s*(.*?)\s*<\/strong>\s*<\/summary>/g,
|
||||
"\n<b>$1</b>\n",
|
||||
)
|
||||
.replace(/<summary>\s*(.*?)\s*<\/summary>/g, "\n<b>$1</b>\n")
|
||||
.replace(/<\/?details>/g, "")
|
||||
.replace(/<\/?strong>/g, (m) => (m === "</strong>" ? "</b>" : "<b>"))
|
||||
.replace(/<br\s*\/?>/g, "\n");
|
||||
}
|
||||
|
||||
releaseContent = normalizeDetailsTags(releaseContent);
|
||||
const formattedContent = convertMarkdownToTelegramHTML(releaseContent);
|
||||
|
||||
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
|
||||
const encodedVersion = encodeURIComponent(version);
|
||||
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild">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${encodedVersion}`,
|
||||
prefer_large_media: true,
|
||||
},
|
||||
parse_mode: "HTML",
|
||||
},
|
||||
);
|
||||
log_success(`✅ Telegram 通知发送成功到 ${chatId}`);
|
||||
} catch (error) {
|
||||
log_error(
|
||||
`❌ Telegram 通知发送失败到 ${chatId}:`,
|
||||
error.response?.data || error.message,
|
||||
error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行函数
|
||||
sendTelegramNotification().catch((error) => {
|
||||
log_error("脚本执行失败:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -8,7 +8,7 @@ const UPDATE_LOG = "UPDATELOG.md";
|
||||
export async function resolveUpdateLog(tag) {
|
||||
const cwd = process.cwd();
|
||||
|
||||
const reTitle = /^## v[\d.]+/;
|
||||
const reTitle = /^## v[\d\.]+/;
|
||||
const reEnd = /^---/;
|
||||
|
||||
const file = path.join(cwd, UPDATE_LOG);
|
||||
@@ -54,7 +54,7 @@ export async function resolveUpdateLogDefault() {
|
||||
|
||||
const data = await fsp.readFile(file, "utf-8");
|
||||
|
||||
const reTitle = /^## v[\d.]+/;
|
||||
const reTitle = /^## v[\d\.]+/;
|
||||
const reEnd = /^---/;
|
||||
|
||||
let isCapturing = false;
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
avoid-breaking-exported-api = true
|
||||
cognitive-complexity-threshold = 25
|
||||
avoid-breaking-exported-api = true
|
||||
Generated
+2195
-2803
File diff suppressed because it is too large
Load Diff
+72
-170
@@ -1,92 +1,85 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.4.3"
|
||||
version = "2.3.0"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||
authors = ["zzzgydi", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
||||
default-run = "clash-verge"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[package.metadata.bundle]
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.5.1", features = [] }
|
||||
tauri-build = { version = "2.2.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
warp = { version = "0.4.2", features = ["server"] }
|
||||
anyhow = "1.0.100"
|
||||
warp = "0.3.7"
|
||||
anyhow = "1.0.98"
|
||||
dirs = "6.0"
|
||||
open = "5.3.2"
|
||||
log = "0.4.28"
|
||||
log = "0.4.27"
|
||||
dunce = "1.0.5"
|
||||
log4rs = "1.3.0"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4.42"
|
||||
sysinfo = { version = "0.37.2", features = ["network", "system"] }
|
||||
boa_engine = "0.21.0"
|
||||
serde_json = "1.0.145"
|
||||
serde_yaml_ng = "0.10.0"
|
||||
chrono = "0.4.41"
|
||||
sysinfo = "0.35.2"
|
||||
boa_engine = "0.20.0"
|
||||
serde_json = "1.0.140"
|
||||
serde_yaml = "0.9.34-deprecated"
|
||||
once_cell = "1.21.3"
|
||||
lazy_static = "1.5.0"
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.6"
|
||||
parking_lot = "0.12.5"
|
||||
percent-encoding = "2.3.2"
|
||||
tokio = { version = "1.48.0", features = [
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3.1"
|
||||
tokio = { version = "1.45.1", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
"sync",
|
||||
] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
reqwest = { version = "0.12.24", features = ["json", "cookies"] }
|
||||
regex = "1.12.2"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
reqwest = { version = "0.12.19", features = ["json", "rustls-tls", "cookies"] }
|
||||
regex = "1.11.1"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
||||
tauri = { version = "2.9.2", features = [
|
||||
image = "0.25.6"
|
||||
imageproc = "0.25.0"
|
||||
tauri = { version = "2.5.1", features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tray-icon",
|
||||
"image-ico",
|
||||
"image-png",
|
||||
] }
|
||||
network-interface = { version = "2.0.3", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.3.3"
|
||||
tauri-plugin-dialog = "2.4.2"
|
||||
tauri-plugin-fs = "2.4.4"
|
||||
tauri-plugin-process = "2.3.1"
|
||||
tauri-plugin-clipboard-manager = "2.3.2"
|
||||
tauri-plugin-deep-link = "2.4.5"
|
||||
tauri-plugin-window-state = "2.4.1"
|
||||
zip = "6.0.0"
|
||||
reqwest_dav = "0.2.2"
|
||||
network-interface = { version = "2.0.1", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
tauri-plugin-dialog = "2.2.2"
|
||||
tauri-plugin-fs = "2.3.0"
|
||||
tauri-plugin-process = "2.2.1"
|
||||
tauri-plugin-clipboard-manager = "2.2.2"
|
||||
tauri-plugin-deep-link = "2.3.0"
|
||||
tauri-plugin-devtools = "2.0.0"
|
||||
tauri-plugin-window-state = "2.2.2"
|
||||
zip = "4.0.0"
|
||||
reqwest_dav = "0.2.1"
|
||||
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||
base64 = "0.22.1"
|
||||
getrandom = "0.3.4"
|
||||
getrandom = "0.3.3"
|
||||
tokio-tungstenite = "0.26.2"
|
||||
futures = "0.3.31"
|
||||
sys-locale = "0.3.2"
|
||||
libc = "0.2.177"
|
||||
gethostname = "1.1.0"
|
||||
scopeguard = "1.2.0"
|
||||
tauri-plugin-notification = "2.3.3"
|
||||
tokio-stream = "0.1.17"
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
"text-decoding",
|
||||
"parking_lot",
|
||||
] }
|
||||
backoff = { version = "0.4.0", features = ["tokio"] }
|
||||
compact_str = { version = "0.9.0", features = ["serde"] }
|
||||
tauri-plugin-http = "2.5.4"
|
||||
flexi_logger = "0.31.7"
|
||||
console-subscriber = { version = "0.5.0", optional = true }
|
||||
tauri-plugin-devtools = { version = "2.0.1" }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
|
||||
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
|
||||
async-trait = "0.1.89"
|
||||
smartstring = { version = "1.0.1", features = ["serde"] }
|
||||
clash_verge_service_ipc = { version = "2.0.21", features = [
|
||||
"client",
|
||||
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
||||
arc-swap = "1.7.1"
|
||||
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"
|
||||
gethostname = "1.0.2"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.9"
|
||||
hex = "0.4.3"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
runas = "=1.2.0"
|
||||
@@ -100,146 +93,55 @@ winapi = { version = "0.3.9", features = [
|
||||
"errhandlingapi",
|
||||
"minwindef",
|
||||
"winerror",
|
||||
"tlhelp32",
|
||||
"processthreadsapi",
|
||||
"winhttp",
|
||||
"winreg",
|
||||
] }
|
||||
windows-sys = { version = "0.61.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
signal-hook = "0.3.18"
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
users = "0.11.0"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2.5.1"
|
||||
tauri-plugin-global-shortcut = "2.3.1"
|
||||
tauri-plugin-updater = "2.9.0"
|
||||
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-global-shortcut = "2.2.1"
|
||||
tauri-plugin-updater = "2.7.1"
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
verge-dev = ["clash_verge_logger/color"]
|
||||
tauri-dev = []
|
||||
tokio-trace = ["console-subscriber"]
|
||||
clippy = ["tauri/test"]
|
||||
tracing = []
|
||||
|
||||
[[bench]]
|
||||
name = "draft_benchmark"
|
||||
path = "benches/draft_benchmark.rs"
|
||||
harness = false
|
||||
verge-dev = []
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
lto = "thin"
|
||||
opt-level = 3
|
||||
debug = false
|
||||
lto = true
|
||||
opt-level = "s"
|
||||
strip = true
|
||||
overflow-checks = false
|
||||
rpath = false
|
||||
|
||||
[profile.dev]
|
||||
incremental = true
|
||||
codegen-units = 64
|
||||
opt-level = 0
|
||||
debug = true
|
||||
strip = "none"
|
||||
overflow-checks = true
|
||||
lto = false
|
||||
rpath = false
|
||||
codegen-units = 256 # 增加编译单元,提升编译速度
|
||||
opt-level = 0 # 禁用优化,进一步提升编译速度
|
||||
debug = true # 保留调试信息
|
||||
strip = false # 不剥离符号,保留调试信息
|
||||
|
||||
[profile.fast-release]
|
||||
inherits = "release"
|
||||
codegen-units = 64
|
||||
incremental = true
|
||||
lto = false
|
||||
opt-level = 0
|
||||
debug = true
|
||||
strip = false
|
||||
inherits = "release" # 继承 release 的配置
|
||||
panic = "abort" # 与 release 相同
|
||||
codegen-units = 256 # 增加编译单元,提升编译速度
|
||||
lto = false # 禁用 LTO,提升编译速度
|
||||
opt-level = 0 # 禁用优化,大幅提升编译速度
|
||||
debug = true # 保留调试信息
|
||||
strip = false # 不剥离符号,保留调试信息
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||
tempfile = "3.20.0"
|
||||
|
||||
[lints.clippy]
|
||||
# Core categories - most important for code safety and correctness
|
||||
correctness = { level = "deny", priority = -1 }
|
||||
suspicious = { level = "deny", priority = -1 }
|
||||
[workspace]
|
||||
members = ["src_crates/crate_mihomo_api"]
|
||||
|
||||
# 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 }
|
||||
|
||||
or_fun_call = "deny"
|
||||
cognitive_complexity = "deny"
|
||||
useless_let_if_seq = "deny"
|
||||
use_self = "deny"
|
||||
tuple_array_conversions = "deny"
|
||||
trait_duplication_in_bounds = "deny"
|
||||
suspicious_operation_groupings = "deny"
|
||||
string_lit_as_bytes = "deny"
|
||||
significant_drop_tightening = "deny"
|
||||
significant_drop_in_scrutinee = "deny"
|
||||
redundant_clone = "deny"
|
||||
# option_if_let_else = "deny" // 过于激进,暂时不开启
|
||||
needless_pass_by_ref_mut = "deny"
|
||||
needless_collect = "deny"
|
||||
missing_const_for_fn = "deny"
|
||||
iter_with_drain = "deny"
|
||||
iter_on_single_items = "deny"
|
||||
iter_on_empty_collections = "deny"
|
||||
# fallible_impl_from = "deny" // 过于激进,暂时不开启
|
||||
equatable_if_let = "deny"
|
||||
collection_is_never_read = "deny"
|
||||
branches_sharing_code = "deny"
|
||||
# [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" }
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use std::hint::black_box;
|
||||
use std::process;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use app_lib::config::IVerge;
|
||||
use app_lib::utils::Draft as DraftNew;
|
||||
|
||||
/// 创建测试数据
|
||||
fn make_draft() -> DraftNew<IVerge> {
|
||||
let verge = IVerge {
|
||||
enable_auto_launch: Some(true),
|
||||
enable_tun_mode: Some(false),
|
||||
..Default::default()
|
||||
};
|
||||
DraftNew::new(verge)
|
||||
}
|
||||
|
||||
pub fn bench_draft(c: &mut Criterion) {
|
||||
let rt = Runtime::new().unwrap_or_else(|e| {
|
||||
eprintln!("Tokio runtime init failed: {e}");
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
let mut group = c.benchmark_group("draft");
|
||||
group.sample_size(100);
|
||||
group.warm_up_time(std::time::Duration::from_millis(300));
|
||||
group.measurement_time(std::time::Duration::from_secs(1));
|
||||
|
||||
group.bench_function("data_mut", |b| {
|
||||
b.iter(|| {
|
||||
let draft = black_box(make_draft());
|
||||
draft.edit_draft(|d| d.enable_tun_mode = Some(true));
|
||||
black_box(&draft.latest_arc().enable_tun_mode);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("draft_mut_first", |b| {
|
||||
b.iter(|| {
|
||||
let draft = black_box(make_draft());
|
||||
draft.edit_draft(|d| d.enable_auto_launch = Some(false));
|
||||
let latest = draft.latest_arc();
|
||||
black_box(&latest.enable_auto_launch);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("draft_mut_existing", |b| {
|
||||
b.iter(|| {
|
||||
let draft = black_box(make_draft());
|
||||
{
|
||||
draft.edit_draft(|d| {
|
||||
d.enable_tun_mode = Some(true);
|
||||
});
|
||||
let latest1 = draft.latest_arc();
|
||||
black_box(&latest1.enable_tun_mode);
|
||||
}
|
||||
draft.edit_draft(|d| {
|
||||
d.enable_tun_mode = Some(false);
|
||||
});
|
||||
let latest2 = draft.latest_arc();
|
||||
black_box(&latest2.enable_tun_mode);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("latest_arc", |b| {
|
||||
b.iter(|| {
|
||||
let draft = black_box(make_draft());
|
||||
let latest = draft.latest_arc();
|
||||
black_box(&latest.enable_auto_launch);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("apply", |b| {
|
||||
b.iter(|| {
|
||||
let draft = black_box(make_draft());
|
||||
{
|
||||
draft.edit_draft(|d| {
|
||||
d.enable_auto_launch = Some(false);
|
||||
});
|
||||
}
|
||||
draft.apply();
|
||||
black_box(&draft);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("discard", |b| {
|
||||
b.iter(|| {
|
||||
let draft = black_box(make_draft());
|
||||
{
|
||||
draft.edit_draft(|d| {
|
||||
d.enable_auto_launch = Some(false);
|
||||
});
|
||||
}
|
||||
draft.discard();
|
||||
black_box(&draft);
|
||||
});
|
||||
});
|
||||
|
||||
group.bench_function("with_data_modify_async", |b| {
|
||||
b.to_async(&rt).iter(|| async {
|
||||
let draft = black_box(make_draft());
|
||||
let _: Result<(), anyhow::Error> = draft
|
||||
.with_data_modify::<_, _, _>(|mut box_data| async move {
|
||||
box_data.enable_auto_launch =
|
||||
Some(!box_data.enable_auto_launch.unwrap_or(false));
|
||||
Ok((box_data, ()))
|
||||
})
|
||||
.await;
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_draft);
|
||||
criterion_main!(benches);
|
||||
+1
-7
@@ -1,9 +1,3 @@
|
||||
fn main() {
|
||||
#[cfg(feature = "clippy")]
|
||||
{
|
||||
println!("cargo:warning=Skipping tauri_build during Clippy");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "clippy"))]
|
||||
tauri_build::build();
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
@@ -17,14 +17,6 @@
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled",
|
||||
"core:window:allow-set-theme",
|
||||
"notification:default",
|
||||
"http:default",
|
||||
"http:allow-fetch",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [{ "url": "https://*/*" }, { "url": "http://*/*" }]
|
||||
},
|
||||
"mihomo:default"
|
||||
"core:window:allow-set-theme"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
# 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.
|
Before Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB |
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
chmod +x /usr/bin/clash-verge-service-install
|
||||
chmod +x /usr/bin/clash-verge-service-uninstall
|
||||
chmod +x /usr/bin/install-service
|
||||
chmod +x /usr/bin/uninstall-service
|
||||
chmod +x /usr/bin/clash-verge-service
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/bash
|
||||
/usr/bin/clash-verge-service-uninstall
|
||||
/usr/bin/uninstall-service
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,3 +11,4 @@ merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
||||
imports_granularity = "Crate"
|
||||
|
||||
+79
-108
@@ -1,64 +1,42 @@
|
||||
use super::CmdResult;
|
||||
use crate::core::sysopt::Sysopt;
|
||||
use crate::{
|
||||
cmd::StringifyErr,
|
||||
feat, logging,
|
||||
utils::{
|
||||
dirs::{self, PathBufExec},
|
||||
logging::Type,
|
||||
},
|
||||
utils::{dirs, logging::Type},
|
||||
wrap_err,
|
||||
};
|
||||
use smartstring::alias::String;
|
||||
use std::path::Path;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tauri::Manager;
|
||||
|
||||
/// 打开应用程序所在目录
|
||||
#[tauri::command]
|
||||
pub async fn open_app_dir() -> CmdResult<()> {
|
||||
let app_dir = dirs::app_home_dir().stringify_err()?;
|
||||
open::that(app_dir).stringify_err()
|
||||
pub fn open_app_dir() -> CmdResult<()> {
|
||||
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
||||
wrap_err!(open::that(app_dir))
|
||||
}
|
||||
|
||||
/// 打开核心所在目录
|
||||
#[tauri::command]
|
||||
pub async fn open_core_dir() -> CmdResult<()> {
|
||||
let core_dir = tauri::utils::platform::current_exe().stringify_err()?;
|
||||
pub fn open_core_dir() -> CmdResult<()> {
|
||||
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
||||
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
|
||||
open::that(core_dir).stringify_err()
|
||||
wrap_err!(open::that(core_dir))
|
||||
}
|
||||
|
||||
/// 打开日志目录
|
||||
#[tauri::command]
|
||||
pub async fn open_logs_dir() -> CmdResult<()> {
|
||||
let log_dir = dirs::app_logs_dir().stringify_err()?;
|
||||
open::that(log_dir).stringify_err()
|
||||
pub fn open_logs_dir() -> CmdResult<()> {
|
||||
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
||||
wrap_err!(open::that(log_dir))
|
||||
}
|
||||
|
||||
/// 打开网页链接
|
||||
#[tauri::command]
|
||||
pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||
open::that(url.as_str()).stringify_err()
|
||||
}
|
||||
|
||||
// TODO 后续可以为前端提供接口,当前作为托盘菜单使用
|
||||
/// 打开 Verge 最新日志
|
||||
#[tauri::command]
|
||||
pub async fn open_app_log() -> CmdResult<()> {
|
||||
open::that(dirs::app_latest_log().stringify_err()?).stringify_err()
|
||||
}
|
||||
|
||||
// TODO 后续可以为前端提供接口,当前作为托盘菜单使用
|
||||
/// 打开 Clash 最新日志
|
||||
#[tauri::command]
|
||||
pub async fn open_core_log() -> CmdResult<()> {
|
||||
open::that(dirs::clash_latest_log().stringify_err()?).stringify_err()
|
||||
wrap_err!(open::that(url))
|
||||
}
|
||||
|
||||
/// 打开/关闭开发者工具
|
||||
#[tauri::command]
|
||||
pub fn open_devtools(app_handle: AppHandle) {
|
||||
pub fn open_devtools(app_handle: tauri::AppHandle) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
if !window.is_devtools_open() {
|
||||
window.open_devtools();
|
||||
@@ -70,14 +48,14 @@ pub fn open_devtools(app_handle: AppHandle) {
|
||||
|
||||
/// 退出应用
|
||||
#[tauri::command]
|
||||
pub async fn exit_app() {
|
||||
feat::quit().await;
|
||||
pub fn exit_app() {
|
||||
feat::quit();
|
||||
}
|
||||
|
||||
/// 重启应用
|
||||
#[tauri::command]
|
||||
pub async fn restart_app() -> CmdResult<()> {
|
||||
feat::restart_app().await;
|
||||
feat::restart_app();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -90,39 +68,36 @@ pub fn get_portable_flag() -> CmdResult<bool> {
|
||||
/// 获取应用目录
|
||||
#[tauri::command]
|
||||
pub fn get_app_dir() -> CmdResult<String> {
|
||||
let app_home_dir = dirs::app_home_dir()
|
||||
.stringify_err()?
|
||||
let app_home_dir = wrap_err!(dirs::app_home_dir())?
|
||||
.to_string_lossy()
|
||||
.into();
|
||||
.to_string();
|
||||
Ok(app_home_dir)
|
||||
}
|
||||
|
||||
/// 获取当前自启动状态
|
||||
#[tauri::command]
|
||||
pub fn get_auto_launch_status() -> CmdResult<bool> {
|
||||
Sysopt::global().get_launch_status().stringify_err()
|
||||
use crate::core::sysopt::Sysopt;
|
||||
wrap_err!(Sysopt::global().get_launch_status())
|
||||
}
|
||||
|
||||
/// 下载图标缓存
|
||||
#[tauri::command]
|
||||
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
||||
let icon_cache_dir = dirs::app_home_dir()
|
||||
.stringify_err()?
|
||||
.join("icons")
|
||||
.join("cache");
|
||||
let icon_path = icon_cache_dir.join(name.as_str());
|
||||
let icon_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache");
|
||||
let icon_path = icon_cache_dir.join(&name);
|
||||
|
||||
if icon_path.exists() {
|
||||
return Ok(icon_path.to_string_lossy().into());
|
||||
return Ok(icon_path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
if !icon_cache_dir.exists() {
|
||||
let _ = fs::create_dir_all(&icon_cache_dir).await;
|
||||
let _ = std::fs::create_dir_all(&icon_cache_dir);
|
||||
}
|
||||
|
||||
let temp_path = icon_cache_dir.join(format!("{}.downloading", name.as_str()));
|
||||
let temp_path = icon_cache_dir.join(format!("{}.downloading", &name));
|
||||
|
||||
let response = reqwest::get(url.as_str()).await.stringify_err()?;
|
||||
let response = wrap_err!(reqwest::get(&url).await)?;
|
||||
|
||||
let content_type = response
|
||||
.headers()
|
||||
@@ -132,7 +107,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
||||
|
||||
let is_image = content_type.starts_with("image/");
|
||||
|
||||
let content = response.bytes().await.stringify_err()?;
|
||||
let content = wrap_err!(response.bytes().await)?;
|
||||
|
||||
let is_html = content.len() > 15
|
||||
&& (content.starts_with(b"<!DOCTYPE html")
|
||||
@@ -141,37 +116,38 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
|
||||
|
||||
if is_image && !is_html {
|
||||
{
|
||||
let mut file = match fs::File::create(&temp_path).await {
|
||||
let mut file = match std::fs::File::create(&temp_path) {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
if icon_path.exists() {
|
||||
return Ok(icon_path.to_string_lossy().into());
|
||||
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());
|
||||
}
|
||||
};
|
||||
file.write_all(content.as_ref()).await.stringify_err()?;
|
||||
file.flush().await.stringify_err()?;
|
||||
|
||||
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
|
||||
}
|
||||
|
||||
if !icon_path.exists() {
|
||||
match fs::rename(&temp_path, &icon_path).await {
|
||||
match std::fs::rename(&temp_path, &icon_path) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
let _ = temp_path.remove_if_exists().await;
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
if icon_path.exists() {
|
||||
return Ok(icon_path.to_string_lossy().into());
|
||||
return Ok(icon_path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = temp_path.remove_if_exists().await;
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
}
|
||||
|
||||
Ok(icon_path.to_string_lossy().into())
|
||||
Ok(icon_path.to_string_lossy().to_string())
|
||||
} else {
|
||||
let _ = temp_path.remove_if_exists().await;
|
||||
Err(format!("下载的内容不是有效图片: {}", url.as_str()).into())
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
Err(format!("下载的内容不是有效图片: {}", url))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,74 +160,66 @@ pub struct IconInfo {
|
||||
|
||||
/// 复制图标文件
|
||||
#[tauri::command]
|
||||
pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
||||
let file_path = Path::new(path.as_str());
|
||||
pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
|
||||
use std::{fs, path::Path};
|
||||
|
||||
let icon_dir = dirs::app_home_dir().stringify_err()?.join("icons");
|
||||
let file_path = Path::new(&path);
|
||||
|
||||
let icon_dir = wrap_err!(dirs::app_home_dir())?.join("icons");
|
||||
if !icon_dir.exists() {
|
||||
let _ = fs::create_dir_all(&icon_dir).await;
|
||||
let _ = fs::create_dir_all(&icon_dir);
|
||||
}
|
||||
let ext: String = match file_path.extension() {
|
||||
Some(e) => e.to_string_lossy().into(),
|
||||
None => "ico".into(),
|
||||
let ext = match file_path.extension() {
|
||||
Some(e) => e.to_string_lossy().to_string(),
|
||||
None => "ico".to_string(),
|
||||
};
|
||||
|
||||
let dest_path = icon_dir.join(format!(
|
||||
"{0}-{1}.{ext}",
|
||||
icon_info.name.as_str(),
|
||||
icon_info.current_t.as_str()
|
||||
icon_info.name, icon_info.current_t
|
||||
));
|
||||
if file_path.exists() {
|
||||
if icon_info.previous_t.trim() != "" {
|
||||
icon_dir
|
||||
.join(format!(
|
||||
"{0}-{1}.png",
|
||||
icon_info.name.as_str(),
|
||||
icon_info.previous_t.as_str()
|
||||
))
|
||||
.remove_if_exists()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
icon_dir
|
||||
.join(format!(
|
||||
"{0}-{1}.ico",
|
||||
icon_info.name.as_str(),
|
||||
icon_info.previous_t.as_str()
|
||||
))
|
||||
.remove_if_exists()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
fs::remove_file(
|
||||
icon_dir.join(format!("{0}-{1}.png", icon_info.name, icon_info.previous_t)),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
fs::remove_file(
|
||||
icon_dir.join(format!("{0}-{1}.ico", icon_info.name, icon_info.previous_t)),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"Copying icon file path: {:?} -> file dist: {:?}",
|
||||
path,
|
||||
dest_path
|
||||
);
|
||||
match fs::copy(file_path, &dest_path).await {
|
||||
Ok(_) => Ok(dest_path.to_string_lossy().into()),
|
||||
Err(err) => Err(err.to_string().into()),
|
||||
match fs::copy(file_path, &dest_path) {
|
||||
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
} else {
|
||||
Err("file not found".into())
|
||||
Err("file not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// 通知UI已准备就绪
|
||||
#[tauri::command]
|
||||
pub fn notify_ui_ready() -> CmdResult<()> {
|
||||
logging!(info, Type::Cmd, "前端UI已准备就绪");
|
||||
crate::utils::resolve::ui::mark_ui_ready();
|
||||
log::info!(target: "app", "前端UI已准备就绪");
|
||||
crate::utils::resolve::mark_ui_ready();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// UI加载阶段
|
||||
#[tauri::command]
|
||||
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
logging!(info, Type::Cmd, "UI加载阶段更新: {}", stage.as_str());
|
||||
log::info!(target: "app", "UI加载阶段更新: {}", stage);
|
||||
|
||||
use crate::utils::resolve::ui::UiReadyStage;
|
||||
use crate::utils::resolve::UiReadyStage;
|
||||
|
||||
let stage_enum = match stage.as_str() {
|
||||
"NotStarted" => UiReadyStage::NotStarted,
|
||||
@@ -260,16 +228,19 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
|
||||
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
|
||||
"Ready" => UiReadyStage::Ready,
|
||||
_ => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Cmd,
|
||||
"Warning: 未知的UI加载阶段: {}",
|
||||
stage.as_str()
|
||||
);
|
||||
return Err(format!("未知的UI加载阶段: {}", stage.as_str()).into());
|
||||
log::warn!(target: "app", "未知的UI加载阶段: {}", stage);
|
||||
return Err(format!("未知的UI加载阶段: {}", stage));
|
||||
}
|
||||
};
|
||||
|
||||
crate::utils::resolve::ui::update_ui_ready_stage(stage_enum);
|
||||
crate::utils::resolve::update_ui_ready_stage(stage_enum);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 重置UI就绪状态
|
||||
#[tauri::command]
|
||||
pub fn reset_ui_ready_state() -> CmdResult<()> {
|
||||
log::info!(target: "app", "重置UI就绪状态");
|
||||
crate::utils::resolve::reset_ui_ready();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
use super::CmdResult;
|
||||
use crate::{cmd::StringifyErr, feat};
|
||||
use feat::LocalBackupFile;
|
||||
use smartstring::alias::String;
|
||||
|
||||
/// Create a local backup
|
||||
#[tauri::command]
|
||||
pub async fn create_local_backup() -> CmdResult<()> {
|
||||
feat::create_local_backup().await.stringify_err()
|
||||
}
|
||||
|
||||
/// List local backups
|
||||
#[tauri::command]
|
||||
pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
|
||||
feat::list_local_backup().await.stringify_err()
|
||||
}
|
||||
|
||||
/// Delete local backup
|
||||
#[tauri::command]
|
||||
pub async fn delete_local_backup(filename: String) -> CmdResult<()> {
|
||||
feat::delete_local_backup(filename).await.stringify_err()
|
||||
}
|
||||
|
||||
/// Restore local backup
|
||||
#[tauri::command]
|
||||
pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
|
||||
feat::restore_local_backup(filename).await.stringify_err()
|
||||
}
|
||||
|
||||
/// Export local backup to a user selected destination
|
||||
#[tauri::command]
|
||||
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
|
||||
feat::export_local_backup(filename, destination)
|
||||
.await
|
||||
.stringify_err()
|
||||
}
|
||||
+149
-153
@@ -1,75 +1,65 @@
|
||||
use super::CmdResult;
|
||||
use crate::utils::dirs;
|
||||
use crate::{
|
||||
cmd::StringifyErr,
|
||||
config::{ClashInfo, Config},
|
||||
constants,
|
||||
core::{CoreManager, handle, validate::CoreConfigValidator},
|
||||
config::*, core::*, feat, module::mihomo::MihomoManager, process::AsyncHandler, wrap_err,
|
||||
};
|
||||
use crate::{feat, logging, utils::logging::Type};
|
||||
use compact_str::CompactString;
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use tokio::fs;
|
||||
use serde_yaml::Mapping;
|
||||
|
||||
/// 复制Clash环境变量
|
||||
#[tauri::command]
|
||||
pub async fn copy_clash_env() -> CmdResult {
|
||||
feat::copy_clash_env().await;
|
||||
pub fn copy_clash_env() -> CmdResult {
|
||||
feat::copy_clash_env();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取Clash信息
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
Ok(Config::clash().await.latest_arc().get_client_info())
|
||||
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
Ok(Config::clash().latest().get_client_info())
|
||||
}
|
||||
|
||||
/// 修改Clash配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||
feat::patch_clash(payload).await.stringify_err()
|
||||
wrap_err!(feat::patch_clash(payload).await)
|
||||
}
|
||||
|
||||
/// 修改Clash模式
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||
feat::change_clash_mode(payload).await;
|
||||
feat::change_clash_mode(payload);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 切换Clash核心
|
||||
#[tauri::command]
|
||||
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
|
||||
logging!(info, Type::Config, "changing core to {clash_core}");
|
||||
log::info!(target: "app", "changing core to {clash_core}");
|
||||
|
||||
match CoreManager::global().change_core(&clash_core).await {
|
||||
match CoreManager::global()
|
||||
.change_core(Some(clash_core.clone()))
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
// 切换内核后重启内核
|
||||
match CoreManager::global().restart_core().await {
|
||||
Ok(_) => {
|
||||
logging!(
|
||||
info,
|
||||
Type::Core,
|
||||
"core changed and restarted to {clash_core}"
|
||||
);
|
||||
handle::Handle::notice_message("config_core::change_success", clash_core);
|
||||
log::info!(target: "app", "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: String =
|
||||
format!("Core changed but failed to restart: {err}").into();
|
||||
handle::Handle::notice_message("config_core::change_error", error_msg.clone());
|
||||
logging!(error, Type::Core, "{error_msg}");
|
||||
let error_msg = format!("Core changed but failed to restart: {}", err);
|
||||
log::error!(target: "app", "{}", error_msg);
|
||||
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||
Ok(Some(error_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg: String = err;
|
||||
logging!(error, Type::Core, "failed to change core: {error_msg}");
|
||||
handle::Handle::notice_message("config_core::change_error", error_msg.clone());
|
||||
let error_msg = err.to_string();
|
||||
log::error!(target: "app", "failed to change core: {error_msg}");
|
||||
handle::Handle::notice_message("config_core::change_error", &error_msg);
|
||||
Ok(Some(error_msg))
|
||||
}
|
||||
}
|
||||
@@ -78,142 +68,151 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
||||
/// 启动核心
|
||||
#[tauri::command]
|
||||
pub async fn start_core() -> CmdResult {
|
||||
let result = CoreManager::global().start_core().await.stringify_err();
|
||||
if result.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
result
|
||||
wrap_err!(CoreManager::global().start_core().await)
|
||||
}
|
||||
|
||||
/// 关闭核心
|
||||
#[tauri::command]
|
||||
pub async fn stop_core() -> CmdResult {
|
||||
let result = CoreManager::global().stop_core().await.stringify_err();
|
||||
if result.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
result
|
||||
wrap_err!(CoreManager::global().stop_core().await)
|
||||
}
|
||||
|
||||
/// 重启核心
|
||||
#[tauri::command]
|
||||
pub async fn restart_core() -> CmdResult {
|
||||
let result = CoreManager::global().restart_core().await.stringify_err();
|
||||
if result.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
result
|
||||
wrap_err!(CoreManager::global().restart_core().await)
|
||||
}
|
||||
|
||||
/// 获取代理延迟
|
||||
#[tauri::command]
|
||||
pub async fn clash_api_get_proxy_delay(
|
||||
name: String,
|
||||
url: Option<String>,
|
||||
timeout: i32,
|
||||
) -> CmdResult<serde_json::Value> {
|
||||
MihomoManager::global()
|
||||
.test_proxy_delay(&name, url, timeout)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 测试URL延迟
|
||||
#[tauri::command]
|
||||
pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||
let result = match feat::test_delay(url).await {
|
||||
Ok(delay) => delay,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
10000u32
|
||||
}
|
||||
};
|
||||
Ok(result)
|
||||
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
|
||||
}
|
||||
|
||||
/// 保存DNS配置到单独文件
|
||||
#[tauri::command]
|
||||
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
||||
use crate::utils::dirs;
|
||||
use serde_yaml_ng;
|
||||
use tokio::fs;
|
||||
use serde_yaml;
|
||||
use std::fs;
|
||||
|
||||
// 获取DNS配置文件路径
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.stringify_err()?
|
||||
.join(constants::files::DNS_CONFIG);
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("dns_config.yaml");
|
||||
|
||||
// 保存DNS配置到文件
|
||||
let yaml_str = serde_yaml_ng::to_string(&dns_config).stringify_err()?;
|
||||
fs::write(&dns_path, yaml_str).await.stringify_err()?;
|
||||
logging!(info, Type::Config, "DNS config saved to {dns_path:?}");
|
||||
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);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 应用或撤销DNS配置
|
||||
#[tauri::command]
|
||||
pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
if apply {
|
||||
// 读取DNS配置文件
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.stringify_err()?
|
||||
.join(constants::files::DNS_CONFIG);
|
||||
pub fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{handle, CoreManager},
|
||||
utils::dirs,
|
||||
};
|
||||
|
||||
if !dns_path.exists() {
|
||||
logging!(warn, Type::Config, "DNS config file not found");
|
||||
return Err("DNS config file not found".into());
|
||||
// 使用spawn来处理异步操作
|
||||
AsyncHandler::spawn(move || async move {
|
||||
if apply {
|
||||
// 读取DNS配置文件
|
||||
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);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !dns_path.exists() {
|
||||
log::warn!(target: "app", "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);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 解析DNS配置并创建patch
|
||||
let patch_config = match serde_yaml::from_str::<serde_yaml::Mapping>(&dns_yaml) {
|
||||
Ok(config) => {
|
||||
let mut patch = serde_yaml::Mapping::new();
|
||||
patch.insert("dns".into(), config.into());
|
||||
patch
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to parse DNS config: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
log::info!(target: "app", "Applying DNS config from file");
|
||||
|
||||
// 重新生成配置,确保DNS配置被正确应用
|
||||
// 这里不调用patch_clash以避免将DNS配置写入config.yaml
|
||||
Config::runtime()
|
||||
.latest()
|
||||
.patch_config(patch_config.clone());
|
||||
|
||||
// 首先重新生成配置
|
||||
if let Err(err) = Config::generate().await {
|
||||
log::error!(target: "app", "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);
|
||||
} else {
|
||||
log::info!(target: "app", "DNS config successfully applied");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
} else {
|
||||
// 当关闭DNS设置时,不需要对配置进行任何修改
|
||||
// 直接重新生成配置,让enhance函数自动跳过DNS配置的加载
|
||||
log::info!(target: "app", "DNS settings disabled, regenerating config");
|
||||
|
||||
// 重新生成配置
|
||||
if let Err(err) = Config::generate().await {
|
||||
log::error!(target: "app", "Failed to regenerate config: {}", err);
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用新配置
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
log::info!(target: "app", "Config regenerated successfully");
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "Failed to apply regenerated config: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let dns_yaml = fs::read_to_string(&dns_path).await.stringify_err_log(|e| {
|
||||
logging!(error, Type::Config, "Failed to read DNS config: {e}");
|
||||
})?;
|
||||
|
||||
// 解析DNS配置
|
||||
let patch_config = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
|
||||
.stringify_err_log(|e| {
|
||||
logging!(error, Type::Config, "Failed to parse DNS config: {e}");
|
||||
})?;
|
||||
|
||||
logging!(info, Type::Config, "Applying DNS config from file");
|
||||
|
||||
// 创建包含DNS配置的patch
|
||||
let mut patch = serde_yaml_ng::Mapping::new();
|
||||
patch.insert("dns".into(), patch_config.into());
|
||||
|
||||
// 应用DNS配置到运行时配置
|
||||
Config::runtime().await.edit_draft(|d| {
|
||||
d.patch_config(patch);
|
||||
});
|
||||
|
||||
// 重新生成配置
|
||||
Config::generate().await.stringify_err_log(|err| {
|
||||
let err = format!("Failed to regenerate config with DNS: {err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
})?;
|
||||
|
||||
// 应用新配置
|
||||
CoreManager::global()
|
||||
.update_config()
|
||||
.await
|
||||
.stringify_err_log(|err| {
|
||||
let err = format!("Failed to apply config with DNS: {err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
})?;
|
||||
|
||||
logging!(info, Type::Config, "DNS config successfully applied");
|
||||
} else {
|
||||
// 当关闭DNS设置时,重新生成配置(不加载DNS配置文件)
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"DNS settings disabled, regenerating config"
|
||||
);
|
||||
|
||||
Config::generate().await.stringify_err_log(|err| {
|
||||
let err = format!("Failed to regenerate config: {err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
})?;
|
||||
|
||||
CoreManager::global()
|
||||
.update_config()
|
||||
.await
|
||||
.stringify_err_log(|err| {
|
||||
let err = format!("Failed to apply regenerated config: {err}");
|
||||
logging!(error, Type::Config, "{err}");
|
||||
})?;
|
||||
|
||||
logging!(info, Type::Config, "Config regenerated successfully");
|
||||
}
|
||||
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -223,8 +222,8 @@ pub fn check_dns_config_exists() -> CmdResult<bool> {
|
||||
use crate::utils::dirs;
|
||||
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.stringify_err()?
|
||||
.join(constants::files::DNS_CONFIG);
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("dns_config.yaml");
|
||||
|
||||
Ok(dns_path.exists())
|
||||
}
|
||||
@@ -233,41 +232,38 @@ pub fn check_dns_config_exists() -> CmdResult<bool> {
|
||||
#[tauri::command]
|
||||
pub async fn get_dns_config_content() -> CmdResult<String> {
|
||||
use crate::utils::dirs;
|
||||
use tokio::fs;
|
||||
use std::fs;
|
||||
|
||||
let dns_path = dirs::app_home_dir()
|
||||
.stringify_err()?
|
||||
.join(constants::files::DNS_CONFIG);
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("dns_config.yaml");
|
||||
|
||||
if !fs::try_exists(&dns_path).await.stringify_err()? {
|
||||
if !dns_path.exists() {
|
||||
return Err("DNS config file not found".into());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&dns_path).await.stringify_err()?.into();
|
||||
let content = fs::read_to_string(&dns_path).map_err(|e| e.to_string())?;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// 验证DNS配置文件
|
||||
#[tauri::command]
|
||||
pub async fn validate_dns_config() -> CmdResult<(bool, String)> {
|
||||
let app_dir = dirs::app_home_dir().stringify_err()?;
|
||||
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
|
||||
use crate::{core::CoreManager, utils::dirs};
|
||||
|
||||
let app_dir = dirs::app_home_dir().map_err(|e| e.to_string())?;
|
||||
let dns_path = app_dir.join("dns_config.yaml");
|
||||
let dns_path_str = dns_path.to_str().unwrap_or_default();
|
||||
|
||||
if !dns_path.exists() {
|
||||
return Ok((false, "DNS config file not found".into()));
|
||||
return Ok((false, "DNS config file not found".to_string()));
|
||||
}
|
||||
|
||||
CoreConfigValidator::validate_config_file(dns_path_str, None)
|
||||
match CoreManager::global()
|
||||
.validate_config_file(dns_path_str, None)
|
||||
.await
|
||||
.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_clash_logs() -> CmdResult<Vec<CompactString>> {
|
||||
let logs = CoreManager::global()
|
||||
.get_clash_logs()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
Ok(logs)
|
||||
{
|
||||
Ok(result) => Ok(result),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ use super::CmdResult;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn entry_lightweight_mode() -> CmdResult {
|
||||
lightweight::entry_lightweight_mode().await;
|
||||
lightweight::entry_lightweight_mode();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn exit_lightweight_mode() -> CmdResult {
|
||||
lightweight::exit_lightweight_mode().await;
|
||||
lightweight::exit_lightweight_mode();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,124 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, cookie::Jar};
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
let cookie_store = Arc::new(Jar::default());
|
||||
|
||||
let client_with_cookies = match 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() {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to create client with cookies for Bahamut Anime: {}",
|
||||
e
|
||||
);
|
||||
client.clone()
|
||||
}
|
||||
};
|
||||
|
||||
let device_url = "https://ani.gamer.com.tw/ajax/getdeviceid.php";
|
||||
let device_id = match client_with_cookies.get(device_url).send().await {
|
||||
Ok(response) => match response.text().await {
|
||||
Ok(text) => 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(),
|
||||
},
|
||||
Err(_) => String::new(),
|
||||
};
|
||||
|
||||
if device_id.is_empty() {
|
||||
return UnlockItem {
|
||||
name: "Bahamut Anime".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
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) => match response.text().await {
|
||||
Ok(body) => {
|
||||
if body.contains("animeSn") {
|
||||
Some(body)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
if token_result.is_none() {
|
||||
return UnlockItem {
|
||||
name: "Bahamut Anime".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let region = match client_with_cookies
|
||||
.get("https://ani.gamer.com.tw/")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => match response.text().await {
|
||||
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}")
|
||||
}),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile region regex for Bahamut Anime: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
UnlockItem {
|
||||
name: "Bahamut Anime".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
use reqwest::Client;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::get_local_date_string;
|
||||
|
||||
pub(super) async fn check_bilibili_china_mainland(client: &Client) -> UnlockItem {
|
||||
let url = "https://api.bilibili.com/pgc/player/web/playurl?avid=82846771&qn=0&type=&otype=json&ep_id=307247&fourk=1&fnver=0&fnval=16&module=bangumi";
|
||||
|
||||
match client.get(url).send().await {
|
||||
Ok(response) => match response.json::<Value>().await {
|
||||
Ok(body) => {
|
||||
let status = body
|
||||
.get("code")
|
||||
.and_then(|v| v.as_i64())
|
||||
.map(|code| {
|
||||
if code == 0 {
|
||||
"Yes"
|
||||
} else if code == -10403 {
|
||||
"No"
|
||||
} else {
|
||||
"Failed"
|
||||
}
|
||||
})
|
||||
.unwrap_or("Failed");
|
||||
|
||||
UnlockItem {
|
||||
name: "哔哩哔哩大陆".to_string(),
|
||||
status: status.to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "哔哩哔哩大陆".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
},
|
||||
Err(_) => UnlockItem {
|
||||
name: "哔哩哔哩大陆".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn check_bilibili_hk_mc_tw(client: &Client) -> UnlockItem {
|
||||
let url = "https://api.bilibili.com/pgc/player/web/playurl?avid=18281381&cid=29892777&qn=0&type=&otype=json&ep_id=183799&fourk=1&fnver=0&fnval=16&module=bangumi";
|
||||
|
||||
match client.get(url).send().await {
|
||||
Ok(response) => match response.json::<Value>().await {
|
||||
Ok(body) => {
|
||||
let status = body
|
||||
.get("code")
|
||||
.and_then(|v| v.as_i64())
|
||||
.map(|code| {
|
||||
if code == 0 {
|
||||
"Yes"
|
||||
} else if code == -10403 {
|
||||
"No"
|
||||
} else {
|
||||
"Failed"
|
||||
}
|
||||
})
|
||||
.unwrap_or("Failed");
|
||||
|
||||
UnlockItem {
|
||||
name: "哔哩哔哩港澳台".to_string(),
|
||||
status: status.to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "哔哩哔哩港澳台".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
},
|
||||
Err(_) => UnlockItem {
|
||||
name: "哔哩哔哩港澳台".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use reqwest::Client;
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_chatgpt_combined(client: &Client) -> Vec<UnlockItem> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
let url_country = "https://chat.openai.com/cdn-cgi/trace";
|
||||
let result_country = client.get(url_country).send().await;
|
||||
|
||||
let region = match result_country {
|
||||
Ok(response) => {
|
||||
if let Ok(body) = response.text().await {
|
||||
let mut map = HashMap::new();
|
||||
for line in body.lines() {
|
||||
if let Some(index) = line.find('=') {
|
||||
let key = &line[..index];
|
||||
let value = &line[index + 1..];
|
||||
map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
map.get("loc").map(|loc| {
|
||||
let emoji = country_code_to_emoji(loc);
|
||||
format!("{emoji}{loc}")
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
let url_ios = "https://ios.chat.openai.com/";
|
||||
let result_ios = client.get(url_ios).send().await;
|
||||
|
||||
let ios_status = match result_ios {
|
||||
Ok(response) => {
|
||||
if let Ok(body) = response.text().await {
|
||||
let body_lower = body.to_lowercase();
|
||||
if body_lower.contains("you may be connected to a disallowed isp") {
|
||||
"Disallowed ISP"
|
||||
} else if body_lower.contains("request is not allowed. please try again later.") {
|
||||
"Yes"
|
||||
} else if body_lower.contains("sorry, you have been blocked") {
|
||||
"Blocked"
|
||||
} else {
|
||||
"Failed"
|
||||
}
|
||||
} else {
|
||||
"Failed"
|
||||
}
|
||||
}
|
||||
Err(_) => "Failed",
|
||||
};
|
||||
|
||||
let url_web = "https://api.openai.com/compliance/cookie_requirements";
|
||||
let result_web = client.get(url_web).send().await;
|
||||
|
||||
let web_status = match result_web {
|
||||
Ok(response) => {
|
||||
if let Ok(body) = response.text().await {
|
||||
let body_lower = body.to_lowercase();
|
||||
if body_lower.contains("unsupported_country") {
|
||||
"Unsupported Country/Region"
|
||||
} else {
|
||||
"Yes"
|
||||
}
|
||||
} else {
|
||||
"Failed"
|
||||
}
|
||||
}
|
||||
Err(_) => "Failed",
|
||||
};
|
||||
|
||||
results.push(UnlockItem {
|
||||
name: "ChatGPT iOS".to_string(),
|
||||
status: ios_status.to_string(),
|
||||
region: region.clone(),
|
||||
check_time: Some(get_local_date_string()),
|
||||
});
|
||||
|
||||
results.push(UnlockItem {
|
||||
name: "ChatGPT Web".to_string(),
|
||||
status: web_status.to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
});
|
||||
|
||||
results
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
use reqwest::Client;
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
const BLOCKED_CODES: [&str; 10] = ["AF", "BY", "CN", "CU", "HK", "IR", "KP", "MO", "RU", "SY"];
|
||||
|
||||
pub(super) async fn check_claude(client: &Client) -> UnlockItem {
|
||||
let url = "https://claude.ai/cdn-cgi/trace";
|
||||
|
||||
match client.get(url).send().await {
|
||||
Ok(response) => match response.text().await {
|
||||
Ok(body) => {
|
||||
let mut country_code: Option<String> = None;
|
||||
|
||||
for line in body.lines() {
|
||||
if let Some(rest) = line.strip_prefix("loc=") {
|
||||
country_code = Some(rest.trim().to_uppercase());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(code) = country_code {
|
||||
let emoji = country_code_to_emoji(&code);
|
||||
let status = if BLOCKED_CODES.contains(&code.as_str()) {
|
||||
"No"
|
||||
} else {
|
||||
"Yes"
|
||||
};
|
||||
|
||||
UnlockItem {
|
||||
name: "Claude".to_string(),
|
||||
status: status.to_string(),
|
||||
region: Some(format!("{emoji}{code}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
} else {
|
||||
UnlockItem {
|
||||
name: "Claude".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Claude".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
},
|
||||
Err(_) => UnlockItem {
|
||||
name: "Claude".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,490 +0,0 @@
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
let device_api_url = "https://disney.api.edge.bamgrid.com/devices";
|
||||
let auth_header =
|
||||
"Bearer ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84";
|
||||
|
||||
let device_req_body = serde_json::json!({
|
||||
"deviceFamily": "browser",
|
||||
"applicationRuntime": "chrome",
|
||||
"deviceProfile": "windows",
|
||||
"attributes": {}
|
||||
});
|
||||
|
||||
let device_result = client
|
||||
.post(device_api_url)
|
||||
.header("authorization", auth_header)
|
||||
.header("content-type", "application/json; charset=UTF-8")
|
||||
.json(&device_req_body)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if device_result.is_err() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
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()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if device_response.status().as_u16() == 403 {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No (IP Banned By Disney+)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let device_body = match device_response.text().await {
|
||||
Ok(body) => body,
|
||||
Err(_) => {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Error: Cannot read response)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
if assertion.is_none() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Error: Cannot extract assertion)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let token_url = "https://disney.api.edge.bamgrid.com/token";
|
||||
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",
|
||||
"urn:ietf:params:oauth:grant-type:token-exchange",
|
||||
),
|
||||
("latitude", "0"),
|
||||
("longitude", "0"),
|
||||
("platform", "browser"),
|
||||
("subject_token", assertion_str.as_str()),
|
||||
(
|
||||
"subject_token_type",
|
||||
"urn:bamtech:params:oauth:token-type:device",
|
||||
),
|
||||
];
|
||||
|
||||
let token_result = client
|
||||
.post(token_url)
|
||||
.header("authorization", auth_header)
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.form(&token_body)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if token_result.is_err() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
let token_body_text = match token_response.text().await {
|
||||
Ok(body) => body,
|
||||
Err(_) => {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Error: Cannot read token response)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if token_body_text.contains("forbidden-location") || token_body_text.contains("403 ERROR") {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No (IP Banned By Disney+)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let token_json: Result<serde_json::Value, _> = serde_json::from_str(&token_body_text);
|
||||
|
||||
let refresh_token = match token_json {
|
||||
Ok(json) => json
|
||||
.get("refresh_token")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
Err(_) => 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
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if refresh_token.is_none() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: format!(
|
||||
"Failed (Error: Cannot extract refresh token, status: {}, response: {})",
|
||||
token_status.as_u16(),
|
||||
token_body_text.chars().take(100).collect::<String>() + "..."
|
||||
),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let graphql_url = "https://disney.api.edge.bamgrid.com/graph/v1/device/graphql";
|
||||
|
||||
let graphql_payload = format!(
|
||||
r#"{{"query":"mutation refreshToken($input: RefreshTokenInput!) {{ refreshToken(refreshToken: $input) {{ activeSession {{ sessionId }} }} }}","variables":{{"input":{{"refreshToken":"{}"}}}}}}"#,
|
||||
refresh_token.unwrap_or_default()
|
||||
);
|
||||
|
||||
let graphql_result = client
|
||||
.post(graphql_url)
|
||||
.header("authorization", auth_header)
|
||||
.header("content-type", "application/json")
|
||||
.body(graphql_payload)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if graphql_result.is_err() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let preview_check = client.get("https://disneyplus.com").send().await;
|
||||
|
||||
let is_unavailable = match preview_check {
|
||||
Ok(response) => {
|
||||
let url = response.url().to_string();
|
||||
url.contains("preview") || url.contains("unavailable")
|
||||
}
|
||||
Err(_) => true,
|
||||
};
|
||||
|
||||
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 = match graphql_response.text().await {
|
||||
Ok(text) => text,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to read Disney+ GraphQL response text: {}",
|
||||
e
|
||||
);
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
|
||||
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) => 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())),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Disney+ main page region regex: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
if let Some(region) = region_from_main {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region} (from main page)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if graphql_body_text.is_empty() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: format!(
|
||||
"Failed (GraphQL error: empty response, status: {})",
|
||||
graphql_status.as_u16()
|
||||
),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
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: "Failed (Regex Error)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
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 = 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"));
|
||||
|
||||
if region_code.is_none() {
|
||||
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
|
||||
Ok(response) => match response.text().await {
|
||||
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())),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Disney+ main page region regex: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
if let Some(region) = region_from_main {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region} (from main page)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
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" {
|
||||
let emoji = country_code_to_emoji("JP");
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if is_unavailable {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
match in_supported_location {
|
||||
Some(false) => {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Soon".to_string(),
|
||||
region: Some(format!("{emoji}{region}(即将上线)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
Some(true) => {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
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})"),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_gemini(client: &Client) -> UnlockItem {
|
||||
let url = "https://gemini.google.com";
|
||||
|
||||
match client.get(url).send().await {
|
||||
Ok(response) => {
|
||||
if let Ok(body) = response.text().await {
|
||||
let is_ok = body.contains("45631641,null,true");
|
||||
let status = if is_ok { "Yes" } else { "No" };
|
||||
|
||||
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}")
|
||||
})
|
||||
});
|
||||
|
||||
UnlockItem {
|
||||
name: "Gemini".to_string(),
|
||||
status: status.to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
} else {
|
||||
UnlockItem {
|
||||
name: "Gemini".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Gemini".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use reqwest::Client;
|
||||
use tauri::command;
|
||||
use tokio::{sync::Mutex, task::JoinSet};
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
mod bahamut;
|
||||
mod bilibili;
|
||||
mod chatgpt;
|
||||
mod claude;
|
||||
mod disney_plus;
|
||||
mod gemini;
|
||||
mod netflix;
|
||||
mod prime_video;
|
||||
mod spotify;
|
||||
mod tiktok;
|
||||
mod types;
|
||||
mod utils;
|
||||
mod youtube;
|
||||
|
||||
pub use types::UnlockItem;
|
||||
|
||||
use bahamut::check_bahamut_anime;
|
||||
use bilibili::{check_bilibili_china_mainland, check_bilibili_hk_mc_tw};
|
||||
use chatgpt::check_chatgpt_combined;
|
||||
use claude::check_claude;
|
||||
use disney_plus::check_disney_plus;
|
||||
use gemini::check_gemini;
|
||||
use netflix::check_netflix;
|
||||
use prime_video::check_prime_video;
|
||||
use spotify::check_spotify;
|
||||
use tiktok::check_tiktok;
|
||||
use youtube::check_youtube_premium;
|
||||
|
||||
#[command]
|
||||
pub async fn get_unlock_items() -> Result<Vec<UnlockItem>, String> {
|
||||
Ok(types::default_unlock_items())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
let client = match Client::builder()
|
||||
.user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.tcp_keepalive(std::time::Duration::from_secs(60))
|
||||
.connection_verbose(true)
|
||||
.build() {
|
||||
Ok(client) => client,
|
||||
Err(e) => return Err(format!("创建HTTP客户端失败: {e}")),
|
||||
};
|
||||
|
||||
let results = Arc::new(Mutex::new(Vec::new()));
|
||||
let mut tasks = JoinSet::new();
|
||||
let client_arc = Arc::new(client);
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_bilibili_china_mainland(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_bilibili_hk_mc_tw(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
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;
|
||||
results.extend(chatgpt_results);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_claude(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_gemini(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_youtube_premium(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_bahamut_anime(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_netflix(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_disney_plus(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_spotify(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_tiktok(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let client = Arc::clone(&client_arc);
|
||||
let results = Arc::clone(&results);
|
||||
tasks.spawn(async move {
|
||||
let result = check_prime_video(&client).await;
|
||||
results.lock().await.push(result);
|
||||
});
|
||||
}
|
||||
|
||||
while let Some(res) = tasks.join_next().await {
|
||||
if let Err(e) = res {
|
||||
eprintln!("任务执行失败: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
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,220 +0,0 @@
|
||||
use reqwest::Client;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
let cdn_result = check_netflix_cdn(client).await;
|
||||
if cdn_result.status == "Yes" {
|
||||
return cdn_result;
|
||||
}
|
||||
|
||||
let url1 = "https://www.netflix.com/title/81280792";
|
||||
let url2 = "https://www.netflix.com/title/70143836";
|
||||
|
||||
let result1 = client
|
||||
.get(url1)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if let Err(e) = &result1 {
|
||||
eprintln!("Netflix请求错误: {e}");
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let result2 = client
|
||||
.get(url2)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if let Err(e) = &result2 {
|
||||
eprintln!("Netflix请求错误: {e}");
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Originals Only".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if status1 == 403 || status2 == 403 {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if status1 == 200 || status1 == 301 || status2 == 200 || status2 == 301 {
|
||||
let test_url = "https://www.netflix.com/title/80018499";
|
||||
match client
|
||||
.get(test_url)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
if let Some(location) = response.headers().get("location")
|
||||
&& let Ok(location_str) = location.to_str()
|
||||
{
|
||||
let parts: Vec<&str> = location_str.split('/').collect();
|
||||
if parts.len() >= 4 {
|
||||
let region_code = parts[3].split('-').next().unwrap_or("unknown");
|
||||
let emoji = country_code_to_emoji(region_code);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region_code}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let emoji = country_code_to_emoji("us");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{}", "us")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("获取Netflix区域信息失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes (但无法获取区域)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: format!("Failed (状态码: {status1}_{status2}"),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
let url = "https://api.fast.com/netflix/speedtest/v2?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=5";
|
||||
|
||||
match client
|
||||
.get(url)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
if response.status().as_u16() == 403 {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "No (IP Banned By Netflix)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
match response.json::<Value>().await {
|
||||
Ok(data) => {
|
||||
if let Some(targets) = data.get("targets").and_then(|t| t.as_array())
|
||||
&& !targets.is_empty()
|
||||
&& let Some(location) = targets[0].get("location")
|
||||
&& let Some(country) = location.get("country").and_then(|c| c.as_str())
|
||||
{
|
||||
let emoji = country_code_to_emoji(country);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{country}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Unknown".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("解析Fast.com API响应失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (解析错误)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Fast.com API请求失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (CDN API)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_prime_video(client: &Client) -> UnlockItem {
|
||||
let url = "https://www.primevideo.com";
|
||||
|
||||
let result = client.get(url).send().await;
|
||||
|
||||
if result.is_err() {
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
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 = 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()));
|
||||
|
||||
if is_blocked {
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "No (Service Not Available)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(region) = region_code {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
if !is_blocked {
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Error: PAGE ERROR)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Error: Unknown Region)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Error: Cannot read response)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
use reqwest::{Client, Url};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_spotify(client: &Client) -> UnlockItem {
|
||||
let url = "https://www.spotify.com/api/content/v1/country-selector?platform=web&format=json";
|
||||
|
||||
match client.get(url).send().await {
|
||||
Ok(response) => {
|
||||
let final_url = response.url().clone();
|
||||
let status_code = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
|
||||
let region = extract_region(&final_url).or_else(|| extract_region_from_body(&body));
|
||||
let status = determine_status(status_code.as_u16(), &body);
|
||||
|
||||
UnlockItem {
|
||||
name: "Spotify".to_string(),
|
||||
status: status.to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Spotify".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_status(status: u16, body: &str) -> &'static str {
|
||||
if status == 403 || status == 451 {
|
||||
return "No";
|
||||
}
|
||||
|
||||
if !(200..300).contains(&status) {
|
||||
return "Failed";
|
||||
}
|
||||
|
||||
let body_lower = body.to_lowercase();
|
||||
if body_lower.contains("not available in your country") {
|
||||
return "No";
|
||||
}
|
||||
|
||||
"Yes"
|
||||
}
|
||||
|
||||
fn extract_region(url: &Url) -> Option<String> {
|
||||
let mut segments = url.path_segments()?;
|
||||
let first_segment = segments.next()?;
|
||||
|
||||
if first_segment.is_empty() || first_segment == "api" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let country_code = first_segment.split('-').next().unwrap_or(first_segment);
|
||||
let upper = country_code.to_uppercase();
|
||||
let emoji = country_code_to_emoji(&upper);
|
||||
Some(format!("{emoji}{upper}"))
|
||||
}
|
||||
|
||||
fn extract_region_from_body(body: &str) -> Option<String> {
|
||||
let marker = "\"countryCode\":\"";
|
||||
if let Some(idx) = body.find(marker) {
|
||||
let start = idx + marker.len();
|
||||
let rest = &body[start..];
|
||||
if let Some(end) = rest.find('"') {
|
||||
let code = rest[..end].to_uppercase();
|
||||
if !code.is_empty() {
|
||||
let emoji = country_code_to_emoji(&code);
|
||||
return Some(format!("{emoji}{code}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_tiktok(client: &Client) -> UnlockItem {
|
||||
let trace_url = "https://www.tiktok.com/cdn-cgi/trace";
|
||||
|
||||
let mut status = String::from("Failed");
|
||||
let mut region = None;
|
||||
|
||||
if let Ok(response) = client.get(trace_url).send().await {
|
||||
let status_code = response.status().as_u16();
|
||||
if let Ok(body) = response.text().await {
|
||||
status = determine_status(status_code, &body).to_string();
|
||||
region = extract_region_from_body(&body);
|
||||
}
|
||||
}
|
||||
|
||||
if (region.is_none() || status == "Failed")
|
||||
&& let Ok(response) = client.get("https://www.tiktok.com/").send().await
|
||||
{
|
||||
let status_code = response.status().as_u16();
|
||||
if let Ok(body) = response.text().await {
|
||||
let fallback_status = determine_status(status_code, &body);
|
||||
let fallback_region = extract_region_from_body(&body);
|
||||
|
||||
if status != "No" {
|
||||
status = fallback_status.to_string();
|
||||
}
|
||||
|
||||
if region.is_none() {
|
||||
region = fallback_region;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UnlockItem {
|
||||
name: "TikTok".to_string(),
|
||||
status,
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_status(status: u16, body: &str) -> &'static str {
|
||||
if status == 403 || status == 451 {
|
||||
return "No";
|
||||
}
|
||||
|
||||
if !(200..300).contains(&status) {
|
||||
return "Failed";
|
||||
}
|
||||
|
||||
let body_lower = body.to_lowercase();
|
||||
if body_lower.contains("access denied")
|
||||
|| body_lower.contains("not available in your region")
|
||||
|| body_lower.contains("tiktok is not available")
|
||||
{
|
||||
return "No";
|
||||
}
|
||||
|
||||
"Yes"
|
||||
}
|
||||
|
||||
fn extract_region_from_body(body: &str) -> Option<String> {
|
||||
static REGION_REGEX: OnceLock<Option<Regex>> = OnceLock::new();
|
||||
let regex = REGION_REGEX
|
||||
.get_or_init(|| Regex::new(r#""region"\s*:\s*"([a-zA-Z-]+)""#).ok())
|
||||
.as_ref()?;
|
||||
|
||||
if let Some(caps) = regex.captures(body)
|
||||
&& let Some(matched) = caps.get(1)
|
||||
{
|
||||
let raw = matched.as_str();
|
||||
let country_code = raw.split('-').next().unwrap_or(raw).to_uppercase();
|
||||
if !country_code.is_empty() {
|
||||
let emoji = country_code_to_emoji(&country_code);
|
||||
return Some(format!("{emoji}{country_code}"));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UnlockItem {
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
pub region: Option<String>,
|
||||
pub check_time: Option<String>,
|
||||
}
|
||||
|
||||
impl UnlockItem {
|
||||
pub fn pending(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
status: "Pending".to_string(),
|
||||
region: None,
|
||||
check_time: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_UNLOCK_ITEM_NAMES: [&str; 13] = [
|
||||
"哔哩哔哩大陆",
|
||||
"哔哩哔哩港澳台",
|
||||
"ChatGPT iOS",
|
||||
"ChatGPT Web",
|
||||
"Claude",
|
||||
"Gemini",
|
||||
"Youtube Premium",
|
||||
"Bahamut Anime",
|
||||
"Netflix",
|
||||
"Disney+",
|
||||
"Prime Video",
|
||||
"Spotify",
|
||||
"TikTok",
|
||||
];
|
||||
|
||||
pub fn default_unlock_items() -> Vec<UnlockItem> {
|
||||
DEFAULT_UNLOCK_ITEM_NAMES
|
||||
.iter()
|
||||
.map(|name| UnlockItem::pending(name))
|
||||
.collect()
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
use chrono::Local;
|
||||
|
||||
pub fn get_local_date_string() -> String {
|
||||
let now = Local::now();
|
||||
now.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
}
|
||||
|
||||
pub fn country_code_to_emoji(country_code: &str) -> String {
|
||||
let country_code = country_code.to_uppercase();
|
||||
if country_code.len() < 2 {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let bytes = country_code.as_bytes();
|
||||
let c1 = 0x1F1E6 + (bytes[0] as u32) - ('A' as u32);
|
||||
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}")))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
||||
let url = "https://www.youtube.com/premium";
|
||||
|
||||
match client.get(url).send().await {
|
||||
Ok(response) => {
|
||||
if let Ok(body) = response.text().await {
|
||||
let body_lower = body.to_lowercase();
|
||||
let mut status = "Failed";
|
||||
let mut region = None;
|
||||
|
||||
if body_lower.contains("youtube premium is not available in your country") {
|
||||
status = "No";
|
||||
} else if body_lower.contains("ad-free") {
|
||||
match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
|
||||
Ok(re) => {
|
||||
if let Some(caps) = re.captures(&body)
|
||||
&& let Some(m) = caps.get(1)
|
||||
{
|
||||
let country_code = m.as_str().trim();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
region = Some(format!("{emoji}{country_code}"));
|
||||
status = "Yes";
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile YouTube Premium regex: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: status.to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
} else {
|
||||
UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use smartstring::alias::String;
|
||||
|
||||
// Common result type used by command functions
|
||||
pub type CmdResult<T = ()> = Result<T, String>;
|
||||
|
||||
// Command modules
|
||||
pub mod app;
|
||||
pub mod backup;
|
||||
pub mod clash;
|
||||
pub mod lightweight;
|
||||
pub mod media_unlock_checker;
|
||||
@@ -23,7 +22,6 @@ pub mod webdav;
|
||||
|
||||
// Re-export all command functions for backwards compatibility
|
||||
pub use app::*;
|
||||
pub use backup::*;
|
||||
pub use clash::*;
|
||||
pub use lightweight::*;
|
||||
pub use media_unlock_checker::*;
|
||||
@@ -38,27 +36,3 @@ pub use uwp::*;
|
||||
pub use validate::*;
|
||||
pub use verge::*;
|
||||
pub use webdav::*;
|
||||
|
||||
pub trait StringifyErr<T> {
|
||||
fn stringify_err(self) -> CmdResult<T>;
|
||||
fn stringify_err_log<F>(self, log_fn: F) -> CmdResult<T>
|
||||
where
|
||||
F: Fn(&str);
|
||||
}
|
||||
|
||||
impl<T, E: std::fmt::Display> StringifyErr<T> for Result<T, E> {
|
||||
fn stringify_err(self) -> CmdResult<T> {
|
||||
self.map_err(|e| e.to_string().into())
|
||||
}
|
||||
|
||||
fn stringify_err_log<F>(self, log_fn: F) -> CmdResult<T>
|
||||
where
|
||||
F: Fn(&str),
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let msg = String::from(e.to_string());
|
||||
log_fn(&msg);
|
||||
msg
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use super::CmdResult;
|
||||
use crate::cmd::StringifyErr;
|
||||
use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery};
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use crate::wrap_err;
|
||||
use network_interface::NetworkInterface;
|
||||
use serde_yaml_ng::Mapping;
|
||||
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> {
|
||||
logging!(debug, Type::Network, "异步获取系统代理配置");
|
||||
|
||||
let current = AsyncProxyQuery::get_system_proxy().await;
|
||||
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))?;
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
@@ -21,41 +21,21 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
);
|
||||
map.insert("bypass".into(), current.bypass.into());
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"返回系统代理配置: enable={}, {}:{}",
|
||||
current.enable,
|
||||
current.host,
|
||||
current.port
|
||||
);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// 获取自动代理配置
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
logging!(debug, Type::Network, "开始获取自动代理配置(事件驱动)");
|
||||
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
|
||||
let current = proxy_manager.get_auto_proxy_cached().await;
|
||||
// 异步请求更新,立即返回缓存数据
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let _ = proxy_manager.get_auto_proxy_async().await;
|
||||
});
|
||||
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))?;
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert("url".into(), current.url.clone().into());
|
||||
map.insert("url".into(), current.url.into());
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"返回自动代理配置(缓存): enable={}, url={}",
|
||||
current.enable,
|
||||
current.url
|
||||
);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
@@ -69,7 +49,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()
|
||||
}
|
||||
@@ -96,7 +76,7 @@ pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
||||
use network_interface::{NetworkInterface, NetworkInterfaceConfig};
|
||||
|
||||
let names = get_network_interfaces();
|
||||
let interfaces = NetworkInterface::show().stringify_err()?;
|
||||
let interfaces = wrap_err!(NetworkInterface::show())?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
|
||||
+317
-409
@@ -1,420 +1,344 @@
|
||||
use super::CmdResult;
|
||||
use super::StringifyErr;
|
||||
use crate::{
|
||||
config::{
|
||||
Config, IProfiles, PrfItem, PrfOption,
|
||||
profiles::{
|
||||
profiles_append_item_with_filedata_safe, profiles_delete_item_safe,
|
||||
profiles_patch_item_safe, profiles_reorder_safe, profiles_save_file_safe,
|
||||
},
|
||||
profiles_append_item_safe,
|
||||
},
|
||||
core::{CoreManager, handle, timer::Timer, tray::Tray},
|
||||
feat, logging,
|
||||
process::AsyncHandler,
|
||||
ret_err,
|
||||
config::{Config, IProfiles, PrfItem, PrfOption},
|
||||
core::{handle, timer::Timer, tray::Tray, CoreManager},
|
||||
feat, logging, ret_err,
|
||||
utils::{dirs, help, logging::Type},
|
||||
wrap_err,
|
||||
};
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
|
||||
// 添加全局互斥锁防止并发配置更新
|
||||
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
|
||||
|
||||
/// 获取配置文件列表
|
||||
#[tauri::command]
|
||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||
logging!(debug, Type::Cmd, "获取配置文件列表");
|
||||
let draft = Config::profiles().await;
|
||||
let data = (**draft.data_arc()).clone();
|
||||
Ok(data)
|
||||
let profiles_result = tokio::time::timeout(
|
||||
Duration::from_secs(3), // 3秒超时
|
||||
tokio::task::spawn_blocking(move || Config::profiles().data().clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
match profiles_result {
|
||||
Ok(Ok(profiles)) => Ok(profiles),
|
||||
Ok(Err(join_err)) => {
|
||||
logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err);
|
||||
Ok(IProfiles {
|
||||
current: None,
|
||||
items: Some(vec![]),
|
||||
})
|
||||
}
|
||||
Err(_) => {
|
||||
// 超时情况
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"获取配置列表超时(3秒),可能存在锁竞争"
|
||||
);
|
||||
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![]),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 增强配置文件
|
||||
#[tauri::command]
|
||||
pub async fn enhance_profiles() -> CmdResult {
|
||||
match feat::enhance_profiles().await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
return Err(e.to_string().into());
|
||||
}
|
||||
}
|
||||
wrap_err!(feat::enhance_profiles().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 导入配置文件
|
||||
#[tauri::command]
|
||||
pub async fn import_profile(url: std::string::String, option: Option<PrfOption>) -> CmdResult {
|
||||
logging!(info, Type::Cmd, "[导入订阅] 开始导入: {}", url);
|
||||
|
||||
// 直接依赖 PrfItem::from_url 自身的超时/重试逻辑,不再使用 tokio::time::timeout 包裹
|
||||
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
|
||||
Ok(it) => {
|
||||
logging!(info, Type::Cmd, "[导入订阅] 下载完成,开始保存配置");
|
||||
it
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "[导入订阅] 下载失败: {}", e);
|
||||
return Err(format!("导入订阅失败: {}", e).into());
|
||||
}
|
||||
};
|
||||
|
||||
match profiles_append_item_safe(item).await {
|
||||
Ok(_) => match profiles_save_file_safe().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Cmd, "[导入订阅] 配置文件保存成功");
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "[导入订阅] 保存配置文件失败: {}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "[导入订阅] 保存配置失败: {}", e);
|
||||
return Err(format!("导入订阅失败: {}", e).into());
|
||||
}
|
||||
}
|
||||
// 立即发送配置变更通知
|
||||
if let Some(uid) = &item.uid {
|
||||
logging!(info, Type::Cmd, "[导入订阅] 发送配置变更通知: {}", uid);
|
||||
handle::Handle::notify_profile_changed(uid.clone());
|
||||
}
|
||||
|
||||
// 异步保存配置文件并发送全局通知
|
||||
let uid_clone = item.uid.clone();
|
||||
if let Some(uid) = uid_clone {
|
||||
// 延迟发送,确保文件已完全写入
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
handle::Handle::notify_profile_changed(uid);
|
||||
}
|
||||
|
||||
logging!(info, Type::Cmd, "[导入订阅] 导入完成: {}", url);
|
||||
Ok(())
|
||||
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))
|
||||
}
|
||||
|
||||
/// 调整profile的顺序
|
||||
/// 重新排序配置文件
|
||||
#[tauri::command]
|
||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
match profiles_reorder_safe(&active_id, &over_id).await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Cmd, "重新排序配置文件");
|
||||
Config::profiles().await.apply();
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::profiles().await.discard();
|
||||
logging!(error, Type::Cmd, "重新排序配置文件失败: {}", err);
|
||||
Err(format!("重新排序配置文件失败: {}", err).into())
|
||||
}
|
||||
}
|
||||
wrap_err!(Config::profiles().data().reorder(active_id, over_id))
|
||||
}
|
||||
|
||||
/// 创建新的profile
|
||||
/// 创建一个新的配置文件
|
||||
/// 创建配置文件
|
||||
#[tauri::command]
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||
match profiles_append_item_with_filedata_safe(&item, file_data).await {
|
||||
Ok(_) => {
|
||||
// 发送配置变更通知
|
||||
if let Some(uid) = &item.uid {
|
||||
logging!(info, Type::Cmd, "[创建订阅] 发送配置变更通知: {}", uid);
|
||||
handle::Handle::notify_profile_changed(uid.clone());
|
||||
}
|
||||
Config::profiles().await.apply();
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::profiles().await.discard();
|
||||
match err.to_string().as_str() {
|
||||
"the file already exists" => Err("the file already exists".into()),
|
||||
_ => Err(format!("add profile error: {err}").into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
}
|
||||
|
||||
/// 更新配置文件
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||
match feat::update_profile(&index, option.as_ref(), true, true).await {
|
||||
Ok(_) => {
|
||||
let _: () = Config::profiles().await.apply();
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
Config::profiles().await.discard();
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
Err(e.to_string().into())
|
||||
}
|
||||
}
|
||||
wrap_err!(feat::update_profile(index, option, Some(true)).await)
|
||||
}
|
||||
|
||||
/// 删除配置文件
|
||||
#[tauri::command]
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
// 使用Send-safe helper函数
|
||||
let should_update = profiles_delete_item_safe(&index).await.stringify_err()?;
|
||||
profiles_save_file_safe().await.stringify_err()?;
|
||||
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
||||
if should_update {
|
||||
Config::profiles().await.apply();
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
// 发送配置变更通知
|
||||
logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", index);
|
||||
handle::Handle::notify_profile_changed(index);
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
return Err(e.to_string().into());
|
||||
}
|
||||
}
|
||||
wrap_err!(CoreManager::global().update_config().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 验证新配置文件的语法
|
||||
async fn validate_new_profile(new_profile: &String) -> Result<(), ()> {
|
||||
logging!(info, Type::Cmd, "正在切换到新配置: {}", new_profile);
|
||||
|
||||
// 获取目标配置文件路径
|
||||
let config_file_result = {
|
||||
let profiles_config = Config::profiles().await;
|
||||
let profiles_data = profiles_config.latest_arc();
|
||||
match profiles_data.get_item(new_profile) {
|
||||
Ok(item) => {
|
||||
if let Some(file) = &item.file {
|
||||
let path = dirs::app_profiles_dir().map(|dir| dir.join(file.as_str()));
|
||||
path.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "获取目标配置信息失败: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 如果获取到文件路径,检查YAML语法
|
||||
if let Some(file_path) = config_file_result {
|
||||
if !file_path.exists() {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"目标配置文件不存在: {}",
|
||||
file_path.display()
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_not_found",
|
||||
format!("{}", file_path.display()),
|
||||
);
|
||||
return Err(());
|
||||
}
|
||||
|
||||
// 超时保护
|
||||
let file_read_result = tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
tokio::fs::read_to_string(&file_path),
|
||||
)
|
||||
.await;
|
||||
|
||||
match file_read_result {
|
||||
Ok(Ok(content)) => {
|
||||
let yaml_parse_result = AsyncHandler::spawn_blocking(move || {
|
||||
serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&content)
|
||||
})
|
||||
.await;
|
||||
|
||||
match yaml_parse_result {
|
||||
Ok(Ok(_)) => {
|
||||
logging!(info, Type::Cmd, "目标配置文件语法正确");
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!(" {err}");
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"目标配置文件存在YAML语法错误:{}",
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_syntax_error",
|
||||
error_msg,
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
Err(join_err) => {
|
||||
let error_msg = format!("YAML解析任务失败: {join_err}");
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_parse_error",
|
||||
error_msg,
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!("无法读取目标配置文件: {err}");
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message("config_validate::file_read_error", error_msg);
|
||||
Err(())
|
||||
}
|
||||
Err(_) => {
|
||||
let error_msg = "读取配置文件超时(5秒)".to_string();
|
||||
logging!(error, Type::Cmd, "{}", error_msg);
|
||||
handle::Handle::notice_message("config_validate::file_read_timeout", error_msg);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 执行配置更新并处理结果
|
||||
async fn restore_previous_profile(prev_profile: &String) -> CmdResult<()> {
|
||||
logging!(info, Type::Cmd, "尝试恢复到之前的配置: {}", prev_profile);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile.to_owned()),
|
||||
items: None,
|
||||
};
|
||||
Config::profiles()
|
||||
.await
|
||||
.edit_draft(|d| d.patch_config(&restore_profiles));
|
||||
Config::profiles().await.apply();
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
logging!(warn, Type::Cmd, "Warning: 异步保存恢复配置文件失败: {e}");
|
||||
}
|
||||
});
|
||||
logging!(info, Type::Cmd, "成功恢复到之前的配置");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_success(current_value: Option<&String>) -> CmdResult<bool> {
|
||||
Config::profiles().await.apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
if let Err(e) = Tray::global().update_tooltip().await {
|
||||
logging!(warn, Type::Cmd, "Warning: 异步更新托盘提示失败: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = Tray::global().update_menu().await {
|
||||
logging!(warn, Type::Cmd, "Warning: 异步更新托盘菜单失败: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = profiles_save_file_safe().await {
|
||||
logging!(warn, Type::Cmd, "Warning: 异步保存配置文件失败: {e}");
|
||||
}
|
||||
|
||||
if let Some(current) = current_value {
|
||||
logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current);
|
||||
handle::Handle::notify_profile_changed(current.to_owned());
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn handle_validation_failure(
|
||||
error_msg: String,
|
||||
current_profile: Option<&String>,
|
||||
) -> CmdResult<bool> {
|
||||
logging!(warn, Type::Cmd, "配置验证失败: {}", error_msg);
|
||||
Config::profiles().await.discard();
|
||||
if let Some(prev_profile) = current_profile {
|
||||
restore_previous_profile(prev_profile).await?;
|
||||
}
|
||||
handle::Handle::notice_message("config_validate::error", error_msg);
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn handle_update_error<E: std::fmt::Display>(e: E) -> CmdResult<bool> {
|
||||
logging!(warn, Type::Cmd, "更新过程发生错误: {}", e,);
|
||||
Config::profiles().await.discard();
|
||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn handle_timeout(current_profile: Option<&String>) -> CmdResult<bool> {
|
||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||
logging!(error, Type::Cmd, "{}", timeout_msg);
|
||||
Config::profiles().await.discard();
|
||||
if let Some(prev_profile) = current_profile {
|
||||
restore_previous_profile(prev_profile).await?;
|
||||
}
|
||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn perform_config_update(
|
||||
current_value: Option<&String>,
|
||||
current_profile: Option<&String>,
|
||||
) -> CmdResult<bool> {
|
||||
defer! {
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
}
|
||||
let update_result = tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
CoreManager::global().update_config(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match update_result {
|
||||
Ok(Ok((true, _))) => handle_success(current_value).await,
|
||||
Ok(Ok((false, error_msg))) => handle_validation_failure(error_msg, current_profile).await,
|
||||
Ok(Err(e)) => handle_update_error(e).await,
|
||||
Err(_) => handle_timeout(current_profile).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
if CURRENT_SWITCHING_PROFILE
|
||||
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_err()
|
||||
{
|
||||
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
|
||||
return Ok(false);
|
||||
}
|
||||
// 获取互斥锁,防止并发执行
|
||||
let _guard = PROFILE_UPDATE_MUTEX.lock().await;
|
||||
|
||||
let target_profile = profiles.current.as_ref();
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"开始修改配置文件,目标profile: {:?}",
|
||||
target_profile
|
||||
);
|
||||
logging!(info, Type::Cmd, true, "开始修改配置文件");
|
||||
|
||||
// 保存当前配置,以便在验证失败时恢复
|
||||
let previous_profile = Config::profiles().await.data_arc().current.clone();
|
||||
logging!(info, Type::Cmd, "当前配置: {:?}", previous_profile);
|
||||
let current_profile = Config::profiles().latest().current.clone();
|
||||
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
|
||||
|
||||
// 如果要切换配置,先检查目标配置文件是否有语法错误
|
||||
if let Some(switch_to_profile) = target_profile
|
||||
&& previous_profile.as_ref() != Some(switch_to_profile)
|
||||
&& validate_new_profile(switch_to_profile).await.is_err()
|
||||
{
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
return Ok(false);
|
||||
}
|
||||
Config::profiles()
|
||||
.await
|
||||
.edit_draft(|d| d.patch_config(&profiles));
|
||||
if let Some(new_profile) = profiles.current.as_ref() {
|
||||
if current_profile.as_ref() != Some(new_profile) {
|
||||
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile);
|
||||
|
||||
perform_config_update(target_profile, previous_profile.as_ref()).await
|
||||
// 获取目标配置文件路径
|
||||
let config_file_result = {
|
||||
let profiles_config = Config::profiles();
|
||||
let profiles_data = profiles_config.latest();
|
||||
match profiles_data.get_item(new_profile) {
|
||||
Ok(item) => {
|
||||
if let Some(file) = &item.file {
|
||||
let path = dirs::app_profiles_dir().map(|dir| dir.join(file));
|
||||
path.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 如果获取到文件路径,检查YAML语法
|
||||
if let Some(file_path) = config_file_result {
|
||||
if !file_path.exists() {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"目标配置文件不存在: {}",
|
||||
file_path.display()
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_not_found",
|
||||
format!("{}", file_path.display()),
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 超时保护
|
||||
let file_read_result = tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
tokio::fs::read_to_string(&file_path),
|
||||
)
|
||||
.await;
|
||||
|
||||
match file_read_result {
|
||||
Ok(Ok(content)) => {
|
||||
let yaml_parse_result = tokio::task::spawn_blocking(move || {
|
||||
serde_yaml::from_str::<serde_yaml::Value>(&content)
|
||||
})
|
||||
.await;
|
||||
|
||||
match yaml_parse_result {
|
||||
Ok(Ok(_)) => {
|
||||
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!(" {}", err);
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"目标配置文件存在YAML语法错误:{}",
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::yaml_syntax_error",
|
||||
&error_msg,
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
Err(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",
|
||||
&error_msg,
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
let error_msg = format!("无法读取目标配置文件: {}", err);
|
||||
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_read_error",
|
||||
&error_msg,
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
Err(_) => {
|
||||
let error_msg = "读取配置文件超时(5秒)".to_string();
|
||||
logging!(error, Type::Cmd, true, "{}", error_msg);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::file_read_timeout",
|
||||
&error_msg,
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新profiles配置
|
||||
logging!(info, Type::Cmd, true, "正在更新配置草稿");
|
||||
|
||||
let current_value = profiles.current.clone();
|
||||
|
||||
let _ = Config::profiles().draft().patch_config(profiles);
|
||||
|
||||
// 为配置更新添加超时保护
|
||||
let update_result = tokio::time::timeout(
|
||||
Duration::from_secs(30), // 30秒超时
|
||||
CoreManager::global().update_config(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// 更新配置并进行验证
|
||||
match update_result {
|
||||
Ok(Ok((true, _))) => {
|
||||
logging!(info, Type::Cmd, true, "配置更新成功");
|
||||
Config::profiles().apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
crate::process::AsyncHandler::spawn(|| async move {
|
||||
if let Err(e) = Tray::global().update_tooltip() {
|
||||
log::warn!(target: "app", "异步更新托盘提示失败: {}", e);
|
||||
}
|
||||
|
||||
if let Err(e) = Tray::global().update_menu() {
|
||||
log::warn!(target: "app", "异步更新托盘菜单失败: {}", e);
|
||||
}
|
||||
|
||||
// 保存配置文件
|
||||
if let Err(e) = Config::profiles().data().save_file() {
|
||||
log::warn!(target: "app", "异步保存配置文件失败: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// 立即通知前端配置变更
|
||||
if let Some(current) = ¤t_value {
|
||||
logging!(info, Type::Cmd, true, "向前端发送配置变更事件: {}", current);
|
||||
handle::Handle::notify_profile_changed(current.clone());
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
Ok(Ok((false, error_msg))) => {
|
||||
logging!(warn, Type::Cmd, true, "配置验证失败: {}", error_msg);
|
||||
Config::profiles().discard();
|
||||
// 如果验证失败,恢复到之前的配置
|
||||
if let Some(prev_profile) = current_profile {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"尝试恢复到之前的配置: {}",
|
||||
prev_profile
|
||||
);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
items: None,
|
||||
};
|
||||
// 静默恢复,不触发验证
|
||||
wrap_err!({ Config::profiles().draft().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);
|
||||
}
|
||||
});
|
||||
|
||||
logging!(info, Type::Cmd, true, "成功恢复到之前的配置");
|
||||
}
|
||||
|
||||
// 发送验证错误通知
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
Ok(false)
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging!(warn, Type::Cmd, true, "更新过程发生错误: {}", e);
|
||||
Config::profiles().discard();
|
||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||
Ok(false)
|
||||
}
|
||||
Err(_) => {
|
||||
// 超时处理
|
||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||
logging!(error, Type::Cmd, true, "{}", timeout_msg);
|
||||
Config::profiles().discard();
|
||||
|
||||
if let Some(prev_profile) = current_profile {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"超时后尝试恢复到之前的配置: {}",
|
||||
prev_profile
|
||||
);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
items: None,
|
||||
};
|
||||
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
|
||||
Config::profiles().apply();
|
||||
}
|
||||
|
||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据profile name修改profiles
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult<bool> {
|
||||
logging!(info, Type::Cmd, "切换配置到: {}", profile_index);
|
||||
pub async fn patch_profiles_config_by_profile_index(
|
||||
_app_handle: tauri::AppHandle,
|
||||
profile_index: String,
|
||||
) -> CmdResult<bool> {
|
||||
logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
|
||||
|
||||
let profiles = IProfiles {
|
||||
current: Some(profile_index),
|
||||
@@ -425,37 +349,30 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
|
||||
|
||||
/// 修改某个profile item的
|
||||
#[tauri::command]
|
||||
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
// 保存修改前检查是否有更新 update_interval
|
||||
let profiles = Config::profiles().await;
|
||||
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
|
||||
&& let Some(new_option) = profile.option.as_ref()
|
||||
{
|
||||
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
|
||||
let new_interval = new_option.update_interval;
|
||||
let old_allow_auto_update = old_profile
|
||||
.option
|
||||
.as_ref()
|
||||
.and_then(|o| o.allow_auto_update);
|
||||
let new_allow_auto_update = new_option.allow_auto_update;
|
||||
(old_interval != new_interval) || (old_allow_auto_update != new_allow_auto_update)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let update_interval_changed =
|
||||
if let Ok(old_profile) = Config::profiles().latest().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
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
profiles_patch_item_safe(&index, &profile)
|
||||
.await
|
||||
.stringify_err()?;
|
||||
// 保存修改
|
||||
wrap_err!(Config::profiles().data().patch_item(index.clone(), profile))?;
|
||||
|
||||
// 如果更新间隔或允许自动更新变更,异步刷新定时器
|
||||
if should_refresh_timer {
|
||||
// 如果更新间隔变更,异步刷新定时器
|
||||
if update_interval_changed {
|
||||
let index_clone = index.clone();
|
||||
crate::process::AsyncHandler::spawn(move || async move {
|
||||
logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器...");
|
||||
if let Err(e) = crate::core::Timer::global().refresh().await {
|
||||
if let Err(e) = crate::core::Timer::global().refresh() {
|
||||
logging!(error, Type::Timer, "刷新定时器失败: {}", e);
|
||||
} else {
|
||||
// 刷新成功后发送自定义事件,不触发配置重载
|
||||
crate::core::handle::Handle::notify_timer_updated(index);
|
||||
crate::core::handle::Handle::notify_timer_updated(index_clone);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -465,45 +382,36 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
|
||||
/// 查看配置文件
|
||||
#[tauri::command]
|
||||
pub async fn view_profile(index: String) -> CmdResult {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_arc();
|
||||
let file = profiles_ref
|
||||
.get_item(&index)
|
||||
.stringify_err()?
|
||||
.file
|
||||
.clone()
|
||||
.ok_or("the file field is null")?;
|
||||
pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
|
||||
let file = {
|
||||
wrap_err!(Config::profiles().latest().get_item(&index))?
|
||||
.file
|
||||
.clone()
|
||||
.ok_or("the file field is null")
|
||||
}?;
|
||||
|
||||
let path = dirs::app_profiles_dir()
|
||||
.stringify_err()?
|
||||
.join(file.as_str());
|
||||
let path = wrap_err!(dirs::app_profiles_dir())?.join(file);
|
||||
if !path.exists() {
|
||||
ret_err!("the file not found");
|
||||
}
|
||||
|
||||
help::open_file(path).stringify_err()
|
||||
wrap_err!(help::open_file(app_handle, path))
|
||||
}
|
||||
|
||||
/// 读取配置文件内容
|
||||
#[tauri::command]
|
||||
pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_arc();
|
||||
PrfItem {
|
||||
file: profiles_ref.get_item(&index).stringify_err()?.file.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
let data = item.read_file().await.stringify_err()?;
|
||||
pub fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
let data = wrap_err!(item.read_file())?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// 获取下一次更新时间
|
||||
#[tauri::command]
|
||||
pub async fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
|
||||
pub fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
|
||||
let timer = Timer::global();
|
||||
let next_time = timer.get_next_update_time(&uid).await;
|
||||
let next_time = timer.get_next_update_time(&uid);
|
||||
Ok(next_time)
|
||||
}
|
||||
|
||||
+69
-12
@@ -1,20 +1,77 @@
|
||||
use super::CmdResult;
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use crate::{core::handle, module::mihomo::MihomoManager, state::proxy::CmdProxyState};
|
||||
use std::{
|
||||
sync::Mutex,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tauri::Manager;
|
||||
|
||||
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(3);
|
||||
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
// TODO: 前端通过 emit 发送更新事件, tray 监听更新事件
|
||||
/// 同步托盘和GUI的代理选择状态
|
||||
#[tauri::command]
|
||||
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
|
||||
use crate::core::tray::Tray;
|
||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
let manager = MihomoManager::global();
|
||||
|
||||
match Tray::global().update_menu().await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Cmd, "Tray proxy selection synced successfully");
|
||||
Ok(())
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||
|
||||
let should_refresh = {
|
||||
let mut state = cmd_proxy_state.lock().unwrap();
|
||||
let now = Instant::now();
|
||||
if now.duration_since(state.last_refresh_time) > PROXIES_REFRESH_INTERVAL {
|
||||
state.need_refresh = true;
|
||||
state.last_refresh_time = now;
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "Failed to sync tray proxy selection: {e}");
|
||||
Err(e.to_string().into())
|
||||
state.need_refresh
|
||||
};
|
||||
|
||||
if should_refresh {
|
||||
let proxies = manager.get_refresh_proxies().await?;
|
||||
{
|
||||
let mut state = cmd_proxy_state.lock().unwrap();
|
||||
state.proxies = proxies;
|
||||
state.need_refresh = false;
|
||||
}
|
||||
log::debug!(target: "app", "proxies刷新成功");
|
||||
}
|
||||
|
||||
let proxies = {
|
||||
let state = cmd_proxy_state.lock().unwrap();
|
||||
state.proxies.clone()
|
||||
};
|
||||
Ok(proxies)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||
|
||||
let should_refresh = {
|
||||
let mut state = cmd_proxy_state.lock().unwrap();
|
||||
let now = Instant::now();
|
||||
if now.duration_since(state.last_refresh_time) > PROVIDERS_REFRESH_INTERVAL {
|
||||
state.need_refresh = true;
|
||||
state.last_refresh_time = now;
|
||||
}
|
||||
state.need_refresh
|
||||
};
|
||||
|
||||
if should_refresh {
|
||||
let manager = MihomoManager::global();
|
||||
let providers = manager.get_providers_proxies().await?;
|
||||
{
|
||||
let mut state = cmd_proxy_state.lock().unwrap();
|
||||
state.providers_proxies = providers;
|
||||
state.need_refresh = false;
|
||||
}
|
||||
log::debug!(target: "app", "providers_proxies刷新成功");
|
||||
}
|
||||
|
||||
let providers_proxies = {
|
||||
let state = cmd_proxy_state.lock().unwrap();
|
||||
state.providers_proxies.clone()
|
||||
};
|
||||
Ok(providers_proxies)
|
||||
}
|
||||
|
||||
@@ -1,112 +1,36 @@
|
||||
use super::CmdResult;
|
||||
use crate::{cmd::StringifyErr, config::*, core::CoreManager, log_err};
|
||||
use anyhow::{Context, anyhow};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use crate::{config::*, wrap_err};
|
||||
use anyhow::Context;
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 获取运行时配置
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
Ok(Config::runtime().await.latest_arc().config.clone())
|
||||
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
Ok(Config::runtime().latest().config.clone())
|
||||
}
|
||||
|
||||
/// 获取运行时YAML配置
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime().await;
|
||||
let runtime = runtime.latest_arc();
|
||||
|
||||
pub fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime();
|
||||
let runtime = runtime.latest();
|
||||
let config = runtime.config.as_ref();
|
||||
config
|
||||
.ok_or_else(|| anyhow!("failed to parse config to yaml file"))
|
||||
.and_then(|config| {
|
||||
serde_yaml_ng::to_string(config)
|
||||
.context("failed to convert config to yaml")
|
||||
.map(|s| s.into())
|
||||
})
|
||||
.stringify_err()
|
||||
wrap_err!(config
|
||||
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||
.and_then(
|
||||
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
|
||||
))
|
||||
}
|
||||
|
||||
/// 获取运行时存在的键
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
Ok(Config::runtime().await.latest_arc().exists_keys.clone())
|
||||
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
Ok(Config::runtime().latest().exists_keys.clone())
|
||||
}
|
||||
|
||||
/// 获取运行时日志
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
Ok(Config::runtime().await.latest_arc().chain_logs.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
|
||||
let runtime = Config::runtime().await;
|
||||
let runtime = runtime.latest_arc();
|
||||
|
||||
let config = runtime
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("failed to parse config to yaml file"))
|
||||
.stringify_err()?;
|
||||
|
||||
if let Some(serde_yaml_ng::Value::Sequence(proxies)) = config.get("proxies") {
|
||||
let mut proxy_name = Some(Some(proxy_chain_exit_node.as_str()));
|
||||
let mut proxies_chain = Vec::new();
|
||||
|
||||
while let Some(proxy) = proxies.iter().find(|proxy| {
|
||||
if let serde_yaml_ng::Value::Mapping(proxy_map) = proxy {
|
||||
proxy_map.get("name").map(|x| x.as_str()) == proxy_name
|
||||
&& proxy_map.get("dialer-proxy").is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
proxies_chain.push(proxy.to_owned());
|
||||
proxy_name = proxy.get("dialer-proxy").map(|x| x.as_str());
|
||||
}
|
||||
|
||||
if let Some(entry_proxy) = proxies
|
||||
.iter()
|
||||
.find(|proxy| proxy.get("name").map(|x| x.as_str()) == proxy_name)
|
||||
&& !proxies_chain.is_empty()
|
||||
{
|
||||
// 添加第一个节点
|
||||
proxies_chain.push(entry_proxy.to_owned());
|
||||
}
|
||||
|
||||
proxies_chain.reverse();
|
||||
|
||||
let mut config: HashMap<String, Vec<serde_yaml_ng::Value>> = HashMap::new();
|
||||
|
||||
config.insert("proxies".into(), proxies_chain);
|
||||
|
||||
serde_yaml_ng::to_string(&config)
|
||||
.context("YAML generation failed")
|
||||
.map(|s| s.into())
|
||||
.stringify_err()
|
||||
} else {
|
||||
Err("failed to get proxies or proxy-groups".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新运行时链式代理配置
|
||||
#[tauri::command]
|
||||
pub async fn update_proxy_chain_config_in_runtime(
|
||||
proxy_chain_config: Option<serde_yaml_ng::Value>,
|
||||
) -> CmdResult<()> {
|
||||
{
|
||||
let runtime = Config::runtime().await;
|
||||
runtime.edit_draft(|d| d.update_proxy_chain_config(proxy_chain_config));
|
||||
runtime.apply();
|
||||
}
|
||||
|
||||
// 生成新的运行配置文件并通知 Clash 核心重新加载
|
||||
let run_path = Config::generate_file(ConfigType::Run)
|
||||
.await
|
||||
.stringify_err()?;
|
||||
log_err!(CoreManager::global().put_configs_force(run_path).await);
|
||||
|
||||
Ok(())
|
||||
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
Ok(Config::runtime().latest().chain_logs.clone())
|
||||
}
|
||||
|
||||
@@ -1,171 +1,116 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
cmd::StringifyErr,
|
||||
config::*,
|
||||
core::{validate::CoreConfigValidator, *},
|
||||
logging,
|
||||
utils::{dirs, logging::Type},
|
||||
};
|
||||
use smartstring::alias::String;
|
||||
use tokio::fs;
|
||||
use crate::{config::*, core::*, utils::dirs, wrap_err};
|
||||
use std::fs;
|
||||
|
||||
/// 保存profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||
let file_data = match file_data {
|
||||
Some(d) => d,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
// 在异步操作前获取必要元数据并释放锁
|
||||
let (rel_path, is_merge_file) = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_guard = profiles.latest_arc();
|
||||
let item = profiles_guard.get_item(&index).stringify_err()?;
|
||||
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
|
||||
let path = item.file.clone().ok_or("file field is null")?;
|
||||
(path, is_merge)
|
||||
};
|
||||
|
||||
// 读取原始内容(在释放profiles_guard后进行)
|
||||
let original_content = PrfItem {
|
||||
file: Some(rel_path.clone()),
|
||||
..Default::default()
|
||||
if file_data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
.read_file()
|
||||
.await
|
||||
.stringify_err()?;
|
||||
|
||||
let profiles_dir = dirs::app_profiles_dir().stringify_err()?;
|
||||
let file_path = profiles_dir.join(rel_path.as_str());
|
||||
let file_path_str = file_path.to_string_lossy().to_string();
|
||||
// 在异步操作前完成所有文件操作
|
||||
let (file_path, original_content, is_merge_file) = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles_guard = profiles.latest();
|
||||
let item = wrap_err!(profiles_guard.get_item(&index))?;
|
||||
// 确定是否为merge类型文件
|
||||
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
|
||||
let content = wrap_err!(item.read_file())?;
|
||||
let path = item.file.clone().ok_or("file field is null")?;
|
||||
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
|
||||
(profiles_dir.join(path), content, is_merge)
|
||||
};
|
||||
|
||||
// 保存新的配置文件
|
||||
fs::write(&file_path, &file_data).await.stringify_err()?;
|
||||
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
let file_path_str = file_path.to_string_lossy().to_string();
|
||||
println!(
|
||||
"[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}",
|
||||
file_path_str,
|
||||
is_merge_file
|
||||
file_path_str, is_merge_file
|
||||
);
|
||||
|
||||
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
|
||||
if is_merge_file {
|
||||
return handle_merge_file(&file_path_str, &file_path, &original_content).await;
|
||||
}
|
||||
|
||||
handle_full_validation(&file_path_str, &file_path, &original_content).await
|
||||
}
|
||||
|
||||
async fn restore_original(
|
||||
file_path: &std::path::Path,
|
||||
original_content: &str,
|
||||
) -> Result<(), String> {
|
||||
fs::write(file_path, original_content).await.stringify_err()
|
||||
}
|
||||
|
||||
fn is_script_error(err: &str, file_path_str: &str) -> bool {
|
||||
file_path_str.ends_with(".js")
|
||||
|| err.contains("Script syntax error")
|
||||
|| err.contains("Script must contain a main function")
|
||||
|| err.contains("Failed to read script file")
|
||||
}
|
||||
|
||||
async fn handle_merge_file(
|
||||
file_path_str: &str,
|
||||
file_path: &std::path::Path,
|
||||
original_content: &str,
|
||||
) -> CmdResult {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] 检测到merge文件,只进行语法验证"
|
||||
);
|
||||
|
||||
match CoreConfigValidator::validate_config_file(file_path_str, Some(true)).await {
|
||||
Ok((true, _)) => {
|
||||
logging!(info, Type::Config, "[cmd配置save] merge文件语法验证通过");
|
||||
if let Err(e) = CoreManager::global().update_config().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"[cmd配置save] 更新整体配置时发生错误: {}",
|
||||
e
|
||||
);
|
||||
} else {
|
||||
handle::Handle::refresh_clash();
|
||||
println!("[cmd配置save] 检测到merge文件,只进行语法验证");
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path_str, Some(true))
|
||||
.await
|
||||
{
|
||||
Ok((true, _)) => {
|
||||
println!("[cmd配置save] merge文件语法验证通过");
|
||||
// 成功后尝试更新整体配置
|
||||
if let Err(e) = CoreManager::global().update_config().await {
|
||||
println!("[cmd配置save] 更新整体配置时发生错误: {}", e);
|
||||
log::warn!(target: "app", "更新整体配置时发生错误: {}", e);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
println!("[cmd配置save] merge文件语法验证失败: {}", error_msg);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
// 发送合并文件专用错误通知
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
return Err(e.to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"[cmd配置save] merge文件语法验证失败: {}",
|
||||
error_msg
|
||||
);
|
||||
restore_original(file_path, original_content).await?;
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
|
||||
restore_original(file_path, original_content).await?;
|
||||
Err(e.to_string().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_full_validation(
|
||||
file_path_str: &str,
|
||||
file_path: &std::path::Path,
|
||||
original_content: &str,
|
||||
) -> CmdResult {
|
||||
match CoreConfigValidator::validate_config_file(file_path_str, None).await {
|
||||
// 非merge文件使用完整验证流程
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path_str, None)
|
||||
.await
|
||||
{
|
||||
Ok((true, _)) => {
|
||||
logging!(info, Type::Config, "[cmd配置save] 验证成功");
|
||||
println!("[cmd配置save] 验证成功");
|
||||
Ok(())
|
||||
}
|
||||
Ok((false, error_msg)) => {
|
||||
logging!(warn, Type::Config, "[cmd配置save] 验证失败: {}", error_msg);
|
||||
restore_original(file_path, original_content).await?;
|
||||
println!("[cmd配置save] 验证失败: {}", error_msg);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
|
||||
// 智能判断错误类型
|
||||
let is_script_error = file_path_str.ends_with(".js")
|
||||
|| error_msg.contains("Script syntax error")
|
||||
|| error_msg.contains("Script must contain a main function")
|
||||
|| error_msg.contains("Failed to read script file");
|
||||
|
||||
if error_msg.contains("YAML syntax error")
|
||||
|| error_msg.contains("Failed to read file:")
|
||||
|| (!file_path_str.ends_with(".js") && !is_script_error(&error_msg, file_path_str))
|
||||
|| (!file_path_str.ends_with(".js") && !is_script_error)
|
||||
{
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] YAML配置文件验证失败,发送通知"
|
||||
);
|
||||
let result = (false, error_msg.to_owned());
|
||||
// 普通YAML错误使用YAML通知处理
|
||||
println!("[cmd配置save] YAML配置文件验证失败,发送通知");
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
|
||||
} else if is_script_error(&error_msg, file_path_str) {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] 脚本文件验证失败,发送通知"
|
||||
);
|
||||
let result = (false, error_msg.to_owned());
|
||||
} else if is_script_error {
|
||||
// 脚本错误使用专门的通知处理
|
||||
println!("[cmd配置save] 脚本文件验证失败,发送通知");
|
||||
let result = (false, error_msg.clone());
|
||||
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[cmd配置save] 其他类型验证失败,发送一般通知"
|
||||
);
|
||||
handle::Handle::notice_message("config_validate::error", error_msg.to_owned());
|
||||
// 普通配置错误使用一般通知
|
||||
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
|
||||
restore_original(file_path, original_content).await?;
|
||||
Err(e.to_string().into())
|
||||
println!("[cmd配置save] 验证过程发生错误: {}", e);
|
||||
// 恢复原始配置文件
|
||||
wrap_err!(fs::write(&file_path, original_content))?;
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,48 @@
|
||||
use super::{CmdResult, StringifyErr};
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
core::service::{self, SERVICE_MANAGER, ServiceStatus},
|
||||
core::{service, CoreManager},
|
||||
utils::i18n::t,
|
||||
};
|
||||
use smartstring::SmartString;
|
||||
|
||||
async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult {
|
||||
if let Err(e) = SERVICE_MANAGER
|
||||
.lock()
|
||||
.await
|
||||
.handle_service_status(&status)
|
||||
.await
|
||||
{
|
||||
let emsg = format!("{} Service failed: {}", op_type, e);
|
||||
return Err(SmartString::from(&*t(emsg.as_str()).await));
|
||||
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");
|
||||
return Err(t(emsg.as_str()));
|
||||
}
|
||||
if CoreManager::global().restart_core().await.is_err() {
|
||||
let emsg = format!("{} {} failed", "Restart", "Core");
|
||||
return Err(t(emsg.as_str()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_service() -> CmdResult {
|
||||
execute_service_operation_sync(ServiceStatus::InstallRequired, "Install").await
|
||||
execute_service_operation(service::install_service(), "Install").await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_service() -> CmdResult {
|
||||
execute_service_operation_sync(ServiceStatus::UninstallRequired, "Uninstall").await
|
||||
execute_service_operation(service::uninstall_service(), "Uninstall").await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reinstall_service() -> CmdResult {
|
||||
execute_service_operation_sync(ServiceStatus::ReinstallRequired, "Reinstall").await
|
||||
execute_service_operation(service::reinstall_service(), "Reinstall").await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn repair_service() -> CmdResult {
|
||||
execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "Repair").await
|
||||
execute_service_operation(service::force_reinstall_service(), "Repair").await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn is_service_available() -> CmdResult<bool> {
|
||||
service::is_service_available().await.stringify_err()?;
|
||||
Ok(true)
|
||||
service::is_service_available()
|
||||
.await
|
||||
.map(|_| true)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
+58
-28
@@ -1,64 +1,94 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
core::{CoreManager, handle, manager::RunningMode},
|
||||
logging,
|
||||
core::{handle, CoreManager},
|
||||
module::sysinfo::PlatformSpecification,
|
||||
utils::logging::Type,
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
sync::atomic::{AtomicI64, Ordering},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
use tokio::time::Instant;
|
||||
|
||||
// 存储应用启动时间的全局变量
|
||||
static APP_START_TIME: Lazy<Instant> = Lazy::new(Instant::now);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| unsafe { libc::geteuid() } == 0);
|
||||
#[cfg(target_os = "windows")]
|
||||
static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| {
|
||||
Token::with_current_process()
|
||||
.and_then(|token| token.privilege_level())
|
||||
.map(|level| level != PrivilegeLevel::NotPrivileged)
|
||||
.unwrap_or(false)
|
||||
static APP_START_TIME: Lazy<AtomicI64> = Lazy::new(|| {
|
||||
// 获取当前系统时间,转换为毫秒级时间戳
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
AtomicI64::new(now)
|
||||
});
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||
let sysinfo = PlatformSpecification::new_sync();
|
||||
let info = format!("{sysinfo:?}");
|
||||
let sysinfo = PlatformSpecification::new_async().await;
|
||||
let info = format!("{:?}", sysinfo);
|
||||
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cliboard = app_handle.clipboard();
|
||||
if cliboard.write_text(info).is_err() {
|
||||
logging!(error, Type::System, "Failed to write to clipboard");
|
||||
log::error!(target: "app", "Failed to write to clipboard");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_system_info() -> CmdResult<String> {
|
||||
let sysinfo = PlatformSpecification::new_sync();
|
||||
let info = format!("{sysinfo:?}");
|
||||
let sysinfo = PlatformSpecification::new_async().await;
|
||||
let info = format!("{:?}", sysinfo);
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// 获取当前内核运行模式
|
||||
#[tauri::command]
|
||||
pub async fn get_running_mode() -> Result<Arc<RunningMode>, String> {
|
||||
Ok(CoreManager::global().get_running_mode())
|
||||
pub async fn get_running_mode() -> Result<String, String> {
|
||||
Ok(CoreManager::global().get_running_mode().await.to_string())
|
||||
}
|
||||
|
||||
/// 获取应用的运行时间(毫秒)
|
||||
#[tauri::command]
|
||||
pub fn get_app_uptime() -> CmdResult<u128> {
|
||||
Ok(APP_START_TIME.elapsed().as_millis())
|
||||
pub fn get_app_uptime() -> CmdResult<i64> {
|
||||
let start_time = APP_START_TIME.load(Ordering::Relaxed);
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
|
||||
Ok(now - start_time)
|
||||
}
|
||||
|
||||
/// 检查应用是否以管理员身份运行
|
||||
#[tauri::command]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn is_admin() -> CmdResult<bool> {
|
||||
Ok(*APPS_RUN_AS_ADMIN)
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
|
||||
let result = Token::with_current_process()
|
||||
.and_then(|token| token.privilege_level())
|
||||
.map(|level| level != PrivilegeLevel::NotPrivileged)
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 非Windows平台检测是否以管理员身份运行
|
||||
#[tauri::command]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn is_admin() -> CmdResult<bool> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Ok(unsafe { libc::geteuid() } == 0)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Ok(unsafe { libc::geteuid() } == 0)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use crate::cmd::CmdResult;
|
||||
use super::CmdResult;
|
||||
|
||||
/// Platform-specific implementation for UWP functionality
|
||||
#[cfg(windows)]
|
||||
mod platform {
|
||||
use crate::cmd::CmdResult;
|
||||
use crate::cmd::StringifyErr;
|
||||
use crate::core::win_uwp;
|
||||
use super::CmdResult;
|
||||
use crate::{core::win_uwp, wrap_err};
|
||||
|
||||
pub fn invoke_uwp_tool() -> CmdResult {
|
||||
win_uwp::invoke_uwptools().stringify_err()
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
wrap_err!(win_uwp::invoke_uwptools().await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +16,7 @@ mod platform {
|
||||
mod platform {
|
||||
use super::CmdResult;
|
||||
|
||||
pub const fn invoke_uwp_tool() -> CmdResult {
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -25,5 +24,5 @@ mod platform {
|
||||
/// Command exposed to Tauri
|
||||
#[tauri::command]
|
||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||
platform::invoke_uwp_tool()
|
||||
platform::invoke_uwp_tool().await
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
core::{validate::CoreConfigValidator, *},
|
||||
logging,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use smartstring::alias::String;
|
||||
use crate::core::*;
|
||||
|
||||
/// 发送脚本验证通知消息
|
||||
#[tauri::command]
|
||||
pub async fn script_validate_notice(status: String, msg: String) -> CmdResult {
|
||||
handle::Handle::notice_message(status.as_str(), msg.as_str());
|
||||
handle::Handle::notice_message(&status, &msg);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -33,29 +28,27 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
|
||||
"config_validate::script_error"
|
||||
};
|
||||
|
||||
logging!(warn, Type::Config, "{} 验证失败: {}", file_type, error_msg);
|
||||
handle::Handle::notice_message(status, error_msg.to_owned());
|
||||
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||
handle::Handle::notice_message(status, error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证指定脚本文件
|
||||
#[tauri::command]
|
||||
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
|
||||
logging!(info, Type::Config, "验证脚本文件: {}", file_path);
|
||||
log::info!(target: "app", "验证脚本文件: {}", file_path);
|
||||
|
||||
match CoreConfigValidator::validate_config_file(&file_path, None).await {
|
||||
match CoreManager::global()
|
||||
.validate_config_file(&file_path, None)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
handle_script_validation_notice(&result, "脚本文件");
|
||||
Ok(result.0) // 返回验证结果布尔值
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
logging!(
|
||||
error,
|
||||
Type::Config,
|
||||
"验证脚本文件过程发生错误: {}",
|
||||
error_msg
|
||||
);
|
||||
log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg);
|
||||
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
|
||||
Ok(false)
|
||||
}
|
||||
@@ -67,13 +60,7 @@ 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;
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[通知] 处理{}验证错误: {}",
|
||||
file_type,
|
||||
error_msg
|
||||
);
|
||||
println!("[通知] 处理{}验证错误: {}", file_type, error_msg);
|
||||
|
||||
// 检查是否为merge文件
|
||||
let is_merge_file = file_type.contains("合并");
|
||||
@@ -110,14 +97,8 @@ pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
|
||||
}
|
||||
};
|
||||
|
||||
logging!(warn, Type::Config, "{} 验证失败: {}", file_type, error_msg);
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"[通知] 发送通知: status={}, msg={}",
|
||||
status,
|
||||
error_msg
|
||||
);
|
||||
handle::Handle::notice_message(status, error_msg.to_owned());
|
||||
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
|
||||
println!("[通知] 发送通知: status={}, msg={}", status, error_msg);
|
||||
handle::Handle::notice_message(status, error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use super::CmdResult;
|
||||
use crate::{cmd::StringifyErr, config::IVerge, feat, utils::draft::SharedBox};
|
||||
use crate::{config::*, feat, wrap_err};
|
||||
|
||||
/// 获取Verge配置
|
||||
#[tauri::command]
|
||||
pub async fn get_verge_config() -> CmdResult<SharedBox<IVerge>> {
|
||||
feat::fetch_verge_config().await.stringify_err()
|
||||
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
|
||||
let verge = Config::verge();
|
||||
let verge_data = verge.data().clone();
|
||||
Ok(IVergeResponse::from(verge_data))
|
||||
}
|
||||
|
||||
/// 修改Verge配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||
feat::patch_verge(&payload, false).await.stringify_err()
|
||||
wrap_err!(feat::patch_verge(payload, false).await)
|
||||
}
|
||||
|
||||
+11
-17
@@ -1,11 +1,6 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
cmd::StringifyErr,
|
||||
config::{Config, IVerge},
|
||||
core, feat,
|
||||
};
|
||||
use crate::{config::*, core, feat, wrap_err};
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use smartstring::alias::String;
|
||||
|
||||
/// 保存 WebDAV 配置
|
||||
#[tauri::command]
|
||||
@@ -16,11 +11,12 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
|
||||
webdav_password: Some(password),
|
||||
..IVerge::default()
|
||||
};
|
||||
Config::verge().await.edit_draft(|e| e.patch_config(&patch));
|
||||
Config::verge().await.apply();
|
||||
|
||||
let verge_data = Config::verge().await.latest_arc();
|
||||
verge_data.save_file().await.stringify_err()?;
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
Config::verge().apply();
|
||||
Config::verge()
|
||||
.data()
|
||||
.save_file()
|
||||
.map_err(|err| err.to_string())?;
|
||||
core::backup::WebDavClient::global().reset();
|
||||
Ok(())
|
||||
}
|
||||
@@ -28,25 +24,23 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
|
||||
/// 创建 WebDAV 备份并上传
|
||||
#[tauri::command]
|
||||
pub async fn create_webdav_backup() -> CmdResult<()> {
|
||||
feat::create_backup_and_upload_webdav()
|
||||
.await
|
||||
.stringify_err()
|
||||
wrap_err!(feat::create_backup_and_upload_webdav().await)
|
||||
}
|
||||
|
||||
/// 列出 WebDAV 上的备份文件
|
||||
#[tauri::command]
|
||||
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
|
||||
feat::list_wevdav_backup().await.stringify_err()
|
||||
wrap_err!(feat::list_wevdav_backup().await)
|
||||
}
|
||||
|
||||
/// 删除 WebDAV 上的备份文件
|
||||
#[tauri::command]
|
||||
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
feat::delete_webdav_backup(filename).await.stringify_err()
|
||||
wrap_err!(feat::delete_webdav_backup(filename).await)
|
||||
}
|
||||
|
||||
/// 从 WebDAV 恢复备份文件
|
||||
#[tauri::command]
|
||||
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
feat::restore_webdav_backup(filename).await.stringify_err()
|
||||
wrap_err!(feat::restore_webdav_backup(filename).await)
|
||||
}
|
||||
|
||||
+46
-108
@@ -1,11 +1,7 @@
|
||||
use crate::config::Config;
|
||||
use crate::constants::{network, tun as tun_const};
|
||||
use crate::utils::dirs::{ipc_path, path_to_str};
|
||||
use crate::utils::{dirs, help};
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
str::FromStr,
|
||||
@@ -15,34 +11,25 @@ use std::{
|
||||
pub struct IClashTemp(pub Mapping);
|
||||
|
||||
impl IClashTemp {
|
||||
pub async fn new() -> Self {
|
||||
pub fn new() -> Self {
|
||||
let template = Self::template();
|
||||
let clash_path_result = dirs::clash_path();
|
||||
let map_result = if let Ok(path) = clash_path_result {
|
||||
help::read_mapping(&path).await
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Failed to get clash path"))
|
||||
};
|
||||
|
||||
match map_result {
|
||||
match dirs::clash_path().and_then(|path| help::read_mapping(&path)) {
|
||||
Ok(mut map) => {
|
||||
template.0.keys().for_each(|key| {
|
||||
if !map.contains_key(key)
|
||||
&& let Some(value) = template.0.get(key)
|
||||
{
|
||||
map.insert(key.clone(), value.clone());
|
||||
if !map.contains_key(key) {
|
||||
map.insert(key.clone(), template.0.get(key).unwrap().clone());
|
||||
}
|
||||
});
|
||||
// 确保 secret 字段存在且不为空
|
||||
if let Some(Value::String(s)) = map.get_mut("secret")
|
||||
&& s.is_empty()
|
||||
{
|
||||
*s = "set-your-secret".into();
|
||||
if let Some(Value::String(s)) = map.get_mut("secret") {
|
||||
if s.is_empty() {
|
||||
*s = "set-your-secret".to_string();
|
||||
}
|
||||
}
|
||||
Self(Self::guard(map))
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, "{err}");
|
||||
log::error!(target: "app", "{err}");
|
||||
template
|
||||
}
|
||||
}
|
||||
@@ -50,51 +37,32 @@ impl IClashTemp {
|
||||
|
||||
pub fn template() -> Self {
|
||||
let mut map = Mapping::new();
|
||||
let mut tun_config = Mapping::new();
|
||||
let mut tun = Mapping::new();
|
||||
let mut cors_map = Mapping::new();
|
||||
|
||||
tun_config.insert("enable".into(), false.into());
|
||||
tun_config.insert("stack".into(), tun_const::DEFAULT_STACK.into());
|
||||
tun_config.insert("auto-route".into(), true.into());
|
||||
tun_config.insert("strict-route".into(), false.into());
|
||||
tun_config.insert("auto-detect-interface".into(), true.into());
|
||||
tun_config.insert("dns-hijack".into(), tun_const::DNS_HIJACK.into());
|
||||
|
||||
tun.insert("enable".into(), false.into());
|
||||
tun.insert("stack".into(), "gvisor".into());
|
||||
tun.insert("auto-route".into(), true.into());
|
||||
tun.insert("strict-route".into(), false.into());
|
||||
tun.insert("auto-detect-interface".into(), true.into());
|
||||
tun.insert("dns-hijack".into(), vec!["any:53"].into());
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
map.insert("redir-port".into(), network::ports::DEFAULT_REDIR.into());
|
||||
map.insert("redir-port".into(), 7895.into());
|
||||
#[cfg(target_os = "linux")]
|
||||
map.insert("tproxy-port".into(), network::ports::DEFAULT_TPROXY.into());
|
||||
|
||||
map.insert("mixed-port".into(), network::ports::DEFAULT_MIXED.into());
|
||||
map.insert("socks-port".into(), network::ports::DEFAULT_SOCKS.into());
|
||||
map.insert("port".into(), network::ports::DEFAULT_HTTP.into());
|
||||
map.insert("log-level".into(), "info".into());
|
||||
map.insert("tproxy-port".into(), 7896.into());
|
||||
map.insert("mixed-port".into(), 7897.into());
|
||||
map.insert("socks-port".into(), 7898.into());
|
||||
map.insert("port".into(), 7899.into());
|
||||
map.insert("log-level".into(), "warning".into());
|
||||
map.insert("allow-lan".into(), false.into());
|
||||
map.insert("ipv6".into(), true.into());
|
||||
map.insert("mode".into(), "rule".into());
|
||||
map.insert(
|
||||
"external-controller".into(),
|
||||
network::DEFAULT_EXTERNAL_CONTROLLER.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(),
|
||||
);
|
||||
map.insert("tun".into(), tun_config.into());
|
||||
map.insert("external-controller".into(), "127.0.0.1:9097".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",
|
||||
@@ -103,6 +71,7 @@ impl IClashTemp {
|
||||
.into(),
|
||||
);
|
||||
map.insert("secret".into(), "set-your-secret".into());
|
||||
map.insert("tun".into(), tun.into());
|
||||
map.insert("external-controller-cors".into(), cors_map.into());
|
||||
map.insert("unified-delay".into(), true.into());
|
||||
Self(map)
|
||||
@@ -116,12 +85,7 @@ 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_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();
|
||||
|
||||
let ctrl = Self::guard_server_ctrl(&config);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
config.insert("redir-port".into(), redir_port.into());
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -131,16 +95,23 @@ impl IClashTemp {
|
||||
config.insert("port".into(), port.into());
|
||||
config.insert("external-controller".into(), ctrl.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(),
|
||||
// 强制覆盖 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(),
|
||||
);
|
||||
config.insert("external-controller-cors".into(), cors_map.into());
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
@@ -150,13 +121,12 @@ impl IClashTemp {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_config(&self) -> Result<()> {
|
||||
pub fn save_config(&self) -> Result<()> {
|
||||
help::save_yaml(
|
||||
&dirs::clash_path()?,
|
||||
&self.0,
|
||||
Some("# Generated by Clash Verge"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn get_mixed_port(&self) -> u16 {
|
||||
@@ -214,9 +184,9 @@ impl IClashTemp {
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(network::ports::DEFAULT_TPROXY);
|
||||
.unwrap_or(7896);
|
||||
if port == 0 {
|
||||
port = network::ports::DEFAULT_TPROXY;
|
||||
port = 7896;
|
||||
}
|
||||
port
|
||||
}
|
||||
@@ -287,28 +257,7 @@ impl IClashTemp {
|
||||
}
|
||||
None => None,
|
||||
})
|
||||
.unwrap_or_else(|| "127.0.0.1:9097".into())
|
||||
}
|
||||
|
||||
pub fn guard_external_controller(config: &Mapping) -> String {
|
||||
// 在初始化阶段,直接返回配置中的值,不进行额外检查
|
||||
// 这样可以避免在配置加载期间的循环依赖
|
||||
Self::guard_server_ctrl(config)
|
||||
}
|
||||
|
||||
pub async fn guard_external_controller_with_setting(config: &Mapping) -> String {
|
||||
// 检查 enable_external_controller 设置,用于运行时配置生成
|
||||
let enable_external_controller = Config::verge()
|
||||
.await
|
||||
.latest_arc()
|
||||
.enable_external_controller
|
||||
.unwrap_or(false);
|
||||
|
||||
if enable_external_controller {
|
||||
Self::guard_server_ctrl(config)
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
.unwrap_or("127.0.0.1:9097".into())
|
||||
}
|
||||
|
||||
pub fn guard_client_ctrl(config: &Mapping) -> String {
|
||||
@@ -323,17 +272,6 @@ 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.into()))
|
||||
.unwrap_or_else(|| {
|
||||
logging!(error, Type::Config, "Failed to get IPC path");
|
||||
crate::constants::network::DEFAULT_EXTERNAL_CONTROLLER.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
|
||||
+87
-169
@@ -1,18 +1,18 @@
|
||||
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
|
||||
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
||||
use crate::{
|
||||
cmd,
|
||||
config::{PrfItem, profiles_append_item_safe},
|
||||
constants::{files, timing},
|
||||
core::{CoreManager, handle, service, tray, validate::CoreConfigValidator},
|
||||
enhance, logging, logging_error,
|
||||
utils::{Draft, dirs, help, logging::Type},
|
||||
config::PrfItem,
|
||||
core::{handle, CoreManager},
|
||||
enhance, logging,
|
||||
process::AsyncHandler,
|
||||
utils::{dirs, help, logging::Type},
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use backoff::{Error as BackoffError, ExponentialBackoff};
|
||||
use smartstring::alias::String;
|
||||
use anyhow::{anyhow, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::OnceCell;
|
||||
use tokio::time::sleep;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||
|
||||
pub struct Config {
|
||||
clash_config: Draft<IClashTemp>,
|
||||
@@ -22,183 +22,140 @@ pub struct Config {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub async fn global() -> &'static Self {
|
||||
static CONFIG: OnceCell<Config> = OnceCell::const_new();
|
||||
CONFIG
|
||||
.get_or_init(|| async {
|
||||
Self {
|
||||
clash_config: Draft::new(IClashTemp::new().await),
|
||||
verge_config: Draft::new(IVerge::new().await),
|
||||
profiles_config: Draft::new(IProfiles::new().await),
|
||||
runtime_config: Draft::new(IRuntime::new()),
|
||||
}
|
||||
})
|
||||
.await
|
||||
pub fn global() -> &'static 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()),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn clash() -> Draft<IClashTemp> {
|
||||
Self::global().await.clash_config.clone()
|
||||
pub fn clash() -> Draft<IClashTemp> {
|
||||
Self::global().clash_config.clone()
|
||||
}
|
||||
|
||||
pub async fn verge() -> Draft<IVerge> {
|
||||
Self::global().await.verge_config.clone()
|
||||
pub fn verge() -> Draft<IVerge> {
|
||||
Self::global().verge_config.clone()
|
||||
}
|
||||
|
||||
pub async fn profiles() -> Draft<IProfiles> {
|
||||
Self::global().await.profiles_config.clone()
|
||||
pub fn profiles() -> Draft<IProfiles> {
|
||||
Self::global().profiles_config.clone()
|
||||
}
|
||||
|
||||
pub async fn runtime() -> Draft<IRuntime> {
|
||||
Self::global().await.runtime_config.clone()
|
||||
pub fn runtime() -> Draft<IRuntime> {
|
||||
Self::global().runtime_config.clone()
|
||||
}
|
||||
|
||||
/// 初始化订阅
|
||||
pub async fn init_config() -> Result<()> {
|
||||
Self::ensure_default_profile_items().await?;
|
||||
|
||||
// init Tun mode
|
||||
if !cmd::system::is_admin().unwrap_or_default()
|
||||
&& service::is_service_available().await.is_err()
|
||||
if Self::profiles()
|
||||
.data()
|
||||
.get_item(&"Merge".to_string())
|
||||
.is_err()
|
||||
{
|
||||
let verge = Self::verge().await;
|
||||
verge.edit_draft(|d| {
|
||||
d.enable_tun_mode = Some(false);
|
||||
});
|
||||
verge.apply();
|
||||
let _ = tray::Tray::global().update_menu().await;
|
||||
|
||||
// 分离数据获取和异步调用避免Send问题
|
||||
let verge_data = Self::verge().await.latest_arc();
|
||||
logging_error!(Type::Core, verge_data.save_file().await);
|
||||
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?;
|
||||
Self::profiles().data().append_item(merge_item.clone())?;
|
||||
}
|
||||
|
||||
let validation_result = Self::generate_and_validate().await?;
|
||||
|
||||
if let Some((msg_type, msg_content)) = validation_result {
|
||||
sleep(timing::STARTUP_ERROR_DELAY).await;
|
||||
handle::Handle::notice_message(msg_type, msg_content);
|
||||
if Self::profiles()
|
||||
.data()
|
||||
.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())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Ensure "Merge" and "Script" profile items exist, adding them if missing.
|
||||
async fn ensure_default_profile_items() -> Result<()> {
|
||||
let profiles = Self::profiles().await;
|
||||
if profiles.latest_arc().get_item("Merge").is_err() {
|
||||
let merge_item = &mut PrfItem::from_merge(Some("Merge".into()))?;
|
||||
profiles_append_item_safe(merge_item).await?;
|
||||
}
|
||||
if profiles.latest_arc().get_item("Script").is_err() {
|
||||
let script_item = &mut PrfItem::from_script(Some("Script".into()))?;
|
||||
profiles_append_item_safe(script_item).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn generate_and_validate() -> Result<Option<(&'static str, String)>> {
|
||||
// 生成运行时配置
|
||||
if let Err(err) = Self::generate().await {
|
||||
logging!(error, Type::Config, "生成运行时配置失败: {}", err);
|
||||
logging!(error, Type::Config, true, "生成运行时配置失败: {}", err);
|
||||
} else {
|
||||
logging!(info, Type::Config, "生成运行时配置成功");
|
||||
logging!(info, Type::Config, true, "生成运行时配置成功");
|
||||
}
|
||||
|
||||
// 生成运行时配置文件并验证
|
||||
let config_result = Self::generate_file(ConfigType::Run).await;
|
||||
let config_result = Self::generate_file(ConfigType::Run);
|
||||
|
||||
if config_result.is_ok() {
|
||||
let validation_result = if config_result.is_ok() {
|
||||
// 验证配置文件
|
||||
logging!(info, Type::Config, "开始验证配置");
|
||||
logging!(info, Type::Config, true, "开始验证配置");
|
||||
|
||||
match CoreConfigValidator::global().validate_config().await {
|
||||
match CoreManager::global().validate_config().await {
|
||||
Ok((is_valid, error_msg)) => {
|
||||
if !is_valid {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
true,
|
||||
"[首次启动] 配置验证失败,使用默认最小配置启动: {}",
|
||||
error_msg
|
||||
);
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::boot_error", &error_msg)
|
||||
.await?;
|
||||
Ok(Some(("config_validate::boot_error", error_msg)))
|
||||
Some(("config_validate::boot_error", error_msg))
|
||||
} else {
|
||||
logging!(info, Type::Config, "配置验证成功");
|
||||
// 前端没有必要知道验证成功的消息,也没有事件驱动
|
||||
// Some(("config_validate::success", String::new()))
|
||||
Ok(None)
|
||||
logging!(info, Type::Config, true, "配置验证成功");
|
||||
Some(("config_validate::success", String::new()))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(warn, Type::Config, "验证过程执行失败: {}", err);
|
||||
logging!(warn, Type::Config, true, "验证进程执行失败: {}", err);
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::process_terminated", "")
|
||||
.await?;
|
||||
Ok(Some(("config_validate::process_terminated", String::new())))
|
||||
Some(("config_validate::process_terminated", String::new()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logging!(warn, Type::Config, "生成配置文件失败,使用默认配置");
|
||||
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
|
||||
CoreManager::global()
|
||||
.use_default_config("config_validate::error", "")
|
||||
.await?;
|
||||
Ok(Some(("config_validate::error", String::new())))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_file(typ: ConfigType) -> Result<PathBuf> {
|
||||
let path = match typ {
|
||||
ConfigType::Run => dirs::app_home_dir()?.join(files::RUNTIME_CONFIG),
|
||||
ConfigType::Check => dirs::app_home_dir()?.join(files::CHECK_CONFIG),
|
||||
Some(("config_validate::error", String::new()))
|
||||
};
|
||||
|
||||
let runtime = Self::runtime().await;
|
||||
let runtime_arc = runtime.latest_arc();
|
||||
let config = runtime_arc
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("failed to get runtime config"))?;
|
||||
|
||||
help::save_yaml(&path, config, Some("# Generated by Clash Verge")).await?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub async fn generate() -> Result<()> {
|
||||
let (config, exists_keys, logs) = enhance::enhance().await;
|
||||
|
||||
Self::runtime().await.edit_draft(|d| {
|
||||
*d = IRuntime {
|
||||
config: Some(config),
|
||||
exists_keys,
|
||||
chain_logs: logs,
|
||||
}
|
||||
});
|
||||
// 在单独的任务中发送通知
|
||||
if let Some((msg_type, msg_content)) = validation_result {
|
||||
AsyncHandler::spawn(move || async move {
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
handle::Handle::notice_message(msg_type, &msg_content);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn verify_config_initialization() {
|
||||
let backoff_strategy = ExponentialBackoff {
|
||||
initial_interval: std::time::Duration::from_millis(100),
|
||||
max_interval: std::time::Duration::from_secs(2),
|
||||
max_elapsed_time: Some(std::time::Duration::from_secs(10)),
|
||||
multiplier: 2.0,
|
||||
..Default::default()
|
||||
/// 将订阅丢到对应的文件中
|
||||
pub fn generate_file(typ: ConfigType) -> Result<PathBuf> {
|
||||
let path = match typ {
|
||||
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
|
||||
ConfigType::Check => dirs::app_home_dir()?.join(CHECK_CONFIG),
|
||||
};
|
||||
|
||||
let operation = || async {
|
||||
if Self::runtime().await.latest_arc().config.is_some() {
|
||||
return Ok::<(), BackoffError<anyhow::Error>>(());
|
||||
}
|
||||
let runtime = Config::runtime();
|
||||
let runtime = runtime.latest();
|
||||
let config = runtime
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("failed to get runtime config"))?;
|
||||
|
||||
Self::generate().await.map_err(BackoffError::transient)
|
||||
help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// 生成订阅存好
|
||||
pub async fn generate() -> Result<()> {
|
||||
let (config, exists_keys, logs) = enhance::enhance().await;
|
||||
|
||||
*Config::runtime().draft() = IRuntime {
|
||||
config: Some(config),
|
||||
exists_keys,
|
||||
chain_logs: logs,
|
||||
};
|
||||
|
||||
if let Err(e) = backoff::future::retry(backoff_strategy, operation).await {
|
||||
logging!(error, Type::Setup, "Config init verification failed: {}", e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,42 +164,3 @@ 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".into())).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::new(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::new(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>>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
|
||||
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Draft<T: Clone + ToOwned> {
|
||||
inner: Arc<Mutex<(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 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);
|
||||
|
||||
#[test]
|
||||
fn test_draft() {
|
||||
let verge = 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));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.draft().enable_tun_mode, Some(false));
|
||||
|
||||
let mut d = draft.draft();
|
||||
d.enable_auto_launch = Some(false);
|
||||
d.enable_tun_mode = Some(true);
|
||||
drop(d);
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||
|
||||
assert_eq!(draft.latest().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.latest().enable_tun_mode, Some(true));
|
||||
|
||||
assert!(draft.apply().is_some());
|
||||
assert!(draft.apply().is_none());
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.data().enable_tun_mode, Some(true));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -1,22 +1,14 @@
|
||||
use crate::utils::dirs::get_encryption_key;
|
||||
use aes_gcm::{
|
||||
Aes256Gcm, Key,
|
||||
aead::{Aead, KeyInit},
|
||||
Aes256Gcm, Key,
|
||||
};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::cell::Cell;
|
||||
use std::future::Future;
|
||||
|
||||
const NONCE_LENGTH: usize = 12;
|
||||
|
||||
// Use task-local context so the flag follows the async task across threads
|
||||
tokio::task_local! {
|
||||
static ENCRYPTION_ACTIVE: Cell<bool>;
|
||||
}
|
||||
|
||||
/// Encrypt data
|
||||
#[allow(deprecated)]
|
||||
pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let encryption_key = get_encryption_key()?;
|
||||
let key = Key::<Aes256Gcm>::from_slice(&encryption_key);
|
||||
@@ -29,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;
|
||||
@@ -38,7 +30,6 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
/// Decrypt data
|
||||
#[allow(deprecated)]
|
||||
pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let encryption_key = get_encryption_key()?;
|
||||
let key = Key::<Aes256Gcm>::from_slice(&encryption_key);
|
||||
@@ -55,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())
|
||||
}
|
||||
@@ -66,45 +57,39 @@ where
|
||||
T: Serialize,
|
||||
S: Serializer,
|
||||
{
|
||||
if is_encryption_active() {
|
||||
let json = serde_json::to_string(value).map_err(serde::ser::Error::custom)?;
|
||||
let encrypted = encrypt_data(&json).map_err(serde::ser::Error::custom)?;
|
||||
serializer.serialize_str(&encrypted)
|
||||
} else {
|
||||
value.serialize(serializer)
|
||||
// 如果序列化失败,返回 None
|
||||
let json = match serde_json::to_string(value) {
|
||||
Ok(j) => j,
|
||||
Err(_) => return serializer.serialize_none(),
|
||||
};
|
||||
|
||||
// 如果加密失败,返回 None
|
||||
match encrypt_data(&json) {
|
||||
Ok(encrypted) => serializer.serialize_str(&encrypted),
|
||||
Err(_) => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize decrypted function
|
||||
pub fn deserialize_encrypted<'a, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
pub fn deserialize_encrypted<'a, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
T: for<'de> Deserialize<'de> + Default,
|
||||
D: Deserializer<'a>,
|
||||
{
|
||||
if is_encryption_active() {
|
||||
let encrypted_opt: Option<String> = Option::deserialize(deserializer)?;
|
||||
// 如果反序列化字符串失败,返回默认值
|
||||
let encrypted = match String::deserialize(deserializer) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Ok(T::default()),
|
||||
};
|
||||
|
||||
match encrypted_opt {
|
||||
Some(encrypted) if !encrypted.is_empty() => {
|
||||
let decrypted_string =
|
||||
decrypt_data(&encrypted).map_err(serde::de::Error::custom)?;
|
||||
serde_json::from_str(&decrypted_string).map_err(serde::de::Error::custom)
|
||||
}
|
||||
_ => Ok(T::default()),
|
||||
}
|
||||
} else {
|
||||
T::deserialize(deserializer)
|
||||
// 如果解密失败,返回默认值
|
||||
let decrypted_string = match decrypt_data(&encrypted) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return Ok(T::default()),
|
||||
};
|
||||
// 如果 JSON 解析失败,返回默认值
|
||||
match serde_json::from_str(&decrypted_string) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(_) => Ok(T::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn with_encryption<F, Fut, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future<Output = R>,
|
||||
{
|
||||
ENCRYPTION_ACTIVE.scope(Cell::new(true), f()).await
|
||||
}
|
||||
|
||||
fn is_encryption_active() -> bool {
|
||||
ENCRYPTION_ACTIVE.try_with(|c| c.get()).unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
mod clash;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod config;
|
||||
mod draft;
|
||||
mod encrypt;
|
||||
mod prfitem;
|
||||
pub mod profiles;
|
||||
mod profiles;
|
||||
mod runtime;
|
||||
mod verge;
|
||||
|
||||
pub use self::{clash::*, config::*, encrypt::*, prfitem::*, profiles::*, runtime::*, verge::*};
|
||||
pub use self::{
|
||||
clash::*, config::*, draft::*, encrypt::*, prfitem::*, profiles::*, runtime::*, verge::*,
|
||||
};
|
||||
|
||||
pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) {
|
||||
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
|
||||
|
||||
+199
-230
@@ -1,17 +1,15 @@
|
||||
use crate::{
|
||||
config::profiles,
|
||||
utils::{
|
||||
dirs, help,
|
||||
network::{NetworkManager, ProxyType},
|
||||
tmpl,
|
||||
},
|
||||
use crate::utils::{
|
||||
dirs, help,
|
||||
network::{NetworkManager, ProxyType},
|
||||
tmpl,
|
||||
};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::time::Duration;
|
||||
use tokio::fs;
|
||||
use serde_yaml::Mapping;
|
||||
use std::{fs, time::Duration};
|
||||
|
||||
use super::Config;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
pub struct PrfItem {
|
||||
@@ -106,10 +104,6 @@ pub struct PrfOption {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub danger_accept_invalid_certs: Option<bool>,
|
||||
|
||||
#[serde(default = "default_allow_auto_update")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allow_auto_update: Option<bool>,
|
||||
|
||||
pub merge: Option<String>,
|
||||
|
||||
pub script: Option<String>,
|
||||
@@ -122,29 +116,25 @@ pub struct PrfOption {
|
||||
}
|
||||
|
||||
impl PrfOption {
|
||||
pub fn merge(one: Option<&Self>, other: Option<&Self>) -> Option<Self> {
|
||||
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
|
||||
match (one, other) {
|
||||
(Some(a_ref), Some(b_ref)) => {
|
||||
let mut result = a_ref.clone();
|
||||
result.user_agent = b_ref.user_agent.clone().or(result.user_agent);
|
||||
result.with_proxy = b_ref.with_proxy.or(result.with_proxy);
|
||||
result.self_proxy = b_ref.self_proxy.or(result.self_proxy);
|
||||
result.danger_accept_invalid_certs = b_ref
|
||||
(Some(mut a), Some(b)) => {
|
||||
a.user_agent = b.user_agent.or(a.user_agent);
|
||||
a.with_proxy = b.with_proxy.or(a.with_proxy);
|
||||
a.self_proxy = b.self_proxy.or(a.self_proxy);
|
||||
a.danger_accept_invalid_certs = b
|
||||
.danger_accept_invalid_certs
|
||||
.or(result.danger_accept_invalid_certs);
|
||||
result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update);
|
||||
result.update_interval = b_ref.update_interval.or(result.update_interval);
|
||||
result.merge = b_ref.merge.clone().or(result.merge);
|
||||
result.script = b_ref.script.clone().or(result.script);
|
||||
result.rules = b_ref.rules.clone().or(result.rules);
|
||||
result.proxies = b_ref.proxies.clone().or(result.proxies);
|
||||
result.groups = b_ref.groups.clone().or(result.groups);
|
||||
result.timeout_seconds = b_ref.timeout_seconds.or(result.timeout_seconds);
|
||||
Some(result)
|
||||
.or(a.danger_accept_invalid_certs);
|
||||
a.update_interval = b.update_interval.or(a.update_interval);
|
||||
a.merge = b.merge.or(a.merge);
|
||||
a.script = b.script.or(a.script);
|
||||
a.rules = b.rules.or(a.rules);
|
||||
a.proxies = b.proxies.or(a.proxies);
|
||||
a.groups = b.groups.or(a.groups);
|
||||
a.timeout_seconds = b.timeout_seconds.or(a.timeout_seconds);
|
||||
Some(a)
|
||||
}
|
||||
(Some(a_ref), None) => Some(a_ref.clone()),
|
||||
(None, Some(b_ref)) => Some(b_ref.clone()),
|
||||
(None, None) => None,
|
||||
t => t.0.or(t.1),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,31 +142,25 @@ impl PrfOption {
|
||||
impl PrfItem {
|
||||
/// From partial item
|
||||
/// must contain `itype`
|
||||
pub async fn from(item: &Self, file_data: Option<String>) -> Result<Self> {
|
||||
pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
|
||||
if item.itype.is_none() {
|
||||
bail!("type should not be null");
|
||||
}
|
||||
|
||||
let itype = item
|
||||
.itype
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("type should not be null"))?;
|
||||
match itype.as_str() {
|
||||
match item.itype.unwrap().as_str() {
|
||||
"remote" => {
|
||||
let url = item
|
||||
.url
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("url should not be null"))?;
|
||||
let name = item.name.as_ref();
|
||||
let desc = item.desc.as_ref();
|
||||
let option = item.option.as_ref();
|
||||
Self::from_url(url, name, desc, option).await
|
||||
if item.url.is_none() {
|
||||
bail!("url should not be null");
|
||||
}
|
||||
let url = item.url.as_ref().unwrap().as_str();
|
||||
let name = item.name;
|
||||
let desc = item.desc;
|
||||
PrfItem::from_url(url, name, desc, item.option).await
|
||||
}
|
||||
"local" => {
|
||||
let name = item.name.clone().unwrap_or_else(|| "Local File".into());
|
||||
let desc = item.desc.clone().unwrap_or_else(|| "".into());
|
||||
let option = item.option.as_ref();
|
||||
Self::from_local(name, desc, file_data, option).await
|
||||
let name = item.name.unwrap_or("Local File".into());
|
||||
let desc = item.desc.unwrap_or("".into());
|
||||
PrfItem::from_local(name, desc, file_data, item.option)
|
||||
}
|
||||
typ => bail!("invalid profile item type \"{typ}\""),
|
||||
}
|
||||
@@ -184,14 +168,14 @@ impl PrfItem {
|
||||
|
||||
/// ## Local type
|
||||
/// create a new item from name/desc
|
||||
pub async fn from_local(
|
||||
pub fn from_local(
|
||||
name: String,
|
||||
desc: String,
|
||||
file_data: Option<String>,
|
||||
option: Option<&PrfOption>,
|
||||
) -> Result<Self> {
|
||||
let uid = help::get_uid("L").into();
|
||||
let file = format!("{uid}.yaml").into();
|
||||
option: Option<PrfOption>,
|
||||
) -> Result<PrfItem> {
|
||||
let uid = help::get_uid("L");
|
||||
let file = format!("{uid}.yaml");
|
||||
let opt_ref = option.as_ref();
|
||||
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
||||
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
||||
@@ -201,31 +185,33 @@ impl PrfItem {
|
||||
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = &mut Self::from_merge(None)?;
|
||||
profiles::profiles_append_item_safe(merge_item).await?;
|
||||
merge = merge_item.uid.clone();
|
||||
let merge_item = PrfItem::from_merge(None)?;
|
||||
Config::profiles().data().append_item(merge_item.clone())?;
|
||||
merge = merge_item.uid;
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = &mut Self::from_script(None)?;
|
||||
profiles::profiles_append_item_safe(script_item).await?;
|
||||
script = script_item.uid.clone();
|
||||
let script_item = PrfItem::from_script(None)?;
|
||||
Config::profiles().data().append_item(script_item.clone())?;
|
||||
script = script_item.uid;
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = &mut Self::from_rules()?;
|
||||
profiles::profiles_append_item_safe(rules_item).await?;
|
||||
rules = rules_item.uid.clone();
|
||||
let rules_item = PrfItem::from_rules()?;
|
||||
Config::profiles().data().append_item(rules_item.clone())?;
|
||||
rules = rules_item.uid;
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = &mut Self::from_proxies()?;
|
||||
profiles::profiles_append_item_safe(proxies_item).await?;
|
||||
proxies = proxies_item.uid.clone();
|
||||
let proxies_item = PrfItem::from_proxies()?;
|
||||
Config::profiles()
|
||||
.data()
|
||||
.append_item(proxies_item.clone())?;
|
||||
proxies = proxies_item.uid;
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = &mut Self::from_groups()?;
|
||||
profiles::profiles_append_item_safe(groups_item).await?;
|
||||
groups = groups_item.uid.clone();
|
||||
let groups_item = PrfItem::from_groups()?;
|
||||
Config::profiles().data().append_item(groups_item.clone())?;
|
||||
groups = groups_item.uid;
|
||||
}
|
||||
Ok(Self {
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("local".into()),
|
||||
name: Some(name),
|
||||
@@ -245,7 +231,7 @@ impl PrfItem {
|
||||
}),
|
||||
home: None,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(file_data.unwrap_or_else(|| tmpl::ITEM_LOCAL.into())),
|
||||
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -253,23 +239,23 @@ impl PrfItem {
|
||||
/// create a new item from url
|
||||
pub async fn from_url(
|
||||
url: &str,
|
||||
name: Option<&String>,
|
||||
desc: Option<&String>,
|
||||
option: Option<&PrfOption>,
|
||||
) -> Result<Self> {
|
||||
let with_proxy = option.is_some_and(|o| o.with_proxy.unwrap_or(false));
|
||||
let self_proxy = option.is_some_and(|o| o.self_proxy.unwrap_or(false));
|
||||
name: Option<String>,
|
||||
desc: Option<String>,
|
||||
option: Option<PrfOption>,
|
||||
) -> Result<PrfItem> {
|
||||
let opt_ref = option.as_ref();
|
||||
let with_proxy = opt_ref.is_some_and(|o| o.with_proxy.unwrap_or(false));
|
||||
let self_proxy = opt_ref.is_some_and(|o| o.self_proxy.unwrap_or(false));
|
||||
let accept_invalid_certs =
|
||||
option.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false));
|
||||
let allow_auto_update = option.map(|o| o.allow_auto_update.unwrap_or(true));
|
||||
let user_agent = option.and_then(|o| o.user_agent.clone());
|
||||
let update_interval = option.and_then(|o| o.update_interval);
|
||||
let timeout = option.and_then(|o| o.timeout_seconds).unwrap_or(20);
|
||||
let mut merge = option.and_then(|o| o.merge.clone());
|
||||
let mut script = option.and_then(|o| o.script.clone());
|
||||
let mut rules = option.and_then(|o| o.rules.clone());
|
||||
let mut proxies = option.and_then(|o| o.proxies.clone());
|
||||
let mut groups = option.and_then(|o| o.groups.clone());
|
||||
opt_ref.is_some_and(|o| o.danger_accept_invalid_certs.unwrap_or(false));
|
||||
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
|
||||
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
||||
let timeout = opt_ref.and_then(|o| o.timeout_seconds).unwrap_or(20);
|
||||
let mut merge = opt_ref.and_then(|o| o.merge.clone());
|
||||
let mut script = opt_ref.and_then(|o| o.script.clone());
|
||||
let mut rules = opt_ref.and_then(|o| o.rules.clone());
|
||||
let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
|
||||
let mut groups = opt_ref.and_then(|o| o.groups.clone());
|
||||
|
||||
// 选择代理类型
|
||||
let proxy_type = if self_proxy {
|
||||
@@ -281,7 +267,7 @@ impl PrfItem {
|
||||
};
|
||||
|
||||
// 使用网络管理器发送请求
|
||||
let resp = match NetworkManager::new()
|
||||
let resp = match NetworkManager::global()
|
||||
.get_with_interrupt(
|
||||
url,
|
||||
proxy_type,
|
||||
@@ -299,34 +285,25 @@ impl PrfItem {
|
||||
};
|
||||
|
||||
let status_code = resp.status();
|
||||
if !status_code.is_success() {
|
||||
if !StatusCode::is_success(&status_code) {
|
||||
bail!("failed to fetch remote profile with status {status_code}")
|
||||
}
|
||||
|
||||
let header = resp.headers();
|
||||
|
||||
// parse the Subscription UserInfo
|
||||
let extra;
|
||||
'extra: {
|
||||
for (k, v) in header.iter() {
|
||||
let key_lower = k.as_str().to_ascii_lowercase();
|
||||
// Accept standard custom-metadata prefixes (x-amz-meta-, x-obs-meta-, x-cos-meta-, etc.).
|
||||
if key_lower
|
||||
.strip_suffix("subscription-userinfo")
|
||||
.is_some_and(|prefix| prefix.is_empty() || prefix.ends_with('-'))
|
||||
{
|
||||
let sub_info = v.to_str().unwrap_or("");
|
||||
extra = Some(PrfExtra {
|
||||
upload: help::parse_str(sub_info, "upload").unwrap_or(0),
|
||||
download: help::parse_str(sub_info, "download").unwrap_or(0),
|
||||
total: help::parse_str(sub_info, "total").unwrap_or(0),
|
||||
expire: help::parse_str(sub_info, "expire").unwrap_or(0),
|
||||
});
|
||||
break 'extra;
|
||||
}
|
||||
let extra = match header.get("Subscription-Userinfo") {
|
||||
Some(value) => {
|
||||
let sub_info = value.to_str().unwrap_or("");
|
||||
Some(PrfExtra {
|
||||
upload: help::parse_str(sub_info, "upload").unwrap_or(0),
|
||||
download: help::parse_str(sub_info, "download").unwrap_or(0),
|
||||
total: help::parse_str(sub_info, "total").unwrap_or(0),
|
||||
expire: help::parse_str(sub_info, "expire").unwrap_or(0),
|
||||
})
|
||||
}
|
||||
extra = None;
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
// parse the Content-Disposition
|
||||
let filename = match header.get("Content-Disposition") {
|
||||
@@ -337,20 +314,19 @@ impl PrfItem {
|
||||
Some(filename) => {
|
||||
let iter = percent_encoding::percent_decode(filename.as_bytes());
|
||||
let filename = iter.decode_utf8().unwrap_or_default();
|
||||
filename.split("''").last().map(|s| s.into())
|
||||
filename.split("''").last().map(|s| s.to_string())
|
||||
}
|
||||
None => match help::parse_str::<String>(filename, "filename") {
|
||||
Some(filename) => {
|
||||
let filename = filename.trim_matches('"');
|
||||
Some(filename.into())
|
||||
Some(filename.to_string())
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
None => Some(
|
||||
crate::utils::help::get_last_part_and_decode(url)
|
||||
.unwrap_or_else(|| "Remote File".into()),
|
||||
crate::utils::help::get_last_part_and_decode(url).unwrap_or("Remote File".into()),
|
||||
),
|
||||
};
|
||||
let update_interval = match update_interval {
|
||||
@@ -367,25 +343,21 @@ impl PrfItem {
|
||||
let home = match header.get("profile-web-page-url") {
|
||||
Some(value) => {
|
||||
let str_value = value.to_str().unwrap_or("");
|
||||
Some(str_value.into())
|
||||
Some(str_value.to_string())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let uid = help::get_uid("R").into();
|
||||
let file = format!("{uid}.yaml").into();
|
||||
let name = name.map(|s| s.to_owned()).unwrap_or_else(|| {
|
||||
filename
|
||||
.map(|s| s.into())
|
||||
.unwrap_or_else(|| "Remote File".into())
|
||||
});
|
||||
let data = resp.text_with_charset()?;
|
||||
let uid = help::get_uid("R");
|
||||
let file = format!("{uid}.yaml");
|
||||
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
|
||||
let data = resp.text_with_charset("utf-8").await?;
|
||||
|
||||
// process the charset "UTF-8 with BOM"
|
||||
let data = data.trim_start_matches('\u{feff}');
|
||||
|
||||
// check the data whether the valid yaml format
|
||||
let yaml = serde_yaml_ng::from_str::<Mapping>(data)
|
||||
let yaml = serde_yaml::from_str::<Mapping>(data)
|
||||
.context("the remote profile data is invalid yaml")?;
|
||||
|
||||
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
|
||||
@@ -393,36 +365,38 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
if merge.is_none() {
|
||||
let merge_item = &mut Self::from_merge(None)?;
|
||||
profiles::profiles_append_item_safe(merge_item).await?;
|
||||
merge = merge_item.uid.clone();
|
||||
let merge_item = PrfItem::from_merge(None)?;
|
||||
Config::profiles().data().append_item(merge_item.clone())?;
|
||||
merge = merge_item.uid;
|
||||
}
|
||||
if script.is_none() {
|
||||
let script_item = &mut Self::from_script(None)?;
|
||||
profiles::profiles_append_item_safe(script_item).await?;
|
||||
script = script_item.uid.clone();
|
||||
let script_item = PrfItem::from_script(None)?;
|
||||
Config::profiles().data().append_item(script_item.clone())?;
|
||||
script = script_item.uid;
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = &mut Self::from_rules()?;
|
||||
profiles::profiles_append_item_safe(rules_item).await?;
|
||||
rules = rules_item.uid.clone();
|
||||
let rules_item = PrfItem::from_rules()?;
|
||||
Config::profiles().data().append_item(rules_item.clone())?;
|
||||
rules = rules_item.uid;
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = &mut Self::from_proxies()?;
|
||||
profiles::profiles_append_item_safe(proxies_item).await?;
|
||||
proxies = proxies_item.uid.clone();
|
||||
let proxies_item = PrfItem::from_proxies()?;
|
||||
Config::profiles()
|
||||
.data()
|
||||
.append_item(proxies_item.clone())?;
|
||||
proxies = proxies_item.uid;
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = &mut Self::from_groups()?;
|
||||
profiles::profiles_append_item_safe(groups_item).await?;
|
||||
groups = groups_item.uid.clone();
|
||||
let groups_item = PrfItem::from_groups()?;
|
||||
Config::profiles().data().append_item(groups_item.clone())?;
|
||||
groups = groups_item.uid;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("remote".into()),
|
||||
name: Some(name),
|
||||
desc: desc.cloned(),
|
||||
desc,
|
||||
file: Some(file),
|
||||
url: Some(url.into()),
|
||||
selected: None,
|
||||
@@ -434,7 +408,6 @@ impl PrfItem {
|
||||
rules,
|
||||
proxies,
|
||||
groups,
|
||||
allow_auto_update,
|
||||
..PrfOption::default()
|
||||
}),
|
||||
home,
|
||||
@@ -445,142 +418,138 @@ impl PrfItem {
|
||||
|
||||
/// ## Merge type (enhance)
|
||||
/// create the enhanced item by using `merge` rule
|
||||
pub fn from_merge(uid: Option<String>) -> Result<Self> {
|
||||
let (id, template) = if let Some(uid) = uid {
|
||||
(uid, tmpl::ITEM_MERGE.into())
|
||||
} else {
|
||||
(help::get_uid("m").into(), tmpl::ITEM_MERGE_EMPTY.into())
|
||||
};
|
||||
let file = format!("{id}.yaml").into();
|
||||
pub fn from_merge(uid: Option<String>) -> Result<PrfItem> {
|
||||
let mut id = help::get_uid("m");
|
||||
let mut template = tmpl::ITEM_MERGE_EMPTY.into();
|
||||
if let Some(uid) = uid {
|
||||
id = uid;
|
||||
template = tmpl::ITEM_MERGE.into();
|
||||
}
|
||||
let file = format!("{id}.yaml");
|
||||
|
||||
Ok(Self {
|
||||
Ok(PrfItem {
|
||||
uid: Some(id),
|
||||
itype: Some("merge".into()),
|
||||
name: None,
|
||||
desc: None,
|
||||
file: Some(file),
|
||||
url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
home: None,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(template),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// ## Script type (enhance)
|
||||
/// create the enhanced item by using javascript quick.js
|
||||
pub fn from_script(uid: Option<String>) -> Result<Self> {
|
||||
let id = if let Some(uid) = uid {
|
||||
uid
|
||||
} else {
|
||||
help::get_uid("s").into()
|
||||
};
|
||||
let file = format!("{id}.js").into(); // js ext
|
||||
Ok(Self {
|
||||
pub fn from_script(uid: Option<String>) -> Result<PrfItem> {
|
||||
let mut id = help::get_uid("s");
|
||||
if let Some(uid) = uid {
|
||||
id = uid;
|
||||
}
|
||||
let file = format!("{id}.js"); // js ext
|
||||
|
||||
Ok(PrfItem {
|
||||
uid: Some(id),
|
||||
itype: Some("script".into()),
|
||||
name: None,
|
||||
desc: None,
|
||||
file: Some(file),
|
||||
url: None,
|
||||
home: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(tmpl::ITEM_SCRIPT.into()),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// ## Rules type (enhance)
|
||||
pub fn from_rules() -> Result<Self> {
|
||||
let uid = help::get_uid("r").into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
pub fn from_rules() -> Result<PrfItem> {
|
||||
let uid = help::get_uid("r");
|
||||
let file = format!("{uid}.yaml"); // yaml ext
|
||||
|
||||
Ok(Self {
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("rules".into()),
|
||||
name: None,
|
||||
desc: None,
|
||||
file: Some(file),
|
||||
url: None,
|
||||
home: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(tmpl::ITEM_RULES.into()),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// ## Proxies type (enhance)
|
||||
pub fn from_proxies() -> Result<Self> {
|
||||
let uid = help::get_uid("p").into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
pub fn from_proxies() -> Result<PrfItem> {
|
||||
let uid = help::get_uid("p");
|
||||
let file = format!("{uid}.yaml"); // yaml ext
|
||||
|
||||
Ok(Self {
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("proxies".into()),
|
||||
name: None,
|
||||
desc: None,
|
||||
file: Some(file),
|
||||
url: None,
|
||||
home: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(tmpl::ITEM_PROXIES.into()),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// ## Groups type (enhance)
|
||||
pub fn from_groups() -> Result<Self> {
|
||||
let uid = help::get_uid("g").into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
pub fn from_groups() -> Result<PrfItem> {
|
||||
let uid = help::get_uid("g");
|
||||
let file = format!("{uid}.yaml"); // yaml ext
|
||||
|
||||
Ok(Self {
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("groups".into()),
|
||||
name: None,
|
||||
desc: None,
|
||||
file: Some(file),
|
||||
url: None,
|
||||
home: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(tmpl::ITEM_GROUPS.into()),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// get the file data
|
||||
pub async fn read_file(&self) -> Result<String> {
|
||||
let file = self
|
||||
.file
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
|
||||
let path = dirs::app_profiles_dir()?.join(file.as_str());
|
||||
let content = fs::read_to_string(path)
|
||||
.await
|
||||
.context("failed to read the file")?;
|
||||
Ok(content.into())
|
||||
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 path = dirs::app_profiles_dir()?.join(file);
|
||||
fs::read_to_string(path).context("failed to read the file")
|
||||
}
|
||||
|
||||
/// save the file data
|
||||
pub async fn save_file(&self, data: String) -> Result<()> {
|
||||
let file = self
|
||||
.file
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("could not find the file"))?;
|
||||
let path = dirs::app_profiles_dir()?.join(file.as_str());
|
||||
fs::write(path, data.as_bytes())
|
||||
.await
|
||||
.context("failed to save the file")
|
||||
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 path = dirs::app_profiles_dir()?.join(file);
|
||||
fs::write(path, data.as_bytes()).context("failed to save the file")
|
||||
}
|
||||
}
|
||||
|
||||
impl PrfItem {
|
||||
/// 获取current指向的订阅的merge
|
||||
pub fn current_merge(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.merge.clone())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的script
|
||||
pub fn current_script(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.script.clone())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的rules
|
||||
pub fn current_rules(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.rules.clone())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的proxies
|
||||
pub fn current_proxies(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.proxies.clone())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的groups
|
||||
pub fn current_groups(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.groups.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// 向前兼容,默认为订阅启用自动更新
|
||||
const fn default_allow_auto_update() -> Option<bool> {
|
||||
Some(true)
|
||||
}
|
||||
|
||||
+267
-420
@@ -1,15 +1,9 @@
|
||||
use super::{PrfOption, prfitem::PrfItem};
|
||||
use crate::utils::{
|
||||
dirs::{self, PathBufExec},
|
||||
help,
|
||||
};
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use super::{prfitem::PrfItem, PrfOption};
|
||||
use crate::utils::{dirs, help};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
use tokio::fs;
|
||||
use serde_yaml::Mapping;
|
||||
use std::{fs, io::Write};
|
||||
|
||||
/// Define the `profiles.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -21,130 +15,102 @@ 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() {
|
||||
$lv.$key = $rv.$key.to_owned();
|
||||
$lv.$key = $rv.$key;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl IProfiles {
|
||||
// Helper to find and remove an item by uid from the items vec, returning its file name (if any).
|
||||
fn take_item_file_by_uid(
|
||||
items: &mut Vec<PrfItem>,
|
||||
target_uid: Option<String>,
|
||||
) -> Option<String> {
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == target_uid {
|
||||
return items.remove(i).file;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn new() -> Self {
|
||||
let path = match dirs::profiles_path() {
|
||||
Ok(p) => p,
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, "{err}");
|
||||
return Self::default();
|
||||
}
|
||||
};
|
||||
|
||||
match help::read_yaml::<Self>(&path).await {
|
||||
pub fn new() -> Self {
|
||||
match dirs::profiles_path().and_then(|path| help::read_yaml::<Self>(&path)) {
|
||||
Ok(mut profiles) => {
|
||||
let items = profiles.items.get_or_insert_with(Vec::new);
|
||||
for item in items.iter_mut() {
|
||||
if item.uid.is_none() {
|
||||
item.uid = Some(help::get_uid("d").into());
|
||||
if profiles.items.is_none() {
|
||||
profiles.items = Some(vec![]);
|
||||
}
|
||||
// compatible with the old old old version
|
||||
if let Some(items) = profiles.items.as_mut() {
|
||||
for item in items.iter_mut() {
|
||||
if item.uid.is_none() {
|
||||
item.uid = Some(help::get_uid("d"));
|
||||
}
|
||||
}
|
||||
}
|
||||
profiles
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, "{err}");
|
||||
Self::default()
|
||||
log::error!(target: "app", "{err}");
|
||||
Self::template()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_file(&self) -> Result<()> {
|
||||
pub fn template() -> Self {
|
||||
Self {
|
||||
items: Some(vec![]),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_file(&self) -> Result<()> {
|
||||
help::save_yaml(
|
||||
&dirs::profiles_path()?,
|
||||
self,
|
||||
Some("# Profiles Config for Clash Verge"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 只修改current,valid和chain
|
||||
pub fn patch_config(&mut self, patch: &Self) {
|
||||
pub fn patch_config(&mut self, patch: IProfiles) -> Result<()> {
|
||||
if self.items.is_none() {
|
||||
self.items = Some(vec![]);
|
||||
}
|
||||
|
||||
if let Some(current) = &patch.current
|
||||
&& let Some(items) = self.items.as_ref()
|
||||
{
|
||||
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.as_ref() == some_uid) {
|
||||
self.current = some_uid.cloned();
|
||||
if items.iter().any(|e| e.uid == some_uid) {
|
||||
self.current = some_uid;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const fn get_current(&self) -> Option<&String> {
|
||||
self.current.as_ref()
|
||||
pub fn get_current(&self) -> Option<String> {
|
||||
self.current.clone()
|
||||
}
|
||||
|
||||
/// get items ref
|
||||
pub const fn get_items(&self) -> Option<&Vec<PrfItem>> {
|
||||
pub fn get_items(&self) -> Option<&Vec<PrfItem>> {
|
||||
self.items.as_ref()
|
||||
}
|
||||
|
||||
/// find the item by the uid
|
||||
pub fn get_item(&self, uid: impl AsRef<str>) -> Result<&PrfItem> {
|
||||
let uid_str = uid.as_ref();
|
||||
|
||||
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
|
||||
if let Some(items) = self.items.as_ref() {
|
||||
let some_uid = Some(uid.clone());
|
||||
|
||||
for each in items.iter() {
|
||||
if let Some(uid_val) = &each.uid
|
||||
&& uid_val.as_str() == uid_str
|
||||
{
|
||||
if each.uid == some_uid {
|
||||
return Ok(each);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bail!("failed to get the profile item \"uid:{}\"", uid_str);
|
||||
}
|
||||
|
||||
pub fn get_item_arc(&self, uid: &str) -> Option<Arc<PrfItem>> {
|
||||
self.items.as_ref().and_then(|items| {
|
||||
items
|
||||
.iter()
|
||||
.find(|it| it.uid.as_deref() == Some(uid))
|
||||
.map(|it| Arc::new(it.clone()))
|
||||
})
|
||||
bail!("failed to get the profile item \"uid:{uid}\"");
|
||||
}
|
||||
|
||||
/// append new item
|
||||
/// if the file_data is some
|
||||
/// then should save the data to file
|
||||
pub async fn append_item(&mut self, item: &mut PrfItem) -> Result<()> {
|
||||
let uid = &item.uid;
|
||||
if uid.is_none() {
|
||||
pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
|
||||
if item.uid.is_none() {
|
||||
bail!("the uid should not be null");
|
||||
}
|
||||
let uid = item.uid.clone();
|
||||
|
||||
// save the file data
|
||||
// move the field value after save
|
||||
@@ -153,20 +119,19 @@ impl IProfiles {
|
||||
bail!("the file should not be null");
|
||||
}
|
||||
|
||||
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.as_str());
|
||||
let file = item.file.clone().unwrap();
|
||||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::write(&path, file_data.as_bytes())
|
||||
.await
|
||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||
fs::File::create(path)
|
||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||
.write(file_data.as_bytes())
|
||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||
}
|
||||
|
||||
if self.current.is_none()
|
||||
&& (item.itype == Some("remote".into()) || item.itype == Some("local".into()))
|
||||
&& (item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string()))
|
||||
{
|
||||
self.current = uid.to_owned();
|
||||
self.current = uid;
|
||||
}
|
||||
|
||||
if self.items.is_none() {
|
||||
@@ -174,43 +139,42 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
if let Some(items) = self.items.as_mut() {
|
||||
items.push(item.to_owned());
|
||||
items.push(item)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
self.save_file()
|
||||
}
|
||||
|
||||
/// reorder items
|
||||
pub async fn reorder(&mut self, active_id: &String, over_id: &String) -> Result<()> {
|
||||
pub fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> {
|
||||
let mut items = self.items.take().unwrap_or_default();
|
||||
let mut old_index = None;
|
||||
let mut new_index = None;
|
||||
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid.as_ref() == Some(active_id) {
|
||||
if items[i].uid == Some(active_id.clone()) {
|
||||
old_index = Some(i);
|
||||
}
|
||||
if items[i].uid.as_ref() == Some(over_id) {
|
||||
if items[i].uid == Some(over_id.clone()) {
|
||||
new_index = Some(i);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if old_index.is_none() || new_index.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let item = items.remove(old_index.unwrap());
|
||||
items.insert(new_index.unwrap(), item);
|
||||
self.items = Some(items);
|
||||
self.save_file().await
|
||||
self.save_file()
|
||||
}
|
||||
|
||||
/// update the item value
|
||||
pub async fn patch_item(&mut self, uid: &String, item: &PrfItem) -> Result<()> {
|
||||
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
|
||||
let mut items = self.items.take().unwrap_or_default();
|
||||
|
||||
for each in items.iter_mut() {
|
||||
if each.uid.as_ref() == Some(uid) {
|
||||
if each.uid == Some(uid.clone()) {
|
||||
patch!(each, item, itype);
|
||||
patch!(each, item, name);
|
||||
patch!(each, item, desc);
|
||||
@@ -222,7 +186,7 @@ impl IProfiles {
|
||||
patch!(each, item, option);
|
||||
|
||||
self.items = Some(items);
|
||||
return self.save_file().await;
|
||||
return self.save_file();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,13 +196,13 @@ impl IProfiles {
|
||||
|
||||
/// be used to update the remote item
|
||||
/// only patch `updated` `extra` `file_data`
|
||||
pub async fn update_item(&mut self, uid: &String, item: &mut PrfItem) -> Result<()> {
|
||||
pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
|
||||
if self.items.is_none() {
|
||||
self.items = Some(vec![]);
|
||||
}
|
||||
|
||||
// find the item
|
||||
let _ = self.get_item(uid)?;
|
||||
let _ = self.get_item(&uid)?;
|
||||
|
||||
if let Some(items) = self.items.as_mut() {
|
||||
let some_uid = Some(uid.clone());
|
||||
@@ -247,26 +211,24 @@ impl IProfiles {
|
||||
if each.uid == some_uid {
|
||||
each.extra = item.extra;
|
||||
each.updated = item.updated;
|
||||
each.home = item.home.to_owned();
|
||||
each.option = PrfOption::merge(each.option.as_ref(), item.option.as_ref());
|
||||
each.home = item.home;
|
||||
each.option = PrfOption::merge(each.option.clone(), item.option);
|
||||
// save the file data
|
||||
// move the field value after save
|
||||
if let Some(file_data) = item.file_data.take() {
|
||||
let file = each.file.take();
|
||||
let file = file.unwrap_or_else(|| {
|
||||
item.file
|
||||
.take()
|
||||
.unwrap_or_else(|| format!("{}.yaml", &uid).into())
|
||||
});
|
||||
let file =
|
||||
file.unwrap_or(item.file.take().unwrap_or(format!("{}.yaml", &uid)));
|
||||
|
||||
// the file must exists
|
||||
each.file = Some(file.clone());
|
||||
|
||||
let path = dirs::app_profiles_dir()?.join(file.as_str());
|
||||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::write(&path, file_data.as_bytes())
|
||||
.await
|
||||
.with_context(|| format!("failed to write to file \"{file}\""))?;
|
||||
fs::File::create(path)
|
||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||
.write(file_data.as_bytes())
|
||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -274,66 +236,137 @@ impl IProfiles {
|
||||
}
|
||||
}
|
||||
|
||||
self.save_file().await
|
||||
self.save_file()
|
||||
}
|
||||
|
||||
/// delete item
|
||||
/// if delete the current then return true
|
||||
pub async fn delete_item(&mut self, uid: &String) -> Result<bool> {
|
||||
let current = self.current.as_ref().unwrap_or(uid);
|
||||
pub fn delete_item(&mut self, uid: String) -> Result<bool> {
|
||||
let current = self.current.as_ref().unwrap_or(&uid);
|
||||
let current = current.clone();
|
||||
let item = self.get_item(uid)?;
|
||||
let item = self.get_item(&uid)?;
|
||||
let merge_uid = item.option.as_ref().and_then(|e| e.merge.clone());
|
||||
let script_uid = item.option.as_ref().and_then(|e| e.script.clone());
|
||||
let rules_uid = item.option.as_ref().and_then(|e| e.rules.clone());
|
||||
let proxies_uid = item.option.as_ref().and_then(|e| e.proxies.clone());
|
||||
let groups_uid = item.option.as_ref().and_then(|e| e.groups.clone());
|
||||
let mut items = self.items.take().unwrap_or_default();
|
||||
let mut index = None;
|
||||
let mut merge_index = None;
|
||||
let mut script_index = None;
|
||||
let mut rules_index = None;
|
||||
let mut proxies_index = None;
|
||||
let mut groups_index = None;
|
||||
|
||||
// remove the main item (if exists) and delete its file
|
||||
if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid.clone())) {
|
||||
let _ = dirs::app_profiles_dir()?
|
||||
.join(file.as_str())
|
||||
.remove_if_exists()
|
||||
.await;
|
||||
// get the index
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == Some(uid.clone()) {
|
||||
index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// remove related extension items (merge, script, rules, proxies, groups)
|
||||
if let Some(file) = Self::take_item_file_by_uid(&mut items, merge_uid.clone()) {
|
||||
let _ = dirs::app_profiles_dir()?
|
||||
.join(file.as_str())
|
||||
.remove_if_exists()
|
||||
.await;
|
||||
if let Some(index) = index {
|
||||
if let Some(file) = items.remove(index).file {
|
||||
let _ = dirs::app_profiles_dir().map(|path| {
|
||||
let path = path.join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(file) = Self::take_item_file_by_uid(&mut items, script_uid.clone()) {
|
||||
let _ = dirs::app_profiles_dir()?
|
||||
.join(file.as_str())
|
||||
.remove_if_exists()
|
||||
.await;
|
||||
// get the merge index
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == merge_uid {
|
||||
merge_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(file) = Self::take_item_file_by_uid(&mut items, rules_uid.clone()) {
|
||||
let _ = dirs::app_profiles_dir()?
|
||||
.join(file.as_str())
|
||||
.remove_if_exists()
|
||||
.await;
|
||||
if let Some(index) = merge_index {
|
||||
if let Some(file) = items.remove(index).file {
|
||||
let _ = dirs::app_profiles_dir().map(|path| {
|
||||
let path = path.join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(file) = Self::take_item_file_by_uid(&mut items, proxies_uid.clone()) {
|
||||
let _ = dirs::app_profiles_dir()?
|
||||
.join(file.as_str())
|
||||
.remove_if_exists()
|
||||
.await;
|
||||
// get the script index
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == script_uid {
|
||||
script_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(file) = Self::take_item_file_by_uid(&mut items, groups_uid.clone()) {
|
||||
let _ = dirs::app_profiles_dir()?
|
||||
.join(file.as_str())
|
||||
.remove_if_exists()
|
||||
.await;
|
||||
if let Some(index) = script_index {
|
||||
if let Some(file) = items.remove(index).file {
|
||||
let _ = dirs::app_profiles_dir().map(|path| {
|
||||
let path = path.join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// get the rules index
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == rules_uid {
|
||||
rules_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(index) = rules_index {
|
||||
if let Some(file) = items.remove(index).file {
|
||||
let _ = dirs::app_profiles_dir().map(|path| {
|
||||
let path = path.join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// get the proxies index
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == proxies_uid {
|
||||
proxies_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(index) = proxies_index {
|
||||
if let Some(file) = items.remove(index).file {
|
||||
let _ = dirs::app_profiles_dir().map(|path| {
|
||||
let path = path.join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// get the groups index
|
||||
for (i, _) in items.iter().enumerate() {
|
||||
if items[i].uid == groups_uid {
|
||||
groups_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(index) = groups_index {
|
||||
if let Some(file) = items.remove(index).file {
|
||||
let _ = dirs::app_profiles_dir().map(|path| {
|
||||
let path = path.join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// delete the original uid
|
||||
if current == *uid {
|
||||
if current == uid {
|
||||
self.current = None;
|
||||
for item in items.iter() {
|
||||
if item.itype == Some("remote".into()) || item.itype == Some("local".into()) {
|
||||
if item.itype == Some("remote".to_string())
|
||||
|| item.itype == Some("local".to_string())
|
||||
{
|
||||
self.current = item.uid.clone();
|
||||
break;
|
||||
}
|
||||
@@ -341,20 +374,20 @@ impl IProfiles {
|
||||
}
|
||||
|
||||
self.items = Some(items);
|
||||
self.save_file().await?;
|
||||
Ok(current == *uid)
|
||||
self.save_file()?;
|
||||
Ok(current == uid)
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅内容
|
||||
pub async fn current_mapping(&self) -> Result<Mapping> {
|
||||
pub fn current_mapping(&self) -> Result<Mapping> {
|
||||
match (self.current.as_ref(), self.items.as_ref()) {
|
||||
(Some(current), Some(items)) => {
|
||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||
let file_path = match item.file.as_ref() {
|
||||
Some(file) => dirs::app_profiles_dir()?.join(file.as_str()),
|
||||
Some(file) => dirs::app_profiles_dir()?.join(file),
|
||||
None => bail!("failed to get the file field"),
|
||||
};
|
||||
return help::read_mapping(&file_path).await;
|
||||
return help::read_mapping(&file_path);
|
||||
}
|
||||
bail!("failed to find the current profile \"uid:{current}\"");
|
||||
}
|
||||
@@ -362,18 +395,88 @@ impl IProfiles {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的merge
|
||||
pub fn current_merge(&self) -> Option<String> {
|
||||
match (self.current.as_ref(), self.items.as_ref()) {
|
||||
(Some(current), Some(items)) => {
|
||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||
let merge = item.option.as_ref().and_then(|e| e.merge.clone());
|
||||
return merge;
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的script
|
||||
pub fn current_script(&self) -> Option<String> {
|
||||
match (self.current.as_ref(), self.items.as_ref()) {
|
||||
(Some(current), Some(items)) => {
|
||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||
let script = item.option.as_ref().and_then(|e| e.script.clone());
|
||||
return script;
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的rules
|
||||
pub fn current_rules(&self) -> Option<String> {
|
||||
match (self.current.as_ref(), self.items.as_ref()) {
|
||||
(Some(current), Some(items)) => {
|
||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||
let rules = item.option.as_ref().and_then(|e| e.rules.clone());
|
||||
return rules;
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的proxies
|
||||
pub fn current_proxies(&self) -> Option<String> {
|
||||
match (self.current.as_ref(), self.items.as_ref()) {
|
||||
(Some(current), Some(items)) => {
|
||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||
let proxies = item.option.as_ref().and_then(|e| e.proxies.clone());
|
||||
return proxies;
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的groups
|
||||
pub fn current_groups(&self) -> Option<String> {
|
||||
match (self.current.as_ref(), self.items.as_ref()) {
|
||||
(Some(current), Some(items)) => {
|
||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||
let groups = item.option.as_ref().and_then(|e| e.groups.clone());
|
||||
return groups;
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断profile是否是current指向的
|
||||
pub fn is_current_profile_index(&self, index: &String) -> bool {
|
||||
self.current.as_ref() == Some(index)
|
||||
pub fn is_current_profile_index(&self, index: String) -> bool {
|
||||
self.current == Some(index)
|
||||
}
|
||||
|
||||
/// 获取所有的profiles(uid,名称)
|
||||
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(&String, &String)>> {
|
||||
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(String, String)>> {
|
||||
self.items.as_ref().map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
if let (Some(uid), Some(name)) = (e.uid.as_ref(), e.name.as_ref()) {
|
||||
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
|
||||
Some((uid, name))
|
||||
} else {
|
||||
None
|
||||
@@ -382,260 +485,4 @@ impl IProfiles {
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// 通过 uid 获取名称
|
||||
pub fn get_name_by_uid(&self, uid: &String) -> Option<&String> {
|
||||
if let Some(items) = &self.items {
|
||||
for item in items {
|
||||
if item.uid.as_ref() == Some(uid) {
|
||||
return item.name.as_ref();
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 以 app 中的 profile 列表为准,删除不再需要的文件
|
||||
pub async 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())
|
||||
&& Self::is_profile_file(file_name)
|
||||
{
|
||||
// 检查是否为全局扩展文件
|
||||
if protected_files.contains(file_name) {
|
||||
logging!(debug, Type::Config, "保护全局扩展配置文件: {file_name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否为活跃文件
|
||||
if !active_files.contains(file_name) {
|
||||
match path.to_path_buf().remove_if_exists().await {
|
||||
Ok(_) => {
|
||||
deleted_files.push(file_name.into());
|
||||
logging!(info, Type::Config, "已清理冗余文件: {file_name}");
|
||||
}
|
||||
Err(e) => {
|
||||
failed_deletions.push(format!("{file_name}: {e}").into());
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"Warning: 清理文件失败: {file_name} - {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = CleanupResult {
|
||||
total_files,
|
||||
deleted_files,
|
||||
failed_deletions,
|
||||
};
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"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".into());
|
||||
protected_files.insert("Script.js".into());
|
||||
|
||||
protected_files
|
||||
}
|
||||
|
||||
/// 获取所有 active profile 关联的文件名
|
||||
fn get_all_active_files(&self) -> HashSet<&str> {
|
||||
let mut active_files: HashSet<&str> = HashSet::new();
|
||||
|
||||
if let Some(items) = &self.items {
|
||||
for item in items {
|
||||
// 收集所有类型 profile 的文件
|
||||
if let Some(file) = &item.file {
|
||||
active_files.insert(file);
|
||||
}
|
||||
|
||||
// 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件
|
||||
if let Some(itype) = &item.itype
|
||||
&& (itype == "remote" || itype == "local")
|
||||
&& let Some(option) = &item.option
|
||||
{
|
||||
// 收集关联的扩展文件
|
||||
if let Some(merge_uid) = &option.merge
|
||||
&& let Ok(merge_item) = self.get_item(merge_uid)
|
||||
&& let Some(file) = &merge_item.file
|
||||
{
|
||||
active_files.insert(file);
|
||||
}
|
||||
|
||||
if let Some(script_uid) = &option.script
|
||||
&& let Ok(script_item) = self.get_item(script_uid)
|
||||
&& let Some(file) = &script_item.file
|
||||
{
|
||||
active_files.insert(file);
|
||||
}
|
||||
|
||||
if let Some(rules_uid) = &option.rules
|
||||
&& let Ok(rules_item) = self.get_item(rules_uid)
|
||||
&& let Some(file) = &rules_item.file
|
||||
{
|
||||
active_files.insert(file);
|
||||
}
|
||||
|
||||
if let Some(proxies_uid) = &option.proxies
|
||||
&& let Ok(proxies_item) = self.get_item(proxies_uid)
|
||||
&& let Some(file) = &proxies_item.file
|
||||
{
|
||||
active_files.insert(file);
|
||||
}
|
||||
|
||||
if let Some(groups_uid) = &option.groups
|
||||
&& let Ok(groups_item) = self.get_item(groups_uid)
|
||||
&& let Some(file) = &groups_item.file
|
||||
{
|
||||
active_files.insert(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊的Send-safe helper函数,完全避免跨await持有guard
|
||||
use crate::config::Config;
|
||||
|
||||
pub async fn profiles_append_item_with_filedata_safe(
|
||||
item: &PrfItem,
|
||||
file_data: Option<String>,
|
||||
) -> Result<()> {
|
||||
let item = &mut PrfItem::from(item, file_data).await?;
|
||||
profiles_append_item_safe(item).await
|
||||
}
|
||||
|
||||
pub async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
profiles.append_item(item).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn profiles_patch_item_safe(index: &String, item: &PrfItem) -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
profiles.patch_item(index, item).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn profiles_delete_item_safe(index: &String) -> Result<bool> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
let deleted = profiles.delete_item(index).await?;
|
||||
Ok((profiles, deleted))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn profiles_reorder_safe(active_id: &String, over_id: &String) -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
profiles.reorder(active_id, over_id).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn profiles_save_file_safe() -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|profiles| async move {
|
||||
profiles.save_file().await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn profiles_draft_update_item_safe(index: &String, item: &mut PrfItem) -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
profiles.update_item(index, item).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use crate::enhance::field::use_keys;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct IRuntime {
|
||||
pub config: Option<Mapping>,
|
||||
@@ -32,15 +30,15 @@ impl IRuntime {
|
||||
let patch_tun = patch.get("tun");
|
||||
if patch_tun.is_some() {
|
||||
let tun = config.get("tun");
|
||||
let mut tun: Mapping = tun.map_or_else(Mapping::new, |val| {
|
||||
val.as_mapping().cloned().unwrap_or_else(Mapping::new)
|
||||
let mut tun = tun.map_or(Mapping::new(), |val| {
|
||||
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||
});
|
||||
let patch_tun = patch_tun.map_or_else(Mapping::new, |val| {
|
||||
val.as_mapping().cloned().unwrap_or_else(Mapping::new)
|
||||
let patch_tun = patch_tun.map_or(Mapping::new(), |val| {
|
||||
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||
});
|
||||
use_keys(&patch_tun).into_iter().for_each(|key| {
|
||||
if let Some(value) = patch_tun.get(key.as_str()) {
|
||||
tun.insert(Value::from(key.as_str()), value.clone());
|
||||
if let Some(value) = patch_tun.get(&key).to_owned() {
|
||||
tun.insert(key.into(), value.clone());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -48,76 +46,4 @@ impl IRuntime {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//跟新链式代理配置文件
|
||||
/// {
|
||||
/// "proxies":[
|
||||
/// {
|
||||
/// name : 入口节点,
|
||||
/// type: xxx
|
||||
/// server: xxx
|
||||
/// port: xxx
|
||||
/// ports: xxx
|
||||
/// password: xxx
|
||||
/// skip-cert-verify: xxx,
|
||||
/// },
|
||||
/// {
|
||||
/// name : hop_node_1_xxxx,
|
||||
/// type: xxx
|
||||
/// server: xxx
|
||||
/// port: xxx
|
||||
/// ports: xxx
|
||||
/// password: xxx
|
||||
/// skip-cert-verify: xxx,
|
||||
/// dialer-proxy : "入口节点"
|
||||
/// },
|
||||
/// {
|
||||
/// name : 出口节点,
|
||||
/// type: xxx
|
||||
/// server: xxx
|
||||
/// port: xxx
|
||||
/// ports: xxx
|
||||
/// password: xxx
|
||||
/// skip-cert-verify: xxx,
|
||||
/// dialer-proxy : "hop_node_1_xxxx"
|
||||
/// }
|
||||
/// ],
|
||||
/// "proxy-groups" : [
|
||||
/// {
|
||||
/// name : "proxy_chain",
|
||||
/// type: "select",
|
||||
/// proxies ["出口节点"]
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
///
|
||||
/// 传入none 为删除
|
||||
pub fn update_proxy_chain_config(&mut self, proxy_chain_config: Option<Value>) {
|
||||
if let Some(config) = self.config.as_mut() {
|
||||
if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") {
|
||||
proxies.iter_mut().for_each(|proxy| {
|
||||
if let Some(proxy) = proxy.as_mapping_mut()
|
||||
&& proxy.get("dialer-proxy").is_some()
|
||||
{
|
||||
proxy.remove("dialer-proxy");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(Value::Sequence(dialer_proxies)) = proxy_chain_config
|
||||
&& let Some(Value::Sequence(proxies)) = config.get_mut("proxies")
|
||||
{
|
||||
for (i, dialer_proxy) in dialer_proxies.iter().enumerate() {
|
||||
if let Some(Value::Mapping(proxy)) = proxies
|
||||
.iter_mut()
|
||||
.find(|proxy| proxy.get("name") == Some(dialer_proxy))
|
||||
&& i != 0
|
||||
&& let Some(dialer_proxy) = dialer_proxies.get(i - 1)
|
||||
{
|
||||
proxy.insert("dialer-proxy".into(), dialer_proxy.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+175
-162
@@ -1,13 +1,10 @@
|
||||
use crate::config::Config;
|
||||
use crate::{
|
||||
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
|
||||
logging,
|
||||
utils::{dirs, help, i18n, logging::Type},
|
||||
config::{deserialize_encrypted, serialize_encrypted, DEFAULT_PAC},
|
||||
utils::{dirs, help, i18n},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use log::LevelFilter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smartstring::alias::String;
|
||||
|
||||
/// ### `verge.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -16,12 +13,6 @@ pub struct IVerge {
|
||||
/// silent | error | warn | info | debug | trace
|
||||
pub app_log_level: Option<String>,
|
||||
|
||||
/// app log max size in KB
|
||||
pub app_log_max_size: Option<u64>,
|
||||
|
||||
/// app log max count
|
||||
pub app_log_max_count: Option<usize>,
|
||||
|
||||
// i18n
|
||||
pub language: Option<String>,
|
||||
|
||||
@@ -46,26 +37,18 @@ pub struct IVerge {
|
||||
pub enable_memory_usage: Option<bool>,
|
||||
|
||||
/// enable group icon
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_group_icon: Option<bool>,
|
||||
|
||||
/// common tray icon
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub common_tray_icon: Option<bool>,
|
||||
|
||||
/// tray icon
|
||||
#[cfg(target_os = "macos")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tray_icon: Option<String>,
|
||||
|
||||
/// menu icon
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub menu_icon: Option<String>,
|
||||
|
||||
/// menu order
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub menu_order: Option<Vec<String>>,
|
||||
|
||||
/// sysproxy tray icon
|
||||
pub sysproxy_tray_icon: Option<bool>,
|
||||
|
||||
@@ -120,7 +103,6 @@ pub struct IVerge {
|
||||
|
||||
/// hotkey map
|
||||
/// format: {func},{key}
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hotkeys: Option<Vec<String>>,
|
||||
|
||||
/// enable global hotkey
|
||||
@@ -140,16 +122,13 @@ pub struct IVerge {
|
||||
pub default_latency_test: Option<String>,
|
||||
|
||||
/// 默认的延迟测试超时时间
|
||||
pub default_latency_timeout: Option<i16>,
|
||||
|
||||
/// 是否自动检测当前节点延迟
|
||||
pub enable_auto_delay_detection: Option<bool>,
|
||||
pub default_latency_timeout: Option<i32>,
|
||||
|
||||
/// 是否使用内部的脚本支持,默认为真
|
||||
pub enable_builtin_enhanced: Option<bool>,
|
||||
|
||||
/// proxy 页面布局 列数
|
||||
pub proxy_layout_column: Option<u8>,
|
||||
pub proxy_layout_column: Option<i32>,
|
||||
|
||||
/// 测试站列表
|
||||
pub test_list: Option<Vec<IVergeTestItem>>,
|
||||
@@ -158,6 +137,9 @@ 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>,
|
||||
@@ -208,12 +190,9 @@ pub struct IVerge {
|
||||
)]
|
||||
pub webdav_password: Option<String>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
|
||||
// pub enable_tray_icon: Option<bool>,
|
||||
/// show proxy groups directly on tray root menu
|
||||
pub tray_inline_proxy_groups: Option<bool>,
|
||||
pub enable_tray_icon: Option<bool>,
|
||||
|
||||
/// 自动进入轻量模式
|
||||
pub enable_auto_light_weight_mode: Option<bool>,
|
||||
@@ -221,14 +200,8 @@ pub struct IVerge {
|
||||
/// 自动进入轻量模式的延迟(分钟)
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
|
||||
/// 启用代理页面自动滚动
|
||||
pub enable_hover_jump_navigator: Option<bool>,
|
||||
|
||||
/// 代理页面自动滚动延迟(毫秒)
|
||||
pub hover_jump_navigator_delay: Option<u64>,
|
||||
|
||||
/// 启用外部控制器
|
||||
pub enable_external_controller: Option<bool>,
|
||||
/// 服务状态跟踪
|
||||
pub service_state: Option<crate::core::service::ServiceState>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
@@ -256,118 +229,26 @@ pub struct IVergeTheme {
|
||||
}
|
||||
|
||||
impl IVerge {
|
||||
/// 有效的clash核心名称
|
||||
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
|
||||
|
||||
/// 验证并修正配置文件中的clash_core值
|
||||
pub async fn validate_and_fix_config() -> Result<()> {
|
||||
let config_path = dirs::verge_path()?;
|
||||
let mut config = match help::read_yaml::<Self>(&config_path).await {
|
||||
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,
|
||||
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'verge-mihomo'",
|
||||
core
|
||||
);
|
||||
config.clash_core = Some("verge-mihomo".into());
|
||||
needs_fix = true;
|
||||
}
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"启动时发现未配置clash_core, 将设置为默认值 'verge-mihomo'"
|
||||
);
|
||||
config.clash_core = Some("verge-mihomo".into());
|
||||
needs_fix = true;
|
||||
}
|
||||
|
||||
// 修正后保存配置
|
||||
if needs_fix {
|
||||
logging!(info, Type::Config, "正在保存修正后的配置文件...");
|
||||
help::save_yaml(&config_path, &config, Some("# Clash Verge Config")).await?;
|
||||
logging!(info, Type::Config, "配置文件修正完成,需要重新加载配置");
|
||||
|
||||
Self::reload_config_after_fix(config).await?;
|
||||
} else {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"clash_core配置验证通过: {:?}",
|
||||
config.clash_core
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 配置修正后重新加载配置
|
||||
async fn reload_config_after_fix(updated_config: Self) -> Result<()> {
|
||||
logging!(
|
||||
info,
|
||||
Type::Config,
|
||||
"内存配置已强制更新,新的clash_core: {:?}",
|
||||
&updated_config.clash_core
|
||||
);
|
||||
|
||||
let config_draft = Config::verge().await;
|
||||
config_draft.edit_draft(|d| {
|
||||
*d = updated_config;
|
||||
});
|
||||
config_draft.apply();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_valid_clash_core(&self) -> String {
|
||||
self.clash_core
|
||||
.clone()
|
||||
.unwrap_or_else(|| "verge-mihomo".into())
|
||||
}
|
||||
|
||||
fn get_system_language() -> String {
|
||||
let sys_lang = sys_locale::get_locale()
|
||||
.unwrap_or_else(|| "en".into())
|
||||
.unwrap_or_else(|| String::from("en"))
|
||||
.to_lowercase();
|
||||
|
||||
let lang_code = sys_lang.split(['_', '-']).next().unwrap_or("en");
|
||||
let supported_languages = i18n::get_supported_languages();
|
||||
|
||||
if supported_languages.contains(&lang_code.into()) {
|
||||
lang_code.into()
|
||||
if supported_languages.contains(&lang_code.to_string()) {
|
||||
lang_code.to_string()
|
||||
} else {
|
||||
String::from("en")
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new() -> Self {
|
||||
match dirs::verge_path() {
|
||||
Ok(path) => match help::read_yaml::<Self>(&path).await {
|
||||
Ok(mut config) => {
|
||||
// compatibility
|
||||
if let Some(start_page) = config.start_page.clone()
|
||||
&& start_page == "/home"
|
||||
{
|
||||
config.start_page = Some(String::from("/"));
|
||||
}
|
||||
config
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, "{err}");
|
||||
Self::template()
|
||||
}
|
||||
},
|
||||
pub fn new() -> Self {
|
||||
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, "{err}");
|
||||
log::error!(target: "app", "{err}");
|
||||
Self::template()
|
||||
}
|
||||
}
|
||||
@@ -375,8 +256,6 @@ impl IVerge {
|
||||
|
||||
pub fn template() -> Self {
|
||||
Self {
|
||||
app_log_max_size: Some(128),
|
||||
app_log_max_count: Some(8),
|
||||
clash_core: Some("verge-mihomo".into()),
|
||||
language: Some(Self::get_system_language()),
|
||||
theme_mode: Some("system".into()),
|
||||
@@ -384,7 +263,7 @@ impl IVerge {
|
||||
env_type: Some("bash".into()),
|
||||
#[cfg(target_os = "windows")]
|
||||
env_type: Some("powershell".into()),
|
||||
start_page: Some("/".into()),
|
||||
start_page: Some("/home".into()),
|
||||
traffic_graph: Some(true),
|
||||
enable_memory_usage: Some(true),
|
||||
enable_group_icon: Some(true),
|
||||
@@ -396,12 +275,11 @@ impl IVerge {
|
||||
tun_tray_icon: Some(false),
|
||||
enable_auto_launch: Some(false),
|
||||
enable_silent_start: Some(false),
|
||||
enable_hover_jump_navigator: Some(true),
|
||||
hover_jump_navigator_delay: Some(280),
|
||||
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"))]
|
||||
@@ -421,44 +299,39 @@ impl IVerge {
|
||||
auto_close_connection: Some(true),
|
||||
auto_check_update: Some(true),
|
||||
enable_builtin_enhanced: Some(true),
|
||||
auto_log_clean: Some(2), // 1: 1天, 2: 7天, 3: 30天, 4: 90天
|
||||
auto_log_clean: Some(3),
|
||||
webdav_url: None,
|
||||
webdav_username: None,
|
||||
webdav_password: None,
|
||||
enable_tray_speed: Some(false),
|
||||
// enable_tray_icon: Some(true),
|
||||
tray_inline_proxy_groups: Some(false),
|
||||
enable_tray_icon: Some(true),
|
||||
enable_global_hotkey: Some(true),
|
||||
enable_auto_light_weight_mode: Some(false),
|
||||
auto_light_weight_minutes: Some(10),
|
||||
enable_dns_settings: Some(false),
|
||||
home_cards: None,
|
||||
enable_external_controller: Some(false),
|
||||
service_state: None,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Save IVerge App Config
|
||||
pub async fn save_file(&self) -> Result<()> {
|
||||
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config")).await
|
||||
pub fn save_file(&self) -> Result<()> {
|
||||
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config"))
|
||||
}
|
||||
|
||||
/// patch verge config
|
||||
/// only save to file
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub fn patch_config(&mut self, patch: &Self) {
|
||||
pub fn patch_config(&mut self, patch: IVerge) {
|
||||
macro_rules! patch {
|
||||
($key: tt) => {
|
||||
if patch.$key.is_some() {
|
||||
self.$key = patch.$key.clone();
|
||||
self.$key = patch.$key;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
patch!(app_log_level);
|
||||
patch!(app_log_max_size);
|
||||
patch!(app_log_max_count);
|
||||
|
||||
patch!(language);
|
||||
patch!(theme_mode);
|
||||
patch!(tray_event);
|
||||
@@ -471,7 +344,6 @@ impl IVerge {
|
||||
#[cfg(target_os = "macos")]
|
||||
patch!(tray_icon);
|
||||
patch!(menu_icon);
|
||||
patch!(menu_order);
|
||||
patch!(common_tray_icon);
|
||||
patch!(sysproxy_tray_icon);
|
||||
patch!(tun_tray_icon);
|
||||
@@ -479,8 +351,7 @@ impl IVerge {
|
||||
patch!(enable_tun_mode);
|
||||
patch!(enable_auto_launch);
|
||||
patch!(enable_silent_start);
|
||||
patch!(enable_hover_jump_navigator);
|
||||
patch!(hover_jump_navigator_delay);
|
||||
patch!(enable_random_port);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
patch!(verge_redir_port);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -512,7 +383,6 @@ impl IVerge {
|
||||
patch!(auto_check_update);
|
||||
patch!(default_latency_test);
|
||||
patch!(default_latency_timeout);
|
||||
patch!(enable_auto_delay_detection);
|
||||
patch!(enable_builtin_enhanced);
|
||||
patch!(proxy_layout_column);
|
||||
patch!(test_list);
|
||||
@@ -522,17 +392,21 @@ impl IVerge {
|
||||
patch!(webdav_username);
|
||||
patch!(webdav_password);
|
||||
patch!(enable_tray_speed);
|
||||
// patch!(enable_tray_icon);
|
||||
patch!(tray_inline_proxy_groups);
|
||||
patch!(enable_tray_icon);
|
||||
patch!(enable_auto_light_weight_mode);
|
||||
patch!(auto_light_weight_minutes);
|
||||
patch!(enable_dns_settings);
|
||||
patch!(home_cards);
|
||||
patch!(enable_external_controller);
|
||||
patch!(service_state);
|
||||
}
|
||||
|
||||
pub const fn get_singleton_port() -> u16 {
|
||||
crate::constants::network::ports::SINGLETON_SERVER
|
||||
/// 在初始化前尝试拿到单例端口的值
|
||||
pub fn get_singleton_port() -> u16 {
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
const SERVER_PORT: u16 = 33331;
|
||||
#[cfg(feature = "verge-dev")]
|
||||
const SERVER_PORT: u16 = 11233;
|
||||
SERVER_PORT
|
||||
}
|
||||
|
||||
/// 获取日志等级
|
||||
@@ -552,3 +426,142 @@ impl IVerge {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct IVergeResponse {
|
||||
pub app_log_level: Option<String>,
|
||||
pub language: Option<String>,
|
||||
pub theme_mode: Option<String>,
|
||||
pub tray_event: Option<String>,
|
||||
pub env_type: Option<String>,
|
||||
pub start_page: Option<String>,
|
||||
pub startup_script: Option<String>,
|
||||
pub traffic_graph: Option<bool>,
|
||||
pub enable_memory_usage: Option<bool>,
|
||||
pub enable_group_icon: Option<bool>,
|
||||
pub common_tray_icon: Option<bool>,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub tray_icon: Option<String>,
|
||||
pub menu_icon: Option<String>,
|
||||
pub sysproxy_tray_icon: Option<bool>,
|
||||
pub tun_tray_icon: Option<bool>,
|
||||
pub enable_tun_mode: Option<bool>,
|
||||
pub enable_auto_launch: Option<bool>,
|
||||
pub enable_silent_start: Option<bool>,
|
||||
pub enable_system_proxy: Option<bool>,
|
||||
pub enable_proxy_guard: Option<bool>,
|
||||
pub enable_global_hotkey: Option<bool>,
|
||||
pub use_default_bypass: Option<bool>,
|
||||
pub system_proxy_bypass: Option<String>,
|
||||
pub proxy_guard_duration: Option<u64>,
|
||||
pub proxy_auto_config: Option<bool>,
|
||||
pub pac_file_content: Option<String>,
|
||||
pub proxy_host: Option<String>,
|
||||
pub theme_setting: Option<IVergeTheme>,
|
||||
pub web_ui_list: Option<Vec<String>>,
|
||||
pub clash_core: Option<String>,
|
||||
pub hotkeys: Option<Vec<String>>,
|
||||
pub auto_close_connection: Option<bool>,
|
||||
pub auto_check_update: Option<bool>,
|
||||
pub default_latency_test: Option<String>,
|
||||
pub default_latency_timeout: Option<i32>,
|
||||
pub enable_builtin_enhanced: Option<bool>,
|
||||
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"))]
|
||||
pub verge_redir_enabled: Option<bool>,
|
||||
#[cfg(target_os = "linux")]
|
||||
pub verge_tproxy_port: Option<u16>,
|
||||
#[cfg(target_os = "linux")]
|
||||
pub verge_tproxy_enabled: Option<bool>,
|
||||
pub verge_mixed_port: Option<u16>,
|
||||
pub verge_socks_port: Option<u16>,
|
||||
pub verge_socks_enabled: Option<bool>,
|
||||
pub verge_port: Option<u16>,
|
||||
pub verge_http_enabled: Option<bool>,
|
||||
pub webdav_url: Option<String>,
|
||||
pub webdav_username: Option<String>,
|
||||
pub webdav_password: Option<String>,
|
||||
pub enable_tray_speed: Option<bool>,
|
||||
pub enable_tray_icon: Option<bool>,
|
||||
pub enable_auto_light_weight_mode: Option<bool>,
|
||||
pub auto_light_weight_minutes: Option<u64>,
|
||||
pub enable_dns_settings: Option<bool>,
|
||||
pub home_cards: Option<serde_json::Value>,
|
||||
pub service_state: Option<crate::core::service::ServiceState>,
|
||||
}
|
||||
|
||||
impl From<IVerge> for IVergeResponse {
|
||||
fn from(verge: IVerge) -> Self {
|
||||
Self {
|
||||
app_log_level: verge.app_log_level,
|
||||
language: verge.language,
|
||||
theme_mode: verge.theme_mode,
|
||||
tray_event: verge.tray_event,
|
||||
env_type: verge.env_type,
|
||||
start_page: verge.start_page,
|
||||
startup_script: verge.startup_script,
|
||||
traffic_graph: verge.traffic_graph,
|
||||
enable_memory_usage: verge.enable_memory_usage,
|
||||
enable_group_icon: verge.enable_group_icon,
|
||||
common_tray_icon: verge.common_tray_icon,
|
||||
#[cfg(target_os = "macos")]
|
||||
tray_icon: verge.tray_icon,
|
||||
menu_icon: verge.menu_icon,
|
||||
sysproxy_tray_icon: verge.sysproxy_tray_icon,
|
||||
tun_tray_icon: verge.tun_tray_icon,
|
||||
enable_tun_mode: verge.enable_tun_mode,
|
||||
enable_auto_launch: verge.enable_auto_launch,
|
||||
enable_silent_start: verge.enable_silent_start,
|
||||
enable_system_proxy: verge.enable_system_proxy,
|
||||
enable_proxy_guard: verge.enable_proxy_guard,
|
||||
enable_global_hotkey: verge.enable_global_hotkey,
|
||||
use_default_bypass: verge.use_default_bypass,
|
||||
system_proxy_bypass: verge.system_proxy_bypass,
|
||||
proxy_guard_duration: verge.proxy_guard_duration,
|
||||
proxy_auto_config: verge.proxy_auto_config,
|
||||
pac_file_content: verge.pac_file_content,
|
||||
proxy_host: verge.proxy_host,
|
||||
theme_setting: verge.theme_setting,
|
||||
web_ui_list: verge.web_ui_list,
|
||||
clash_core: verge.clash_core,
|
||||
hotkeys: verge.hotkeys,
|
||||
auto_close_connection: verge.auto_close_connection,
|
||||
auto_check_update: verge.auto_check_update,
|
||||
default_latency_test: verge.default_latency_test,
|
||||
default_latency_timeout: verge.default_latency_timeout,
|
||||
enable_builtin_enhanced: verge.enable_builtin_enhanced,
|
||||
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"))]
|
||||
verge_redir_enabled: verge.verge_redir_enabled,
|
||||
#[cfg(target_os = "linux")]
|
||||
verge_tproxy_port: verge.verge_tproxy_port,
|
||||
#[cfg(target_os = "linux")]
|
||||
verge_tproxy_enabled: verge.verge_tproxy_enabled,
|
||||
verge_mixed_port: verge.verge_mixed_port,
|
||||
verge_socks_port: verge.verge_socks_port,
|
||||
verge_socks_enabled: verge.verge_socks_enabled,
|
||||
verge_port: verge.verge_port,
|
||||
verge_http_enabled: verge.verge_http_enabled,
|
||||
webdav_url: verge.webdav_url,
|
||||
webdav_username: verge.webdav_username,
|
||||
webdav_password: verge.webdav_password,
|
||||
enable_tray_speed: verge.enable_tray_speed,
|
||||
enable_tray_icon: verge.enable_tray_icon,
|
||||
enable_auto_light_weight_mode: verge.enable_auto_light_weight_mode,
|
||||
auto_light_weight_minutes: verge.auto_light_weight_minutes,
|
||||
enable_dns_settings: verge.enable_dns_settings,
|
||||
home_cards: verge.home_cards,
|
||||
service_state: verge.service_state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod network {
|
||||
pub const DEFAULT_PROXY_HOST: &str = "127.0.0.1";
|
||||
pub const DEFAULT_EXTERNAL_CONTROLLER: &str = "127.0.0.1:9097";
|
||||
|
||||
pub mod ports {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub const DEFAULT_REDIR: u16 = 7895;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const DEFAULT_TPROXY: u16 = 7896;
|
||||
pub const DEFAULT_MIXED: u16 = 7897;
|
||||
pub const DEFAULT_SOCKS: u16 = 7898;
|
||||
pub const DEFAULT_HTTP: u16 = 7899;
|
||||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
pub const SINGLETON_SERVER: u16 = 33331;
|
||||
#[cfg(feature = "verge-dev")]
|
||||
pub const SINGLETON_SERVER: u16 = 11233;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod bypass {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const DEFAULT: &str = "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")]
|
||||
pub const DEFAULT: &str =
|
||||
"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")]
|
||||
pub const DEFAULT: &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>";
|
||||
}
|
||||
|
||||
pub mod timing {
|
||||
use super::Duration;
|
||||
|
||||
pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(500);
|
||||
pub const CONFIG_RELOAD_DELAY: Duration = Duration::from_millis(300);
|
||||
pub const EVENT_EMIT_DELAY: Duration = Duration::from_millis(20);
|
||||
pub const STARTUP_ERROR_DELAY: Duration = Duration::from_secs(2);
|
||||
pub const ERROR_BATCH_DELAY: Duration = Duration::from_millis(300);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const SERVICE_WAIT_MAX: Duration = Duration::from_millis(3000);
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const SERVICE_WAIT_INTERVAL: Duration = Duration::from_millis(200);
|
||||
}
|
||||
|
||||
pub mod retry {
|
||||
pub const EVENT_EMIT_THRESHOLD: u64 = 10;
|
||||
}
|
||||
|
||||
pub mod files {
|
||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||
pub const DNS_CONFIG: &str = "dns_config.yaml";
|
||||
pub const WINDOW_STATE: &str = "window_state.json";
|
||||
}
|
||||
|
||||
pub mod error_patterns {
|
||||
pub const CONNECTION_ERRORS: &[&str] = &[
|
||||
"Failed to create connection",
|
||||
"The system cannot find the file specified",
|
||||
"operation timed out",
|
||||
"connection refused",
|
||||
];
|
||||
}
|
||||
|
||||
pub mod tun {
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const DEFAULT_STACK: &str = "mixed";
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub const DEFAULT_STACK: &str = "gvisor";
|
||||
|
||||
pub const DNS_HIJACK: &[&str] = &["any:53"];
|
||||
}
|
||||
@@ -1,565 +0,0 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
#[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".into(),
|
||||
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)) => {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"异步获取自动代理成功: enable={}, url={}",
|
||||
proxy.enable,
|
||||
proxy.url
|
||||
);
|
||||
proxy
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging!(warn, Type::Network, "Warning: 异步获取自动代理失败: {e}");
|
||||
AsyncAutoproxy::default()
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(warn, Type::Network, "Warning: 异步获取自动代理超时");
|
||||
AsyncAutoproxy::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 异步获取系统代理配置
|
||||
pub async fn get_system_proxy() -> AsyncSysproxy {
|
||||
match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await {
|
||||
Ok(Ok(proxy)) => {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"异步获取系统代理成功: enable={}, {}:{}",
|
||||
proxy.enable,
|
||||
proxy.host,
|
||||
proxy.port
|
||||
);
|
||||
proxy
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging!(warn, Type::Network, "Warning: 异步获取系统代理失败: {e}");
|
||||
AsyncSysproxy::default()
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(warn, Type::Network, "Warning: 异步获取系统代理超时");
|
||||
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::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW};
|
||||
|
||||
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 {
|
||||
logging!(debug, Type::Network, "无法打开注册表项");
|
||||
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]);
|
||||
logging!(debug, Type::Network, "从注册表读取到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 {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}"
|
||||
);
|
||||
|
||||
if pac_url.is_empty() && auto_detect != 0 {
|
||||
pac_url = "auto-detect".into();
|
||||
}
|
||||
|
||||
Ok(AsyncAutoproxy {
|
||||
enable: true,
|
||||
url: pac_url,
|
||||
})
|
||||
} else {
|
||||
logging!(debug, Type::Network, "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);
|
||||
crate::logging!(
|
||||
debug,
|
||||
crate::utils::logging::Type::Network,
|
||||
"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().into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crate::logging!(
|
||||
debug,
|
||||
crate::utils::logging::Type::Network,
|
||||
"解析结果: 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")
|
||||
&& !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
|
||||
&& output.status.success()
|
||||
{
|
||||
let mode: String = String::from_utf8_lossy(&output.stdout).trim().into();
|
||||
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
|
||||
&& pac_output.status.success()
|
||||
{
|
||||
let pac_url: String = String::from_utf8_lossy(&pac_output.stdout)
|
||||
.trim()
|
||||
.trim_matches('\'')
|
||||
.trim_matches('"')
|
||||
.into();
|
||||
|
||||
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::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW};
|
||||
|
||||
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 proxy_server = if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
|
||||
let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
|
||||
String::from_utf16_lossy(&buffer[..end_pos])
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// 读取代理绕过列表
|
||||
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 bypass_list =
|
||||
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());
|
||||
String::from_utf16_lossy(&bypass_buffer[..end_pos])
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
RegCloseKey(hkey);
|
||||
|
||||
if !proxy_server.is_empty() {
|
||||
// 解析服务器地址和端口
|
||||
let (host, port) = if let Some(colon_pos) = proxy_server.rfind(':') {
|
||||
let host = proxy_server[..colon_pos].into();
|
||||
let port = proxy_server[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
|
||||
(host, port)
|
||||
} else {
|
||||
(proxy_server, 8080)
|
||||
};
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"从注册表读取到代理设置: {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);
|
||||
logging!(debug, Type::Network, "scutil proxy output: {stdout}");
|
||||
|
||||
let mut http_enabled = false;
|
||||
let mut http_host = String::new();
|
||||
let mut http_port = 8080u16;
|
||||
let mut exceptions: Vec<String> = 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().into();
|
||||
}
|
||||
} else if line.contains("HTTPPort") {
|
||||
if let Some(port_part) = line.split(':').nth(1)
|
||||
&& 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.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
&& let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy)
|
||||
{
|
||||
return Ok(proxy_info);
|
||||
}
|
||||
|
||||
if let Ok(https_proxy) = std::env::var("https_proxy")
|
||||
&& 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
|
||||
&& mode_output.status.success()
|
||||
{
|
||||
let mode: String = String::from_utf8_lossy(&mode_output.stdout).trim().into();
|
||||
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)
|
||||
&& host_output.status.success()
|
||||
&& port_output.status.success()
|
||||
{
|
||||
let host: String = String::from_utf8_lossy(&host_output.stdout)
|
||||
.trim()
|
||||
.trim_matches('\'')
|
||||
.trim_matches('"')
|
||||
.into();
|
||||
|
||||
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: String = url[..colon_pos].into();
|
||||
let port = url[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
|
||||
(host, port)
|
||||
} else {
|
||||
(url.into(), 8080)
|
||||
};
|
||||
|
||||
if host.is_empty() {
|
||||
return Err(anyhow!("无效的代理URL"));
|
||||
}
|
||||
|
||||
Ok(AsyncSysproxy {
|
||||
enable: true,
|
||||
host,
|
||||
port,
|
||||
bypass: std::env::var("no_proxy").unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
+60
-108
@@ -1,24 +1,18 @@
|
||||
use crate::constants::files::DNS_CONFIG;
|
||||
use crate::{
|
||||
config::Config,
|
||||
logging,
|
||||
process::AsyncHandler,
|
||||
utils::{dirs, logging::Type},
|
||||
};
|
||||
use crate::{config::Config, utils::dirs};
|
||||
use anyhow::Error;
|
||||
use arc_swap::{ArcSwap, ArcSwapOption};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use reqwest_dav::list_cmd::{ListEntity, ListFile};
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::{consts::OS, temp_dir},
|
||||
fs,
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{fs, time::timeout};
|
||||
use tokio::time::timeout;
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
// 应用版本常量,来自 tauri.conf.json
|
||||
@@ -45,71 +39,65 @@ enum Operation {
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
const fn timeout(&self) -> u64 {
|
||||
fn timeout(&self) -> u64 {
|
||||
match self {
|
||||
Self::Upload => TIMEOUT_UPLOAD,
|
||||
Self::Download => TIMEOUT_DOWNLOAD,
|
||||
Self::List => TIMEOUT_LIST,
|
||||
Self::Delete => TIMEOUT_DELETE,
|
||||
Operation::Upload => TIMEOUT_UPLOAD,
|
||||
Operation::Download => TIMEOUT_DOWNLOAD,
|
||||
Operation::List => TIMEOUT_LIST,
|
||||
Operation::Delete => TIMEOUT_DELETE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WebDavClient {
|
||||
config: Arc<ArcSwapOption<WebDavConfig>>,
|
||||
clients: Arc<ArcSwap<HashMap<Operation, reqwest_dav::Client>>>,
|
||||
config: Arc<Mutex<Option<WebDavConfig>>>,
|
||||
clients: Arc<Mutex<HashMap<Operation, reqwest_dav::Client>>>,
|
||||
}
|
||||
|
||||
impl WebDavClient {
|
||||
pub fn global() -> &'static Self {
|
||||
pub fn global() -> &'static WebDavClient {
|
||||
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
|
||||
WEBDAV_CLIENT.get_or_init(|| Self {
|
||||
config: Arc::new(ArcSwapOption::new(None)),
|
||||
clients: Arc::new(ArcSwap::new(Arc::new(HashMap::new()))),
|
||||
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
|
||||
config: Arc::new(Mutex::new(None)),
|
||||
clients: Arc::new(Mutex::new(HashMap::new())),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_client(&self, op: Operation) -> Result<reqwest_dav::Client, Error> {
|
||||
// 先尝试从缓存获取
|
||||
{
|
||||
let clients_map = self.clients.load();
|
||||
if let Some(client) = clients_map.get(&op) {
|
||||
let clients = self.clients.lock();
|
||||
if let Some(client) = clients.get(&op) {
|
||||
return Ok(client.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// 获取或创建配置
|
||||
let config = {
|
||||
// 首先检查是否已有配置
|
||||
let existing_config = self.config.load();
|
||||
|
||||
if let Some(cfg_arc) = existing_config.clone() {
|
||||
(*cfg_arc).clone()
|
||||
let mut lock = self.config.lock();
|
||||
if let Some(cfg) = lock.as_ref() {
|
||||
cfg.clone()
|
||||
} else {
|
||||
// 释放锁后获取异步配置
|
||||
let verge = Config::verge().await.data_arc();
|
||||
let verge = Config::verge().latest().clone();
|
||||
if verge.webdav_url.is_none()
|
||||
|| verge.webdav_username.is_none()
|
||||
|| verge.webdav_password.is_none()
|
||||
{
|
||||
let msg: String = "Unable to create web dav client, please make sure the webdav config is correct".into();
|
||||
let msg = "Unable to create web dav client, please make sure the webdav config is correct".to_string();
|
||||
return Err(anyhow::Error::msg(msg));
|
||||
}
|
||||
|
||||
let config = WebDavConfig {
|
||||
url: verge
|
||||
.webdav_url
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.trim_end_matches('/')
|
||||
.into(),
|
||||
username: verge.webdav_username.as_ref().cloned().unwrap_or_default(),
|
||||
password: verge.webdav_password.as_ref().cloned().unwrap_or_default(),
|
||||
.to_string(),
|
||||
username: verge.webdav_username.unwrap_or_default(),
|
||||
password: verge.webdav_password.unwrap_or_default(),
|
||||
};
|
||||
|
||||
// 存储配置到 ArcSwapOption
|
||||
self.config.store(Some(Arc::new(config.clone())));
|
||||
*lock = Some(config.clone());
|
||||
config
|
||||
}
|
||||
};
|
||||
@@ -120,7 +108,10 @@ impl WebDavClient {
|
||||
reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(op.timeout()))
|
||||
.user_agent(format!("clash-verge/{APP_VERSION} ({OS} WebDAV-Client)"))
|
||||
.user_agent(format!(
|
||||
"clash-verge/{} ({} WebDAV-Client)",
|
||||
APP_VERSION, OS
|
||||
))
|
||||
.redirect(reqwest::redirect::Policy::custom(|attempt| {
|
||||
// 允许所有请求类型的重定向,包括PUT
|
||||
if attempt.previous().len() >= 5 {
|
||||
@@ -129,61 +120,42 @@ impl WebDavClient {
|
||||
attempt.follow()
|
||||
}
|
||||
}))
|
||||
.build()?,
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.set_host(config.url.into())
|
||||
.set_auth(reqwest_dav::Auth::Basic(
|
||||
config.username.into(),
|
||||
config.password.into(),
|
||||
))
|
||||
.set_host(config.url)
|
||||
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
|
||||
.build()?;
|
||||
|
||||
// 尝试检查目录是否存在,如果不存在尝试创建
|
||||
// 尝试检查目录是否存在,如果不存在尝试创建,但创建失败不报错
|
||||
if client
|
||||
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
match client.mkcol(dirs::BACKUP_DIR).await {
|
||||
Ok(_) => logging!(info, Type::Backup, "Successfully created backup directory"),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Backup,
|
||||
"Warning: Failed to create backup directory: {}",
|
||||
e
|
||||
);
|
||||
// 清除缓存,强制下次重新尝试
|
||||
self.reset();
|
||||
return Err(anyhow::Error::msg(format!(
|
||||
"Failed to create backup directory: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
let _ = client.mkcol(dirs::BACKUP_DIR).await;
|
||||
}
|
||||
|
||||
// 缓存客户端(替换 Arc<Mutex<HashMap<...>>> 的写法)
|
||||
// 缓存客户端
|
||||
{
|
||||
let mut map = (**self.clients.load()).clone();
|
||||
map.insert(op, client.clone());
|
||||
self.clients.store(map.into());
|
||||
let mut clients = self.clients.lock();
|
||||
clients.insert(op, client.clone());
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub fn reset(&self) {
|
||||
self.config.store(None);
|
||||
self.clients.store(Arc::new(HashMap::new()));
|
||||
*self.config.lock() = None;
|
||||
self.clients.lock().clear();
|
||||
}
|
||||
|
||||
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
|
||||
let client = self.get_client(Operation::Upload).await?;
|
||||
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name).into();
|
||||
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name);
|
||||
|
||||
// 读取文件并上传,如果失败尝试一次重试
|
||||
let file_content = fs::read(&file_path).await?;
|
||||
let file_content = fs::read(&file_path)?;
|
||||
|
||||
// 添加超时保护
|
||||
let upload_result = timeout(
|
||||
@@ -194,11 +166,7 @@ impl WebDavClient {
|
||||
|
||||
match upload_result {
|
||||
Err(_) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Backup,
|
||||
"Warning: Upload timed out, retrying once"
|
||||
);
|
||||
log::warn!("Upload timed out, retrying once");
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
timeout(
|
||||
Duration::from_secs(TIMEOUT_UPLOAD),
|
||||
@@ -209,11 +177,7 @@ impl WebDavClient {
|
||||
}
|
||||
|
||||
Ok(Err(e)) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Backup,
|
||||
"Warning: 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),
|
||||
@@ -233,7 +197,7 @@ impl WebDavClient {
|
||||
let fut = async {
|
||||
let response = client.get(path.as_str()).await?;
|
||||
let content = response.bytes().await?;
|
||||
fs::write(&storage_path, &content).await?;
|
||||
fs::write(&storage_path, &content)?;
|
||||
Ok::<(), Error>(())
|
||||
};
|
||||
|
||||
@@ -271,53 +235,41 @@ impl WebDavClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_backup() -> Result<(String, PathBuf), Error> {
|
||||
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: String = format!("{OS}-backup-{now}.zip").into();
|
||||
let zip_path = temp_dir().join(zip_file_name.as_str());
|
||||
let zip_file_name = format!("{}-backup-{}.zip", OS, now);
|
||||
let zip_path = temp_dir().join(&zip_file_name);
|
||||
|
||||
let value = zip_path.clone();
|
||||
let file = AsyncHandler::spawn_blocking(move || std::fs::File::create(&value)).await??;
|
||||
let file = fs::File::create(&zip_path)?;
|
||||
let mut zip = zip::ZipWriter::new(file);
|
||||
zip.add_directory("profiles/", SimpleFileOptions::default())?;
|
||||
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
|
||||
if let Ok(mut entries) = fs::read_dir(dirs::app_profiles_dir()?).await {
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) {
|
||||
for entry in entries {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
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);
|
||||
let backup_path = format!("profiles/{}", entry.file_name().to_str().unwrap());
|
||||
zip.start_file(backup_path, options)?;
|
||||
let file_content = fs::read(&path).await?;
|
||||
zip.write_all(&file_content)?;
|
||||
zip.write_all(fs::read(path).unwrap().as_slice())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
zip.start_file(dirs::CLASH_CONFIG, options)?;
|
||||
zip.write_all(fs::read(dirs::clash_path()?).await?.as_slice())?;
|
||||
zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?;
|
||||
|
||||
let verge_text = fs::read_to_string(dirs::verge_path()?).await?;
|
||||
let mut verge_config: serde_json::Value = serde_yaml_ng::from_str(&verge_text)?;
|
||||
let mut verge_config: serde_json::Value =
|
||||
serde_yaml::from_str(&fs::read_to_string(dirs::verge_path()?)?)?;
|
||||
if let Some(obj) = verge_config.as_object_mut() {
|
||||
obj.remove("webdav_username");
|
||||
obj.remove("webdav_password");
|
||||
obj.remove("webdav_url");
|
||||
}
|
||||
zip.start_file(dirs::VERGE_CONFIG, options)?;
|
||||
zip.write_all(serde_yaml_ng::to_string(&verge_config)?.as_bytes())?;
|
||||
|
||||
let dns_config_path = dirs::app_home_dir()?.join(DNS_CONFIG);
|
||||
if dns_config_path.exists() {
|
||||
zip.start_file(DNS_CONFIG, options)?;
|
||||
zip.write_all(fs::read(&dns_config_path).await?.as_slice())?;
|
||||
}
|
||||
zip.write_all(serde_yaml::to_string(&verge_config)?.as_bytes())?;
|
||||
|
||||
zip.start_file(dirs::PROFILE_YAML, options)?;
|
||||
zip.write_all(fs::read(dirs::profiles_path()?).await?.as_slice())?;
|
||||
zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?;
|
||||
zip.finish()?;
|
||||
Ok((zip_file_name, zip_path))
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user