mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-28 00:42:08 +08:00
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5780915d16 | ||
|
|
eba4818e99 | ||
|
|
abdf8cafcb | ||
|
|
a91a7692f1 | ||
|
|
cbd5ed73a5 | ||
|
|
635d345b1a | ||
|
|
88153a65eb | ||
|
|
71aef2e83c | ||
|
|
c5dcef6f1e | ||
|
|
3a892043c4 | ||
|
|
5d0961943c | ||
|
|
70ae63c534 | ||
|
|
096a9f0d32 | ||
|
|
4a4b72a5d6 | ||
|
|
5363a4affb | ||
|
|
6b883346d2 | ||
|
|
e3ad94c056 | ||
|
|
a23b0ddc6b | ||
|
|
fbf94c3a41 | ||
|
|
2d3619d2ec | ||
|
|
16ee4aa135 | ||
|
|
ddfb476fc5 | ||
|
|
02e5e0e4a6 | ||
|
|
0498ba3e4f | ||
|
|
f39644ee6b | ||
|
|
488f022c05 | ||
|
|
eefd025806 | ||
|
|
10ca0a5d0c | ||
|
|
324c8c9016 | ||
|
|
02520d7392 | ||
|
|
5b8edbaead | ||
|
|
1ed8050947 | ||
|
|
3d9a79a3fb | ||
|
|
8b88ddb7f2 | ||
|
|
02bd739ab6 | ||
|
|
b6c7af82a7 |
@@ -4,8 +4,8 @@
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:2": {
|
||||
"version": "24.22.0",
|
||||
"pnpmVersion": "12.14.0"
|
||||
"version": "24.22.1",
|
||||
"pnpmVersion": "12.15.0"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "1.99.1",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Issue Guidelines / Issue 提交指引
|
||||
|
||||
Use these suggestions to make reports easier to assess. State what you observed
|
||||
and what you could not verify; unknown details are fine.
|
||||
|
||||
以下建议有助于维护者评估问题。请说明实际观察到的现象和无法确认的部分;未知信息可以留空。
|
||||
|
||||
- Search existing issues by symptom, conditions, and outcome. Add new evidence
|
||||
to an existing report when it describes the same problem.
|
||||
按现象、条件和结果搜索已有 Issue。若描述的是同一问题,请在原 Issue 中补充新证据。
|
||||
- Keep each issue to one distinct user problem or requested outcome. Split
|
||||
unrelated findings instead of bundling an audit or checklist.
|
||||
每个 Issue 聚焦一个独立的问题或需求结果;无关发现请拆开,不要把审计清单打包成一个 Issue。
|
||||
- For bugs, include the version, operating system, steps, and actual versus
|
||||
expected behavior when known. A root-cause diagnosis is not required. Share
|
||||
only relevant logs and redact tokens or personal information.
|
||||
反馈 Bug 时,尽可能提供版本、操作系统、复现步骤及实际与预期行为。无需先判断根因;只提供相关日志,并遮盖令牌和个人信息。
|
||||
- For feature requests, describe the user task, current limitation, desired
|
||||
outcome, and affected platforms. An implementation proposal is optional.
|
||||
提交功能请求时,请描述用户要完成的任务、当前限制、期望结果和适用平台;实现方案不是必填项。
|
||||
- Separate observations from guesses. An AI or scanner finding is a hypothesis
|
||||
until its affected configuration, concrete impact, and supporting evidence
|
||||
are stated; do not present a list of theoretical risks as confirmed bugs.
|
||||
区分观察结果和推测。AI 或扫描器的发现应先视为假设;请说明受影响的配置、具体影响和支持证据,不要把理论风险清单写成已确认的 Bug。
|
||||
- Do not post secrets, personal data, or sensitive exploit details in a public
|
||||
issue. Ask maintainers how to share sensitive security information safely.
|
||||
请勿在公开 Issue 中发布密钥、个人信息或敏感利用细节;如涉及安全敏感信息,请先询问维护者如何安全提供。
|
||||
- AI assistance is welcome; human contributors do not need to disclose it. If
|
||||
an AI agent or automation creates or co-authors an issue, add a final body line
|
||||
such as `Assisted by: GPT-5.6 High`. Omit effort when the runtime does not
|
||||
report it. Disclosure is transparency only, not evidence for or against issue
|
||||
quality.
|
||||
欢迎使用 AI 辅助,人工提交者无需披露。如果 Issue 由 AI Agent 或自动化创建或共同撰写,请在正文末尾添加披露行,例如 `Assisted by: GPT-5.6 High`。运行环境未提供 effort 时可省略。披露仅用于提高透明度,不作为 Issue 质量的正面或负面证据。
|
||||
@@ -10,6 +10,8 @@ body:
|
||||
value: |
|
||||
## 在提交问题之前,请确认以下事项:
|
||||
|
||||
提交前可参考 [Issue 提交指引](https://github.com/clash-verge-rev/clash-verge-rev/blob/dev/.github/ISSUE_GUIDELINES.md),说明可观察到的问题和证据;不确定的原因请标为猜测。
|
||||
|
||||
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 填写一个简洁明了的标题,以便他人快速检索
|
||||
@@ -19,6 +21,8 @@ body:
|
||||
|
||||
## Before submitting the issue, please make sure of the following checklist:
|
||||
|
||||
See the [Issue guidelines](https://github.com/clash-verge-rev/clash-verge-rev/blob/dev/.github/ISSUE_GUIDELINES.md) for how to describe observed behavior and evidence; mark uncertain causes as hypotheses.
|
||||
|
||||
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
|
||||
|
||||
@@ -9,12 +9,16 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
## 在提交问题之前,请确认以下事项:
|
||||
可参考 [Issue 提交指引](https://github.com/clash-verge-rev/clash-verge-rev/blob/dev/.github/ISSUE_GUIDELINES.md),描述用户任务、当前限制和期望结果。
|
||||
|
||||
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) 版本测试,确保该功能还未实现
|
||||
5. 请 **务必** 按照模板规范详细描述问题,否则 issue 将会被关闭
|
||||
## Before submitting the issue, please make sure of the following checklist:
|
||||
See the [Issue guidelines](https://github.com/clash-verge-rev/clash-verge-rev/blob/dev/.github/ISSUE_GUIDELINES.md) for describing the user task, current limitation, and desired outcome.
|
||||
|
||||
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
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
## Problem
|
||||
|
||||
What breaks or what is missing. Link the issue if one exists (`Fixes #123`).
|
||||
What breaks or what is missing. Product bug fixes and features require a
|
||||
pre-existing issue; link it here (`Fixes #123` only when this PR fully resolves
|
||||
it). Maintenance work (such as chore, perf, docs, refactor, test, CI/build,
|
||||
dependency, style, routine localization, or release changes) does not require
|
||||
an issue link; link one when it defines the scope. For reverts, link the PR or
|
||||
commit being reverted. For security-sensitive fixes that cannot be tracked
|
||||
publicly, follow maintainer-approved private tracking.
|
||||
|
||||
## Approach
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -282,7 +282,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -426,7 +426,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -524,7 +524,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6.1.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
name: Contract Parity
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "src-tauri/**/*.rs"
|
||||
- "crates/**/*.rs"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- "src-tauri/Cargo.toml"
|
||||
- "crates/**/Cargo.toml"
|
||||
- "rust-toolchain.toml"
|
||||
- "src-tauri/tauri.conf*.json"
|
||||
- "src/services/bindings.ts"
|
||||
- "scripts/gen-contract.mjs"
|
||||
- "scripts/check-contract-changes.mjs"
|
||||
- "biome.json"
|
||||
- "biome.jsonc"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "scripts/contract-inputs.json"
|
||||
- ".github/workflows/contract-parity.yml"
|
||||
- ".husky/pre-push"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
jobs:
|
||||
contract-parity:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: 'v1-rust'
|
||||
key: 'rust-shared-stable-ubuntu-22.04-x86_64-unknown-linux-gnu-contract'
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
cache-workspace-crates: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Provision sidecar placeholders for tauri-build
|
||||
run: |
|
||||
mkdir -p src-tauri/sidecar src-tauri/resources dist
|
||||
touch dist/index.html
|
||||
for bin in clash-verge-service clash-verge-service-install clash-verge-service-uninstall verge-mihomo verge-mihomo-alpha; do
|
||||
touch "src-tauri/sidecar/${bin}-x86_64-unknown-linux-gnu"
|
||||
done
|
||||
|
||||
- name: Check generated contract without changing the checkout
|
||||
run: pnpm gen:contract --check
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
|
||||
@@ -2,11 +2,23 @@ name: Frontend Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "src/**"
|
||||
- "**/*.js"
|
||||
- "**/*.ts"
|
||||
- "**/*.tsx"
|
||||
- "**/*.css"
|
||||
- "**/*.scss"
|
||||
- "**/*.json"
|
||||
- "**/*.md"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "vite.config.*"
|
||||
- ".github/workflows/frontend-check.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
@@ -17,46 +29,17 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Check frontend changes
|
||||
id: check_frontend
|
||||
uses: dorny/paths-filter@v4
|
||||
with:
|
||||
filters: |
|
||||
frontend:
|
||||
- 'src/**'
|
||||
- '**/*.js'
|
||||
- '**/*.ts'
|
||||
- '**/*.tsx'
|
||||
- '**/*.css'
|
||||
- '**/*.scss'
|
||||
- '**/*.json'
|
||||
- '**/*.md'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'knip.json'
|
||||
- 'eslint.config.ts'
|
||||
- 'tsconfig.json'
|
||||
- 'vite.config.*'
|
||||
|
||||
- name: Skip if no frontend changes
|
||||
if: steps.check_frontend.outputs.frontend != 'true'
|
||||
run: echo "No frontend changes, skipping frontend checks."
|
||||
|
||||
- name: Install pnpm
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Restore pnpm cache
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
@@ -65,94 +48,21 @@ jobs:
|
||||
pnpm-shared-stable-${{ runner.os }}-
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
|
||||
- name: Run Prettier check
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Run ESLint
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm lint
|
||||
|
||||
- name: Run TypeScript typecheck
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Run tests
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm test
|
||||
|
||||
- name: Run Knip
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm knip:check
|
||||
|
||||
- name: Architecture gates (listen/emit only in the event bus, invoke only in services)
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: node scripts/check-architecture.mjs listen emit invoke
|
||||
|
||||
contract-parity:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Check contract inputs
|
||||
id: check_inputs
|
||||
uses: dorny/paths-filter@v4
|
||||
with:
|
||||
filters: scripts/contract-inputs.json
|
||||
|
||||
- name: Skip if contract inputs unchanged
|
||||
if: steps.check_inputs.outputs.contract != 'true'
|
||||
run: echo "No contract inputs changed, skipping parity check."
|
||||
|
||||
- name: Install Rust Stable
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Rust Cache
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: 'v1-rust'
|
||||
key: 'rust-shared-stable-ubuntu-22.04-x86_64-unknown-linux-gnu-contract'
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
cache-workspace-crates: true
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install pnpm
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
|
||||
- name: Provision sidecar placeholders for tauri-build
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
run: |
|
||||
mkdir -p src-tauri/sidecar src-tauri/resources dist
|
||||
touch dist/index.html
|
||||
for bin in clash-verge-service clash-verge-service-install clash-verge-service-uninstall verge-mihomo verge-mihomo-alpha; do
|
||||
touch "src-tauri/sidecar/${bin}-x86_64-unknown-linux-gnu"
|
||||
done
|
||||
|
||||
- name: Check generated contract without changing the checkout
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
run: pnpm gen:contract --check
|
||||
|
||||
@@ -2,6 +2,13 @@ name: Clippy Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "src-tauri/**"
|
||||
- "crates/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- "rust-toolchain.toml"
|
||||
- ".github/workflows/lint-clippy.yml"
|
||||
workflow_dispatch:
|
||||
env:
|
||||
HUSKY: 0
|
||||
@@ -23,28 +30,6 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Check src-tauri changes
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
id: check_changes
|
||||
uses: dorny/paths-filter@v4
|
||||
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@v7
|
||||
|
||||
@@ -79,7 +64,7 @@ jobs:
|
||||
run: cargo clippy-all
|
||||
|
||||
- name: Run Windows security tests
|
||||
if: matrix.os == 'windows-latest' && (github.event_name == 'workflow_dispatch' || steps.check_changes.outputs.rust == 'true')
|
||||
if: matrix.os == 'windows-latest'
|
||||
working-directory: ./src-tauri
|
||||
run: cargo test --all-features --target ${{ matrix.target }}
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -331,7 +331,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
@@ -466,7 +466,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -547,7 +547,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -573,7 +573,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -638,7 +638,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
@@ -6,6 +6,10 @@ name: Check Formatting
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "src-tauri/**"
|
||||
- "**/*.rs"
|
||||
- ".github/workflows/rustfmt.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -19,25 +23,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Check Rust changes
|
||||
id: check_rust
|
||||
uses: dorny/paths-filter@v4
|
||||
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
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.22.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
nodejs 24.22.0
|
||||
pnpm 12.14.0
|
||||
nodejs 24.22.1
|
||||
pnpm 12.15.0
|
||||
rust 1.99.1
|
||||
|
||||
@@ -18,22 +18,25 @@ These rules apply to every change, whether human- or agent-authored. They match
|
||||
the ownership evidence the AI-slop review evaluates (see
|
||||
[`pr-ai-slop-review.md`](.github/workflows/pr-ai-slop-review.md)).
|
||||
|
||||
1. **Issue first.** Non-trivial changes require a pre-existing issue describing
|
||||
the problem. If none exists, ask the maintainers to open or approve one
|
||||
before implementing.
|
||||
1. **Issue first.** Product bug fixes and features require a pre-existing issue;
|
||||
self-scoped maintenance work does not. Follow the PR template for linkage and
|
||||
revert references. If a required issue is missing, ask maintainers to open or
|
||||
approve one before implementing. For security-sensitive fixes that cannot be
|
||||
tracked publicly, follow maintainer-approved private tracking.
|
||||
2. **Scope discipline.** Every changed file must be justifiable from the linked
|
||||
issue. No drive-by refactors, renames, formatting churn, or dependency bumps
|
||||
unrelated to the problem being fixed.
|
||||
issue or the stated PR scope. No drive-by refactors, renames, formatting
|
||||
churn, or dependency bumps unrelated to the work.
|
||||
3. **Author accountability.** AI assistance is welcome, but the contributor owns
|
||||
the result: understand the change, describe the problem and approach in your
|
||||
own words, and verify the change against the reported behavior before
|
||||
submitting.
|
||||
4. **Tests are justified, not default.** Do not add tests, test scaffolding, or
|
||||
speculative defensive code unless the linked issue demands them. When a test
|
||||
is genuinely necessary — it reproduces the reported regression or guards
|
||||
behavior whose breakage would otherwise go unnoticed — keep it minimal and
|
||||
state in the PR body why it is needed. Bulk test files and defensive
|
||||
programming for hypothetical failure modes are PR bloat, not rigor.
|
||||
speculative defensive code unless the linked issue or stated task demands
|
||||
them. When a test is genuinely necessary — it reproduces the reported
|
||||
regression or guards behavior whose breakage would otherwise go unnoticed —
|
||||
keep it minimal and state in the PR body why it is needed. Bulk test files
|
||||
and defensive programming for hypothetical failure modes are PR bloat, not
|
||||
rigor.
|
||||
5. **Comments state constraints, not narration.** Write a comment only for a
|
||||
non-obvious constraint the code cannot express; never restate what the code
|
||||
does.
|
||||
@@ -44,13 +47,12 @@ the ownership evidence the AI-slop review evaluates (see
|
||||
real evidence instead: reproduction steps, failure output, targeted tests.
|
||||
8. **Minimal diffs.** Match the surrounding code's style, naming, and comment
|
||||
density. Do not introduce new dependencies or restructure working code unless
|
||||
the issue demands it.
|
||||
9. **Disclose AI automation.** When an agent produces or co-produces a change,
|
||||
append a footer line to the PR body with the model and effort used (e.g.
|
||||
`Assisted by: GPT-5.6 High`). The PR template intentionally omits this line —
|
||||
the agent adds it itself, humans are not asked to declare anything. Effort may
|
||||
be omitted when the runtime does not report it. Disclosure is transparency
|
||||
only; it does not substitute for any rule above.
|
||||
the linked issue or stated task demands it.
|
||||
9. **Disclose AI automation.** When an agent creates or co-creates a PR or Issue,
|
||||
append a final body line with the model and effort used (e.g.
|
||||
`Assisted by: GPT-5.6 High`). Effort may be omitted when the runtime does not
|
||||
report it. Human contributors are not asked to disclose AI assistance.
|
||||
Disclosure is transparency only and carries no positive or negative weight.
|
||||
10. **Compiled workflows.** The AI-slop review policy in
|
||||
[pr-ai-slop-review.md](.github/workflows/pr-ai-slop-review.md) is compiled:
|
||||
after editing it, run `gh aw compile` and commit the regenerated
|
||||
@@ -59,8 +61,10 @@ the ownership evidence the AI-slop review evaluates (see
|
||||
[`template/Changelog.md`](template/Changelog.md): one line per
|
||||
user-visible change, no internals.
|
||||
|
||||
## Pull Request Shape
|
||||
## Issue and PR Workflows
|
||||
|
||||
Describe three things, briefly: the problem (with issue link), why this approach
|
||||
solves it, and what changed. See
|
||||
[`PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md).
|
||||
When drafting, triaging, splitting, or implementing from a community issue, use
|
||||
the conditional checklist in [`.github/ISSUE_GUIDELINES.md`](.github/ISSUE_GUIDELINES.md).
|
||||
Do not make GitHub issue changes without explicit authorization. For PRs, follow
|
||||
the directly loaded [PR template](.github/PULL_REQUEST_TEMPLATE.md); do not
|
||||
duplicate its prompts here.
|
||||
|
||||
Generated
+73
-160
@@ -1119,7 +1119,7 @@ dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"libc",
|
||||
"log",
|
||||
"minisign-verify 0.3.0",
|
||||
"minisign-verify",
|
||||
"nanoid",
|
||||
"network-interface",
|
||||
"objc2",
|
||||
@@ -4170,12 +4170,6 @@ dependencies = [
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.3.0"
|
||||
@@ -5999,12 +5993,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.21.3"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
|
||||
checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"ordered-multimap",
|
||||
"trim-in-place",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6625,7 +6620,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"sigchld",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
@@ -6645,17 +6639,6 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "sigchld"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"os_pipe",
|
||||
"signal-hook 0.3.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.1.17"
|
||||
@@ -6666,16 +6649,6 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
@@ -6881,6 +6854,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "static_vcruntime"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.9.0"
|
||||
@@ -6940,9 +6919,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e"
|
||||
checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"serde",
|
||||
@@ -7189,9 +7168,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.12.1"
|
||||
version = "2.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed99ee9694a2deb776d91cae48ac7411ddfc89ecae2f9b5041111d8c88f2ace9"
|
||||
checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -7237,16 +7216,16 @@ dependencies = [
|
||||
"tray-icon",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com 0.39.1",
|
||||
"webview2-com",
|
||||
"window-vibrancy",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.7.1"
|
||||
version = "2.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59563ca5b331fd97f27ee9672ec8d36d33bb3c9e2c9f6710a1a5687256052e45"
|
||||
checksum = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -7258,6 +7237,8 @@ dependencies = [
|
||||
"semver 1.0.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"static_vcruntime",
|
||||
"strsim",
|
||||
"tauri-utils",
|
||||
"tauri-winres",
|
||||
"walkdir",
|
||||
@@ -7265,11 +7246,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.7.1"
|
||||
version = "2.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e50213e463086ecc283d7e9f51c30cc0fd304ad457425158018be26dbf0d0a80"
|
||||
checksum = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"base64 0.23.1",
|
||||
"brotli",
|
||||
"ico",
|
||||
"json-patch",
|
||||
@@ -7292,9 +7273,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.7.1"
|
||||
version = "2.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8129fb43bb6e0d6b73ad3315cd172beaab17cd28899c594b1b32c80edbbfd99c"
|
||||
checksum = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -7350,9 +7331,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-clipboard-manager"
|
||||
version = "2.4.1"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "333d3b6beeb1c2c7fc040233d4ea7b6c03a9f8f039592b7400833c43fb5b568a"
|
||||
checksum = "9be5ed8d94c33afe8377e885119069f6508780659271d0a25cfc54d441f2bc81"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"log",
|
||||
@@ -7365,9 +7346,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.6.1"
|
||||
version = "2.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ac7a92a46ab5c88f44532ca50906d6e448a948d4ddf8c5376ab125e3260f736"
|
||||
checksum = "1b3c64e24ebcbc63aaa2d88a9c51e2507084b46620baf39ecb0baa0738a08bb4"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"plist",
|
||||
@@ -7413,9 +7394,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.8.1"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf9a5c92e39bdd84f22f6be9230e39d0c4e140345f71ffe6058dca4f71127cf"
|
||||
checksum = "b7461aaacf1901915ea7b9f5cd74f8224a351ed96af0a62c07fce934003cb34b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
@@ -7431,9 +7412,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.6.0"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "466eb32466fe5021b6728b06862f9ff14a9f7e1af890047654e03a96d7c0e1bb"
|
||||
checksum = "5d58eb1d4984e535f47a642ffec09a2da6f5c18a12b5902d12126cb7626d628b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
@@ -7455,9 +7436,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-global-shortcut"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bd2e1f725891a1613af8f25bf918ba3357654b0fd3f2d794c0ce76cbdd92ed2"
|
||||
checksum = "93ff17919fe09852d269bd37b1d3d2e993b9dbb514afe7acbf3346c1d3627e2d"
|
||||
dependencies = [
|
||||
"global-hotkey",
|
||||
"log",
|
||||
@@ -7470,9 +7451,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-http"
|
||||
version = "2.8.0"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93e9d6b3319e101bc1be27cfa0fb4054d7784a12509c67ead0033b82180e5ed1"
|
||||
checksum = "2c57e3451553f95a1db3bfed6a358a77ce5080b6399a242c6eadac113f97abd2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cookie_store",
|
||||
@@ -7489,7 +7470,7 @@ dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"url",
|
||||
"urlpattern 0.3.0",
|
||||
"urlpattern",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7519,9 +7500,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-notification"
|
||||
version = "2.5.1"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "533770889abb60a0407d5a32096182f0e6463f5a3dfaa675be94580aa14070e1"
|
||||
checksum = "b939d7b19c8af1381a72ef8474b3aba2b56c1ddac2c23f0c8e96abbcf86b198b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify-rust",
|
||||
@@ -7534,13 +7515,14 @@ dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
"time",
|
||||
"url",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.7.0"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d39404b2572927e0d3ed30a7ca226037f7508d22e79c96ee6ef0d42b6a4b3345"
|
||||
checksum = "53810be1b4fed22bb6ac3f4cab5a2b80d9391d1af436bda145607532bc0bb219"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"glob",
|
||||
@@ -7570,11 +7552,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "094bdbc47e5efe36544ba21b3a53ab5c724150236cf55e6c9be46191deb6eab6"
|
||||
checksum = "8567d42b2c7264355daa1df415ec77f52aa42e885924c86a969be2e920e05521"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"libc",
|
||||
"log",
|
||||
"open",
|
||||
"os_pipe",
|
||||
@@ -7587,13 +7570,14 @@ dependencies = [
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.13.1"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cb0b2ea3e85ca287990d3859a29cc5cb18024240fd78f62e027fb7963670f18"
|
||||
checksum = "7259989d4fe11b2bf51f7d671642249b1837c4ae2da1430dc713ae38f98ceeec"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs 7.0.0",
|
||||
@@ -7601,8 +7585,9 @@ dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"libc",
|
||||
"log",
|
||||
"minisign-verify 0.2.5",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest 0.13.5",
|
||||
@@ -7624,9 +7609,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-wdio-webdriver"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52c5e97428174a52b7f4357bbc4535fcce52c6a06c8ee7a379502f7cda1b6045"
|
||||
checksum = "4f76676d510d271d80abcf8b0552987b5b4f0eae5e3f906e9b5660d43c97c62e"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -7650,9 +7635,9 @@ dependencies = [
|
||||
"tracing",
|
||||
"uuid",
|
||||
"webkit2gtk",
|
||||
"webview2-com 0.38.2",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"webview2-com",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7672,9 +7657,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.12.1"
|
||||
version = "2.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413"
|
||||
checksum = "7c55766d99cea69055904d1735b93c2049302b2e09effd462120deb5581d78e9"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"dpi",
|
||||
@@ -7691,15 +7676,15 @@ dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com 0.39.1",
|
||||
"webview2-com",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.12.1"
|
||||
version = "2.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22"
|
||||
checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@@ -7717,7 +7702,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com 0.39.1",
|
||||
"webview2-com",
|
||||
"windows 0.62.2",
|
||||
"wry",
|
||||
]
|
||||
@@ -7755,9 +7740,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.10.1"
|
||||
version = "2.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee"
|
||||
checksum = "362ab21cd4cc713f2a02b1a9b5688198b1541cfb6907c4281e9190e0f0ef4aa3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brotli",
|
||||
@@ -7786,7 +7771,7 @@ dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
"toml 1.1.6+spec-1.1.0",
|
||||
"url",
|
||||
"urlpattern 0.6.0",
|
||||
"urlpattern",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
]
|
||||
@@ -7905,7 +7890,7 @@ dependencies = [
|
||||
"regex",
|
||||
"semver 0.11.0",
|
||||
"sha2 0.9.9",
|
||||
"signal-hook 0.1.17",
|
||||
"signal-hook",
|
||||
"terminfo",
|
||||
"termios",
|
||||
"thiserror 1.0.69",
|
||||
@@ -8503,6 +8488,12 @@ dependencies = [
|
||||
"petgraph",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trim-in-place"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daa60874b45b168f67f3f9ba1ee821ad77144d6fa19cef1cd1874a7450a948da"
|
||||
|
||||
[[package]]
|
||||
name = "triomphe"
|
||||
version = "0.1.16"
|
||||
@@ -8595,47 +8586,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-property"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
|
||||
dependencies = [
|
||||
"unic-char-range",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-range"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
|
||||
|
||||
[[package]]
|
||||
name = "unic-common"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
|
||||
|
||||
[[package]]
|
||||
name = "unic-ucd-ident"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
|
||||
dependencies = [
|
||||
"unic-char-property",
|
||||
"unic-char-range",
|
||||
"unic-ucd-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-ucd-version"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
|
||||
dependencies = [
|
||||
"unic-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
@@ -8695,18 +8645,6 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlpattern"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"unic-ucd-ident",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlpattern"
|
||||
version = "0.6.0"
|
||||
@@ -9098,20 +9036,6 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
|
||||
dependencies = [
|
||||
"webview2-com-macros",
|
||||
"webview2-com-sys 0.38.2",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.39.1"
|
||||
@@ -9119,7 +9043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6"
|
||||
dependencies = [
|
||||
"webview2-com-macros",
|
||||
"webview2-com-sys 0.39.1",
|
||||
"webview2-com-sys",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
@@ -9135,17 +9059,6 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com-sys"
|
||||
version = "0.38.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
|
||||
dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com-sys"
|
||||
version = "0.39.1"
|
||||
@@ -9885,7 +9798,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"webview2-com 0.39.1",
|
||||
"webview2-com",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows-version",
|
||||
|
||||
+2
-2
@@ -61,8 +61,8 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra
|
||||
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
|
||||
tracing = "0.1.44"
|
||||
|
||||
tauri = { version = "2.12.1" }
|
||||
tauri-plugin-clipboard-manager = "2.4.1"
|
||||
tauri = { version = "2.12.4" }
|
||||
tauri-plugin-clipboard-manager = "2.5.0"
|
||||
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
|
||||
anyhow = "1.0.104"
|
||||
chrono = "0.4.45"
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
|
||||
- 修复 首页 网站测速失败后卡片一直显示测试中
|
||||
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
|
||||
- 修复 代理 在全局模式下无法展开节点组
|
||||
- 修复 自定义 CSS 无法修改编辑器字体和字号的问题
|
||||
- 修复恢复备份的同时修改配置,可能导致配置丢失或错乱的问题
|
||||
- 修复切换规则模式与配置变更并发时可能丢失或错乱的问题
|
||||
|
||||
**🐧 Linux**
|
||||
|
||||
@@ -17,6 +21,9 @@
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为
|
||||
- 新增「IP 信息」自动刷新开关,关闭后仍可手动刷新
|
||||
- 新增 Muse AI 解锁检测,针对 Muse AI 特有的 Cloudflare 人机验证增加「Risky」状态标记
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ mod chatgpt;
|
||||
mod claude;
|
||||
mod disney_plus;
|
||||
mod gemini;
|
||||
mod muse;
|
||||
mod netflix;
|
||||
mod prime_video;
|
||||
mod spotify;
|
||||
@@ -30,6 +31,7 @@ enum UnlockCheck {
|
||||
ChatGptWeb,
|
||||
Claude,
|
||||
Gemini,
|
||||
MuseAi,
|
||||
YouTubePremium,
|
||||
BahamutAnime,
|
||||
Netflix,
|
||||
@@ -46,6 +48,7 @@ impl UnlockCheck {
|
||||
Self::ChatGptWeb,
|
||||
Self::Claude,
|
||||
Self::Gemini,
|
||||
Self::MuseAi,
|
||||
Self::YouTubePremium,
|
||||
Self::BahamutAnime,
|
||||
Self::Netflix,
|
||||
@@ -66,6 +69,7 @@ impl UnlockCheck {
|
||||
Self::ChatGptWeb => chatgpt::CHATGPT_WEB_NAME,
|
||||
Self::Claude => claude::CLAUDE_NAME,
|
||||
Self::Gemini => gemini::GEMINI_NAME,
|
||||
Self::MuseAi => muse::MUSE_AI_NAME,
|
||||
Self::YouTubePremium => youtube::YOUTUBE_PREMIUM_NAME,
|
||||
Self::BahamutAnime => bahamut::BAHAMUT_ANIME_NAME,
|
||||
Self::Netflix => netflix::NETFLIX_NAME,
|
||||
@@ -83,6 +87,7 @@ impl UnlockCheck {
|
||||
Self::ChatGptWeb => chatgpt::check_chatgpt(client).await,
|
||||
Self::Claude => claude::check_claude(client).await,
|
||||
Self::Gemini => gemini::check_gemini(client).await,
|
||||
Self::MuseAi => muse::check_muse_ai(client).await,
|
||||
Self::YouTubePremium => youtube::check_youtube_premium(client).await,
|
||||
Self::BahamutAnime => bahamut::check_bahamut_anime(client).await,
|
||||
Self::Netflix => netflix::check_netflix(client).await,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::utils::{classify_restricted_status, get_trace_location};
|
||||
|
||||
use super::UnlockItem;
|
||||
|
||||
pub(crate) const MUSE_AI_NAME: &str = "Muse AI";
|
||||
|
||||
const MUSE_URL: &str = "https://muse.ai";
|
||||
const CLOUDFLARE_TRACE_URL: &str = "https://www.cloudflare.com/cdn-cgi/trace";
|
||||
|
||||
// The aymh ("Are You a Human") gateway answers plain HTTP requests with a
|
||||
// generic Meta error page that always contains the Facebook logo asset.
|
||||
const AYMH_ERROR_MARKERS: &[&str] = &["Sorry, something went wrong", "static.facebook.com/images/logos"];
|
||||
|
||||
pub(super) async fn check_muse_ai(client: &Client) -> UnlockItem {
|
||||
// muse.ai itself is served by Vercel, so resolve the exit region through
|
||||
// the generic Cloudflare trace endpoint instead of a service-specific one.
|
||||
let region = get_trace_location(client, CLOUDFLARE_TRACE_URL)
|
||||
.await
|
||||
.map(|code| code.trim().to_ascii_uppercase())
|
||||
.map(|code| UnlockItem::region_label(&code));
|
||||
|
||||
let Ok(response) = client.get(MUSE_URL).send().await else {
|
||||
return UnlockItem::checked(MUSE_AI_NAME, "Failed (Network Connection)", region);
|
||||
};
|
||||
|
||||
let status = response.status();
|
||||
let landed_on_aymh = response.url().host_str() == Some("auth.muse.ai");
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
|
||||
// Every region is redirected to the aymh anti-bot gateway before the app
|
||||
// loads, and a non-browser HTTP client cannot finish the JavaScript
|
||||
// challenge. It returns the Meta error page, so reaching this point only
|
||||
// proves the exit can connect to Muse's auth infrastructure; availability
|
||||
// cannot be verified over plain HTTP and is reported as `Risky`.
|
||||
let behind_anti_bot = landed_on_aymh || AYMH_ERROR_MARKERS.iter().any(|marker| body.contains(marker));
|
||||
|
||||
let result = if behind_anti_bot {
|
||||
"Risky"
|
||||
} else if status.is_success() {
|
||||
"Yes"
|
||||
} else {
|
||||
classify_restricted_status(status).unwrap_or("Failed")
|
||||
};
|
||||
|
||||
UnlockItem::checked(MUSE_AI_NAME, result, region)
|
||||
}
|
||||
+16
-15
@@ -14,7 +14,7 @@
|
||||
"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:build": "tsrs -p . --noEmit && vite build",
|
||||
"web:serve": "vite preview",
|
||||
"prebuild": "node scripts/prebuild.mjs",
|
||||
"updater": "node scripts/updater.mjs",
|
||||
@@ -32,7 +32,7 @@
|
||||
"i18n:types": "node scripts/generate-i18n-keys.mjs",
|
||||
"gen:contract": "node scripts/gen-contract.mjs",
|
||||
"arch:check": "node scripts/check-architecture.mjs listen emit invoke",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck": "tsrs -p . --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"knip:check": "knip",
|
||||
@@ -54,13 +54,13 @@
|
||||
"@mui/icons-material": "^9.4.0",
|
||||
"@mui/material": "^9.4.0",
|
||||
"@tanstack/react-virtual": "^3.14.13",
|
||||
"@tauri-apps/api": "2.12.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.4.0",
|
||||
"@tauri-apps/plugin-dialog": "2.8.0",
|
||||
"@tauri-apps/plugin-fs": "2.6.0",
|
||||
"@tauri-apps/plugin-http": "2.8.0",
|
||||
"@tauri-apps/plugin-opener": "2.7.0",
|
||||
"@tauri-apps/plugin-updater": "2.13.1",
|
||||
"@tauri-apps/api": "2.12.4",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.5.0",
|
||||
"@tauri-apps/plugin-dialog": "2.8.3",
|
||||
"@tauri-apps/plugin-fs": "2.7.1",
|
||||
"@tauri-apps/plugin-http": "2.8.3",
|
||||
"@tauri-apps/plugin-opener": "2.7.2",
|
||||
"@tauri-apps/plugin-updater": "2.14.0",
|
||||
"ahooks": "^3.10.0",
|
||||
"cidr-block": "^2.3.0",
|
||||
"compare-versions": "^6.1.1",
|
||||
@@ -79,7 +79,7 @@
|
||||
"react-dom": "19.3.1",
|
||||
"react-error-boundary": "6.1.6",
|
||||
"react-hook-form": "^7.88.0",
|
||||
"react-i18next": "17.0.19",
|
||||
"react-i18next": "17.1.8",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-router": "^8.4.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
@@ -95,7 +95,7 @@
|
||||
"@biomejs/biome": "^2.5.14",
|
||||
"@eslint-react/eslint-plugin": "^5.20.5",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tauri-apps/cli": "2.12.1",
|
||||
"@tauri-apps/cli": "2.12.3",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.13.6",
|
||||
"@types/react": "19.3.0",
|
||||
@@ -103,7 +103,7 @@
|
||||
"@types/validator": "^13.15.10",
|
||||
"@vitejs/plugin-legacy": "^8.2.3",
|
||||
"@vitejs/plugin-react": "^6.1.1",
|
||||
"@wdio/tauri-service": "1.4.0",
|
||||
"@wdio/tauri-service": "1.5.0",
|
||||
"adm-zip": "^0.6.1",
|
||||
"axios": "^1.20.0",
|
||||
"cli-color": "^2.0.4",
|
||||
@@ -123,17 +123,18 @@
|
||||
"https-proxy-agent": "^9.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.7.0",
|
||||
"knip": "6.41.0",
|
||||
"knip": "6.43.0",
|
||||
"lint-staged": "^17.5.1",
|
||||
"sass": "^1.104.1",
|
||||
"tar": "^7.5.22",
|
||||
"terser": "^5.51.2",
|
||||
"tsrs": "0.20.0-ts7.1.0-beta",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.70.0",
|
||||
"vite": "^8.3.0",
|
||||
"vite-plugin-svgr": "^5.2.0",
|
||||
"vitest": "^5.0.1",
|
||||
"webdriverio": "10.0.2",
|
||||
"webdriverio": "10.1.0",
|
||||
"ws": "8.22.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -146,5 +147,5 @@
|
||||
]
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@12.14.0"
|
||||
"packageManager": "pnpm@12.15.0"
|
||||
}
|
||||
|
||||
Generated
+495
-614
File diff suppressed because it is too large
Load Diff
@@ -10,3 +10,11 @@ allowBuilds:
|
||||
unrs-resolver: true
|
||||
minimumReleaseAgeExclude:
|
||||
- dayjs@1.11.21
|
||||
- "@tsrs/darwin-arm64@0.20.0-ts7.1.0-beta"
|
||||
- "@tsrs/darwin-x64@0.20.0-ts7.1.0-beta"
|
||||
- "@tsrs/linux-arm64-gnu@0.20.0-ts7.1.0-beta"
|
||||
- "@tsrs/linux-arm64-musl@0.20.0-ts7.1.0-beta"
|
||||
- "@tsrs/linux-x64-gnu@0.20.0-ts7.1.0-beta"
|
||||
- "@tsrs/linux-x64-musl@0.20.0-ts7.1.0-beta"
|
||||
- "@tsrs/win32-x64@0.20.0-ts7.1.0-beta"
|
||||
- tsrs@0.20.0-ts7.1.0-beta
|
||||
|
||||
@@ -79,6 +79,13 @@
|
||||
"matchPackageNames": ["typescript"],
|
||||
"allowedVersions": "<7.0.0 || >=7.1.0"
|
||||
},
|
||||
{
|
||||
"description": "Review tsrs bumps alone; the version encodes the pinned TypeScript release",
|
||||
"matchPackageNames": ["tsrs", "/^@tsrs//"],
|
||||
"groupName": "tsrs",
|
||||
"rangeStrategy": "pin",
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "Prevent project from supply chain attack",
|
||||
"matchDatasources": ["npm"],
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"pnpm-lock.yaml",
|
||||
"pnpm-workspace.yaml",
|
||||
"scripts/contract-inputs.json",
|
||||
".github/workflows/frontend-check.yml",
|
||||
".github/workflows/contract-parity.yml",
|
||||
".husky/pre-push"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -434,12 +434,13 @@ for (let iteration = 0; iteration < repeat && !interrupted; iteration++) {
|
||||
devicePixelRatio,
|
||||
])
|
||||
run.viewport = { dom: viewport, driver: await browser.getWindowRect() }
|
||||
const driverScale = run.viewport.driver.width / 1000
|
||||
assert(
|
||||
run.viewport.driver.width / viewport[2] === 1000 &&
|
||||
run.viewport.driver.height / viewport[2] === 700 &&
|
||||
viewport[0] === 1000 &&
|
||||
viewport[0] === 1000 &&
|
||||
viewport[1] > 0 &&
|
||||
viewport[1] <= 700,
|
||||
viewport[1] <= 700 &&
|
||||
(driverScale === 1 || driverScale === viewport[2]) &&
|
||||
run.viewport.driver.height / driverScale === 700,
|
||||
`unexpected window dimensions: ${JSON.stringify(run.viewport)}`,
|
||||
)
|
||||
assert(
|
||||
|
||||
+11
-11
@@ -29,7 +29,7 @@ perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.7.1", features = [] }
|
||||
tauri-build = { version = "2.7.3", features = [] }
|
||||
|
||||
[dependencies]
|
||||
clash-verge-draft = { workspace = true }
|
||||
@@ -72,12 +72,12 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch
|
||||
"guard",
|
||||
] }
|
||||
network-interface = { version = "2.0.5", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.4.0"
|
||||
tauri-plugin-opener = "2.7.0"
|
||||
tauri-plugin-dialog = "2.8.1"
|
||||
tauri-plugin-fs = "2.6.0"
|
||||
tauri-plugin-shell = "2.5.0"
|
||||
tauri-plugin-opener = "2.7.2"
|
||||
tauri-plugin-dialog = "2.8.3"
|
||||
tauri-plugin-fs = "2.7.1"
|
||||
tauri-plugin-process = "2.4.0"
|
||||
tauri-plugin-deep-link = "2.6.1"
|
||||
tauri-plugin-deep-link = "2.6.2"
|
||||
tauri-plugin-window-state = "2.5.0"
|
||||
zip = "9.0.4"
|
||||
flate2 = "1.1.10"
|
||||
@@ -90,10 +90,10 @@ getrandom = "0.4.3"
|
||||
futures = "0.3.34"
|
||||
gethostname = "1.1.0"
|
||||
scopeguard = "1.2.0"
|
||||
tauri-plugin-notification = "2.5.1"
|
||||
tauri-plugin-notification = "2.7.0"
|
||||
tokio-stream = "0.1.19"
|
||||
backon = { version = "1.6.0", features = ["tokio-sleep"] }
|
||||
tauri-plugin-http = "2.8.0"
|
||||
tauri-plugin-http = "2.8.3"
|
||||
tracing = { workspace = true }
|
||||
tauri-plugin-devtools = { version = "2.2.2", optional = true }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
|
||||
@@ -120,7 +120,7 @@ libc = "0.2.191"
|
||||
clash-verge-signal = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
tauri-plugin-wdio-webdriver = { version = "=1.4.0", optional = true }
|
||||
tauri-plugin-wdio-webdriver = { version = "=1.5.0", optional = true }
|
||||
objc2 = "0.6.5"
|
||||
objc2-foundation = { version = "0.3.2", features = [
|
||||
"NSString",
|
||||
@@ -153,8 +153,8 @@ windows-sys.workspace = true
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2.7.0"
|
||||
tauri-plugin-global-shortcut = "2.4.0"
|
||||
tauri-plugin-updater = "2.13.1"
|
||||
tauri-plugin-global-shortcut = "2.4.1"
|
||||
tauri-plugin-updater = "2.14.0"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true }
|
||||
|
||||
+10
-5
@@ -1,10 +1,9 @@
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows")
|
||||
&& std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc")
|
||||
&& std::env::var_os("CARGO_FEATURE_CLIPPY").is_some()
|
||||
{
|
||||
// Tauri's resource is linked to the app binary, but not to Rust unit-test harnesses.
|
||||
// Give mock-context builds their own manifest so tests can resolve TaskDialogIndirect.
|
||||
// Tauri's resource is linked to the app binary, but not to Rust unit-test harnesses or examples.
|
||||
// Give non-app targets their own manifest so tests and examples can resolve TaskDialogIndirect.
|
||||
let out_dir = std::env::var_os("OUT_DIR").ok_or_else(|| std::io::Error::other("OUT_DIR is not set"))?;
|
||||
let manifest_path = std::path::PathBuf::from(out_dir).join("windows-test-manifest.xml");
|
||||
std::fs::write(
|
||||
@@ -25,8 +24,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
</assembly>
|
||||
"#,
|
||||
)?;
|
||||
println!("cargo:rustc-link-arg=/MANIFEST:EMBED");
|
||||
println!("cargo:rustc-link-arg=/MANIFESTINPUT:{}", manifest_path.display());
|
||||
let manifest_display = manifest_path.display();
|
||||
if std::env::var_os("CARGO_FEATURE_CLIPPY").is_some() {
|
||||
println!("cargo:rustc-link-arg=/MANIFEST:EMBED");
|
||||
println!("cargo:rustc-link-arg=/MANIFESTINPUT:{manifest_display}");
|
||||
} else {
|
||||
println!("cargo:rustc-link-arg-examples=/MANIFEST:EMBED");
|
||||
println!("cargo:rustc-link-arg-examples=/MANIFESTINPUT:{manifest_display}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "clippy")]
|
||||
|
||||
@@ -184,84 +184,41 @@ impl IClashTemp {
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn guard_redir_port(config: &Mapping) -> u16 {
|
||||
fn guard_port_value(config: &Mapping, key: &str, default: u16) -> u16 {
|
||||
let mut port = config
|
||||
.get("redir-port")
|
||||
.get(key)
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(7895);
|
||||
.unwrap_or(default);
|
||||
if port == 0 {
|
||||
port = 7895;
|
||||
port = default;
|
||||
}
|
||||
port
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn guard_redir_port(config: &Mapping) -> u16 {
|
||||
Self::guard_port_value(config, "redir-port", network::ports::DEFAULT_REDIR)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn guard_tproxy_port(config: &Mapping) -> u16 {
|
||||
let mut port = config
|
||||
.get("tproxy-port")
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(network::ports::DEFAULT_TPROXY);
|
||||
if port == 0 {
|
||||
port = network::ports::DEFAULT_TPROXY;
|
||||
}
|
||||
port
|
||||
Self::guard_port_value(config, "tproxy-port", network::ports::DEFAULT_TPROXY)
|
||||
}
|
||||
|
||||
fn guard_mixed_port(config: &Mapping) -> u16 {
|
||||
let raw_value = config.get("mixed-port");
|
||||
|
||||
let mut port = raw_value
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(network::ports::DEFAULT_MIXED);
|
||||
|
||||
if port == 0 {
|
||||
port = network::ports::DEFAULT_MIXED;
|
||||
}
|
||||
|
||||
port
|
||||
Self::guard_port_value(config, "mixed-port", network::ports::DEFAULT_MIXED)
|
||||
}
|
||||
|
||||
fn guard_socks_port(config: &Mapping) -> u16 {
|
||||
let mut port = config
|
||||
.get("socks-port")
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(7898);
|
||||
if port == 0 {
|
||||
port = 7898;
|
||||
}
|
||||
port
|
||||
Self::guard_port_value(config, "socks-port", network::ports::DEFAULT_SOCKS)
|
||||
}
|
||||
|
||||
fn guard_port(config: &Mapping) -> u16 {
|
||||
let mut port = config
|
||||
.get("port")
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(7899);
|
||||
if port == 0 {
|
||||
port = 7899;
|
||||
}
|
||||
port
|
||||
Self::guard_port_value(config, "port", network::ports::DEFAULT_HTTP)
|
||||
}
|
||||
|
||||
pub(super) fn guard_server_ctrl(config: &Mapping) -> String {
|
||||
|
||||
@@ -14,6 +14,15 @@ mod verge;
|
||||
pub(crate) use self::config::Config;
|
||||
pub use self::{clash::*, encrypt::*, mixed_port::*, prfitem::*, profiles::*, verge::*};
|
||||
|
||||
macro_rules! patch_opt_field {
|
||||
($lv:expr, $rv:expr, $key:tt) => {
|
||||
if ($rv.$key).is_some() {
|
||||
$lv.$key = $rv.$key.clone();
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use patch_opt_field;
|
||||
|
||||
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;";
|
||||
}
|
||||
|
||||
+69
-108
@@ -1,3 +1,5 @@
|
||||
use super::patch_opt_field;
|
||||
|
||||
use crate::{
|
||||
config::profiles,
|
||||
utils::{
|
||||
@@ -131,19 +133,18 @@ impl PrfOption {
|
||||
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.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);
|
||||
patch_opt_field!(result, b_ref, user_agent);
|
||||
patch_opt_field!(result, b_ref, with_proxy);
|
||||
patch_opt_field!(result, b_ref, self_proxy);
|
||||
patch_opt_field!(result, b_ref, danger_accept_invalid_certs);
|
||||
patch_opt_field!(result, b_ref, allow_auto_update);
|
||||
patch_opt_field!(result, b_ref, update_interval);
|
||||
patch_opt_field!(result, b_ref, merge);
|
||||
patch_opt_field!(result, b_ref, script);
|
||||
patch_opt_field!(result, b_ref, rules);
|
||||
patch_opt_field!(result, b_ref, proxies);
|
||||
patch_opt_field!(result, b_ref, groups);
|
||||
patch_opt_field!(result, b_ref, timeout_seconds);
|
||||
Some(result)
|
||||
}
|
||||
(Some(a_ref), None) => Some(a_ref.clone()),
|
||||
@@ -181,6 +182,36 @@ impl PrfItem {
|
||||
}
|
||||
}
|
||||
|
||||
async fn append_companion_item(item: &mut Self) -> Result<Option<String>> {
|
||||
profiles::profiles_append_item_safe(item).await?;
|
||||
Ok(item.uid.clone())
|
||||
}
|
||||
|
||||
async fn ensure_companion_items(
|
||||
merge: &mut Option<String>,
|
||||
script: &mut Option<String>,
|
||||
rules: &mut Option<String>,
|
||||
proxies: &mut Option<String>,
|
||||
groups: &mut Option<String>,
|
||||
) -> Result<()> {
|
||||
if merge.is_none() {
|
||||
*merge = Self::append_companion_item(&mut Self::from_merge(None)).await?;
|
||||
}
|
||||
if script.is_none() {
|
||||
*script = Self::append_companion_item(&mut Self::from_script(None)).await?;
|
||||
}
|
||||
if rules.is_none() {
|
||||
*rules = Self::append_companion_item(&mut Self::from_rules()).await?;
|
||||
}
|
||||
if proxies.is_none() {
|
||||
*proxies = Self::append_companion_item(&mut Self::from_proxies()).await?;
|
||||
}
|
||||
if groups.is_none() {
|
||||
*groups = Self::append_companion_item(&mut Self::from_groups()).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn from_local(
|
||||
name: String,
|
||||
desc: String,
|
||||
@@ -197,31 +228,7 @@ impl PrfItem {
|
||||
let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
|
||||
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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = &mut Self::from_rules();
|
||||
profiles::profiles_append_item_safe(rules_item).await?;
|
||||
rules = rules_item.uid.clone();
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = &mut Self::from_proxies();
|
||||
profiles::profiles_append_item_safe(proxies_item).await?;
|
||||
proxies = proxies_item.uid.clone();
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = &mut Self::from_groups();
|
||||
profiles::profiles_append_item_safe(groups_item).await?;
|
||||
groups = groups_item.uid.clone();
|
||||
}
|
||||
Self::ensure_companion_items(&mut merge, &mut script, &mut rules, &mut proxies, &mut groups).await?;
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("local".into()),
|
||||
@@ -375,31 +382,7 @@ impl PrfItem {
|
||||
bail!("profile does not contain `proxies` or `proxy-providers`");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
if rules.is_none() {
|
||||
let rules_item = &mut Self::from_rules();
|
||||
profiles::profiles_append_item_safe(rules_item).await?;
|
||||
rules = rules_item.uid.clone();
|
||||
}
|
||||
if proxies.is_none() {
|
||||
let proxies_item = &mut Self::from_proxies();
|
||||
profiles::profiles_append_item_safe(proxies_item).await?;
|
||||
proxies = proxies_item.uid.clone();
|
||||
}
|
||||
if groups.is_none() {
|
||||
let groups_item = &mut Self::from_groups();
|
||||
profiles::profiles_append_item_safe(groups_item).await?;
|
||||
groups = groups_item.uid.clone();
|
||||
}
|
||||
Self::ensure_companion_items(&mut merge, &mut script, &mut rules, &mut proxies, &mut groups).await?;
|
||||
|
||||
Ok(Self {
|
||||
uid: Some(uid),
|
||||
@@ -461,46 +444,30 @@ impl PrfItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn from_rules() -> Self {
|
||||
let uid = help::get_uid("r").into();
|
||||
fn from_seq_item(prefix: &str, itype: &str, template: &str) -> Self {
|
||||
let uid = help::get_uid(prefix).into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("rules".into()),
|
||||
itype: Some(itype.into()),
|
||||
file: Some(file),
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(tmpl::ITEM_RULES.into()),
|
||||
file_data: Some(template.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn from_rules() -> Self {
|
||||
Self::from_seq_item("r", "rules", tmpl::ITEM_RULES)
|
||||
}
|
||||
|
||||
fn from_proxies() -> Self {
|
||||
let uid = help::get_uid("p").into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("proxies".into()),
|
||||
file: Some(file),
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(tmpl::ITEM_PROXIES.into()),
|
||||
..Default::default()
|
||||
}
|
||||
Self::from_seq_item("p", "proxies", tmpl::ITEM_PROXIES)
|
||||
}
|
||||
|
||||
fn from_groups() -> Self {
|
||||
let uid = help::get_uid("g").into();
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Self {
|
||||
uid: Some(uid),
|
||||
itype: Some("groups".into()),
|
||||
file: Some(file),
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(tmpl::ITEM_GROUPS.into()),
|
||||
..Default::default()
|
||||
}
|
||||
Self::from_seq_item("g", "groups", tmpl::ITEM_GROUPS)
|
||||
}
|
||||
|
||||
pub(crate) async fn read_file(&self) -> Result<String> {
|
||||
@@ -516,26 +483,20 @@ impl PrfItem {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! current_field {
|
||||
($name:ident, $field:ident) => {
|
||||
pub(crate) fn $name(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.$field.as_ref())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl PrfItem {
|
||||
pub(crate) fn current_merge(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.merge.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn current_script(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.script.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn current_rules(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.rules.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn current_proxies(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.proxies.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn current_groups(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.groups.as_ref())
|
||||
}
|
||||
current_field!(current_merge, merge);
|
||||
current_field!(current_script, script);
|
||||
current_field!(current_rules, rules);
|
||||
current_field!(current_proxies, proxies);
|
||||
current_field!(current_groups, groups);
|
||||
}
|
||||
|
||||
fn allow_auto_update_enabled(option: Option<&PrfOption>) -> bool {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::{
|
||||
PrfOption,
|
||||
PrfOption, patch_opt_field,
|
||||
prfitem::{PrfItem, PrfSelected, normalize_profile_home_url},
|
||||
};
|
||||
use crate::core::notify::{Refresh, announce};
|
||||
@@ -86,14 +86,6 @@ impl ProfileDeletePlan {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! patch {
|
||||
($lv: expr, $rv: expr, $key: tt) => {
|
||||
if ($rv.$key).is_some() {
|
||||
$lv.$key = $rv.$key.to_owned();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl IProfiles {
|
||||
fn take_item_file_by_uid(items: &mut Vec<PrfItem>, target_uid: Option<&str>) -> Option<String> {
|
||||
let index = items.iter().position(|item| item.uid.as_deref() == target_uid)?;
|
||||
@@ -244,15 +236,15 @@ impl IProfiles {
|
||||
|
||||
for each in items.iter_mut() {
|
||||
if each.uid.as_deref() == Some(uid) {
|
||||
patch!(each, item, itype);
|
||||
patch!(each, item, name);
|
||||
patch!(each, item, desc);
|
||||
patch!(each, item, file);
|
||||
patch!(each, item, url);
|
||||
patch!(each, item, selected);
|
||||
patch!(each, item, extra);
|
||||
patch!(each, item, updated);
|
||||
patch!(each, item, option);
|
||||
patch_opt_field!(each, item, itype);
|
||||
patch_opt_field!(each, item, name);
|
||||
patch_opt_field!(each, item, desc);
|
||||
patch_opt_field!(each, item, file);
|
||||
patch_opt_field!(each, item, url);
|
||||
patch_opt_field!(each, item, selected);
|
||||
patch_opt_field!(each, item, extra);
|
||||
patch_opt_field!(each, item, updated);
|
||||
patch_opt_field!(each, item, option);
|
||||
|
||||
self.items = Some(items);
|
||||
return self.save_file().await;
|
||||
@@ -420,6 +412,21 @@ pub(crate) async fn profiles_append_item_with_filedata_safe(item: &PrfItem, file
|
||||
profiles_append_item_safe(item).await
|
||||
}
|
||||
|
||||
/// Applies `edit` to the committed profiles; commits only when the edit succeeds.
|
||||
async fn with_profiles_edit<F, Fut>(edit: F) -> Result<()>
|
||||
where
|
||||
F: FnOnce(IProfiles) -> Fut + Send,
|
||||
Fut: std::future::Future<Output = Result<IProfiles>> + Send,
|
||||
{
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|profiles| async move {
|
||||
let profiles = edit(profiles).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> {
|
||||
let profiles = Config::profiles().await;
|
||||
profiles_append_item_to_safe(&profiles, item).await
|
||||
@@ -435,43 +442,19 @@ pub(super) async fn profiles_append_item_to_safe(profiles: &Draft<IProfiles>, it
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_patch_item_safe(index: &str, item: &PrfItem) -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
profiles.patch_item(index, item).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
with_profiles_edit(|mut profiles| async move { profiles.patch_item(index, item).await.map(|_| profiles) }).await
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_reorder_safe(active_id: &str, over_id: &str) -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
profiles.reorder(active_id, over_id).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
with_profiles_edit(|mut profiles| async move { profiles.reorder(active_id, over_id).await.map(|_| profiles) }).await
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_save_file_safe() -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|profiles| async move {
|
||||
profiles.save_file().await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
with_profiles_edit(|profiles| async move { profiles.save_file().await.map(|_| profiles) }).await
|
||||
}
|
||||
|
||||
pub(crate) async fn profiles_update_item_safe(index: &str, item: &mut PrfItem) -> Result<()> {
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
profiles.update_item(index, item).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
with_profiles_edit(|mut profiles| async move { profiles.update_item(index, item).await.map(|_| profiles) }).await
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
@@ -713,19 +696,21 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn current_item_mut(profiles: &mut IProfiles) -> Option<&mut PrfItem> {
|
||||
let current = profiles.current.clone()?;
|
||||
profiles
|
||||
.items
|
||||
.as_mut()?
|
||||
.iter_mut()
|
||||
.find(|item| item.uid.as_ref() == Some(¤t))
|
||||
}
|
||||
|
||||
/// Records a backend-made selection so the next core start restores it.
|
||||
pub(crate) async fn record_selected_node(group_name: &str, node: &str) -> Result<()> {
|
||||
let group_name = String::from(group_name);
|
||||
let node = String::from(node);
|
||||
persist_selected_update(true, move |mut profiles| async move {
|
||||
let Some(current) = profiles.current.clone() else {
|
||||
return Ok((profiles, false));
|
||||
};
|
||||
let Some(item) = profiles
|
||||
.items
|
||||
.as_mut()
|
||||
.and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(¤t)))
|
||||
else {
|
||||
let Some(item) = current_item_mut(&mut profiles) else {
|
||||
return Ok((profiles, false));
|
||||
};
|
||||
|
||||
@@ -760,14 +745,7 @@ fn remove_selected_node(selected: &mut Vec<PrfSelected>, group_name: &str) -> bo
|
||||
|
||||
pub(crate) async fn forget_selected_node(group_name: &str) -> Result<()> {
|
||||
persist_selected_update(true, move |mut profiles| async move {
|
||||
let Some(current) = profiles.current.clone() else {
|
||||
return Ok((profiles, false));
|
||||
};
|
||||
let Some(item) = profiles
|
||||
.items
|
||||
.as_mut()
|
||||
.and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(¤t)))
|
||||
else {
|
||||
let Some(item) = current_item_mut(&mut profiles) else {
|
||||
return Ok((profiles, false));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::config::Config;
|
||||
use crate::{
|
||||
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
|
||||
config::{DEFAULT_PAC, deserialize_encrypted, patch_opt_field, serialize_encrypted},
|
||||
constants::network,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
@@ -432,98 +432,90 @@ impl IVerge {
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub fn patch_config(&mut self, patch: &Self) {
|
||||
macro_rules! patch {
|
||||
($key: tt) => {
|
||||
if patch.$key.is_some() {
|
||||
self.$key = patch.$key.clone();
|
||||
}
|
||||
};
|
||||
}
|
||||
patch_opt_field!(self, patch, app_log_level);
|
||||
patch_opt_field!(self, patch, app_log_max_size);
|
||||
patch_opt_field!(self, patch, app_log_max_count);
|
||||
|
||||
patch!(app_log_level);
|
||||
patch!(app_log_max_size);
|
||||
patch!(app_log_max_count);
|
||||
|
||||
patch!(language);
|
||||
patch!(theme_mode);
|
||||
patch!(tray_event);
|
||||
patch!(env_type);
|
||||
patch!(start_page);
|
||||
patch!(startup_script);
|
||||
patch!(traffic_graph);
|
||||
patch!(enable_memory_usage);
|
||||
patch!(enable_group_icon);
|
||||
patch!(pause_render_traffic_stats_on_blur);
|
||||
patch_opt_field!(self, patch, language);
|
||||
patch_opt_field!(self, patch, theme_mode);
|
||||
patch_opt_field!(self, patch, tray_event);
|
||||
patch_opt_field!(self, patch, env_type);
|
||||
patch_opt_field!(self, patch, start_page);
|
||||
patch_opt_field!(self, patch, startup_script);
|
||||
patch_opt_field!(self, patch, traffic_graph);
|
||||
patch_opt_field!(self, patch, enable_memory_usage);
|
||||
patch_opt_field!(self, patch, enable_group_icon);
|
||||
patch_opt_field!(self, patch, pause_render_traffic_stats_on_blur);
|
||||
#[cfg(target_os = "macos")]
|
||||
patch!(tray_icon);
|
||||
patch!(menu_icon);
|
||||
patch!(menu_order);
|
||||
patch!(proxy_group_tools_position);
|
||||
patch!(notice_position);
|
||||
patch!(collapse_navbar);
|
||||
patch!(common_tray_icon);
|
||||
patch!(sysproxy_tray_icon);
|
||||
patch!(tun_tray_icon);
|
||||
patch_opt_field!(self, patch, tray_icon);
|
||||
patch_opt_field!(self, patch, menu_icon);
|
||||
patch_opt_field!(self, patch, menu_order);
|
||||
patch_opt_field!(self, patch, proxy_group_tools_position);
|
||||
patch_opt_field!(self, patch, notice_position);
|
||||
patch_opt_field!(self, patch, collapse_navbar);
|
||||
patch_opt_field!(self, patch, common_tray_icon);
|
||||
patch_opt_field!(self, patch, sysproxy_tray_icon);
|
||||
patch_opt_field!(self, patch, tun_tray_icon);
|
||||
|
||||
patch!(enable_tun_mode);
|
||||
patch!(enable_auto_launch);
|
||||
patch!(enable_silent_start);
|
||||
patch!(enable_hover_jump_navigator);
|
||||
patch!(hover_jump_navigator_delay);
|
||||
patch_opt_field!(self, patch, enable_tun_mode);
|
||||
patch_opt_field!(self, patch, enable_auto_launch);
|
||||
patch_opt_field!(self, patch, enable_silent_start);
|
||||
patch_opt_field!(self, patch, enable_hover_jump_navigator);
|
||||
patch_opt_field!(self, patch, hover_jump_navigator_delay);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
patch!(verge_redir_port);
|
||||
patch_opt_field!(self, patch, verge_redir_port);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
patch!(verge_redir_enabled);
|
||||
patch_opt_field!(self, patch, verge_redir_enabled);
|
||||
#[cfg(target_os = "linux")]
|
||||
patch!(verge_tproxy_port);
|
||||
patch_opt_field!(self, patch, verge_tproxy_port);
|
||||
#[cfg(target_os = "linux")]
|
||||
patch!(verge_tproxy_enabled);
|
||||
patch!(verge_mixed_port);
|
||||
patch!(verge_socks_port);
|
||||
patch!(verge_socks_enabled);
|
||||
patch!(verge_port);
|
||||
patch!(verge_http_enabled);
|
||||
patch!(enable_system_proxy);
|
||||
patch!(enable_proxy_guard);
|
||||
patch!(enable_bypass_check);
|
||||
patch!(use_default_bypass);
|
||||
patch!(system_proxy_bypass);
|
||||
patch!(proxy_guard_duration);
|
||||
patch!(proxy_auto_config);
|
||||
patch!(pac_file_content);
|
||||
patch!(proxy_host);
|
||||
patch!(theme_setting);
|
||||
patch!(web_ui_list);
|
||||
patch!(clash_core);
|
||||
patch!(hotkeys);
|
||||
patch!(enable_global_hotkey);
|
||||
patch_opt_field!(self, patch, verge_tproxy_enabled);
|
||||
patch_opt_field!(self, patch, verge_mixed_port);
|
||||
patch_opt_field!(self, patch, verge_socks_port);
|
||||
patch_opt_field!(self, patch, verge_socks_enabled);
|
||||
patch_opt_field!(self, patch, verge_port);
|
||||
patch_opt_field!(self, patch, verge_http_enabled);
|
||||
patch_opt_field!(self, patch, enable_system_proxy);
|
||||
patch_opt_field!(self, patch, enable_proxy_guard);
|
||||
patch_opt_field!(self, patch, enable_bypass_check);
|
||||
patch_opt_field!(self, patch, use_default_bypass);
|
||||
patch_opt_field!(self, patch, system_proxy_bypass);
|
||||
patch_opt_field!(self, patch, proxy_guard_duration);
|
||||
patch_opt_field!(self, patch, proxy_auto_config);
|
||||
patch_opt_field!(self, patch, pac_file_content);
|
||||
patch_opt_field!(self, patch, proxy_host);
|
||||
patch_opt_field!(self, patch, theme_setting);
|
||||
patch_opt_field!(self, patch, web_ui_list);
|
||||
patch_opt_field!(self, patch, clash_core);
|
||||
patch_opt_field!(self, patch, hotkeys);
|
||||
patch_opt_field!(self, patch, enable_global_hotkey);
|
||||
|
||||
patch!(auto_close_connection);
|
||||
patch!(auto_check_update);
|
||||
patch!(default_latency_test);
|
||||
patch!(default_latency_timeout);
|
||||
patch!(enable_auto_delay_detection);
|
||||
patch!(auto_delay_detection_interval_minutes);
|
||||
patch!(enable_builtin_enhanced);
|
||||
patch!(proxy_layout_column);
|
||||
patch!(test_list);
|
||||
patch!(auto_log_clean);
|
||||
patch!(enable_auto_backup_schedule);
|
||||
patch!(auto_backup_interval_hours);
|
||||
patch!(auto_backup_on_change);
|
||||
patch_opt_field!(self, patch, auto_close_connection);
|
||||
patch_opt_field!(self, patch, auto_check_update);
|
||||
patch_opt_field!(self, patch, default_latency_test);
|
||||
patch_opt_field!(self, patch, default_latency_timeout);
|
||||
patch_opt_field!(self, patch, enable_auto_delay_detection);
|
||||
patch_opt_field!(self, patch, auto_delay_detection_interval_minutes);
|
||||
patch_opt_field!(self, patch, enable_builtin_enhanced);
|
||||
patch_opt_field!(self, patch, proxy_layout_column);
|
||||
patch_opt_field!(self, patch, test_list);
|
||||
patch_opt_field!(self, patch, auto_log_clean);
|
||||
patch_opt_field!(self, patch, enable_auto_backup_schedule);
|
||||
patch_opt_field!(self, patch, auto_backup_interval_hours);
|
||||
patch_opt_field!(self, patch, auto_backup_on_change);
|
||||
|
||||
patch!(webdav_url);
|
||||
patch!(webdav_username);
|
||||
patch!(webdav_password);
|
||||
patch_opt_field!(self, patch, webdav_url);
|
||||
patch_opt_field!(self, patch, webdav_username);
|
||||
patch_opt_field!(self, patch, webdav_password);
|
||||
#[cfg(target_os = "macos")]
|
||||
patch!(enable_tray_speed);
|
||||
patch!(tray_proxy_groups_display_mode);
|
||||
patch!(tray_inline_outbound_modes);
|
||||
patch!(enable_auto_light_weight_mode);
|
||||
patch!(auto_light_weight_minutes);
|
||||
patch!(enable_dns_settings);
|
||||
patch!(home_cards);
|
||||
patch!(enable_external_controller);
|
||||
patch_opt_field!(self, patch, enable_tray_speed);
|
||||
patch_opt_field!(self, patch, tray_proxy_groups_display_mode);
|
||||
patch_opt_field!(self, patch, tray_inline_outbound_modes);
|
||||
patch_opt_field!(self, patch, enable_auto_light_weight_mode);
|
||||
patch_opt_field!(self, patch, auto_light_weight_minutes);
|
||||
patch_opt_field!(self, patch, enable_dns_settings);
|
||||
patch_opt_field!(self, patch, home_cards);
|
||||
patch_opt_field!(self, patch, enable_external_controller);
|
||||
}
|
||||
|
||||
pub fn get_log_level(&self) -> LevelFilter {
|
||||
|
||||
@@ -14,6 +14,13 @@ pub mod network {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod proxy {
|
||||
/// Single source of the macOS default bypass list; both proxy backends compose from it.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const MACOS_DEFAULT_BYPASS: &str =
|
||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||
}
|
||||
|
||||
pub mod timing {
|
||||
use super::Duration;
|
||||
|
||||
@@ -75,6 +82,7 @@ pub mod tun {
|
||||
"auto-redirect",
|
||||
"auto-detect-interface",
|
||||
"dns-hijack",
|
||||
"dns-mode",
|
||||
"strict-route",
|
||||
"mtu",
|
||||
];
|
||||
|
||||
@@ -7,11 +7,10 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::RwLock;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
Arc, OnceLock,
|
||||
OnceLock,
|
||||
atomic::{AtomicBool, AtomicI64, Ordering},
|
||||
},
|
||||
time::{Duration, UNIX_EPOCH},
|
||||
@@ -82,8 +81,9 @@ impl Default for AutoBackupSettings {
|
||||
}
|
||||
|
||||
pub struct AutoBackupManager {
|
||||
settings: Arc<RwLock<AutoBackupSettings>>,
|
||||
settings_tx: watch::Sender<AutoBackupSettings>,
|
||||
// The watch channel is the single settings store; no duplicate copies.
|
||||
settings_rx: watch::Receiver<AutoBackupSettings>,
|
||||
runner_started: AtomicBool,
|
||||
exec_lock: Mutex<()>,
|
||||
last_backup: AtomicI64,
|
||||
@@ -93,10 +93,10 @@ impl AutoBackupManager {
|
||||
pub fn global() -> &'static Self {
|
||||
static INSTANCE: OnceLock<AutoBackupManager> = OnceLock::new();
|
||||
INSTANCE.get_or_init(|| {
|
||||
let (tx, _rx) = watch::channel(AutoBackupSettings::default());
|
||||
let (settings_tx, settings_rx) = watch::channel(AutoBackupSettings::default());
|
||||
Self {
|
||||
settings: Arc::new(RwLock::new(AutoBackupSettings::default())),
|
||||
settings_tx: tx,
|
||||
settings_tx,
|
||||
settings_rx,
|
||||
runner_started: AtomicBool::new(false),
|
||||
exec_lock: Mutex::new(()),
|
||||
last_backup: AtomicI64::new(0),
|
||||
@@ -106,9 +106,6 @@ impl AutoBackupManager {
|
||||
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
let settings = Self::load_settings().await;
|
||||
{
|
||||
*self.settings.write() = settings;
|
||||
}
|
||||
let _ = self.settings_tx.send(settings);
|
||||
self.maybe_start_runner(settings);
|
||||
Ok(())
|
||||
@@ -116,9 +113,6 @@ impl AutoBackupManager {
|
||||
|
||||
pub async fn refresh_settings(&self) -> Result<()> {
|
||||
let settings = Self::load_settings().await;
|
||||
{
|
||||
*self.settings.write() = settings;
|
||||
}
|
||||
let _ = self.settings_tx.send(settings);
|
||||
self.maybe_start_runner(settings);
|
||||
Ok(())
|
||||
@@ -193,7 +187,7 @@ impl AutoBackupManager {
|
||||
}
|
||||
|
||||
async fn execute_trigger(&self, trigger: AutoBackupTrigger) -> Result<()> {
|
||||
let snapshot = *self.settings.read();
|
||||
let snapshot = *self.settings_rx.borrow();
|
||||
|
||||
if trigger.is_schedule() && !snapshot.schedule_enabled {
|
||||
return Ok(());
|
||||
|
||||
@@ -466,12 +466,3 @@ impl Hotkey {
|
||||
(del_list, add_list)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Hotkey {
|
||||
fn drop(&mut self) {
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
if let Err(e) = app_handle.global_shortcut().unregister_all() {
|
||||
logging!(error, Type::Hotkey, "Error unregistering all hotkeys: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,47 @@ impl LightweightState {
|
||||
|
||||
static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8);
|
||||
|
||||
static WINDOW_CLOSE_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
|
||||
static WEBVIEW_FOCUS_HANDLER_ID: AtomicU32 = AtomicU32::new(0);
|
||||
struct WindowListener {
|
||||
handler_id: AtomicU32,
|
||||
event: &'static str,
|
||||
label: &'static str,
|
||||
}
|
||||
|
||||
impl WindowListener {
|
||||
fn setup(&self, on_event: impl Fn() + Send + 'static) {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
let previous_handler_id = self.handler_id.swap(0, Ordering::AcqRel);
|
||||
if previous_handler_id != 0 {
|
||||
window.unlisten(previous_handler_id);
|
||||
logging!(debug, Type::Lightweight, "覆盖旧的{}监听", self.label);
|
||||
}
|
||||
let handler_id = window.listen(self.event, move |_event| on_event());
|
||||
self.handler_id.store(handler_id, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&self) {
|
||||
let id = self.handler_id.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
window.unlisten(id);
|
||||
}
|
||||
logging!(debug, Type::Lightweight, "取消了{}监听", self.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static WINDOW_CLOSE_LISTENER: WindowListener = WindowListener {
|
||||
handler_id: AtomicU32::new(0),
|
||||
event: "tauri://close-requested",
|
||||
label: "窗口关闭",
|
||||
};
|
||||
|
||||
static WEBVIEW_FOCUS_LISTENER: WindowListener = WindowListener {
|
||||
handler_id: AtomicU32::new(0),
|
||||
event: "tauri://focus",
|
||||
label: "窗口焦点",
|
||||
};
|
||||
|
||||
static CANCEL_TX: Mutex<Option<oneshot::Sender<()>>> = Mutex::new(None);
|
||||
|
||||
@@ -154,55 +193,27 @@ pub async fn add_light_weight_timer() {
|
||||
}
|
||||
|
||||
fn setup_window_close_listener() {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
let previous_handler_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if previous_handler_id != 0 {
|
||||
window.unlisten(previous_handler_id);
|
||||
logging!(debug, Type::Lightweight, "覆盖旧的窗口关闭监听");
|
||||
}
|
||||
let handler_id = window.listen("tauri://close-requested", move |_event| {
|
||||
std::mem::drop(AsyncHandler::spawn(|| async {
|
||||
setup_light_weight_timer().await;
|
||||
}));
|
||||
logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时");
|
||||
});
|
||||
WINDOW_CLOSE_HANDLER_ID.store(handler_id, Ordering::Release);
|
||||
}
|
||||
WINDOW_CLOSE_LISTENER.setup(|| {
|
||||
std::mem::drop(AsyncHandler::spawn(|| async {
|
||||
setup_light_weight_timer().await;
|
||||
}));
|
||||
logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时");
|
||||
});
|
||||
}
|
||||
|
||||
fn cancel_window_close_listener() {
|
||||
let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
window.unlisten(id);
|
||||
}
|
||||
logging!(debug, Type::Lightweight, "取消了窗口关闭监听");
|
||||
}
|
||||
WINDOW_CLOSE_LISTENER.cancel();
|
||||
}
|
||||
|
||||
fn setup_webview_focus_listener() {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
let previous_handler_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if previous_handler_id != 0 {
|
||||
window.unlisten(previous_handler_id);
|
||||
logging!(debug, Type::Lightweight, "覆盖旧的窗口焦点监听");
|
||||
}
|
||||
let handler_id = window.listen("tauri://focus", move |_event| {
|
||||
cancel_light_weight_timer();
|
||||
logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时");
|
||||
});
|
||||
WEBVIEW_FOCUS_HANDLER_ID.store(handler_id, Ordering::Release);
|
||||
}
|
||||
WEBVIEW_FOCUS_LISTENER.setup(|| {
|
||||
cancel_light_weight_timer();
|
||||
logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时");
|
||||
});
|
||||
}
|
||||
|
||||
fn cancel_webview_focus_listener() {
|
||||
let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel);
|
||||
if id != 0 {
|
||||
if let Some(window) = WindowManager::get_main_window() {
|
||||
window.unlisten(id);
|
||||
}
|
||||
logging!(debug, Type::Lightweight, "取消了窗口焦点监听");
|
||||
}
|
||||
WEBVIEW_FOCUS_LISTENER.cancel();
|
||||
}
|
||||
|
||||
async fn setup_light_weight_timer() {
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
notification::{self, FailedOperation},
|
||||
runstate::RUN_STATE,
|
||||
service,
|
||||
sysopt::Sysopt,
|
||||
sysopt::{Sysopt, format_bypass},
|
||||
},
|
||||
process::AsyncHandler,
|
||||
utils::{
|
||||
@@ -204,8 +204,6 @@ const fn refusal_classification(service_usable: Option<bool>) -> SysproxyFailure
|
||||
}
|
||||
|
||||
const LOOPBACK_HOST: &str = "127.0.0.1";
|
||||
const MACOS_DEFAULT_BYPASS: &str =
|
||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||
const MAX_SERVICE_BYPASS_LEN: usize = 8192;
|
||||
|
||||
static SERVICE_PROXY_OPERATIONS: ServiceProxyOperations = ServiceProxyOperations::new();
|
||||
@@ -327,13 +325,7 @@ fn service_bypass(verge: &IVerge) -> Result<String> {
|
||||
let custom = verge.system_proxy_bypass.as_deref().unwrap_or("");
|
||||
ensure!(!custom.contains('\0'), "system proxy bypass contains NUL");
|
||||
|
||||
let mut bypass = if custom.is_empty() {
|
||||
MACOS_DEFAULT_BYPASS.to_owned()
|
||||
} else if verge.use_default_bypass.unwrap_or(true) {
|
||||
format!("{MACOS_DEFAULT_BYPASS},{custom}")
|
||||
} else {
|
||||
custom.to_owned()
|
||||
};
|
||||
let mut bypass: String = format_bypass(verge.use_default_bypass.unwrap_or(true), custom).into();
|
||||
truncate_utf8(&mut bypass, MAX_SERVICE_BYPASS_LEN);
|
||||
Ok(bypass)
|
||||
}
|
||||
|
||||
@@ -593,50 +593,71 @@ fn packaged_service_tool_path(file_name: &str, packaged_path: impl FnOnce() -> R
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
pub(super) fn run_maybe_elevated(program: &Path, hide_window: bool) -> Result<std::process::ExitStatus> {
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use runas::Command as RunasCommand;
|
||||
use std::os::windows::process::CommandExt as _;
|
||||
|
||||
let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall.exe", || {
|
||||
Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall.exe"))
|
||||
})?;
|
||||
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
|
||||
let token = Token::with_current_process()?;
|
||||
let level = token.privilege_level()?;
|
||||
let status = match level {
|
||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
|
||||
_ => StdCommand::new(uninstall_path).creation_flags(0x08000000).status()?,
|
||||
PrivilegeLevel::NotPrivileged => {
|
||||
let mut command = RunasCommand::new(program);
|
||||
if hide_window {
|
||||
command.show(false);
|
||||
}
|
||||
command.status()?
|
||||
}
|
||||
_ => {
|
||||
let mut command = StdCommand::new(program);
|
||||
if hide_window {
|
||||
// 0x08000000 is CREATE_NO_WINDOW, the direct counterpart of runas's show(false).
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
command.status()?
|
||||
}
|
||||
};
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
fn uninstaller_tool_path(file_name: &str, packaged_path: impl FnOnce() -> Result<PathBuf>) -> Result<PathBuf> {
|
||||
let uninstall_path = packaged_service_tool_path(file_name, packaged_path)?;
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
Ok(uninstall_path)
|
||||
}
|
||||
|
||||
fn ensure_uninstall_success(status: &std::process::ExitStatus) -> Result<()> {
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall.exe", || {
|
||||
Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall.exe"))
|
||||
})?;
|
||||
|
||||
let status = run_maybe_elevated(&uninstall_path, true)?;
|
||||
ensure_uninstall_success(&status)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall", || {
|
||||
let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall", || {
|
||||
Ok(tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-uninstall"))
|
||||
})?;
|
||||
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
|
||||
let elevator = crate::utils::help::linux_elevator();
|
||||
let status = if linux_running_as_root() {
|
||||
StdCommand::new(&uninstall_path).status()?
|
||||
@@ -666,14 +687,7 @@ fn uninstall_service() -> Result<()> {
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
ensure_uninstall_success(&status)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -688,14 +702,10 @@ fn linux_running_as_root() -> bool {
|
||||
fn uninstall_service() -> Result<()> {
|
||||
logging!(info, Type::Service, "uninstall service");
|
||||
|
||||
let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall", || {
|
||||
let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall", || {
|
||||
Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall"))
|
||||
})?;
|
||||
|
||||
if !uninstall_path.exists() {
|
||||
bail!(format!("uninstaller not found: {uninstall_path:?}"));
|
||||
}
|
||||
|
||||
let uninstall_path = macos_service_tool_path(&uninstall_path)?;
|
||||
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
|
||||
|
||||
@@ -711,14 +721,7 @@ fn uninstall_service() -> Result<()> {
|
||||
|
||||
let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?;
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
ensure_uninstall_success(&status)
|
||||
}
|
||||
|
||||
fn install_service() -> Result<()> {
|
||||
|
||||
@@ -241,20 +241,19 @@ async fn current_os_proxy_state(sys: Sysproxy, auto: Autoproxy) -> OsProxyState
|
||||
}
|
||||
|
||||
pub(crate) struct Sysopt {
|
||||
update_lock: TokioMutex<()>,
|
||||
// Owns the proxy target pair; every reader and writer goes through this lock.
|
||||
update_lock: TokioMutex<(Sysproxy, Autoproxy)>,
|
||||
guard_operation_lock: TokioMutex<()>,
|
||||
reset_sysproxy: AtomicBool,
|
||||
inner_proxy: Arc<RwLock<(Sysproxy, Autoproxy)>>,
|
||||
guard: Arc<RwLock<GuardMonitor>>,
|
||||
}
|
||||
|
||||
impl Default for Sysopt {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
update_lock: TokioMutex::new(()),
|
||||
update_lock: TokioMutex::new((Sysproxy::default(), Autoproxy::default())),
|
||||
guard_operation_lock: TokioMutex::new(()),
|
||||
reset_sysproxy: AtomicBool::new(false),
|
||||
inner_proxy: Arc::new(RwLock::new((Sysproxy::default(), Autoproxy::default()))),
|
||||
guard: Arc::new(RwLock::new(GuardMonitor::new(GuardType::None, Duration::from_secs(30)))),
|
||||
}
|
||||
}
|
||||
@@ -269,10 +268,9 @@ static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
static BYPASS_SEPARATOR: &str = ",";
|
||||
#[cfg(target_os = "macos")]
|
||||
static DEFAULT_BYPASS: &str =
|
||||
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||
static DEFAULT_BYPASS: &str = crate::constants::proxy::MACOS_DEFAULT_BYPASS;
|
||||
|
||||
fn format_bypass(use_default: bool, custom_bypass: &str) -> String {
|
||||
pub(super) fn format_bypass(use_default: bool, custom_bypass: &str) -> String {
|
||||
if custom_bypass.is_empty() {
|
||||
DEFAULT_BYPASS.into()
|
||||
} else if use_default {
|
||||
@@ -361,7 +359,7 @@ impl Sysopt {
|
||||
}
|
||||
}
|
||||
while self.access_guard().read().get_state().is_pendding() {
|
||||
tokio::task::yield_now().await;
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
// `start()` false may also mean a guard was already running.
|
||||
!self.access_guard().read().get_state().is_stopped()
|
||||
@@ -413,14 +411,17 @@ impl Sysopt {
|
||||
}
|
||||
|
||||
/// init the sysproxy
|
||||
#[allow(
|
||||
clippy::significant_drop_tightening,
|
||||
reason = "the update lock must stay held until the OS write finishes; wait_idle relies on it"
|
||||
)]
|
||||
pub(super) async fn update_sysproxy(&self) -> Result<()> {
|
||||
let _lock = self.update_lock.lock().await;
|
||||
let mut inner_proxy = self.update_lock.lock().await;
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
// Configured, not live: this runs while the Core is being started or restarted, and
|
||||
// asking a Core that is not up yet would only fall back here anyway.
|
||||
let port = MixedPort::desired().await;
|
||||
let pac_port = server::embedded_server_port()?;
|
||||
// 先 await, 避免持有锁导致的 Send 问题
|
||||
let bypass = get_bypass().await;
|
||||
|
||||
let (sys_enable, pac_enable, proxy_host, proxy_guard) = (
|
||||
@@ -431,7 +432,7 @@ impl Sysopt {
|
||||
);
|
||||
|
||||
let (sys, auto, guard_type) = {
|
||||
let (sys, auto) = &mut *self.inner_proxy.write();
|
||||
let (sys, auto) = &mut *inner_proxy;
|
||||
sys.host = proxy_host.into();
|
||||
sys.port = port;
|
||||
sys.bypass = bypass.into();
|
||||
@@ -528,6 +529,10 @@ impl Sysopt {
|
||||
}
|
||||
|
||||
/// reset the sysproxy
|
||||
#[allow(
|
||||
clippy::significant_drop_tightening,
|
||||
reason = "the update lock must stay held until the disable finishes; wait_idle relies on it"
|
||||
)]
|
||||
pub(super) async fn reset_sysproxy(&self) -> Result<()> {
|
||||
if self
|
||||
.reset_sysproxy
|
||||
@@ -539,13 +544,13 @@ impl Sysopt {
|
||||
defer! {
|
||||
self.reset_sysproxy.store(false, Ordering::SeqCst);
|
||||
}
|
||||
let _lock = self.update_lock.lock().await;
|
||||
let mut inner_proxy = self.update_lock.lock().await;
|
||||
let _guard_operation = self.guard_operation_lock.lock().await;
|
||||
let drained = self.stop_proxy_guard_locked().await;
|
||||
|
||||
// 直接关闭所有代理
|
||||
let (sys, auto) = {
|
||||
let (sys, auto) = &mut *self.inner_proxy.write();
|
||||
let (sys, auto) = &mut *inner_proxy;
|
||||
sys.enable = false;
|
||||
auto.enable = false;
|
||||
(sys.clone(), auto.clone())
|
||||
|
||||
@@ -422,12 +422,11 @@ fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<&str, &str> {
|
||||
let mut parts = item.split(',');
|
||||
match (parts.next(), parts.next()) {
|
||||
(Some(func), Some(key)) => {
|
||||
// 托盘菜单中的 `accelerator` 属性,在 Linux/Windows 中都不支持小键盘按键的解析
|
||||
if key.to_uppercase().contains("NUMPAD") {
|
||||
None
|
||||
} else {
|
||||
Some((func, key))
|
||||
}
|
||||
// 托盘菜单的 `accelerator` 在 Linux/Windows 下不支持小键盘按键,Windows 下也不支持 Win 键(前端上报为 `CMD`)
|
||||
let uppercase_key = key.to_uppercase();
|
||||
let unsupported =
|
||||
uppercase_key.contains("NUMPAD") || (cfg!(windows) && uppercase_key.contains("CMD"));
|
||||
if unsupported { None } else { Some((func, key)) }
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -637,7 +636,8 @@ async fn create_tray_menu(
|
||||
.tray_proxy_groups_display_mode
|
||||
.as_deref()
|
||||
.unwrap_or("default");
|
||||
let include_proxy_groups = options.include_proxy_groups && tray_proxy_groups_display_mode != "disable";
|
||||
let include_proxy_groups =
|
||||
options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable");
|
||||
|
||||
let proxy_group_order_map = runtime_proxy_groups_order;
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use crate::utils::dirs;
|
||||
use crate::{core::service, utils::dirs};
|
||||
use anyhow::{Result, bail};
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use runas::Command as RunasCommand;
|
||||
use std::process::Command as StdCommand;
|
||||
|
||||
pub fn invoke_uwptools() -> Result<()> {
|
||||
let resource_dir = dirs::app_resources_dir()?;
|
||||
@@ -14,13 +11,7 @@ pub fn invoke_uwptools() -> Result<()> {
|
||||
bail!("enableLoopback exe not found");
|
||||
}
|
||||
|
||||
let token = Token::with_current_process()?;
|
||||
let level = token.privilege_level()?;
|
||||
|
||||
match level {
|
||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(tool_path).status()?,
|
||||
_ => StdCommand::new(tool_path).status()?,
|
||||
};
|
||||
service::run_maybe_elevated(&tool_path, false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// This function is exported for use by the Clash core
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
function main(config, _name) {
|
||||
if (config.mode === 'script') {
|
||||
config.mode = 'rule'
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// This function is exported for use by the Clash core
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
function main(config, _name) {
|
||||
if (Array.isArray(config.proxies)) {
|
||||
config.proxies.forEach((p, i) => {
|
||||
if (p.type === 'hysteria' && typeof p.alpn === 'string') {
|
||||
config.proxies[i].alpn = [p.alpn]
|
||||
}
|
||||
})
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -24,8 +24,8 @@ pub enum ChainType {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ChainSupport {
|
||||
ClashMeta,
|
||||
ClashMetaAlpha,
|
||||
Mihomo,
|
||||
MihomoAlpha,
|
||||
}
|
||||
|
||||
// Helper trait to allow async conversion
|
||||
@@ -53,26 +53,14 @@ impl AsyncChainItemFrom for Option<ChainItem> {
|
||||
uid,
|
||||
data: ChainType::Merge(help::read_mapping(&path).await.ok()?),
|
||||
}),
|
||||
"rules" => {
|
||||
"rules" | "proxies" | "groups" => {
|
||||
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
|
||||
Some(ChainItem {
|
||||
uid,
|
||||
data: ChainType::Rules(seq_map),
|
||||
})
|
||||
}
|
||||
"proxies" => {
|
||||
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
|
||||
Some(ChainItem {
|
||||
uid,
|
||||
data: ChainType::Proxies(seq_map),
|
||||
})
|
||||
}
|
||||
"groups" => {
|
||||
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
|
||||
Some(ChainItem {
|
||||
uid,
|
||||
data: ChainType::Groups(seq_map),
|
||||
})
|
||||
let data = match itype {
|
||||
"rules" => ChainType::Rules(seq_map),
|
||||
"proxies" => ChainType::Proxies(seq_map),
|
||||
_ => ChainType::Groups(seq_map),
|
||||
};
|
||||
Some(ChainItem { uid, data })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -81,23 +69,15 @@ impl AsyncChainItemFrom for Option<ChainItem> {
|
||||
impl ChainItem {
|
||||
/// 内建支持一些脚本
|
||||
pub fn builtin() -> Vec<(ChainSupport, Self)> {
|
||||
// meta 的一些处理
|
||||
let meta_guard = Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
|
||||
// meta 1.13.2 alpn string 转 数组
|
||||
let hy_alpn = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
|
||||
// meta 的一些处理
|
||||
let meta_guard_alpha = Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
|
||||
// meta 1.13.2 alpn string 转 数组
|
||||
let hy_alpn_alpha = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
// data intentionally empty: the enhance loop dispatches on uid to native implementations
|
||||
let hy_alpn = Self::to_script("verge_hy_alpn", "");
|
||||
let meta_guard = Self::to_script("verge_meta_guard", "");
|
||||
|
||||
vec![
|
||||
(ChainSupport::ClashMeta, hy_alpn),
|
||||
(ChainSupport::ClashMeta, meta_guard),
|
||||
(ChainSupport::ClashMetaAlpha, hy_alpn_alpha),
|
||||
(ChainSupport::ClashMetaAlpha, meta_guard_alpha),
|
||||
(ChainSupport::Mihomo, hy_alpn.clone()),
|
||||
(ChainSupport::Mihomo, meta_guard.clone()),
|
||||
(ChainSupport::MihomoAlpha, hy_alpn),
|
||||
(ChainSupport::MihomoAlpha, meta_guard),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -114,7 +94,7 @@ impl ChainSupport {
|
||||
match core {
|
||||
Some(core) => matches!(
|
||||
(self, core.as_str()),
|
||||
(Self::ClashMeta, "verge-mihomo") | (Self::ClashMetaAlpha, "verge-mihomo-alpha")
|
||||
(Self::Mihomo, "verge-mihomo") | (Self::MihomoAlpha, "verge-mihomo-alpha")
|
||||
),
|
||||
None => true,
|
||||
}
|
||||
|
||||
@@ -24,18 +24,7 @@ fn lowercase_key(key: &str) -> Value {
|
||||
Value::from(key.as_str())
|
||||
}
|
||||
|
||||
pub fn use_lowercase(config: &Mapping) -> Mapping {
|
||||
let mut lowercased = Mapping::new();
|
||||
|
||||
for (key, value) in config.into_iter() {
|
||||
if let Some(key_str) = key.as_str() {
|
||||
lowercased.insert(lowercase_key(key_str), value.clone());
|
||||
}
|
||||
}
|
||||
lowercased
|
||||
}
|
||||
|
||||
pub fn use_lowercase_owned(config: Mapping) -> Mapping {
|
||||
pub fn use_lowercase(config: Mapping) -> Mapping {
|
||||
let mut lowercased = Mapping::new();
|
||||
|
||||
for (key, value) in config {
|
||||
|
||||
@@ -17,7 +17,7 @@ fn deep_merge(a: &mut Value, b: Value) {
|
||||
}
|
||||
|
||||
pub fn use_merge(merge: &Mapping, mut config: Mapping) -> Mapping {
|
||||
for (key, value) in use_lowercase(merge) {
|
||||
for (key, value) in use_lowercase(merge.clone()) {
|
||||
match (config.get_mut(&key), value) {
|
||||
(Some(Value::Mapping(existing)), Value::Mapping(overlay)) if key.as_str() == Some("dns") => {
|
||||
existing.extend(overlay);
|
||||
|
||||
@@ -8,7 +8,7 @@ mod tun;
|
||||
|
||||
use self::{
|
||||
chain::{AsyncChainItemFrom as _, ChainItem, ChainType},
|
||||
field::{use_keys, use_lowercase_owned, use_sort},
|
||||
field::{use_keys, use_lowercase, use_sort},
|
||||
merge::use_merge,
|
||||
script::use_script,
|
||||
seq::{SeqMap, use_seq},
|
||||
@@ -199,35 +199,25 @@ async fn collect_profile_items(profiles: &IProfiles) -> Result<ProfileItems> {
|
||||
|
||||
let name = current_item.name.clone().unwrap_or_default();
|
||||
|
||||
let ProfileItems {
|
||||
merge_item: default_merge,
|
||||
script_item: default_script,
|
||||
rules_item: default_rules,
|
||||
proxies_item: default_proxies,
|
||||
groups_item: default_groups,
|
||||
global_merge: default_global_merge,
|
||||
global_script: default_global_script,
|
||||
..
|
||||
} = ProfileItems::default();
|
||||
|
||||
let (merge_item, script_item, rules_item, proxies_item, groups_item, global_merge, global_script) = tokio::join!(
|
||||
chain_item_or_default(profiles.get_item(&merge_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Merge(Mapping::new()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item(&script_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item(&rules_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Rules(SeqMap::default()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item(&proxies_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Proxies(SeqMap::default()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item(&groups_uid).ok(), || ChainItem {
|
||||
uid: "".into(),
|
||||
data: ChainType::Groups(SeqMap::default()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item("Merge").ok(), || ChainItem {
|
||||
uid: "Merge".into(),
|
||||
data: ChainType::Merge(Mapping::new()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item("Script").ok(), || ChainItem {
|
||||
uid: "Script".into(),
|
||||
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
|
||||
},),
|
||||
chain_item_or_default(profiles.get_item(&merge_uid).ok(), || default_merge,),
|
||||
chain_item_or_default(profiles.get_item(&script_uid).ok(), || default_script,),
|
||||
chain_item_or_default(profiles.get_item(&rules_uid).ok(), || default_rules,),
|
||||
chain_item_or_default(profiles.get_item(&proxies_uid).ok(), || default_proxies,),
|
||||
chain_item_or_default(profiles.get_item(&groups_uid).ok(), || default_groups,),
|
||||
chain_item_or_default(profiles.get_item("Merge").ok(), || default_global_merge,),
|
||||
chain_item_or_default(profiles.get_item("Script").ok(), || default_global_script,),
|
||||
);
|
||||
|
||||
Ok(ProfileItems {
|
||||
@@ -243,16 +233,16 @@ async fn collect_profile_items(profiles: &IProfiles) -> Result<ProfileItems> {
|
||||
})
|
||||
}
|
||||
|
||||
async fn process_global_items(
|
||||
async fn process_override_items(
|
||||
mut config: Mapping,
|
||||
mut exists_keys: Vec<String>,
|
||||
mut result_map: HashMap<String, ResultLog>,
|
||||
global_merge: ChainItem,
|
||||
global_script: ChainItem,
|
||||
merge_item: ChainItem,
|
||||
script_item: ChainItem,
|
||||
profile_name: &String,
|
||||
authoritative: &AuthoritativeFields,
|
||||
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
if let ChainType::Merge(merge) = global_merge.data {
|
||||
if let ChainType::Merge(merge) = merge_item.data {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
let before = authoritative.current(&config);
|
||||
config = use_merge(&merge, config);
|
||||
@@ -262,11 +252,11 @@ async fn process_global_items(
|
||||
.map(discarded_note)
|
||||
.collect();
|
||||
if !notes.is_empty() {
|
||||
result_map.entry(global_merge.uid).or_default().extend(notes);
|
||||
result_map.entry(merge_item.uid).or_default().extend(notes);
|
||||
}
|
||||
}
|
||||
|
||||
if let ChainType::Script(script) = global_script.data {
|
||||
if let ChainType::Script(script) = script_item.data {
|
||||
let before = authoritative.current(&config);
|
||||
let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await;
|
||||
exists_keys.extend(changed_keys);
|
||||
@@ -277,7 +267,7 @@ async fn process_global_items(
|
||||
.into_iter()
|
||||
.map(discarded_note),
|
||||
);
|
||||
result_map.insert(global_script.uid, logs);
|
||||
result_map.insert(script_item.uid, logs);
|
||||
}
|
||||
|
||||
(config, exists_keys, result_map)
|
||||
@@ -544,46 +534,6 @@ fn ensure_lan_bind_address(mut config: Mapping) -> Mapping {
|
||||
config
|
||||
}
|
||||
|
||||
async fn process_profile_items(
|
||||
mut config: Mapping,
|
||||
mut exists_keys: Vec<String>,
|
||||
mut result_map: HashMap<String, ResultLog>,
|
||||
merge_item: ChainItem,
|
||||
script_item: ChainItem,
|
||||
profile_name: &String,
|
||||
authoritative: &AuthoritativeFields,
|
||||
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
if let ChainType::Merge(merge) = merge_item.data {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
let before = authoritative.current(&config);
|
||||
config = use_merge(&merge, config);
|
||||
let notes: ResultLog = authoritative
|
||||
.overridden(&before, &authoritative.current(&config))
|
||||
.into_iter()
|
||||
.map(discarded_note)
|
||||
.collect();
|
||||
if !notes.is_empty() {
|
||||
result_map.entry(merge_item.uid).or_default().extend(notes);
|
||||
}
|
||||
}
|
||||
|
||||
if let ChainType::Script(script) = script_item.data {
|
||||
let before = authoritative.current(&config);
|
||||
let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await;
|
||||
exists_keys.extend(changed_keys);
|
||||
config = res_config;
|
||||
logs.extend(
|
||||
authoritative
|
||||
.overridden(&before, &authoritative.current(&config))
|
||||
.into_iter()
|
||||
.map(discarded_note),
|
||||
);
|
||||
result_map.insert(script_item.uid, logs);
|
||||
}
|
||||
|
||||
(config, exists_keys, result_map)
|
||||
}
|
||||
|
||||
/// Merges the app's clash config over the profile; switches are parameters to keep it pure.
|
||||
fn merge_default_config(
|
||||
mut config: Mapping,
|
||||
@@ -664,7 +614,7 @@ fn apply_builtin_scripts(mut config: Mapping, clash_core: Option<String>, enable
|
||||
.collect();
|
||||
if !items.is_empty() {
|
||||
// The JS path saw a lowercased view; lowercase once, only when a builtin runs.
|
||||
config = use_lowercase_owned(config);
|
||||
config = use_lowercase(config);
|
||||
for item in items {
|
||||
logging!(debug, Type::Core, "run builtin script {}", item.uid);
|
||||
config = match item.uid.as_str() {
|
||||
@@ -935,7 +885,7 @@ pub async fn enhance(
|
||||
|
||||
let authoritative = AuthoritativeFields::capture(&config, &gui_tun_keys, dns_settings);
|
||||
|
||||
let (config, exists_keys, result_map) = process_global_items(
|
||||
let (config, exists_keys, result_map) = process_override_items(
|
||||
config,
|
||||
exists_keys,
|
||||
result_map,
|
||||
@@ -946,7 +896,7 @@ pub async fn enhance(
|
||||
)
|
||||
.await;
|
||||
|
||||
let (config, exists_keys, result_map) = process_profile_items(
|
||||
let (config, exists_keys, result_map) = process_override_items(
|
||||
config,
|
||||
exists_keys,
|
||||
result_map,
|
||||
@@ -1428,7 +1378,7 @@ mod authoritative_field_tests {
|
||||
mod tests {
|
||||
use super::{
|
||||
AuthoritativeFields, ChainItem, ChainType, Mapping, cleanup_proxy_groups, ensure_lan_bind_address,
|
||||
process_global_items, process_profile_items, use_keys,
|
||||
process_override_items, use_keys,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -1476,7 +1426,7 @@ mod tests {
|
||||
data: ChainType::Merge(merge.clone()),
|
||||
};
|
||||
let profile_name = "test-profile".into();
|
||||
let (config, keys, logs) = process_global_items(
|
||||
let (config, keys, logs) = process_override_items(
|
||||
config,
|
||||
vec![],
|
||||
HashMap::new(),
|
||||
@@ -1486,7 +1436,7 @@ mod tests {
|
||||
&authoritative,
|
||||
)
|
||||
.await;
|
||||
let (config, _, logs) = process_profile_items(
|
||||
let (config, _, logs) = process_override_items(
|
||||
config,
|
||||
keys,
|
||||
logs,
|
||||
@@ -1564,7 +1514,7 @@ mod tests {
|
||||
|
||||
let profile_name = "test-profile".into();
|
||||
let authoritative = AuthoritativeFields::capture(&config, &[], Mapping::new());
|
||||
let (config, exists_keys, result_map) = process_global_items(
|
||||
let (config, exists_keys, result_map) = process_override_items(
|
||||
config,
|
||||
exists_keys,
|
||||
HashMap::new(),
|
||||
@@ -1574,7 +1524,7 @@ mod tests {
|
||||
&authoritative,
|
||||
)
|
||||
.await;
|
||||
let (config, exists_keys, _) = process_profile_items(
|
||||
let (config, exists_keys, _) = process_override_items(
|
||||
config,
|
||||
exists_keys,
|
||||
result_map,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{process::AsyncHandler, utils::tmpl};
|
||||
|
||||
use super::field::{use_lowercase, use_lowercase_owned};
|
||||
use super::field::use_lowercase;
|
||||
use anyhow::{Error, Result};
|
||||
use boa_engine::{Context, JsString, JsValue, Source, js_string, native_function::NativeFunction, property::Attribute};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
@@ -22,10 +22,10 @@ type EvalOutcome = (Result<Mapping, std::string::String>, Vec<(String, String)>)
|
||||
pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult {
|
||||
// The template is identity-plus-lowercasing; skip the Boa round-trip.
|
||||
if script == tmpl::ITEM_SCRIPT {
|
||||
return (use_lowercase_owned(config), vec![], vec![]);
|
||||
return (use_lowercase(config), vec![], vec![]);
|
||||
}
|
||||
|
||||
let json = match serde_json::to_string(&use_lowercase(&config)) {
|
||||
let json = match serde_json::to_string(&use_lowercase(config.clone())) {
|
||||
Ok(json) if json.len() <= MAX_JSON_SIZE => json,
|
||||
Ok(_) => {
|
||||
let message = "Configuration size exceeds maximum allowed size";
|
||||
@@ -173,7 +173,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
|
||||
}
|
||||
|
||||
match parse_json_safely(&result) {
|
||||
Ok(config) => (Ok(use_lowercase_owned(config)), outputs.lock().to_vec()),
|
||||
Ok(config) => (Ok(use_lowercase(config)), outputs.lock().to_vec()),
|
||||
Err(err) => {
|
||||
logging!(
|
||||
error,
|
||||
|
||||
@@ -28,7 +28,7 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("fake-ip");
|
||||
|
||||
if current_mode == "fake-ip" || !dns_val.contains_key(Value::from("enhanced-mode")) {
|
||||
if current_mode == "fake-ip" {
|
||||
revise!(dns_val, "enable", true);
|
||||
revise!(dns_val, "ipv6", ipv6_val);
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ async fn finalize_restored_verge_config(
|
||||
webdav_username: Option<String>,
|
||||
webdav_password: Option<String>,
|
||||
) -> Result<()> {
|
||||
// The lock must cover every restored layer; an unlocked apply races a concurrently staged draft.
|
||||
let config_write = Config::lock_config_write().await;
|
||||
|
||||
// A broken restored `verge.yaml` is a restore failure, not a reason to load defaults.
|
||||
let mut restored = help::read_yaml::<IVerge>(&verge_path()?).await?;
|
||||
restored.webdav_url = webdav_url;
|
||||
@@ -43,43 +46,42 @@ async fn finalize_restored_verge_config(
|
||||
let restored_clash = IClashTemp::new().await;
|
||||
let clash_draft = Config::clash().await;
|
||||
clash_draft.edit_draft(|d| {
|
||||
*d = restored_clash.clone();
|
||||
*d = restored_clash;
|
||||
});
|
||||
clash_draft.apply();
|
||||
|
||||
let restored_profiles = IProfiles::new().await;
|
||||
let profiles_draft = Config::profiles().await;
|
||||
profiles_draft.edit_draft(|d| {
|
||||
*d = restored_profiles.clone();
|
||||
*d = restored_profiles;
|
||||
});
|
||||
profiles_draft.apply();
|
||||
|
||||
let verge_draft = Config::verge().await;
|
||||
{
|
||||
// Hold the write lock so a concurrent patch cannot stage a draft the proxy write reads.
|
||||
let _config_write = Config::lock_config_write().await;
|
||||
verge_draft.edit_draft(|d| {
|
||||
*d = restored.clone();
|
||||
});
|
||||
verge_draft.apply();
|
||||
verge_draft.edit_draft(|d| {
|
||||
*d = restored.clone();
|
||||
});
|
||||
verge_draft.apply();
|
||||
|
||||
// Turn it off here; a failing core side effect below would otherwise skip this step.
|
||||
if !restored.enable_system_proxy.unwrap_or_default() {
|
||||
let result = async {
|
||||
let _lifecycle = CoreManager::global().lifecycle_lock.lock().await;
|
||||
proxy_control::apply().await?;
|
||||
proxy_control::refresh_guard().await
|
||||
}
|
||||
.await;
|
||||
if let Err(err) = result {
|
||||
logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}");
|
||||
if SystemProxyStateUnknown::is_in(&err) {
|
||||
return Err(err);
|
||||
}
|
||||
// Turn it off here; a failing core side effect below would otherwise skip this step.
|
||||
if !restored.enable_system_proxy.unwrap_or_default() {
|
||||
let result = async {
|
||||
let _lifecycle = CoreManager::global().lifecycle_lock.lock().await;
|
||||
proxy_control::apply().await?;
|
||||
proxy_control::refresh_guard().await
|
||||
}
|
||||
.await;
|
||||
if let Err(err) = result {
|
||||
logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}");
|
||||
if SystemProxyStateUnknown::is_in(&err) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `patch_verge` re-acquires the lock through `try_lock_config_write`.
|
||||
drop(config_write);
|
||||
|
||||
// Run configuration side effects without rewriting the already-restored file.
|
||||
if let Err(err) = super::patch_verge(&restored, true).await {
|
||||
logging!(error, Type::Backup, "Failed to apply restored verge config: {err:#}");
|
||||
@@ -134,14 +136,35 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Backup creation strips the WebDAV credentials, so the live ones are kept.
|
||||
async fn restore_backup_archive(path: PathBuf) -> Result<()> {
|
||||
let (webdav_url, webdav_username, webdav_password) = {
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_arc();
|
||||
(
|
||||
verge.webdav_url.clone(),
|
||||
verge.webdav_username.clone(),
|
||||
verge.webdav_password.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let home_dir = app_home_dir()?;
|
||||
AsyncHandler::spawn_blocking(move || -> Result<()> {
|
||||
let file = std::fs::File::open(&path)?;
|
||||
let mut zip = zip::ZipArchive::new(file)?;
|
||||
zip.extract(&home_dir)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|join| anyhow!("backup extraction task failed: {join}"))??;
|
||||
|
||||
// Held for the config-data transition only; extraction is plain filesystem work.
|
||||
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
|
||||
finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename))]
|
||||
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
let verge = Config::verge().await;
|
||||
let verge_data = verge.latest_arc();
|
||||
let webdav_url = verge_data.webdav_url.clone();
|
||||
let webdav_username = verge_data.webdav_username.clone();
|
||||
let webdav_password = verge_data.webdav_password.clone();
|
||||
|
||||
let backup_storage_path = app_home_dir()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))?
|
||||
.join(filename.as_str());
|
||||
@@ -158,12 +181,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
||||
err
|
||||
})?;
|
||||
|
||||
let value = backup_storage_path.clone();
|
||||
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??;
|
||||
let mut zip = zip::ZipArchive::new(file)?;
|
||||
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
|
||||
zip.extract(app_home_dir()?)?;
|
||||
let res = finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await;
|
||||
let res = restore_backup_archive(backup_storage_path.clone()).await;
|
||||
let _ = backup_storage_path.remove_if_exists().await;
|
||||
res
|
||||
}
|
||||
@@ -332,22 +350,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
|
||||
return Err(anyhow!("Backup file not found: {}", filename));
|
||||
}
|
||||
|
||||
let (webdav_url, webdav_username, webdav_password) = {
|
||||
let verge = Config::verge().await;
|
||||
let verge = verge.latest_arc();
|
||||
(
|
||||
verge.webdav_url.clone(),
|
||||
verge.webdav_username.clone(),
|
||||
verge.webdav_password.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??;
|
||||
let mut zip = zip::ZipArchive::new(file)?;
|
||||
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
|
||||
zip.extract(app_home_dir()?)?;
|
||||
finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await?;
|
||||
Ok(())
|
||||
restore_backup_archive(target_path).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename, destination = %destination))]
|
||||
|
||||
+12
-45
@@ -3,7 +3,7 @@ use crate::core::notify::{Refresh, announce};
|
||||
use crate::{
|
||||
config::{Config, MixedPort},
|
||||
core::{CoreManager, handle, tray},
|
||||
feat::clean_async,
|
||||
feat::run_exit_cleanup_flow,
|
||||
process::AsyncHandler,
|
||||
utils,
|
||||
};
|
||||
@@ -14,15 +14,8 @@ use smartstring::alias::String;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> = LazyLock::new(|| {
|
||||
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
|
||||
.with_safe_default_protocol_versions()
|
||||
.expect("Failed to set TLS versions")
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
Arc::new(config)
|
||||
});
|
||||
static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> =
|
||||
LazyLock::new(|| Arc::new(utils::network::build_webpki_rooted_tls_config().expect("Failed to set TLS versions")));
|
||||
|
||||
pub async fn restart_clash_core() {
|
||||
match CoreManager::global().restart_core().await {
|
||||
@@ -38,38 +31,9 @@ pub async fn restart_clash_core() {
|
||||
|
||||
pub async fn restart_app() {
|
||||
logging!(debug, Type::System, "启动重启应用流程");
|
||||
handle::Handle::global().set_is_exiting();
|
||||
|
||||
Config::apply_all_and_save_file().await;
|
||||
|
||||
let cleanup_result = clean_async().await;
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
"资源清理完成,退出代码: {}",
|
||||
if cleanup_result.all_success { 0 } else { 1 }
|
||||
);
|
||||
let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppRestartCoreStopFailed).await;
|
||||
|
||||
if !cleanup_result.core_stopped {
|
||||
handle::Handle::global().clear_is_exiting();
|
||||
// A failed stop may still have marked the core stopped, and exit cleanup skipped the DNS restore.
|
||||
// A core that kept running keeps its DNS, whatever an unapplied config draft says.
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let manager = CoreManager::global();
|
||||
let _lifecycle = manager.lifecycle_lock.lock().await;
|
||||
if matches!(
|
||||
*manager.get_running_mode(),
|
||||
crate::core::manager::RunningMode::NotRunning
|
||||
) {
|
||||
crate::utils::resolve::dns::sync_public_dns().await;
|
||||
}
|
||||
}
|
||||
handle::Handle::notice(
|
||||
NoticeStatus::AppRestartCoreStopFailed,
|
||||
cleanup_result.stop_error.unwrap_or_default(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -103,11 +67,14 @@ pub async fn change_clash_mode(mode: String) -> Result<(), String> {
|
||||
}
|
||||
|
||||
let clash = Config::clash().await;
|
||||
clash.edit_draft(|d| d.patch_config(&mapping));
|
||||
clash.apply();
|
||||
|
||||
let clash_data = clash.data_arc();
|
||||
if clash_data.save_config().await.is_ok() {
|
||||
// Serialize with patch transactions: an unlocked commit can flush a staged draft mid-rollback.
|
||||
let save_result = {
|
||||
let _config_write = Config::lock_config_write().await;
|
||||
clash.edit_draft(|d| d.patch_config(&mapping));
|
||||
clash.apply();
|
||||
clash.data_arc().save_config().await
|
||||
};
|
||||
if save_result.is_ok() {
|
||||
announce(Refresh::Clash);
|
||||
tray::Tray::global().update_menu_and_icon().await;
|
||||
}
|
||||
|
||||
@@ -47,26 +47,11 @@ pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) {
|
||||
record_switched_node(group_name, proxy_name).await;
|
||||
announce(Refresh::Proxies);
|
||||
let _ = tray::Tray::global().update_menu().await;
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Tray, "切换代理失败: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
match handle::Handle::mihomo()
|
||||
.select_node_for_group(group_name, proxy_name)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Tray, "代理切换回退成功: {} -> {}", group_name, proxy_name);
|
||||
record_switched_node(group_name, proxy_name).await;
|
||||
let _ = tray::Tray::global().update_menu().await;
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Tray, "代理切换最终失败: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result<Option<(String, Option<PrfOption>)>> {
|
||||
|
||||
@@ -130,9 +130,7 @@ pub async fn open_or_close_dashboard() {
|
||||
logging!(info, Type::Window, "Window toggle result: {result:?}");
|
||||
}
|
||||
|
||||
pub async fn quit() {
|
||||
logging!(debug, Type::System, "启动退出流程");
|
||||
// 设置退出标志
|
||||
pub(crate) async fn run_exit_cleanup_flow(stop_failed_notice: NoticeStatus) -> CleanupResult {
|
||||
handle::Handle::global().set_is_exiting();
|
||||
|
||||
Config::apply_all_and_save_file().await;
|
||||
@@ -162,11 +160,18 @@ pub async fn quit() {
|
||||
}
|
||||
}
|
||||
handle::Handle::notice(
|
||||
NoticeStatus::AppQuitCoreStopFailed,
|
||||
cleanup_result.stop_error.unwrap_or_default(),
|
||||
stop_failed_notice,
|
||||
cleanup_result.stop_error.clone().unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
cleanup_result
|
||||
}
|
||||
|
||||
pub async fn quit() {
|
||||
logging!(debug, Type::System, "启动退出流程");
|
||||
let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppQuitCoreStopFailed).await;
|
||||
|
||||
utils::server::shutdown_embedded_server();
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
app_handle.exit(if cleanup_result.all_success { 0 } else { 1 });
|
||||
|
||||
@@ -354,7 +354,6 @@ pub(super) async fn init_dns_config() -> Result<()> {
|
||||
Value::String("*.arpa".into()),
|
||||
Value::String("time.*.com".into()),
|
||||
Value::String("ntp.*.com".into()),
|
||||
Value::String("time.*.com".into()),
|
||||
Value::String("+.market.xiaomi.com".into()),
|
||||
Value::String("localhost.ptlogin2.qq.com".into()),
|
||||
Value::String("*.msftncsi.com".into()),
|
||||
@@ -509,11 +508,9 @@ pub async fn init_resources() -> Result<()> {
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let res_dir = dirs::app_resources_dir()?;
|
||||
|
||||
if !app_dir.exists() {
|
||||
std::mem::drop(fs::create_dir_all(&app_dir).await);
|
||||
}
|
||||
// app_home is created by `ensure_directories` in `init_config`, which always runs first.
|
||||
if !res_dir.exists() {
|
||||
std::mem::drop(fs::create_dir_all(&res_dir).await);
|
||||
fs::create_dir_all(&res_dir).await?;
|
||||
}
|
||||
|
||||
let file_list = ["Country.mmdb", "ASN.mmdb", "geoip.dat", "geosite.dat"];
|
||||
|
||||
@@ -48,6 +48,17 @@ enum TlsRootMode {
|
||||
StaticWebpkiRoots,
|
||||
}
|
||||
|
||||
/// ALPN stays unset: raw TLS connectors offer none, while reqwest needs the h2 offer.
|
||||
pub(crate) fn build_webpki_rooted_tls_config() -> Result<rustls::ClientConfig> {
|
||||
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
|
||||
.with_safe_default_protocol_versions()?
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub struct NetworkManager;
|
||||
|
||||
impl NetworkManager {
|
||||
@@ -102,13 +113,7 @@ impl NetworkManager {
|
||||
}
|
||||
|
||||
fn build_static_webpki_tls_config() -> Result<rustls::ClientConfig> {
|
||||
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
let mut config =
|
||||
rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
|
||||
.with_safe_default_protocol_versions()?
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
|
||||
let mut config = build_webpki_rooted_tls_config()?;
|
||||
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
||||
|
||||
Ok(config)
|
||||
@@ -155,6 +160,23 @@ impl NetworkManager {
|
||||
detail.contains("protocolversion") || detail.contains("protocol version")
|
||||
}
|
||||
|
||||
/// `allow_fallback` stays false when the caller already disabled certificate verification.
|
||||
async fn request_with_static_webpki_fallback<T, F, Fut>(allow_fallback: bool, mut request: F) -> Result<T>
|
||||
where
|
||||
F: FnMut(TlsRootMode) -> Fut,
|
||||
Fut: std::future::Future<Output = Result<T>>,
|
||||
{
|
||||
match request(TlsRootMode::PlatformVerifier).await {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) if allow_fallback && Self::should_retry_with_static_webpki_roots(&err) => {
|
||||
request(TlsRootMode::StaticWebpkiRoots).await.map_err(|fallback_err| {
|
||||
fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed")
|
||||
})
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_with_tls_mode(
|
||||
&self,
|
||||
url: &str,
|
||||
@@ -264,34 +286,21 @@ impl NetworkManager {
|
||||
user_agent: Option<String>,
|
||||
accept_invalid_certs: bool,
|
||||
) -> Result<HttpResponse> {
|
||||
let platform_result = self
|
||||
.get_with_tls_mode(
|
||||
url,
|
||||
proxy_type,
|
||||
timeout_secs,
|
||||
user_agent.clone(),
|
||||
accept_invalid_certs,
|
||||
TlsRootMode::PlatformVerifier,
|
||||
)
|
||||
.await;
|
||||
|
||||
match platform_result {
|
||||
Ok(response) => Ok(response),
|
||||
Err(err) if !accept_invalid_certs && Self::should_retry_with_static_webpki_roots(&err) => self
|
||||
.get_with_tls_mode(
|
||||
Self::request_with_static_webpki_fallback(!accept_invalid_certs, |tls_root_mode| {
|
||||
let user_agent = user_agent.clone();
|
||||
async move {
|
||||
self.get_with_tls_mode(
|
||||
url,
|
||||
proxy_type,
|
||||
timeout_secs,
|
||||
user_agent,
|
||||
accept_invalid_certs,
|
||||
TlsRootMode::StaticWebpkiRoots,
|
||||
tls_root_mode,
|
||||
)
|
||||
.await
|
||||
.map_err(|fallback_err| {
|
||||
fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed")
|
||||
}),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_bytes_with_tls_mode(
|
||||
@@ -340,19 +349,10 @@ impl NetworkManager {
|
||||
timeout_secs: Option<u64>,
|
||||
max_bytes: usize,
|
||||
) -> Result<Vec<u8>> {
|
||||
let platform_result = self
|
||||
.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, TlsRootMode::PlatformVerifier)
|
||||
.await;
|
||||
|
||||
match platform_result {
|
||||
Ok(body) => Ok(body),
|
||||
Err(err) if Self::should_retry_with_static_webpki_roots(&err) => self
|
||||
.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, TlsRootMode::StaticWebpkiRoots)
|
||||
Self::request_with_static_webpki_fallback(true, |tls_root_mode| async move {
|
||||
self.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, tls_root_mode)
|
||||
.await
|
||||
.map_err(|fallback_err| {
|
||||
fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed")
|
||||
}),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::core::notify::NoticeStatus;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::{
|
||||
@@ -97,18 +95,6 @@ async fn resolve_setup() {
|
||||
resolve_done();
|
||||
}
|
||||
|
||||
pub async fn resolve_reset_async() -> Result<(), anyhow::Error> {
|
||||
CoreManager::global().stop_core().await?;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use dns::restore_public_dns;
|
||||
restore_public_dns().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_scheme() {
|
||||
logging_error!(Type::Setup, init::init_scheme());
|
||||
}
|
||||
|
||||
@@ -4,10 +4,19 @@ import {
|
||||
VisibilityOffOutlined,
|
||||
VisibilityOutlined,
|
||||
} from '@mui/icons-material'
|
||||
import { Box, Button, IconButton, Skeleton, Typography } from '@mui/material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
Skeleton,
|
||||
Switch,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { useEffect } from 'foxact/use-abortable-effect'
|
||||
import { useIntersection } from 'foxact/use-intersection'
|
||||
import { useLocalStorage } from 'foxact/use-local-storage'
|
||||
import type { XOR } from 'foxts/ts-xor'
|
||||
import {
|
||||
forwardRef,
|
||||
@@ -99,9 +108,13 @@ const IPInfoCardContainer = forwardRef<HTMLElement, React.PropsWithChildren>(
|
||||
export const IpInfoCard = () => {
|
||||
const { t } = useTranslation()
|
||||
const [showIp, setShowIp] = useState(false)
|
||||
const [autoRefresh, setAutoRefresh] = useLocalStorage(
|
||||
'ip-info-auto-refresh',
|
||||
true,
|
||||
)
|
||||
const appWindow = useMemo(() => getCurrentWebviewWindow(), [])
|
||||
|
||||
// Once intersected, refreshes stay enabled until unmount.
|
||||
// Once intersected, the card stays eligible for refresh until unmount.
|
||||
const [containerRef, hasIntersected, _resetIntersected] = useIntersection({
|
||||
rootMargin: '0px',
|
||||
})
|
||||
@@ -151,6 +164,8 @@ export const IpInfoCard = () => {
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefresh) return
|
||||
|
||||
let timer: number | null = null
|
||||
|
||||
// Prefer DOM intersection state; Tauri visibility is unreliable on some platforms.
|
||||
@@ -190,7 +205,7 @@ export const IpInfoCard = () => {
|
||||
if (timer != null) clearInterval(timer)
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||
}
|
||||
}, [hasIntersected])
|
||||
}, [autoRefresh, hasIntersected])
|
||||
|
||||
const toggleShowIp = useCallback(() => {
|
||||
setShowIp((prev) => !prev)
|
||||
@@ -364,12 +379,25 @@ export const IpInfoCard = () => {
|
||||
fontSize: '0.7rem',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption">
|
||||
{t('home.components.ipInfo.labels.autoRefresh')}
|
||||
{countdown.type === 'countdown'
|
||||
? `: ${countdown.remainingSeconds}s`
|
||||
: '...'}
|
||||
</Typography>
|
||||
<FormControlLabel
|
||||
sx={{ m: 0 }}
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
checked={autoRefresh}
|
||||
onChange={(_, checked) => setAutoRefresh(checked)}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="caption">
|
||||
{t('home.components.ipInfo.labels.autoRefresh')}
|
||||
{autoRefresh &&
|
||||
(countdown.type === 'countdown'
|
||||
? `: ${countdown.remainingSeconds}s`
|
||||
: '...')}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
|
||||
@@ -608,7 +608,7 @@ function NormalProxyGroups(props: { mode: string }) {
|
||||
/>
|
||||
</DragDropProvider>
|
||||
|
||||
{mode === 'rule' && (
|
||||
{(mode === 'rule' || mode === 'global') && (
|
||||
<ProxyGroupNavigator
|
||||
proxyGroupNames={proxyGroupNames}
|
||||
onGroupLocation={handleGroupLocationByName}
|
||||
|
||||
@@ -211,7 +211,7 @@ export const useRenderList = (
|
||||
if (!proxyView) return []
|
||||
if (isChainMode)
|
||||
return selectedGroup ? [selectedGroup] : [CHAIN_DELAY_GROUP]
|
||||
return mode === 'rule' || mode === 'script'
|
||||
return mode === 'rule' || mode === 'global'
|
||||
? proxyView.groups.map(({ name }) => name)
|
||||
: proxyView.global
|
||||
? [proxyView.global.name]
|
||||
@@ -257,7 +257,7 @@ export const useRenderList = (
|
||||
}))
|
||||
}
|
||||
|
||||
const useRule = mode === 'rule' || mode === 'script'
|
||||
const useRule = mode === 'rule' || mode === 'global'
|
||||
const renderGroups = useRule
|
||||
? proxyView.groups
|
||||
: proxyView.global === null
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Button, ButtonGroup } from '@mui/material'
|
||||
|
||||
interface Props {
|
||||
value?: string
|
||||
onChange?: (value: string) => void
|
||||
}
|
||||
|
||||
export const DnsModeSwitch = (props: Props) => {
|
||||
const { value, onChange } = props
|
||||
|
||||
return (
|
||||
<ButtonGroup size="small" sx={{ my: '4px' }}>
|
||||
<Button
|
||||
variant={!value ? 'contained' : 'outlined'}
|
||||
onClick={() => onChange?.('')}
|
||||
sx={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
Auto
|
||||
</Button>
|
||||
<Button
|
||||
variant={value === 'disabled' ? 'contained' : 'outlined'}
|
||||
onClick={() => onChange?.('disabled')}
|
||||
sx={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button
|
||||
variant={value === 'native' ? 'contained' : 'outlined'}
|
||||
onClick={() => onChange?.('native')}
|
||||
sx={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
Native
|
||||
</Button>
|
||||
<Button
|
||||
variant={value === 'hijack' ? 'contained' : 'outlined'}
|
||||
onClick={() => onChange?.('hijack')}
|
||||
sx={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
Hijack
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import { showNotice } from '@/services/notice-service'
|
||||
import getSystem from '@/utils/get-system'
|
||||
import { areValidIpCidrs } from '@/utils/network'
|
||||
|
||||
import { DnsModeSwitch } from './dns-mode-switch'
|
||||
import { StackModeSwitch } from './stack-mode-switch'
|
||||
|
||||
const OS = getSystem()
|
||||
@@ -52,6 +53,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
dnsHijack: ['any:53'],
|
||||
strictRoute: false,
|
||||
mtu: 1500,
|
||||
dnsMode: '',
|
||||
})
|
||||
|
||||
const routeExcludeAddressItems = splitRouteExcludeAddress(
|
||||
@@ -84,6 +86,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
dnsHijack: clash?.tun['dns-hijack'] ?? ['any:53'],
|
||||
strictRoute: clash?.tun['strict-route'] ?? false,
|
||||
mtu: clash?.tun.mtu ?? 1500,
|
||||
dnsMode: clash?.tun['dns-mode'] ?? '',
|
||||
})
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
@@ -117,6 +120,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
: {}),
|
||||
'auto-detect-interface': values.autoDetectInterface,
|
||||
'dns-hijack': values.dnsHijack[0] === '' ? [] : values.dnsHijack,
|
||||
'dns-mode': values.dnsMode,
|
||||
'strict-route': values.strictRoute,
|
||||
mtu: values.mtu ?? 1500,
|
||||
}
|
||||
@@ -163,6 +167,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
'auto-detect-interface': true,
|
||||
'dns-hijack': ['any:53'],
|
||||
'route-exclude-address': [],
|
||||
'dns-mode': '',
|
||||
'strict-route': false,
|
||||
mtu: 1500,
|
||||
}
|
||||
@@ -176,6 +181,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
dnsHijack: ['any:53'],
|
||||
strictRoute: false,
|
||||
mtu: 1500,
|
||||
dnsMode: '',
|
||||
})
|
||||
await patchClash({ tun })
|
||||
await mutateClash(
|
||||
@@ -212,6 +218,25 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItemText
|
||||
primary={t('settings.modals.tun.fields.dnsMode')}
|
||||
sx={{ maxWidth: 'fit-content' }}
|
||||
/>
|
||||
<TooltipIcon
|
||||
title={t('settings.modals.tun.tooltips.dnsMode')}
|
||||
sx={{ opacity: 0.7 }}
|
||||
/>
|
||||
<Box sx={{ marginLeft: 'auto' }}>
|
||||
<DnsModeSwitch
|
||||
value={values.dnsMode}
|
||||
onChange={(value) => {
|
||||
setValues((v) => ({ ...v, dnsMode: value }))
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: '5px 2px' }}>
|
||||
<ListItemText primary={t('settings.modals.tun.fields.device')} />
|
||||
<TextField
|
||||
|
||||
@@ -495,11 +495,13 @@
|
||||
"strictRoute": "توجيه صارم",
|
||||
"autoDetectInterface": "الكشف التلقائي عن الواجهة",
|
||||
"dnsHijack": "اختطاف DNS",
|
||||
"dnsMode": "وضع DNS",
|
||||
"mtu": "وحدة الإرسال القصوى",
|
||||
"autoRedirect": "Auto Redirect"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "Please use , to separate multiple DNS servers",
|
||||
"dnsMode": "يحافظ Auto على السلوك القديم؛ يتطلب hijack تفعيل توجيه صارم على Windows أو Auto Redirect على Linux",
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"failed": "Failed",
|
||||
"risky": "غير مؤكد",
|
||||
"completed": "Completed",
|
||||
"disallowedIsp": "Disallowed ISP",
|
||||
"originalsOnly": "Originals Only",
|
||||
|
||||
@@ -495,11 +495,13 @@
|
||||
"strictRoute": "Strenges Routing",
|
||||
"autoDetectInterface": "Netzwerkschnittstelle automatisch auswählen",
|
||||
"dnsHijack": "DNS-Hijacking",
|
||||
"dnsMode": "DNS-Modus",
|
||||
"mtu": "Maximale Übertragungseinheit",
|
||||
"autoRedirect": "Automatische Umleitung"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "Mehrere DNS-Server durch Kommas trennen",
|
||||
"dnsMode": "Auto behält das alte Verhalten bei; hijack erfordert Strenges Routing unter Windows oder Automatische Umleitung unter Linux",
|
||||
"autoRedirect": "Richtet TCP-Umleitungen per nftables/iptables automatisch ein"
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"yes": "Unterstützt",
|
||||
"no": "Nicht unterstützt",
|
||||
"failed": "Test fehlgeschlagen",
|
||||
"risky": "Ungewiss",
|
||||
"completed": "Prüfung abgeschlossen",
|
||||
"disallowedIsp": "Nicht zugelassener Internetdienstanbieter",
|
||||
"originalsOnly": "Nur Original",
|
||||
|
||||
@@ -495,11 +495,13 @@
|
||||
"strictRoute": "Strict Route",
|
||||
"autoDetectInterface": "Auto Detect Interface",
|
||||
"dnsHijack": "DNS Hijack",
|
||||
"dnsMode": "DNS Mode",
|
||||
"mtu": "Max Transmission Unit",
|
||||
"autoRedirect": "Auto Redirect"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "Please use , to separate multiple DNS servers",
|
||||
"dnsMode": "Auto keeps the legacy behavior; hijack requires Strict Route on Windows or Auto Redirect on Linux",
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"failed": "Failed",
|
||||
"risky": "Risky",
|
||||
"completed": "Completed",
|
||||
"disallowedIsp": "Disallowed ISP",
|
||||
"originalsOnly": "Originals Only",
|
||||
|
||||
@@ -495,11 +495,13 @@
|
||||
"strictRoute": "Enrutamiento estricto",
|
||||
"autoDetectInterface": "Detectar automáticamente la interfaz de salida del tráfico",
|
||||
"dnsHijack": "Secuestro de DNS",
|
||||
"dnsMode": "Modo DNS",
|
||||
"mtu": "Unidad máxima de transmisión",
|
||||
"autoRedirect": "Auto Redirect"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "Please use , to separate multiple DNS servers",
|
||||
"dnsMode": "Auto mantiene el comportamiento anterior; hijack requiere Enrutamiento estricto en Windows o Auto Redirect en Linux",
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"yes": "Soportado",
|
||||
"no": "No soportado",
|
||||
"failed": "Prueba fallida",
|
||||
"risky": "Dudoso",
|
||||
"completed": "Detección completada",
|
||||
"disallowedIsp": "Proveedor de servicios de Internet no permitido",
|
||||
"originalsOnly": "Solo originales",
|
||||
|
||||
@@ -495,11 +495,13 @@
|
||||
"strictRoute": "مسیر دقیق",
|
||||
"autoDetectInterface": "تشخیص خودکار رابط",
|
||||
"dnsHijack": "ربایش DNS",
|
||||
"dnsMode": "حالت DNS",
|
||||
"mtu": "واحد حداکثر انتقال",
|
||||
"autoRedirect": "Auto Redirect"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "لطفا برای جدا کردن چندین سرور DNS از , استفاده کنید.",
|
||||
"dnsMode": "Auto رفتار قدیمی را حفظ میکند؛ hijack در ویندوز نیاز به مسیر دقیق یا در لینوکس نیاز به Auto Redirect دارد",
|
||||
"autoRedirect": "پیکربندی خودکار ریدایرکتهای TCP در nftables/iptables"
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"failed": "Failed",
|
||||
"risky": "نامشخص",
|
||||
"completed": "Completed",
|
||||
"disallowedIsp": "Disallowed ISP",
|
||||
"originalsOnly": "Originals Only",
|
||||
|
||||
@@ -495,11 +495,13 @@
|
||||
"strictRoute": "Rute Ketat",
|
||||
"autoDetectInterface": "Deteksi Antarmuka Otomatis",
|
||||
"dnsHijack": "Pembajakan DNS",
|
||||
"dnsMode": "Mode DNS",
|
||||
"mtu": "Unit Transmisi Maksimum",
|
||||
"autoRedirect": "Auto Redirect"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "Please use , to separate multiple DNS servers",
|
||||
"dnsMode": "Auto mempertahankan perilaku lama; hijack memerlukan Rute Ketat di Windows atau Auto Redirect di Linux",
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"failed": "Failed",
|
||||
"risky": "Tidak pasti",
|
||||
"completed": "Completed",
|
||||
"disallowedIsp": "Disallowed ISP",
|
||||
"originalsOnly": "Originals Only",
|
||||
|
||||
@@ -495,11 +495,13 @@
|
||||
"strictRoute": "厳格なルート",
|
||||
"autoDetectInterface": "トラフィックの出口インターフェースを自動選択",
|
||||
"dnsHijack": "DNSハイジャック",
|
||||
"dnsMode": "DNSモード",
|
||||
"mtu": "最大転送単位",
|
||||
"autoRedirect": "Auto Redirect"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "Please use , to separate multiple DNS servers",
|
||||
"dnsMode": "Auto は従来の動作を維持します。hijack には Windows で厳格なルートまたは Linux で Auto Redirect が必要です",
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"yes": "サポートする",
|
||||
"no": "サポートしない",
|
||||
"failed": "テストに失敗しました。",
|
||||
"risky": "要確認",
|
||||
"completed": "検査完了",
|
||||
"disallowedIsp": "許可されていないインターネットサービスプロバイダー",
|
||||
"originalsOnly": "オリジナルのみ",
|
||||
|
||||
@@ -495,11 +495,13 @@
|
||||
"strictRoute": "엄격 라우팅",
|
||||
"autoDetectInterface": "인터페이스 자동 감지",
|
||||
"dnsHijack": "DNS 하이재킹",
|
||||
"dnsMode": "DNS 모드",
|
||||
"mtu": "MTU",
|
||||
"autoRedirect": "Auto Redirect"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "여러 DNS 서버는 쉼표(,)로 구분하세요",
|
||||
"dnsMode": "Auto는 기존 동작을 유지합니다. hijack은 Windows에서 엄격 라우팅 또는 Linux에서 Auto Redirect가 필요합니다",
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"yes": "예",
|
||||
"no": "아니오",
|
||||
"failed": "실패",
|
||||
"risky": "확인 필요",
|
||||
"completed": "완료",
|
||||
"disallowedIsp": "허용되지 않는 ISP",
|
||||
"originalsOnly": "오리지널만",
|
||||
|
||||
@@ -495,11 +495,13 @@
|
||||
"strictRoute": "Rota Estrita",
|
||||
"autoDetectInterface": "Detectar Interface Automaticamente",
|
||||
"dnsHijack": "Sequestro de DNS",
|
||||
"dnsMode": "Modo DNS",
|
||||
"mtu": "Unidade Máxima de Transmissão",
|
||||
"autoRedirect": "Redirecionamento Automático"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "Use , para separar vários servidores DNS",
|
||||
"dnsMode": "Auto mantém o comportamento antigo; hijack requer Rota Estrita no Windows ou Redirecionamento Automático no Linux",
|
||||
"autoRedirect": "Configura automaticamente os redirecionamentos TCP do nftables/iptables"
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
"failed": "Falhou",
|
||||
"risky": "Incerto",
|
||||
"completed": "Concluído",
|
||||
"disallowedIsp": "ISP não permitido",
|
||||
"originalsOnly": "Somente originais",
|
||||
|
||||
@@ -495,11 +495,13 @@
|
||||
"strictRoute": "Строгая маршрутизация",
|
||||
"autoDetectInterface": "Автоопределение интерфейса",
|
||||
"dnsHijack": "Перехват DNS",
|
||||
"dnsMode": "Режим DNS",
|
||||
"mtu": "MTU",
|
||||
"autoRedirect": "Автоматическое перенаправление"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "Используйте запятую для разделения нескольких DNS-серверов",
|
||||
"dnsMode": "Auto сохраняет прежнее поведение; для hijack нужна Строгая маршрутизация в Windows или Автоматическое перенаправление в Linux",
|
||||
"autoRedirect": "Автоматически настраивает перенаправление TCP через nftables/iptables"
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"yes": "Да",
|
||||
"no": "Нет",
|
||||
"failed": "Ошибка",
|
||||
"risky": "Неясно",
|
||||
"completed": "Завершено",
|
||||
"disallowedIsp": "Провайдер заблокирован",
|
||||
"originalsOnly": "Только Originals",
|
||||
|
||||
@@ -495,11 +495,13 @@
|
||||
"strictRoute": "Katı Yönlendirme",
|
||||
"autoDetectInterface": "Arayüzü Otomatik Algıla",
|
||||
"dnsHijack": "DNS Ele Geçirme",
|
||||
"dnsMode": "DNS Modu",
|
||||
"mtu": "Maksimum İletim Birimi",
|
||||
"autoRedirect": "Otomatik Yönlendirme"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "Birden fazla DNS sunucusunu ayırmak için lütfen , kullanın",
|
||||
"dnsMode": "Auto eski davranışı korur; hijack için Windows'ta Katı Yönlendirme veya Linux'ta Otomatik Yönlendirme gerekir",
|
||||
"autoRedirect": "nftables/iptables TCP yönlendirmelerini otomatik olarak yapılandırır"
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"yes": "Evet",
|
||||
"no": "Hayır",
|
||||
"failed": "Başarısız",
|
||||
"risky": "Belirsiz",
|
||||
"completed": "Tamamlandı",
|
||||
"disallowedIsp": "İzin Verilmeyen ISP",
|
||||
"originalsOnly": "Yalnızca Orijinaller",
|
||||
|
||||
@@ -495,11 +495,13 @@
|
||||
"strictRoute": "Катгый маршрутлау",
|
||||
"autoDetectInterface": "Интерфейсны автоматик ачыклау",
|
||||
"dnsHijack": "DNS'ны үзгәртеп тоту (hijack)",
|
||||
"dnsMode": "DNS режиме",
|
||||
"mtu": "MTU (макс. тапшыру берәмлеге)",
|
||||
"autoRedirect": "Auto Redirect"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "Please use , to separate multiple DNS servers",
|
||||
"dnsMode": "Auto иске тәртипне саклый; hijack өчен Windows'ta Катгый маршрутлау яки Linux'ta Auto Redirect кирәк",
|
||||
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"failed": "Failed",
|
||||
"risky": "Билгесез",
|
||||
"completed": "Completed",
|
||||
"disallowedIsp": "Disallowed ISP",
|
||||
"originalsOnly": "Originals Only",
|
||||
|
||||
@@ -495,11 +495,13 @@
|
||||
"strictRoute": "严格路由",
|
||||
"autoDetectInterface": "自动选择流量出口接口",
|
||||
"dnsHijack": "DNS 劫持",
|
||||
"dnsMode": "DNS 模式",
|
||||
"mtu": "最大传输单元",
|
||||
"autoRedirect": "自动重定向"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "多个 DNS 服务器请使用 , 分隔",
|
||||
"dnsMode": "Auto 保持旧行为;hijack 需 Windows 开启严格路由或 Linux 开启自动重定向",
|
||||
"autoRedirect": "自动配置 nftables/iptables 的 TCP 重定向"
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"yes": "支持",
|
||||
"no": "不支持",
|
||||
"failed": "测试失败",
|
||||
"risky": "存疑",
|
||||
"completed": "检测完成",
|
||||
"disallowedIsp": "不允许的 ISP",
|
||||
"originalsOnly": "仅限原创",
|
||||
|
||||
@@ -495,11 +495,13 @@
|
||||
"strictRoute": "嚴格路由",
|
||||
"autoDetectInterface": "自動偵測流量輸出介面",
|
||||
"dnsHijack": "DNS 綁架",
|
||||
"dnsMode": "DNS 模式",
|
||||
"mtu": "最大傳輸單位",
|
||||
"autoRedirect": "自動重導"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "請用半形逗號來區隔多個 DNS 伺服器",
|
||||
"dnsMode": "Auto 保持舊行為;hijack 需 Windows 開啟嚴格路由或 Linux 開啟自動重導",
|
||||
"autoRedirect": "自動配置 nftables/iptables 的 TCP 重導"
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"yes": "支援",
|
||||
"no": "不支援",
|
||||
"failed": "測試失敗",
|
||||
"risky": "存疑",
|
||||
"completed": "檢測完成",
|
||||
"disallowedIsp": "不允許的網際網路服務供應商",
|
||||
"originalsOnly": "僅限原創",
|
||||
|
||||
@@ -55,7 +55,7 @@ const wrapCssInjectionWithScope = (css?: string) => {
|
||||
return null
|
||||
}
|
||||
const scopeRoot = CSS_INJECTION_SCOPE_ROOT
|
||||
const scopeLimit = CSS_INJECTION_SCOPE_LIMIT
|
||||
const scopeLimit = `${CSS_INJECTION_SCOPE_LIMIT} > *`
|
||||
const scopedBlock = `@scope (${scopeRoot}) to (${scopeLimit}) {
|
||||
${css}
|
||||
}`
|
||||
|
||||
@@ -40,6 +40,7 @@ const STATUS_LABEL_KEYS: Record<string, string> = {
|
||||
Yes: 'tests.statuses.test.yes',
|
||||
No: 'tests.statuses.test.no',
|
||||
Failed: 'tests.statuses.test.failed',
|
||||
Risky: 'tests.statuses.test.risky',
|
||||
Completed: 'tests.statuses.test.completed',
|
||||
'Disallowed ISP': 'tests.statuses.test.disallowedIsp',
|
||||
'Originals Only': 'tests.statuses.test.originalsOnly',
|
||||
@@ -196,7 +197,7 @@ const UnlockPage = () => {
|
||||
if (status === 'Pending') return 'default'
|
||||
if (status === 'Yes') return 'success'
|
||||
if (status === 'No') return 'error'
|
||||
if (status === 'Soon') return 'warning'
|
||||
if (status === 'Soon' || status === 'Risky') return 'warning'
|
||||
if (status.includes('Failed')) return 'error'
|
||||
if (status === 'Completed') return 'info'
|
||||
if (
|
||||
@@ -222,7 +223,8 @@ const UnlockPage = () => {
|
||||
const getStatusBorderColor = (status: string) => {
|
||||
if (status === 'Yes') return theme.palette.success.main
|
||||
if (status === 'No') return theme.palette.error.main
|
||||
if (status === 'Soon') return theme.palette.warning.main
|
||||
if (status === 'Soon' || status === 'Risky')
|
||||
return theme.palette.warning.main
|
||||
if (status.includes('Failed')) return theme.palette.error.main
|
||||
if (status === 'Completed') return theme.palette.info.main
|
||||
return theme.palette.divider
|
||||
|
||||
@@ -704,9 +704,11 @@ export const translationKeys = [
|
||||
'settings.modals.tun.fields.strictRoute',
|
||||
'settings.modals.tun.fields.autoDetectInterface',
|
||||
'settings.modals.tun.fields.dnsHijack',
|
||||
'settings.modals.tun.fields.dnsMode',
|
||||
'settings.modals.tun.fields.mtu',
|
||||
'settings.modals.tun.fields.autoRedirect',
|
||||
'settings.modals.tun.tooltips.dnsHijack',
|
||||
'settings.modals.tun.tooltips.dnsMode',
|
||||
'settings.modals.tun.tooltips.autoRedirect',
|
||||
'settings.modals.tun.messages.applied',
|
||||
'settings.modals.tun.messages.invalidRouteExcludeAddress',
|
||||
|
||||
@@ -1190,6 +1190,7 @@ export interface TranslationResources {
|
||||
autoRoute: string
|
||||
device: string
|
||||
dnsHijack: string
|
||||
dnsMode: string
|
||||
mtu: string
|
||||
routeExcludeAddress: string
|
||||
stack: string
|
||||
@@ -1204,6 +1205,7 @@ export interface TranslationResources {
|
||||
tooltips: {
|
||||
autoRedirect: string
|
||||
dnsHijack: string
|
||||
dnsMode: string
|
||||
}
|
||||
}
|
||||
update: {
|
||||
|
||||
Vendored
+1
@@ -40,6 +40,7 @@ interface IConfigData {
|
||||
'auto-redirect'?: boolean
|
||||
'auto-detect-interface': boolean
|
||||
'dns-hijack': string[]
|
||||
'dns-mode'?: string
|
||||
'route-exclude-address'?: string[]
|
||||
'strict-route': boolean
|
||||
mtu: number
|
||||
|
||||
Reference in New Issue
Block a user