mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-27 08:02:09 +08:00
Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
010c037dde | ||
|
|
e017d4d9d4 | ||
|
|
10ca0a5d0c | ||
|
|
324c8c9016 | ||
|
|
02520d7392 | ||
|
|
5b8edbaead | ||
|
|
1ed8050947 | ||
|
|
3d9a79a3fb | ||
|
|
8b88ddb7f2 | ||
|
|
02bd739ab6 | ||
|
|
b6c7af82a7 | ||
|
|
5e6a696087 | ||
|
|
27d8a71d41 | ||
|
|
9dd5aa676b | ||
|
|
4e5ac8683b | ||
|
|
7965711945 | ||
|
|
396d343a3a | ||
|
|
b729e59650 | ||
|
|
a3cc58013b | ||
|
|
cef66cd745 | ||
|
|
3b7bfa10fd | ||
|
|
b238175f18 | ||
|
|
8f00096a9f | ||
|
|
9f498a85d0 | ||
|
|
e014b913d2 | ||
|
|
f38ceae3fb | ||
|
|
089635c8bf | ||
|
|
dc5a8a5ff4 | ||
|
|
476ddb63c3 | ||
|
|
c6b51171a1 | ||
|
|
d4287970bc | ||
|
|
b20fdc9b96 | ||
|
|
5b10331022 | ||
|
|
3ec858eb9f |
@@ -4,11 +4,11 @@
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:2": {
|
||||
"version": "24.21.0",
|
||||
"pnpmVersion": "12.11.2"
|
||||
"version": "24.22.1",
|
||||
"pnpmVersion": "12.15.0"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "1.99.0",
|
||||
"version": "1.99.1",
|
||||
"profile": "default"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/git: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.21.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -206,7 +206,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -282,7 +282,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -347,7 +347,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -426,7 +426,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -454,7 +454,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -524,7 +524,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6.1.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
|
||||
- name: Install Rust Stable
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: dtolnay/rust-toolchain@1.99.0
|
||||
uses: dtolnay/rust-toolchain@1.99.1
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Pnpm Cache
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
run: |
|
||||
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
- uses: actions/setup-node@v7
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Restore pnpm cache
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
- uses: actions/setup-node@v7
|
||||
if: steps.check_inputs.outputs.contract == 'true'
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.1'
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -244,7 +244,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -311,7 +311,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -331,7 +331,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
@@ -389,7 +389,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -446,7 +446,7 @@ jobs:
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Add Rust Target
|
||||
@@ -466,7 +466,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
@@ -487,7 +487,7 @@ jobs:
|
||||
|
||||
- name: Add Rust Target
|
||||
run: |
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
|
||||
rustup target list --installed
|
||||
echo "Rust target ${{ matrix.target }} installed."
|
||||
|
||||
@@ -547,7 +547,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.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.21.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.21.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
@@ -22,13 +22,13 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.0'
|
||||
toolchain: '1.99.1'
|
||||
targets: x86_64-pc-windows-msvc
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.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.21.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.21.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.21.0'
|
||||
node-version: '24.22.1'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
name: Install pnpm
|
||||
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
nodejs 24.21.0
|
||||
pnpm 12.11.2
|
||||
rust 1.99.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
+116
-204
@@ -1119,7 +1119,7 @@ dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"libc",
|
||||
"log",
|
||||
"minisign-verify 0.3.0",
|
||||
"minisign-verify",
|
||||
"nanoid",
|
||||
"network-interface",
|
||||
"objc2",
|
||||
@@ -1138,8 +1138,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml_ng",
|
||||
"sha2 0.11.0",
|
||||
"smartstring",
|
||||
"sha2 0.11.1",
|
||||
"socket2",
|
||||
"specta",
|
||||
"specta-typescript",
|
||||
@@ -1169,13 +1168,14 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tristr",
|
||||
"warp",
|
||||
"webpki-roots",
|
||||
"windows 0.62.2",
|
||||
"windows-service",
|
||||
"windows-sys 0.61.2",
|
||||
"winreg 0.56.0",
|
||||
"zip 9.0.1",
|
||||
"zip 9.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1247,7 +1247,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"sha2 0.11.1",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"sysproxy",
|
||||
@@ -1462,9 +1462,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.4.2"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||
checksum = "9eb6feb175df65d0a3ccd3e73484e940d0ac664e8034f0519fd49a93db3071d9"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
@@ -3951,9 +3954,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
version = "0.2.191"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
@@ -4048,11 +4051,11 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rust2"
|
||||
version = "0.21.0"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fde178a3caf126c440fa15628147772d8ee590a39477d711353fbe2e58a73a5b"
|
||||
checksum = "7277c9742229dc25243236d578b6471a798c554512c6f6a04365ec2e87c2a3bb"
|
||||
dependencies = [
|
||||
"sha2 0.11.0",
|
||||
"sha2 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4167,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"
|
||||
@@ -5343,9 +5340,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppmd-rust"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
|
||||
checksum = "196a7c80b9a7652aba7cc070827516c2abe4ccdf53d128e1944003cf5726cff1"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
@@ -5996,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]]
|
||||
@@ -6067,9 +6065,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.45"
|
||||
version = "0.23.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
@@ -6410,9 +6408,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
version = "1.0.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -6597,9 +6595,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
||||
checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
@@ -6622,7 +6620,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"sigchld",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
@@ -6642,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"
|
||||
@@ -6663,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"
|
||||
@@ -6744,18 +6720,6 @@ dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.5"
|
||||
@@ -6878,6 +6842,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"
|
||||
@@ -6937,9 +6907,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",
|
||||
@@ -7186,9 +7156,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",
|
||||
@@ -7234,16 +7204,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",
|
||||
@@ -7255,6 +7225,8 @@ dependencies = [
|
||||
"semver 1.0.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"static_vcruntime",
|
||||
"strsim",
|
||||
"tauri-utils",
|
||||
"tauri-winres",
|
||||
"walkdir",
|
||||
@@ -7262,11 +7234,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",
|
||||
@@ -7289,9 +7261,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",
|
||||
@@ -7347,9 +7319,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",
|
||||
@@ -7362,9 +7334,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",
|
||||
@@ -7410,9 +7382,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",
|
||||
@@ -7428,9 +7400,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",
|
||||
@@ -7452,9 +7424,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",
|
||||
@@ -7467,9 +7439,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",
|
||||
@@ -7486,13 +7458,13 @@ dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
"tokio",
|
||||
"url",
|
||||
"urlpattern 0.3.0",
|
||||
"urlpattern",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-mihomo"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?branch=main#7a03d16f4d39b1bb885b7babe66b9e153e34b615"
|
||||
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?rev=7a03d16f4d39b1bb885b7babe66b9e153e34b615#7a03d16f4d39b1bb885b7babe66b9e153e34b615"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"clashmap",
|
||||
@@ -7516,9 +7488,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",
|
||||
@@ -7531,13 +7503,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",
|
||||
@@ -7567,11 +7540,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",
|
||||
@@ -7584,13 +7558,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",
|
||||
@@ -7598,8 +7573,9 @@ dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"libc",
|
||||
"log",
|
||||
"minisign-verify 0.2.5",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest 0.13.5",
|
||||
@@ -7621,9 +7597,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",
|
||||
@@ -7647,9 +7623,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]]
|
||||
@@ -7669,9 +7645,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",
|
||||
@@ -7688,15 +7664,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",
|
||||
@@ -7714,7 +7690,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com 0.39.1",
|
||||
"webview2-com",
|
||||
"windows 0.62.2",
|
||||
"wry",
|
||||
]
|
||||
@@ -7752,9 +7728,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",
|
||||
@@ -7783,7 +7759,7 @@ dependencies = [
|
||||
"thiserror 2.0.20",
|
||||
"toml 1.1.6+spec-1.1.0",
|
||||
"url",
|
||||
"urlpattern 0.6.0",
|
||||
"urlpattern",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
]
|
||||
@@ -7902,7 +7878,7 @@ dependencies = [
|
||||
"regex",
|
||||
"semver 0.11.0",
|
||||
"sha2 0.9.9",
|
||||
"signal-hook 0.1.17",
|
||||
"signal-hook",
|
||||
"terminfo",
|
||||
"termios",
|
||||
"thiserror 1.0.69",
|
||||
@@ -8073,9 +8049,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.2"
|
||||
version = "1.53.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
|
||||
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -8135,9 +8111,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.19"
|
||||
version = "0.7.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
|
||||
checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -8500,6 +8476,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"
|
||||
@@ -8511,6 +8493,14 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tristr"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"specta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
@@ -8592,47 +8582,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"
|
||||
@@ -8692,18 +8641,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"
|
||||
@@ -9095,20 +9032,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"
|
||||
@@ -9116,7 +9039,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",
|
||||
]
|
||||
@@ -9132,17 +9055,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"
|
||||
@@ -9882,7 +9794,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"webview2-com 0.39.1",
|
||||
"webview2-com",
|
||||
"windows 0.62.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows-version",
|
||||
@@ -10150,9 +10062,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "9.0.1"
|
||||
version = "9.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e49ade69c4434c55dd651d36fe05f38e0578db948b773cbc01c48c4b5e560692"
|
||||
checksum = "a954114b47a54df49f83d2722814e86b23d4bb1b54a1b90fb7d92b41619ddc22"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bzip2",
|
||||
@@ -10202,27 +10114,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
checksum = "db3ae9bf90af6e40197bef767eeefc46ad6397420fe873830bd85e5fbbfb3c70"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
checksum = "ded520f16b29e1a9b771c722221b2b0a0398c2316d9bb3c42b458a5414d7434b"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
version = "2.1.2+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
|
||||
+5
-6
@@ -61,13 +61,13 @@ 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"
|
||||
criterion = { version = "0.8.2", features = ["async_tokio"] }
|
||||
tokio = { version = "1.53.2", features = [
|
||||
tokio = { version = "1.53.3", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
@@ -85,10 +85,9 @@ reqwest = { version = "0.13.5", features = [
|
||||
] }
|
||||
rust_iso3166 = "0.2.0"
|
||||
|
||||
smartstring = { version = "1.0.1" }
|
||||
|
||||
serde = { version = "1.0.229" }
|
||||
serde_json = { version = "1.0.151" }
|
||||
tristr = { path = "../tristr", default-features = false, features = ["serde", "specta"] }
|
||||
serde_json = { version = "1.0.154" }
|
||||
serde_yaml_ng = { version = "0.10.0" }
|
||||
bitflags = { version = "2.13.2" }
|
||||
socket2 = "0.6.5"
|
||||
|
||||
+10
-2
@@ -3,20 +3,28 @@
|
||||
<details>
|
||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||
|
||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置的问题
|
||||
- 修复「首页」网站测速失败后卡片一直显示测试中的问题
|
||||
- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口
|
||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
|
||||
- 修复 首页 网站测速失败后卡片一直显示测试中
|
||||
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
|
||||
|
||||
**🐧 Linux**
|
||||
|
||||
- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 优化「链式代理」:开启前需确认网速影响提示,空面板新增「关闭」按钮
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: عام
|
||||
profiles: الملفات التعريفية
|
||||
proxies: وكلاء
|
||||
testLatency: اختبار التأخير
|
||||
testingLatency: جارٍ اختبار التأخير…
|
||||
systemProxy: وكيل النظام
|
||||
tunMode: وضع TUN
|
||||
closeAllConnections: إغلاق كل الاتصالات
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Global
|
||||
profiles: Profile
|
||||
proxies: Proxy
|
||||
testLatency: Latenz testen
|
||||
testingLatency: Latenz wird getestet…
|
||||
systemProxy: Systemproxy
|
||||
tunMode: TUN-Modus
|
||||
closeAllConnections: Alle Verbindungen schließen
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Global
|
||||
profiles: Profiles
|
||||
proxies: Proxies
|
||||
testLatency: Test Latency
|
||||
testingLatency: Testing latency…
|
||||
systemProxy: System Proxy
|
||||
tunMode: TUN Mode
|
||||
closeAllConnections: Close All Connections
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Global
|
||||
profiles: Perfiles
|
||||
proxies: Proxies
|
||||
testLatency: Probar latencia
|
||||
testingLatency: Probando latencia…
|
||||
systemProxy: Proxy del sistema
|
||||
tunMode: Modo TUN
|
||||
closeAllConnections: Cerrar todas las conexiones
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: سراسری
|
||||
profiles: پروفایلها
|
||||
proxies: پروکسیها
|
||||
testLatency: آزمایش تأخیر
|
||||
testingLatency: در حال آزمایش تأخیر…
|
||||
systemProxy: پروکسی سیستم
|
||||
tunMode: حالت TUN
|
||||
closeAllConnections: بستن همه اتصالها
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Global
|
||||
profiles: Profil
|
||||
proxies: Proksi
|
||||
testLatency: Uji Latensi
|
||||
testingLatency: Menguji latensi…
|
||||
systemProxy: Proksi Sistem
|
||||
tunMode: Mode TUN
|
||||
closeAllConnections: Tutup Semua Koneksi
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: グローバル
|
||||
profiles: プロファイル
|
||||
proxies: プロキシ
|
||||
testLatency: 遅延テスト
|
||||
testingLatency: 遅延をテスト中…
|
||||
systemProxy: システムプロキシ
|
||||
tunMode: TUN モード
|
||||
closeAllConnections: すべての接続を閉じる
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: 글로벌
|
||||
profiles: 프로필
|
||||
proxies: 프록시
|
||||
testLatency: 지연 테스트
|
||||
testingLatency: 지연 테스트 중…
|
||||
systemProxy: 시스템 프록시
|
||||
tunMode: TUN 모드
|
||||
closeAllConnections: 모든 연결 닫기
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Global
|
||||
profiles: Perfis
|
||||
proxies: Proxies
|
||||
testLatency: Test Latency
|
||||
testingLatency: Testing latency…
|
||||
systemProxy: Proxy do sistema
|
||||
tunMode: Modo TUN
|
||||
closeAllConnections: Fechar todas as conexões
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Глобальный
|
||||
profiles: Профили
|
||||
proxies: Прокси
|
||||
testLatency: Проверить задержку
|
||||
testingLatency: Проверка задержки…
|
||||
systemProxy: Системный прокси
|
||||
tunMode: Режим TUN
|
||||
closeAllConnections: Закрыть все соединения
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Küresel
|
||||
profiles: Profiller
|
||||
proxies: Vekil'ler
|
||||
testLatency: Gecikmeyi Test Et
|
||||
testingLatency: Gecikme test ediliyor…
|
||||
systemProxy: Sistem Vekil'i
|
||||
tunMode: TUN Modu
|
||||
closeAllConnections: Tüm Bağlantıları Kapat
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: Глобаль
|
||||
profiles: Профильләр
|
||||
proxies: Проксилар
|
||||
testLatency: Кичектерүне тикшерү
|
||||
testingLatency: Кичектерү тикшерелә…
|
||||
systemProxy: Системалы прокси
|
||||
tunMode: TUN режимы
|
||||
closeAllConnections: Барлык тоташуларны ябу
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: 全局
|
||||
profiles: 订阅
|
||||
proxies: 代理
|
||||
testLatency: 测试延迟
|
||||
testingLatency: 正在测试延迟…
|
||||
systemProxy: 系统代理
|
||||
tunMode: TUN 模式
|
||||
closeAllConnections: 关闭所有连接
|
||||
|
||||
@@ -70,8 +70,6 @@ tray:
|
||||
global: 全域
|
||||
profiles: 訂閱
|
||||
proxies: 代理
|
||||
testLatency: 測試延遲
|
||||
testingLatency: 正在測試延遲…
|
||||
systemProxy: 系統代理
|
||||
tunMode: 虛擬網路介面卡模式
|
||||
closeAllConnections: 關閉所有連線
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
//! Linux-only termination signal bridge.
|
||||
//!
|
||||
//! Upstream Tauri delivers session-end and termination events to the app on
|
||||
//! Windows and macOS, but has no equivalent on Linux: a desktop session may
|
||||
//! end without any window or run event reaching the process. Linux builds
|
||||
//! keep this crate to listen for SIGTERM, SIGINT and SIGHUP and forward them
|
||||
//! to the app quit flow, so system proxy and TUN cleanup still runs before
|
||||
//! the process dies. Non-Linux builds compile this crate to nothing and must
|
||||
//! not depend on it.
|
||||
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use std::sync::{
|
||||
OnceLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -5,20 +17,13 @@ use std::sync::{
|
||||
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
#[cfg(unix)]
|
||||
mod unix;
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
pub(crate) static RUNTIME: OnceLock<Option<tokio::runtime::Runtime>> = OnceLock::new();
|
||||
pub(crate) static SHUTDOWN_LATCH: ShutdownLatch = ShutdownLatch::new();
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ShutdownOutcome {
|
||||
Committed,
|
||||
Canceled,
|
||||
}
|
||||
|
||||
/// Serializes signal-triggered cleanups: a signal arriving while a quit
|
||||
/// attempt is still in flight is dropped instead of overlapping it.
|
||||
pub(crate) struct ShutdownLatch {
|
||||
is_cleaning_up: AtomicBool,
|
||||
}
|
||||
@@ -36,17 +41,18 @@ impl ShutdownLatch {
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn finish(&self, outcome: ShutdownOutcome) {
|
||||
if matches!(outcome, ShutdownOutcome::Canceled) {
|
||||
self.is_cleaning_up.store(false, Ordering::Release);
|
||||
}
|
||||
fn finish(&self) {
|
||||
self.is_cleaning_up.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns the Linux signal loop on a dedicated runtime so it survives the
|
||||
/// teardown of any app-owned runtime. The handler runs once per accepted
|
||||
/// signal; the quit flow it invokes owns the exit-or-cancel decision.
|
||||
pub fn register<F, Fut>(f: F)
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
RUNTIME.get_or_init(|| match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => Some(rt),
|
||||
@@ -61,36 +67,22 @@ where
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(unix)]
|
||||
unix::register(f);
|
||||
|
||||
#[cfg(windows)]
|
||||
windows::register(f);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ShutdownLatch, ShutdownOutcome};
|
||||
use super::ShutdownLatch;
|
||||
|
||||
#[test]
|
||||
fn canceled_shutdown_releases_latch_and_permits_next_attempt() {
|
||||
fn finished_attempt_releases_latch_for_the_next_signal() {
|
||||
let latch = ShutdownLatch::new();
|
||||
|
||||
assert!(latch.try_begin());
|
||||
assert!(!latch.try_begin());
|
||||
|
||||
latch.finish(ShutdownOutcome::Canceled);
|
||||
latch.finish();
|
||||
|
||||
assert!(latch.try_begin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_shutdown_retains_latch_and_suppresses_repeats() {
|
||||
let latch = ShutdownLatch::new();
|
||||
|
||||
assert!(latch.try_begin());
|
||||
latch.finish(ShutdownOutcome::Committed);
|
||||
|
||||
assert!(!latch.try_begin());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
|
||||
use crate::{RUNTIME, SHUTDOWN_LATCH, ShutdownOutcome};
|
||||
use crate::{RUNTIME, SHUTDOWN_LATCH};
|
||||
|
||||
pub fn register<F, Fut>(f: F)
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
|
||||
Fut: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
if let Some(Some(rt)) = RUNTIME.get() {
|
||||
rt.spawn(async move {
|
||||
@@ -60,7 +60,8 @@ where
|
||||
}
|
||||
logging!(info, Type::SystemSignal, "Caught signal {}", signal_name);
|
||||
|
||||
SHUTDOWN_LATCH.finish(f().await);
|
||||
f().await;
|
||||
SHUTDOWN_LATCH.finish();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use tokio::signal::windows;
|
||||
|
||||
use crate::{RUNTIME, SHUTDOWN_LATCH, ShutdownOutcome};
|
||||
|
||||
pub fn register<F, Fut>(f: F)
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = ShutdownOutcome> + Send + 'static,
|
||||
{
|
||||
if let Some(Some(rt)) = RUNTIME.get() {
|
||||
rt.spawn(async move {
|
||||
let mut ctrl_c = match windows::ctrl_c() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+C: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut ctrl_close = match windows::ctrl_close() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Close: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut ctrl_shutdown = match windows::ctrl_shutdown() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Shutdown: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut ctrl_logoff = match windows::ctrl_logoff() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(error, Type::SystemSignal, "Failed to register Ctrl+Logoff: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
let signal_name;
|
||||
tokio::select! {
|
||||
_ = ctrl_c.recv() => {
|
||||
signal_name = "Ctrl+C";
|
||||
}
|
||||
_ = ctrl_close.recv() => {
|
||||
signal_name = "Ctrl+Close";
|
||||
}
|
||||
_ = ctrl_shutdown.recv() => {
|
||||
signal_name = "Ctrl+Shutdown";
|
||||
}
|
||||
_ = ctrl_logoff.recv() => {
|
||||
signal_name = "Ctrl+Logoff";
|
||||
}
|
||||
}
|
||||
|
||||
if !SHUTDOWN_LATCH.try_begin() {
|
||||
logging!(
|
||||
info,
|
||||
Type::SystemSignal,
|
||||
"Already shutting down, ignoring repeated signal: {}",
|
||||
signal_name
|
||||
);
|
||||
continue;
|
||||
}
|
||||
logging!(info, Type::SystemSignal, "Caught Windows signal: {}", signal_name);
|
||||
|
||||
SHUTDOWN_LATCH.finish(f().await);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logging!(
|
||||
error,
|
||||
Type::SystemSignal,
|
||||
"register shutdown signal failed, RUNTIME is not available"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ serde = { workspace = true, features = ["derive"] }
|
||||
sysinfo = { version = "0.39.6", features = ["network", "system"] }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
libc = "0.2.190"
|
||||
libc = "0.2.191"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
deelevate = { workspace = true }
|
||||
|
||||
+15
-15
@@ -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",
|
||||
@@ -75,11 +75,11 @@
|
||||
"monaco-yaml": "^5.5.1",
|
||||
"nanoid": "^6.0.1",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "19.3.0",
|
||||
"react-dom": "19.3.0",
|
||||
"react": "19.3.1",
|
||||
"react-dom": "19.3.1",
|
||||
"react-error-boundary": "6.1.6",
|
||||
"react-hook-form": "^7.88.0",
|
||||
"react-i18next": "17.0.15",
|
||||
"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,7 +123,7 @@
|
||||
"https-proxy-agent": "^9.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.7.0",
|
||||
"knip": "6.39.0",
|
||||
"knip": "6.43.0",
|
||||
"lint-staged": "^17.5.1",
|
||||
"sass": "^1.104.1",
|
||||
"tar": "^7.5.22",
|
||||
@@ -133,7 +133,7 @@
|
||||
"vite": "^8.3.0",
|
||||
"vite-plugin-svgr": "^5.2.0",
|
||||
"vitest": "^5.0.1",
|
||||
"webdriverio": "10.0.0",
|
||||
"webdriverio": "10.1.0",
|
||||
"ws": "8.22.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -146,5 +146,5 @@
|
||||
]
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@12.11.2"
|
||||
"packageManager": "pnpm@12.15.0"
|
||||
}
|
||||
|
||||
Generated
+528
-728
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,17 @@
|
||||
"matchManagers": ["npm"],
|
||||
"groupName": "npm dependencies"
|
||||
},
|
||||
{
|
||||
"description": "Update Tauri npm packages and Cargo crates together so frontend and backend stay in sync",
|
||||
"matchPackageNames": [
|
||||
"@tauri-apps/**",
|
||||
"@wdio/tauri-service",
|
||||
"tauri",
|
||||
"tauri-*"
|
||||
],
|
||||
"groupName": "tauri",
|
||||
"addLabels": ["tauri"]
|
||||
},
|
||||
{
|
||||
"description": "Group all GitHub Actions updates into a single PR",
|
||||
"matchManagers": ["github-actions"],
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "1.99.0"
|
||||
channel = "1.99.1"
|
||||
components = ["rustfmt", "clippy"]
|
||||
|
||||
@@ -22,7 +22,7 @@ try {
|
||||
'run',
|
||||
'-p',
|
||||
'clash-verge',
|
||||
'--bin',
|
||||
'--example',
|
||||
'frontend-contract',
|
||||
'--locked',
|
||||
'-q',
|
||||
|
||||
@@ -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(
|
||||
|
||||
+22
-19
@@ -29,13 +29,12 @@ 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 }
|
||||
clash-verge-media-unlock = { workspace = true }
|
||||
clash-verge-logging = { workspace = true }
|
||||
clash-verge-signal = { workspace = true }
|
||||
clash-verge-i18n = { workspace = true }
|
||||
clash-verge-limiter = { workspace = true }
|
||||
tauri-plugin-clash-verge-sysinfo = { workspace = true }
|
||||
@@ -53,9 +52,9 @@ anyhow = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tristr = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml_ng = { workspace = true }
|
||||
smartstring = { workspace = true, features = ["serde"] }
|
||||
bitflags = { workspace = true }
|
||||
socket2 = { workspace = true }
|
||||
warp = { version = "0.4.3", features = ["server"] }
|
||||
@@ -73,51 +72,55 @@ 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.1"
|
||||
zip = "9.0.4"
|
||||
flate2 = "1.1.10"
|
||||
reqwest_dav = "0.3.3"
|
||||
aes-gcm = { version = "0.11.1" }
|
||||
base64 = "0.23.1"
|
||||
minisign-verify = "0.3.0"
|
||||
sha2 = "0.11"
|
||||
sha2 = "0.11.1"
|
||||
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", branch = "main" }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
|
||||
async-trait = "0.1.92"
|
||||
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.6", version = "2.7.6", features = [
|
||||
"client",
|
||||
] }
|
||||
arc-swap = "1.9.2"
|
||||
tokio-rustls = "0.26.6"
|
||||
rustls = { version = "0.23.45", features = ["ring"] }
|
||||
rustls = { version = "0.23.46", features = ["ring"] }
|
||||
webpki-roots = "1.0.9"
|
||||
dark-light = "3.0.0"
|
||||
bytes = "1.12.1"
|
||||
tokio-util = { version = "0.7.19", features = ["time"] }
|
||||
tokio-util = { version = "0.7.20", features = ["time"] }
|
||||
specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] }
|
||||
tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
|
||||
specta-typescript = "=0.0.12"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.190"
|
||||
libc = "0.2.191"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# Upstream has no session-end signal support on Linux; see crates/clash-verge-signal.
|
||||
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",
|
||||
@@ -150,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")]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Dev-only target: Tauri ships everything under src/bin/ to /usr/bin in deb/rpm
|
||||
// bundles (PR #8131), so this generator must stay an example, never a bin.
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = std::env::args_os().skip(1);
|
||||
let output = args
|
||||
+11
-14
@@ -5,8 +5,8 @@ use crate::{
|
||||
feat,
|
||||
utils::dirs,
|
||||
};
|
||||
use smartstring::alias::String;
|
||||
use tauri::{AppHandle, Manager as _, ipc::Channel};
|
||||
use tristr::alias::String;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
@@ -59,10 +59,10 @@ pub async fn restart_app() -> CmdResult<()> {
|
||||
#[specta::specta]
|
||||
pub async fn install_update(
|
||||
app_handle: AppHandle,
|
||||
version: std::string::String,
|
||||
version: String,
|
||||
on_event: Channel<DownloadEvent>,
|
||||
) -> CmdResult<bool> {
|
||||
let version: String = version.into();
|
||||
let version: String = version;
|
||||
SilentUpdater::global()
|
||||
.install_update(&app_handle, &version, |event| {
|
||||
let _ = on_event.send(event);
|
||||
@@ -79,25 +79,22 @@ pub fn cancel_update_download() {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_app_dir() -> CmdResult<std::string::String> {
|
||||
pub fn get_app_dir() -> CmdResult<String> {
|
||||
let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into();
|
||||
Ok(app_home_dir)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn download_icon_cache(
|
||||
url: std::string::String,
|
||||
name: std::string::String,
|
||||
) -> CmdResult<std::string::String> {
|
||||
let url: String = url.into();
|
||||
let name: String = name.into();
|
||||
feat::download_icon_cache(url, name).await.map(Into::into)
|
||||
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
||||
let url: String = url;
|
||||
let name: String = name;
|
||||
feat::download_icon_cache(url, name).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn copy_icon_file(path: std::string::String, icon_info: feat::IconInfo) -> CmdResult<std::string::String> {
|
||||
let path: String = path.into();
|
||||
feat::copy_icon_file(path, icon_info).await.map(Into::into)
|
||||
pub async fn copy_icon_file(path: String, icon_info: feat::IconInfo) -> CmdResult<String> {
|
||||
let path: String = path;
|
||||
feat::copy_icon_file(path, icon_info).await
|
||||
}
|
||||
|
||||
+11
-11
@@ -1,7 +1,7 @@
|
||||
use super::CmdResult;
|
||||
use crate::{cmd::StringifyErr as _, feat};
|
||||
use feat::LocalBackupFile;
|
||||
use smartstring::alias::String;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
@@ -17,15 +17,15 @@ pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn delete_local_backup(filename: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
pub async fn delete_local_backup(filename: String) -> CmdResult<()> {
|
||||
let filename: String = filename;
|
||||
feat::delete_local_backup(filename).await.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn restore_local_backup(filename: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
|
||||
let filename: String = filename;
|
||||
feat::restore_local_backup(filename)
|
||||
.await
|
||||
.map_err(|error| super::proxy_aware_error(&error))
|
||||
@@ -33,15 +33,15 @@ pub async fn restore_local_backup(filename: std::string::String) -> CmdResult<()
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn import_local_backup(source: std::string::String) -> CmdResult<std::string::String> {
|
||||
let source: String = source.into();
|
||||
feat::import_local_backup(source).await.map(Into::into).stringify_err()
|
||||
pub async fn import_local_backup(source: String) -> CmdResult<String> {
|
||||
let source: String = source;
|
||||
feat::import_local_backup(source).await.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn export_local_backup(filename: std::string::String, destination: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
let destination: String = destination.into();
|
||||
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
|
||||
let filename: String = filename;
|
||||
let destination: String = destination;
|
||||
feat::export_local_backup(filename, destination).await.stringify_err()
|
||||
}
|
||||
|
||||
+16
-16
@@ -12,8 +12,8 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use smartstring::alias::String;
|
||||
use tokio::fs;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
@@ -37,8 +37,8 @@ pub async fn patch_clash_config(payload: crate::ipc::YamlMapping) -> CmdResult {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult {
|
||||
let payload: String = payload.into();
|
||||
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
||||
let payload: String = payload;
|
||||
feat::change_clash_mode(payload)
|
||||
.await
|
||||
.with_error_code("CLASH_MODE_UPDATE_FAILED")
|
||||
@@ -47,14 +47,14 @@ pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult {
|
||||
/// Reads the saved mode without depending on strict mihomo `/configs` deserialization.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_clash_mode() -> CmdResult<Option<std::string::String>> {
|
||||
pub async fn get_clash_mode() -> CmdResult<Option<String>> {
|
||||
Ok(Config::clash().await.data_arc().get_mode())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn change_clash_core(clash_core: std::string::String) -> CmdResult<Option<CommandFailure>> {
|
||||
let clash_core: String = clash_core.into();
|
||||
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<CommandFailure>> {
|
||||
let clash_core: String = clash_core;
|
||||
logging!(info, Type::Config, "changing core to {clash_core}");
|
||||
|
||||
match CoreManager::global().change_core(&clash_core).await {
|
||||
@@ -112,8 +112,8 @@ pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeRepor
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn test_delay(url: std::string::String) -> CmdResult<u32> {
|
||||
let url: String = url.into();
|
||||
pub async fn test_delay(url: String) -> CmdResult<u32> {
|
||||
let url: String = url;
|
||||
let result = match feat::test_delay(url).await {
|
||||
Ok(delay) => delay,
|
||||
Err(e) => {
|
||||
@@ -148,12 +148,12 @@ pub fn take_dns_override_notice() -> bool {
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_dns_override(
|
||||
profile_uid: std::string::String,
|
||||
profile_uid: String,
|
||||
enabled: bool,
|
||||
confirmation: Option<std::string::String>,
|
||||
confirmation: Option<String>,
|
||||
) -> CmdResult<feat::DnsOverrideOutcome> {
|
||||
let profile_uid: String = profile_uid.into();
|
||||
let confirmation = confirmation.map(String::from);
|
||||
let profile_uid: String = profile_uid;
|
||||
let confirmation = confirmation;
|
||||
feat::set_dns_override(profile_uid, enabled, confirmation)
|
||||
.await
|
||||
.map_err(|error| proxy_aware_coded_error(&error, "DNS_OVERRIDE_UPDATE_FAILED"))
|
||||
@@ -212,14 +212,14 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_dns_config_content() -> CmdResult<Option<std::string::String>> {
|
||||
pub async fn get_dns_config_content() -> CmdResult<Option<String>> {
|
||||
use crate::utils::dirs;
|
||||
use tokio::fs;
|
||||
|
||||
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
|
||||
|
||||
match fs::read_to_string(&dns_path).await {
|
||||
Ok(content) => Ok(Some(content)),
|
||||
Ok(content) => Ok(Some(content.into())),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(error) => Err(CommandFailure::plain(error)),
|
||||
}
|
||||
@@ -244,7 +244,7 @@ pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_clash_logs() -> CmdResult<Vec<std::string::String>> {
|
||||
pub async fn get_clash_logs() -> CmdResult<Vec<String>> {
|
||||
let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default();
|
||||
Ok(logs)
|
||||
Ok(logs.into_iter().collect())
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ pub use clash_verge_media_unlock::UnlockItem;
|
||||
use reqwest::{Client, Proxy};
|
||||
use std::time::Duration;
|
||||
use tauri::{command, ipc::Channel};
|
||||
use tristr::alias::String;
|
||||
|
||||
#[command]
|
||||
#[specta::specta]
|
||||
@@ -23,7 +24,7 @@ async fn create_client() -> Result<Client, String> {
|
||||
.cookie_store(true)
|
||||
.proxy(proxy)
|
||||
.build()
|
||||
.map_err(|error| format!("创建HTTP客户端失败: {error}"))
|
||||
.map_err(|error| format!("创建HTTP客户端失败: {error}").into())
|
||||
}
|
||||
|
||||
#[command]
|
||||
@@ -40,5 +41,7 @@ pub async fn check_media_unlock(on_complete: Channel<UnlockItem>) -> Result<Vec<
|
||||
#[specta::specta]
|
||||
pub async fn check_media_unlock_item(name: String) -> Result<UnlockItem, String> {
|
||||
let client = create_client().await?;
|
||||
clash_verge_media_unlock::check_media_unlock_item(&client, &name).await
|
||||
clash_verge_media_unlock::check_media_unlock_item(&client, &name)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use smartstring::alias::String;
|
||||
use tristr::alias::String;
|
||||
|
||||
pub type CmdResult<T = ()> = Result<T, CommandFailure>;
|
||||
|
||||
@@ -8,9 +8,9 @@ pub type CmdResult<T = ()> = Result<T, CommandFailure>;
|
||||
pub struct CommandFailure {
|
||||
/// Stable frontend identifier, when classified.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub code: Option<std::string::String>,
|
||||
pub code: Option<String>,
|
||||
/// Human-readable diagnostic detail.
|
||||
pub detail: std::string::String,
|
||||
pub detail: String,
|
||||
/// Requested operation when known by the command.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub operation: Option<crate::core::notification::FailedOperation>,
|
||||
@@ -20,8 +20,8 @@ impl CommandFailure {
|
||||
/// A failure with a stable code.
|
||||
pub fn coded(code: &str, detail: impl std::fmt::Display) -> Self {
|
||||
Self {
|
||||
code: Some(code.to_owned()),
|
||||
detail: detail.to_string(),
|
||||
code: Some(code.into()),
|
||||
detail: detail.to_string().into(),
|
||||
operation: None,
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ impl CommandFailure {
|
||||
pub fn plain(detail: impl std::fmt::Display) -> Self {
|
||||
Self {
|
||||
code: None,
|
||||
detail: detail.to_string(),
|
||||
detail: detail.to_string().into(),
|
||||
operation: None,
|
||||
}
|
||||
}
|
||||
@@ -50,16 +50,6 @@ impl From<&str> for CommandFailure {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::String> for CommandFailure {
|
||||
fn from(detail: std::string::String) -> Self {
|
||||
Self {
|
||||
code: None,
|
||||
detail,
|
||||
operation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for CommandFailure {
|
||||
fn from(detail: String) -> Self {
|
||||
Self::plain(detail)
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::core::{proxy_control, sysopt::Sysopt};
|
||||
use gethostname::gethostname;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
use tauri_plugin_clash_verge_sysinfo;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
@@ -24,8 +25,8 @@ pub async fn get_sys_proxy() -> CmdResult<SystemProxy> {
|
||||
|
||||
Ok(SystemProxy {
|
||||
enable,
|
||||
server: format!("{host}:{port}"),
|
||||
bypass,
|
||||
server: format!("{host}:{port}").into(),
|
||||
bypass: bypass.into(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,7 +40,10 @@ pub async fn get_auto_proxy() -> CmdResult<AutoProxy> {
|
||||
};
|
||||
let Autoproxy { enable, url } = auto_proxy;
|
||||
|
||||
Ok(AutoProxy { enable, url })
|
||||
Ok(AutoProxy {
|
||||
enable,
|
||||
url: url.into(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -52,10 +56,10 @@ pub fn get_embedded_server_port() -> CmdResult<u16> {
|
||||
#[specta::specta]
|
||||
pub fn get_system_hostname() -> String {
|
||||
match gethostname().into_string() {
|
||||
Ok(name) => name,
|
||||
Ok(name) => name.into(),
|
||||
Err(os_string) => {
|
||||
let fallback = format!("{os_string:?}");
|
||||
fallback.trim_matches('"').to_string()
|
||||
fallback.trim_matches('"').into()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +68,9 @@ pub fn get_system_hostname() -> String {
|
||||
#[specta::specta]
|
||||
pub fn get_network_interfaces() -> Vec<String> {
|
||||
tauri_plugin_clash_verge_sysinfo::list_network_interfaces()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -77,7 +84,7 @@ pub fn get_network_interfaces_info() -> CmdResult<Vec<crate::ipc::NetworkInterfa
|
||||
let mut result = Vec::new();
|
||||
|
||||
for interface in interfaces {
|
||||
if names.contains(&interface.name) {
|
||||
if names.iter().any(|name| name == &interface.name) {
|
||||
result.push(interface.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,17 +19,17 @@ use crate::{
|
||||
use clash_verge_draft::{Draft, SharedDraft};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tristr::alias::String;
|
||||
|
||||
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn profile_import_error(err: &anyhow::Error) -> std::string::String {
|
||||
fn profile_import_error(err: &anyhow::Error) -> String {
|
||||
if let Some(cause) = err.chain().find(|cause| cause.to_string().contains("TLS 1.0/1.1")) {
|
||||
return cause.to_string();
|
||||
return cause.to_string().into();
|
||||
}
|
||||
|
||||
format!("导入订阅失败: {err:#}")
|
||||
format!("导入订阅失败: {err:#}").into()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -68,7 +68,7 @@ pub async fn enhance_profiles() -> CmdResult<ValidationOutcome> {
|
||||
#[tauri::command]
|
||||
#[tracing::instrument(skip_all, level = "info", fields(url = %help::mask_url(&url), uid = tracing::field::Empty))]
|
||||
#[specta::specta]
|
||||
pub async fn import_profile(url: std::string::String, option: Option<PrfOption>) -> CmdResult {
|
||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
|
||||
Ok(it) => it,
|
||||
Err(e) => {
|
||||
@@ -98,9 +98,9 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn reorder_profile(active_id: std::string::String, over_id: std::string::String) -> CmdResult {
|
||||
let active_id: String = active_id.into();
|
||||
let over_id: String = over_id.into();
|
||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
||||
let active_id: String = active_id;
|
||||
let over_id: String = over_id;
|
||||
match profiles_reorder_safe(&active_id, &over_id).await {
|
||||
Ok(_) => {
|
||||
logging!(info, Type::Cmd, "重新排序配置文件: {} -> {}", active_id, over_id);
|
||||
@@ -121,9 +121,9 @@ pub async fn reorder_profile(active_id: std::string::String, over_id: std::strin
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn create_profile(item: ProfileCreate, file_data: Option<std::string::String>) -> CmdResult {
|
||||
pub async fn create_profile(item: ProfileCreate, file_data: Option<String>) -> CmdResult {
|
||||
let item: PrfItem = item.into();
|
||||
let file_data = file_data.map(String::from);
|
||||
let file_data = file_data;
|
||||
match profiles_append_item_with_filedata_safe(&item, file_data).await {
|
||||
Ok(_) => {
|
||||
profiles_save_file_safe()
|
||||
@@ -138,8 +138,8 @@ pub async fn create_profile(item: ProfileCreate, file_data: Option<std::string::
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn update_profile(index: std::string::String, option: Option<PrfOption>) -> CmdResult {
|
||||
let index: String = index.into();
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||
let index: String = index;
|
||||
match feat::update_profile(&index, option.as_ref(), true).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
@@ -151,8 +151,8 @@ pub async fn update_profile(index: std::string::String, option: Option<PrfOption
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn delete_profile(index: std::string::String) -> CmdResult {
|
||||
let index: String = index.into();
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
let index: String = index;
|
||||
let profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
||||
|
||||
let profiles = Config::profiles().await;
|
||||
@@ -282,7 +282,9 @@ async fn handle_validation_failure(
|
||||
) -> CmdResult<ValidationOutcome> {
|
||||
logging!(warn, Type::Cmd, "配置验证失败: {}", outcome);
|
||||
discard_and_restore(current_profile).await?;
|
||||
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
|
||||
if !matches!(outcome, ValidationOutcome::Busy) {
|
||||
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
|
||||
}
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
@@ -311,9 +313,6 @@ async fn perform_config_update(
|
||||
current_value: Option<&String>,
|
||||
current_profile: Option<&String>,
|
||||
) -> CmdResult<ValidationOutcome> {
|
||||
defer! {
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
}
|
||||
let update_result = run_profile_config_update_transition(|| CoreManager::global().update_config_forced()).await;
|
||||
|
||||
match update_result {
|
||||
@@ -326,7 +325,6 @@ async fn perform_config_update(
|
||||
#[derive(Default, serde::Deserialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct ProfileSelection {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub current: Option<String>,
|
||||
}
|
||||
|
||||
@@ -334,13 +332,9 @@ pub struct ProfileSelection {
|
||||
#[serde(default)]
|
||||
pub struct ProfileCreate {
|
||||
#[serde(rename = "type")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub itype: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub desc: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub url: Option<String>,
|
||||
pub option: Option<PrfOption>,
|
||||
}
|
||||
@@ -362,19 +356,12 @@ impl From<ProfileCreate> for PrfItem {
|
||||
#[serde(default)]
|
||||
pub struct ProfilePatch {
|
||||
#[serde(rename = "type")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub itype: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub desc: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub file: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub url: Option<String>,
|
||||
pub selected: Option<Vec<crate::config::PrfSelected>>,
|
||||
#[specta(type = Option<std::collections::BTreeMap<std::string::String, std::string::String>>)]
|
||||
pub latency_test_urls: Option<std::collections::BTreeMap<String, String>>,
|
||||
pub extra: Option<crate::config::PrfExtra>,
|
||||
pub updated: Option<usize>,
|
||||
pub option: Option<PrfOption>,
|
||||
@@ -389,7 +376,6 @@ impl From<ProfilePatch> for PrfItem {
|
||||
file: patch.file,
|
||||
url: patch.url,
|
||||
selected: patch.selected,
|
||||
latency_test_urls: patch.latency_test_urls,
|
||||
extra: patch.extra,
|
||||
updated: patch.updated,
|
||||
option: patch.option,
|
||||
@@ -409,6 +395,9 @@ pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult<Vali
|
||||
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
|
||||
return Ok(ValidationOutcome::Busy);
|
||||
}
|
||||
defer! {
|
||||
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
|
||||
}
|
||||
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
||||
|
||||
let profiles = IProfiles {
|
||||
@@ -437,9 +426,9 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn patch_profile(index: std::string::String, profile: ProfilePatch) -> CmdResult {
|
||||
pub async fn patch_profile(index: String, profile: ProfilePatch) -> CmdResult {
|
||||
let profile: PrfItem = profile.into();
|
||||
let index: String = index.into();
|
||||
let index: String = index;
|
||||
let profiles = Config::profiles().await;
|
||||
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
|
||||
&& let Some(new_option) = profile.option.as_ref()
|
||||
@@ -480,7 +469,7 @@ pub async fn patch_profile(index: std::string::String, profile: ProfilePatch) ->
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn view_profile(index: std::string::String) -> CmdResult {
|
||||
pub async fn view_profile(index: String) -> CmdResult {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_arc();
|
||||
let file = profiles_ref
|
||||
@@ -505,7 +494,7 @@ pub async fn view_profile(index: std::string::String) -> CmdResult {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::string::String> {
|
||||
pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let item = {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_arc();
|
||||
@@ -525,7 +514,7 @@ pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::str
|
||||
.join(file.as_str());
|
||||
match tokio::fs::try_exists(&path).await {
|
||||
Ok(true) => {}
|
||||
Ok(false) => return Ok(std::string::String::new()),
|
||||
Ok(false) => return Ok(String::new()),
|
||||
Err(err) => {
|
||||
return Err(coded_error(
|
||||
"PROFILE_READ_FAILED",
|
||||
@@ -536,12 +525,12 @@ pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::str
|
||||
}
|
||||
|
||||
let data = item.read_file().await.with_error_code("PROFILE_READ_FAILED")?;
|
||||
Ok(data.into())
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_next_update_time(uid: std::string::String) -> CmdResult<Option<i64>> {
|
||||
pub async fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
|
||||
let timer = Timer::global();
|
||||
let next_time = timer.get_next_update_time(&uid).await;
|
||||
Ok(next_time)
|
||||
|
||||
@@ -15,6 +15,7 @@ use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
/// Merges one selection pair into fresh backend state to avoid stale-list overwrites.
|
||||
#[tauri::command]
|
||||
@@ -48,7 +49,7 @@ fn runtime_group_order(config: Option<&Mapping>) -> Vec<String> {
|
||||
.filter_map(|name| name.as_str())
|
||||
.filter(|name| !name.is_empty() && *name != "GLOBAL")
|
||||
.filter(|name| seen.insert(*name))
|
||||
.map(str::to_owned)
|
||||
.map(String::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
|
||||
.into_iter()
|
||||
.flat_map(|providers| providers.keys())
|
||||
.filter_map(|name| name.as_str())
|
||||
.map(str::to_owned)
|
||||
.map(String::from)
|
||||
.collect::<Vec<_>>();
|
||||
all_providers.sort();
|
||||
let flag = |group: &Mapping, key: &str| group.get(key).and_then(|value| value.as_bool()) == Some(true);
|
||||
@@ -75,7 +76,7 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
|
||||
.flatten()
|
||||
.filter_map(|group| group.as_mapping())
|
||||
.filter_map(|group| {
|
||||
let name = group.get("name")?.as_str()?.to_owned();
|
||||
let name: String = group.get("name")?.as_str()?.into();
|
||||
let providers = if flag(group, "include-all") || flag(group, "include-all-providers") {
|
||||
all_providers.clone()
|
||||
} else {
|
||||
@@ -85,14 +86,14 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|provider| provider.as_str())
|
||||
.map(str::to_owned)
|
||||
.map(String::from)
|
||||
.collect()
|
||||
};
|
||||
let exclude_types = group
|
||||
.get("exclude-type")
|
||||
.and_then(|types| types.as_str())
|
||||
.filter(|types| !types.is_empty())
|
||||
.map(|types| types.split('|').map(str::to_owned).collect())
|
||||
.map(|types| types.split('|').map(String::from).collect())
|
||||
.unwrap_or_default();
|
||||
Some((
|
||||
name,
|
||||
@@ -108,32 +109,20 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_proxy_view() -> CmdResult<ProxyViewV1> {
|
||||
proxy_view(None).await
|
||||
}
|
||||
|
||||
pub(crate) async fn proxy_view(provider_timeout: Option<std::time::Duration>) -> CmdResult<ProxyViewV1> {
|
||||
let runtime = Config::runtime().await;
|
||||
let latest_runtime = runtime.latest_arc();
|
||||
let runtime_group_order = runtime_group_order(latest_runtime.config.as_ref());
|
||||
let group_scopes = runtime_group_scopes(latest_runtime.config.as_ref());
|
||||
|
||||
let mihomo = Handle::mihomo();
|
||||
let (proxies, providers) = tokio::join!(mihomo.get_proxies(), async {
|
||||
match provider_timeout {
|
||||
Some(timeout) => tokio::time::timeout(timeout, mihomo.get_proxy_providers())
|
||||
.await
|
||||
.ok()
|
||||
.and_then(Result::ok),
|
||||
None => mihomo.get_proxy_providers().await.ok(),
|
||||
}
|
||||
});
|
||||
let (proxies, providers) = tokio::join!(mihomo.get_proxies(), mihomo.get_proxy_providers(),);
|
||||
let proxies = proxies.stringify_err()?;
|
||||
|
||||
Ok(ProxyViewBuilder::build(ProxyViewInput {
|
||||
runtime_group_order,
|
||||
group_scopes,
|
||||
proxies,
|
||||
providers,
|
||||
providers: providers.ok(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::CmdResult;
|
||||
use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager, utils::yaml_emitter};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use smartstring::alias::String;
|
||||
use std::collections::HashMap;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
@@ -17,7 +17,7 @@ pub async fn get_runtime_config() -> CmdResult<Option<crate::ipc::YamlMapping>>
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_runtime_yaml() -> CmdResult<std::string::String> {
|
||||
pub async fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime().await;
|
||||
let runtime = runtime.latest_arc();
|
||||
|
||||
@@ -36,22 +36,17 @@ pub async fn get_runtime_logs() -> CmdResult<RuntimeLogs> {
|
||||
|
||||
#[derive(serde::Serialize, specta::Type)]
|
||||
#[serde(transparent)]
|
||||
pub struct RuntimeLogs(
|
||||
#[specta(type = HashMap<std::string::String, Vec<(std::string::String, std::string::String)>>)]
|
||||
HashMap<String, Vec<(String, String)>>,
|
||||
);
|
||||
pub struct RuntimeLogs(HashMap<String, Vec<(String, String)>>);
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn take_discarded_keys_notice() -> Option<std::string::String> {
|
||||
crate::enhance::take_discarded_keys_notice().map(Into::into)
|
||||
pub fn take_discarded_keys_notice() -> Option<String> {
|
||||
crate::enhance::take_discarded_keys_notice()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_runtime_proxy_chain_config(
|
||||
proxy_chain_exit_node: std::string::String,
|
||||
) -> CmdResult<std::string::String> {
|
||||
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
|
||||
let runtime = Config::runtime().await;
|
||||
let runtime = runtime.latest_arc();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::CmdResult;
|
||||
use super::{CmdResult, CommandFailure};
|
||||
use crate::{
|
||||
cmd::StringifyErr as _,
|
||||
cmd::validate::{ValidationNoticeTarget, handle_validation_notice},
|
||||
@@ -11,18 +11,15 @@ use crate::{
|
||||
utils::dirs,
|
||||
};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use smartstring::alias::String;
|
||||
use tokio::fs;
|
||||
use tristr::alias::String;
|
||||
|
||||
/// 保存profiles的配置
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn save_profile_file(
|
||||
index: std::string::String,
|
||||
file_data: Option<std::string::String>,
|
||||
) -> CmdResult<ValidationOutcome> {
|
||||
let index: String = index.into();
|
||||
let file_data = file_data.map(String::from);
|
||||
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult<ValidationOutcome> {
|
||||
let index: String = index;
|
||||
let file_data = file_data;
|
||||
let file_data = match file_data {
|
||||
Some(d) => d,
|
||||
None => return Ok(ValidationOutcome::Valid),
|
||||
@@ -153,7 +150,7 @@ async fn handle_saved_profile_file(
|
||||
Err(e) => {
|
||||
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {e:#}");
|
||||
restore_original(file_path, original_content, original_existed).await?;
|
||||
return Err(e.to_string().into());
|
||||
return Err(CommandFailure::plain(e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +178,7 @@ async fn handle_saved_profile_file(
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, "[cmd配置save] 运行时配置应用错误: {err:#}");
|
||||
restore_original(file_path, original_content, original_existed).await?;
|
||||
Err(err.to_string().into())
|
||||
Err(CommandFailure::plain(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::{
|
||||
service::{SERVICE_MANAGER, ServiceStatus, request_runtime_provider_sync},
|
||||
},
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, serde::Serialize, specta::Type)]
|
||||
#[serde(tag = "status", rename_all = "camelCase")]
|
||||
@@ -74,7 +75,7 @@ where
|
||||
if reasons.is_empty() {
|
||||
return Err(proxy_aware_coded_error(&error, error_code));
|
||||
}
|
||||
let reason = reasons.join("\n");
|
||||
let reason: String = reasons.join("\n").into();
|
||||
fallback(reason.clone())
|
||||
.await
|
||||
.map_err(|error| proxy_aware_coded_error(&error, "SERVICE_SIDECAR_FAILED"))?;
|
||||
@@ -179,7 +180,7 @@ mod tests {
|
||||
let error = match availability {
|
||||
Some(availability) => anyhow::Error::new(InstallationVerificationError {
|
||||
status: InstallationStatus {
|
||||
service_sha256: String::new(),
|
||||
service_sha256: std::string::String::new(),
|
||||
protocol: ProtocolInfo::current(),
|
||||
cores: vec![CoreInspection {
|
||||
name: "verge-mihomo.exe".into(),
|
||||
@@ -203,7 +204,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
result,
|
||||
Ok(ServiceInstallOutcome::Sidecar {
|
||||
reason: format!("verge-mihomo.exe: {reason}")
|
||||
reason: format!("verge-mihomo.exe: {reason}").into()
|
||||
})
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::core::{
|
||||
notification::{self, PendingFailure},
|
||||
runstate::{RUN_STATE, RunStateView},
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
/// Returns one coherent core/service snapshot instead of independently refreshed state.
|
||||
#[tauri::command]
|
||||
|
||||
@@ -4,18 +4,14 @@ use crate::{
|
||||
config::{Config, IVerge},
|
||||
core, feat,
|
||||
};
|
||||
use smartstring::alias::String;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn save_webdav_config(
|
||||
url: std::string::String,
|
||||
username: std::string::String,
|
||||
password: std::string::String,
|
||||
) -> CmdResult<()> {
|
||||
let url: String = url.into();
|
||||
let username: String = username.into();
|
||||
let password: String = password.into();
|
||||
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
|
||||
let url: String = url;
|
||||
let username: String = username;
|
||||
let password: String = password;
|
||||
let patch = IVerge {
|
||||
webdav_url: Some(url),
|
||||
webdav_username: Some(username),
|
||||
@@ -48,15 +44,15 @@ pub async fn list_webdav_backup() -> CmdResult<Vec<crate::ipc::WebDavFile>> {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn delete_webdav_backup(filename: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
let filename: String = filename;
|
||||
feat::delete_webdav_backup(filename).await.stringify_err()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn restore_webdav_backup(filename: std::string::String) -> CmdResult<()> {
|
||||
let filename: String = filename.into();
|
||||
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
|
||||
let filename: String = filename;
|
||||
feat::restore_webdav_backup(filename)
|
||||
.await
|
||||
.map_err(|error| super::proxy_aware_error(&error))
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
str::FromStr as _,
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct IClashTemp(pub Mapping);
|
||||
@@ -84,12 +85,12 @@ impl IClashTemp {
|
||||
#[cfg(unix)]
|
||||
map.insert(
|
||||
"external-controller-unix".into(),
|
||||
Self::guard_external_controller_ipc().into(),
|
||||
Self::guard_external_controller_ipc().as_str().into(),
|
||||
);
|
||||
#[cfg(windows)]
|
||||
map.insert(
|
||||
"external-controller-pipe".into(),
|
||||
Self::guard_external_controller_ipc().into(),
|
||||
Self::guard_external_controller_ipc().as_str().into(),
|
||||
);
|
||||
map.insert("tun".into(), tun_config.into());
|
||||
cors_map.insert("allow-private-network".into(), true.into());
|
||||
@@ -134,12 +135,18 @@ impl IClashTemp {
|
||||
config.insert("mixed-port".into(), mixed_port.into());
|
||||
config.insert("socks-port".into(), socks_port.into());
|
||||
config.insert("port".into(), port.into());
|
||||
config.insert("external-controller".into(), ctrl.into());
|
||||
config.insert("external-controller".into(), ctrl.as_str().into());
|
||||
|
||||
#[cfg(unix)]
|
||||
config.insert("external-controller-unix".into(), external_controller_unix.into());
|
||||
config.insert(
|
||||
"external-controller-unix".into(),
|
||||
external_controller_unix.as_str().into(),
|
||||
);
|
||||
#[cfg(windows)]
|
||||
config.insert("external-controller-pipe".into(), external_controller_pipe.into());
|
||||
config.insert(
|
||||
"external-controller-pipe".into(),
|
||||
external_controller_pipe.as_str().into(),
|
||||
);
|
||||
config
|
||||
}
|
||||
|
||||
@@ -166,9 +173,9 @@ impl IClashTemp {
|
||||
port: Self::guard_port(config),
|
||||
server: Self::guard_client_ctrl(config),
|
||||
secret: config.get("secret").and_then(|value| match value {
|
||||
Value::String(val_str) => Some(val_str.clone()),
|
||||
Value::Bool(val_bool) => Some(val_bool.to_string()),
|
||||
Value::Number(val_num) => Some(val_num.to_string()),
|
||||
Value::String(val_str) => Some(val_str.clone().into()),
|
||||
Value::Bool(val_bool) => Some(val_bool.to_string().into()),
|
||||
Value::Number(val_num) => Some(val_num.to_string().into()),
|
||||
_ => None,
|
||||
}),
|
||||
}
|
||||
@@ -180,7 +187,7 @@ impl IClashTemp {
|
||||
/// 的严格 `BaseConfig` 反序列化,因此即使核心返回的字段与插件结构体不匹配也能取到。
|
||||
pub fn get_mode(&self) -> Option<String> {
|
||||
self.0.get("mode").and_then(|value| match value {
|
||||
Value::String(val_str) => Some(val_str.clone()),
|
||||
Value::String(val_str) => Some(val_str.clone().into()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
@@ -276,7 +283,7 @@ impl IClashTemp {
|
||||
false => Cow::Borrowed(val_str),
|
||||
};
|
||||
|
||||
SocketAddr::from_str(&val).ok().map(|s| s.to_string())
|
||||
SocketAddr::from_str(&val).ok().map(|s| s.to_string().into())
|
||||
}
|
||||
None => None,
|
||||
})
|
||||
@@ -290,7 +297,7 @@ impl IClashTemp {
|
||||
if socket.ip().is_unspecified() {
|
||||
socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
||||
}
|
||||
socket.to_string()
|
||||
socket.to_string().into()
|
||||
}
|
||||
Err(_) => "127.0.0.1:9097".into(),
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@ use backon::{ExponentialBuilder, Retryable as _};
|
||||
use clash_verge_draft::Draft;
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
use tokio::sync::{Mutex, MutexGuard, OnceCell};
|
||||
use tokio::time::sleep;
|
||||
use tristr::alias::String;
|
||||
|
||||
pub(crate) struct Config {
|
||||
clash_config: Draft<IClashTemp>,
|
||||
@@ -205,7 +205,7 @@ impl Config {
|
||||
Self::write_runtime_file(&yaml).await
|
||||
}
|
||||
|
||||
pub(crate) async fn runtime_config_yaml() -> Result<std::string::String> {
|
||||
pub(crate) async fn runtime_config_yaml() -> Result<String> {
|
||||
let runtime = Self::runtime().await;
|
||||
AsyncHandler::spawn_blocking(move || {
|
||||
let runtime_lastest = runtime.latest_arc();
|
||||
@@ -216,7 +216,7 @@ impl Config {
|
||||
.or_else(|| runtime_data.config.as_ref())
|
||||
.ok_or_else(|| anyhow!("failed to generate runtime config, might need to restart application"))?;
|
||||
let yaml_str = serde_yaml_ng::to_string(config)?;
|
||||
Ok(format!("# Generated by Clash Verge\n\n{}", yaml_str))
|
||||
Ok(format!("# Generated by Clash Verge\n\n{}", yaml_str).into())
|
||||
})
|
||||
.await
|
||||
.map_err(|join| anyhow!("runtime serialization task failed: {join}"))?
|
||||
|
||||
@@ -4,8 +4,8 @@ use clash_verge_draft::DraftTransaction;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use smartstring::alias::String;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tristr::alias::String;
|
||||
|
||||
use super::{Config, IVerge};
|
||||
use crate::core::handle::Handle;
|
||||
@@ -110,7 +110,6 @@ impl DnsOverrideState {
|
||||
pub struct ProfileDnsSettings {
|
||||
pub enabled: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub confirmation: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::cell::Cell;
|
||||
use std::future::Future;
|
||||
use tristr::alias::String;
|
||||
|
||||
const NONCE_LENGTH: usize = 12;
|
||||
|
||||
@@ -28,7 +29,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
|
||||
let mut combined = nonce.to_vec();
|
||||
combined.extend(ciphertext);
|
||||
Ok(STANDARD.encode(combined))
|
||||
Ok(STANDARD.encode(combined).into())
|
||||
}
|
||||
|
||||
pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
@@ -46,7 +47,10 @@ pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error
|
||||
.decrypt(nonce.into(), ciphertext)
|
||||
.map_err(|e| format!("Decryption failed: {e}"))?;
|
||||
|
||||
String::from_utf8(plaintext).map_err(|e| e.into())
|
||||
// from_utf8 exists only on the std String type; the tristr alias shadows the bare name.
|
||||
std::string::String::from_utf8(plaintext)
|
||||
.map_err(|e| e.into())
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
pub fn serialize_encrypted<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
|
||||
@@ -18,6 +18,7 @@ use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::Mutex;
|
||||
use serde_yaml_ng::Value;
|
||||
use std::{collections::HashSet, net::SocketAddr, str::FromStr as _};
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct MixedPortFallback {
|
||||
@@ -141,8 +142,8 @@ impl Config {
|
||||
}
|
||||
|
||||
pub(crate) fn block_startup_core(error: &anyhow::Error) {
|
||||
*STARTUP_CORE_BLOCK_REASON.lock() = Some(error.to_string());
|
||||
report_fallback_error(error.to_string());
|
||||
*STARTUP_CORE_BLOCK_REASON.lock() = Some(error.to_string().into());
|
||||
report_fallback_error(error.to_string().into());
|
||||
}
|
||||
|
||||
pub(crate) fn startup_core_block_reason() -> Option<String> {
|
||||
|
||||
@@ -9,9 +9,9 @@ use crate::{
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::time::Duration;
|
||||
use tokio::fs;
|
||||
use tristr::alias::String;
|
||||
// TODO, use other re-export
|
||||
use reqwest_dav::re_exports::url::form_urlencoded;
|
||||
use tauri::Url;
|
||||
@@ -30,33 +30,23 @@ pub(super) fn normalize_profile_home_url(raw: &str) -> Option<String> {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct PrfItem {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub uid: Option<String>,
|
||||
|
||||
/// enum value: remote | local | script | merge
|
||||
#[serde(rename = "type")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub itype: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub file: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub desc: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub url: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub selected: Option<Vec<PrfSelected>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::collections::BTreeMap<std::string::String, std::string::String>>)]
|
||||
pub latency_test_urls: Option<std::collections::BTreeMap<String, String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra: Option<PrfExtra>,
|
||||
|
||||
@@ -66,20 +56,16 @@ pub struct PrfItem {
|
||||
pub option: Option<PrfOption>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub home: Option<String>,
|
||||
|
||||
#[serde(skip)]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub file_data: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct PrfSelected {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub name: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub now: Option<String>,
|
||||
}
|
||||
|
||||
@@ -95,7 +81,6 @@ pub struct PrfExtra {
|
||||
#[serde(default)]
|
||||
pub struct PrfOption {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub user_agent: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -118,15 +103,10 @@ pub struct PrfOption {
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allow_auto_update: Option<bool>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub merge: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub script: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub rules: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub proxies: Option<String>,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub groups: Option<String>,
|
||||
}
|
||||
|
||||
@@ -191,7 +171,7 @@ impl PrfItem {
|
||||
file_data: Option<String>,
|
||||
option: Option<&PrfOption>,
|
||||
) -> Result<Self> {
|
||||
let uid = help::get_uid("L").into();
|
||||
let uid = help::get_uid("L");
|
||||
let file = format!("{uid}.yaml").into();
|
||||
let opt_ref = option.as_ref();
|
||||
let update_interval = opt_ref.and_then(|o| o.update_interval);
|
||||
@@ -233,7 +213,6 @@ impl PrfItem {
|
||||
desc: Some(desc),
|
||||
file: Some(file),
|
||||
url: None,
|
||||
latency_test_urls: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: Some(PrfOption {
|
||||
@@ -365,11 +344,11 @@ impl PrfItem {
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(normalize_profile_home_url);
|
||||
|
||||
let uid = help::get_uid("R").into();
|
||||
let uid = help::get_uid("R");
|
||||
let file = format!("{uid}.yaml").into();
|
||||
let name = name
|
||||
.map(|s| s.to_owned())
|
||||
.unwrap_or_else(|| filename.map(|s| s.into()).unwrap_or_else(|| "Remote File".into()));
|
||||
.unwrap_or_else(|| filename.unwrap_or_else(|| "Remote File".into()));
|
||||
let data = resp.text();
|
||||
|
||||
let data = data.trim_start_matches('\u{feff}');
|
||||
@@ -413,7 +392,6 @@ impl PrfItem {
|
||||
desc: desc.cloned(),
|
||||
file: Some(file),
|
||||
url: Some(url.as_str().into()),
|
||||
latency_test_urls: None,
|
||||
selected: None,
|
||||
extra,
|
||||
option: Some(PrfOption {
|
||||
@@ -436,7 +414,7 @@ impl PrfItem {
|
||||
let (id, template) = if let Some(uid) = uid {
|
||||
(uid, tmpl::ITEM_MERGE.into())
|
||||
} else {
|
||||
(help::get_uid("m").into(), tmpl::ITEM_MERGE_EMPTY.into())
|
||||
(help::get_uid("m"), tmpl::ITEM_MERGE_EMPTY.into())
|
||||
};
|
||||
let file = format!("{id}.yaml").into();
|
||||
|
||||
@@ -451,11 +429,7 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
pub(super) fn from_script(uid: Option<String>) -> Self {
|
||||
let id = if let Some(uid) = uid {
|
||||
uid
|
||||
} else {
|
||||
help::get_uid("s").into()
|
||||
};
|
||||
let id = if let Some(uid) = uid { uid } else { help::get_uid("s") };
|
||||
let file = format!("{id}.js").into(); // js ext
|
||||
Self {
|
||||
uid: Some(id),
|
||||
@@ -468,7 +442,7 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
fn from_rules() -> Self {
|
||||
let uid = help::get_uid("r").into();
|
||||
let uid = help::get_uid("r");
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Self {
|
||||
@@ -482,7 +456,7 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
fn from_proxies() -> Self {
|
||||
let uid = help::get_uid("p").into();
|
||||
let uid = help::get_uid("p");
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Self {
|
||||
@@ -496,7 +470,7 @@ impl PrfItem {
|
||||
}
|
||||
|
||||
fn from_groups() -> Self {
|
||||
let uid = help::get_uid("g").into();
|
||||
let uid = help::get_uid("g");
|
||||
let file = format!("{uid}.yaml").into(); // yaml ext
|
||||
|
||||
Self {
|
||||
|
||||
@@ -17,7 +17,6 @@ use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
@@ -30,6 +29,7 @@ use std::{
|
||||
};
|
||||
use tauri_plugin_mihomo::models::{Proxies, ProxyType};
|
||||
use tokio::{fs, task::JoinHandle};
|
||||
use tristr::alias::String;
|
||||
|
||||
pub(crate) static PROFILE_WRITE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
|
||||
|
||||
@@ -51,7 +51,6 @@ const SELECTED_NODES_FIRST_PASS_BUDGET: Duration = Duration::from_secs(3);
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct IProfiles {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub current: Option<String>,
|
||||
|
||||
pub items: Option<Vec<PrfItem>>,
|
||||
@@ -115,7 +114,7 @@ impl IProfiles {
|
||||
let mut home_changed = false;
|
||||
for item in items.iter_mut() {
|
||||
if item.uid.is_none() {
|
||||
item.uid = Some(help::get_uid("d").into());
|
||||
item.uid = Some(help::get_uid("d"));
|
||||
}
|
||||
if item.itype.as_deref() == Some("remote") {
|
||||
item.option
|
||||
@@ -250,7 +249,6 @@ impl IProfiles {
|
||||
patch!(each, item, file);
|
||||
patch!(each, item, url);
|
||||
patch!(each, item, selected);
|
||||
patch!(each, item, latency_test_urls);
|
||||
patch!(each, item, extra);
|
||||
patch!(each, item, updated);
|
||||
patch!(each, item, option);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tristr::alias::String;
|
||||
|
||||
use crate::enhance::field::use_keys;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use log::LevelFilter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smartstring::alias::String;
|
||||
use tristr::alias::String;
|
||||
|
||||
/// ### `verge.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
|
||||
|
||||
@@ -75,6 +75,7 @@ pub mod tun {
|
||||
"auto-redirect",
|
||||
"auto-detect-interface",
|
||||
"dns-hijack",
|
||||
"dns-mode",
|
||||
"strict-route",
|
||||
"mtu",
|
||||
];
|
||||
|
||||
@@ -20,6 +20,7 @@ use tokio::{
|
||||
fs,
|
||||
sync::{Mutex, watch},
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
const DEFAULT_INTERVAL_HOURS: u64 = 24;
|
||||
const MIN_INTERVAL_HOURS: u64 = 1;
|
||||
@@ -211,7 +212,7 @@ impl AutoBackupManager {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let file_name = create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug()).into()).await?;
|
||||
let file_name = create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug())).await?;
|
||||
self.last_backup.store(Local::now().timestamp(), Ordering::Release);
|
||||
|
||||
if let Err(err) = cleanup_auto_backups().await {
|
||||
@@ -239,8 +240,8 @@ impl AutoBackupManager {
|
||||
|
||||
fn append_auto_suffix(file_name: &str, slug: &str) -> String {
|
||||
match file_name.rsplit_once('.') {
|
||||
Some((stem, ext)) => format!("{stem}{AUTO_MARKER}{slug}.{ext}"),
|
||||
None => format!("{file_name}{AUTO_MARKER}{slug}"),
|
||||
Some((stem, ext)) => format!("{stem}{AUTO_MARKER}{slug}.{ext}").into(),
|
||||
None => format!("{file_name}{AUTO_MARKER}{slug}").into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ use arc_swap::{ArcSwap, ArcSwapOption};
|
||||
use backon::{ConstantBuilder, Retryable as _};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use reqwest_dav::list_cmd::{ListEntity, ListFile, ListMultiStatus};
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::{consts::OS, temp_dir},
|
||||
@@ -15,6 +14,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{fs, time::timeout};
|
||||
use tristr::alias::String;
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::{config::Config, core::handle, core::lightweight::entry_lightweight_m
|
||||
use anyhow::{Result, bail};
|
||||
use arc_swap::ArcSwap;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
@@ -18,6 +17,7 @@ use std::{
|
||||
},
|
||||
};
|
||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt as _, ShortcutState};
|
||||
use tristr::alias::String;
|
||||
|
||||
/// Enum representing all available hotkey functions
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
str::FromStr as _,
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
/// The config key of the Mixed Port, named once so callers stop spelling it out.
|
||||
pub(crate) const MIXED_PORT_KEY: &str = "mixed-port";
|
||||
@@ -163,7 +164,7 @@ pub(crate) fn probe_listener(request: &ListenerProbe) -> ListenerProbeOutcome {
|
||||
Ok(address) => address,
|
||||
Err(error) => {
|
||||
return ListenerProbeOutcome::Invalid {
|
||||
message: error.to_string(),
|
||||
message: error.to_string().into(),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -201,7 +202,7 @@ pub(crate) fn probe_proxy_port_change(
|
||||
Ok(claims) => claims,
|
||||
Err(error) => {
|
||||
return ListenerProbeOutcome::Invalid {
|
||||
message: format!("current proxy listener configuration is invalid: {error:#}"),
|
||||
message: format!("current proxy listener configuration is invalid: {error:#}").into(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -212,7 +213,7 @@ pub(crate) fn probe_proxy_port_change(
|
||||
Ok(claims) => claims,
|
||||
Err(error) => {
|
||||
return ListenerProbeOutcome::Invalid {
|
||||
message: format!("candidate proxy listener configuration is invalid: {error:#}"),
|
||||
message: format!("candidate proxy listener configuration is invalid: {error:#}").into(),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -401,7 +402,8 @@ fn probe_claims(claims: &[BindClaim]) -> ListenerProbeOutcome {
|
||||
claim.socket_addr(),
|
||||
transport_name(claim.transport),
|
||||
error
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
};
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -412,7 +414,8 @@ fn probe_claims(claims: &[BindClaim]) -> ListenerProbeOutcome {
|
||||
claim.socket_addr(),
|
||||
transport_name(claim.transport),
|
||||
error
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -739,7 +742,7 @@ mod tests {
|
||||
let port = listener.local_addr()?.port();
|
||||
assert_eq!(
|
||||
probe_listener(&ListenerProbe {
|
||||
address: format!("0.0.0.0:{port}"),
|
||||
address: format!("0.0.0.0:{port}").into(),
|
||||
transports: vec![ListenerTransport::Tcp],
|
||||
}),
|
||||
ListenerProbeOutcome::Conflict {
|
||||
@@ -755,7 +758,7 @@ mod tests {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
|
||||
let port = listener.local_addr()?.port();
|
||||
let outcome = probe_listener(&ListenerProbe {
|
||||
address: format!("127.0.0.1:{port}"),
|
||||
address: format!("127.0.0.1:{port}").into(),
|
||||
transports: vec![ListenerTransport::Tcp],
|
||||
});
|
||||
assert_eq!(
|
||||
@@ -773,7 +776,7 @@ mod tests {
|
||||
let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, 0))?;
|
||||
let port = listener.local_addr()?.port();
|
||||
let outcome = probe_listener(&ListenerProbe {
|
||||
address: format!("127.0.0.1:{port}"),
|
||||
address: format!("127.0.0.1:{port}").into(),
|
||||
transports: vec![ListenerTransport::Tcp],
|
||||
});
|
||||
assert_eq!(
|
||||
@@ -809,7 +812,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
probe_listener(&ListenerProbe {
|
||||
address: format!("127.0.0.1:{port}"),
|
||||
address: format!("127.0.0.1:{port}").into(),
|
||||
transports: vec![ListenerTransport::Tcp],
|
||||
}),
|
||||
ListenerProbeOutcome::Available
|
||||
@@ -822,7 +825,7 @@ mod tests {
|
||||
let socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))?;
|
||||
let port = socket.local_addr()?.port();
|
||||
let outcome = probe_listener(&ListenerProbe {
|
||||
address: format!("127.0.0.1:{port}"),
|
||||
address: format!("127.0.0.1:{port}").into(),
|
||||
transports: vec![ListenerTransport::Udp],
|
||||
});
|
||||
assert_eq!(
|
||||
|
||||
@@ -15,7 +15,6 @@ use clash_verge_draft::DraftTransaction;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use clash_verge_service_ipc::StageRuntimeOutcome;
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::PathBuf,
|
||||
@@ -23,6 +22,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tauri_plugin_mihomo::Error as MihomoError;
|
||||
use tristr::alias::String;
|
||||
|
||||
/// A staging result separated from the policy that acts on it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -92,7 +92,7 @@ async fn ask_to_stage(path: &std::path::Path) -> StageAttempt {
|
||||
match crate::core::service::stage_runtime_by_service(path).await {
|
||||
Ok(StageRequest::Answered(outcome)) => StageAttempt::Answered(outcome),
|
||||
Ok(StageRequest::Refused { code, message }) => {
|
||||
let message = message.into();
|
||||
let message = message;
|
||||
if StageRequest::is_about_the_bundle(code) {
|
||||
StageAttempt::RefusedTheBundle(message)
|
||||
} else {
|
||||
|
||||
@@ -7,10 +7,10 @@ use crate::core::service::{SERVICE_MANAGER, ServiceStatus};
|
||||
use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::path::Path;
|
||||
#[cfg(target_os = "windows")]
|
||||
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
const fn should_wait_for_service(tun_enabled: bool, service_ready: bool, is_admin: bool) -> bool {
|
||||
@@ -194,7 +194,7 @@ async fn run_service_start_with_sidecar_fallback<Start, StartFuture, Check, Chec
|
||||
start_service: Start,
|
||||
confirm_idle: Check,
|
||||
start_sidecar: Fallback,
|
||||
record_unavailable: impl FnOnce(std::string::String),
|
||||
record_unavailable: impl FnOnce(String),
|
||||
) -> Result<()>
|
||||
where
|
||||
Start: FnOnce() -> StartFuture,
|
||||
@@ -217,7 +217,7 @@ where
|
||||
// A lost start response can hide a running core; the shared check must resolve occupancy.
|
||||
if let Err(probe_error) = confirm_idle().await {
|
||||
let error = error.context(format!("Sidecar fallback was not safe: {probe_error:#}"));
|
||||
record_unavailable(format!("{error:#}"));
|
||||
record_unavailable(format!("{error:#}").into());
|
||||
return Err(error);
|
||||
}
|
||||
logging!(
|
||||
@@ -650,7 +650,7 @@ impl CoreManager {
|
||||
|error| self.start_sidecar_after_service_failure(error),
|
||||
|reason| {
|
||||
crate::core::runstate::RUN_STATE
|
||||
.observe(crate::core::runstate::ServiceHealth::Unavailable(reason));
|
||||
.observe(crate::core::runstate::ServiceHealth::Unavailable(reason.into()));
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -688,7 +688,7 @@ impl CoreManager {
|
||||
}
|
||||
let reason = match state.health {
|
||||
ServiceHealth::Unavailable(reason) => reason,
|
||||
ServiceHealth::VersionMismatch => "registered service is unavailable or incompatible".to_owned(),
|
||||
ServiceHealth::VersionMismatch => "registered service is unavailable or incompatible".into(),
|
||||
_ => return Ok(()),
|
||||
};
|
||||
if let Err(error) = clash_verge_service_ipc::execution::check_sidecar_available()
|
||||
@@ -712,7 +712,7 @@ impl CoreManager {
|
||||
if Handle::global().is_exiting() {
|
||||
return Err(error.context("application exited before Sidecar fallback"));
|
||||
}
|
||||
let reason = format!("{error:#}");
|
||||
let reason: String = format!("{error:#}").into();
|
||||
RUN_STATE.allow_sidecar_after_service_refusal(reason.clone())?;
|
||||
logging!(warn, Type::Core, "Starting Sidecar with Service unavailable: {reason}");
|
||||
let result = self.start_core_by_sidecar().await;
|
||||
@@ -1590,7 +1590,7 @@ mod tests {
|
||||
started.store(true, Ordering::SeqCst);
|
||||
Ok(())
|
||||
},
|
||||
|reason| store.observe(ServiceHealth::Unavailable(reason)),
|
||||
|reason| store.observe(ServiceHealth::Unavailable(reason.into())),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ use std::num::NonZeroUsize;
|
||||
use std::path::Path;
|
||||
use tauri_plugin_mihomo::MihomoExt as _;
|
||||
use tauri_plugin_shell::ShellExt as _;
|
||||
use tristr::alias::String;
|
||||
|
||||
pub(super) fn explain_core_occupancy(error: anyhow::Error) -> anyhow::Error {
|
||||
let detail = error.to_string();
|
||||
@@ -27,7 +28,7 @@ pub(super) fn explain_core_occupancy(error: anyhow::Error) -> anyhow::Error {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn sidecar_config_without_tun(yaml: &str) -> Result<std::string::String> {
|
||||
fn sidecar_config_without_tun(yaml: &str) -> Result<String> {
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
|
||||
let mut config: Mapping = serde_yaml_ng::from_str(yaml)?;
|
||||
@@ -40,7 +41,7 @@ fn sidecar_config_without_tun(yaml: &str) -> Result<std::string::String> {
|
||||
tun.as_mapping_mut()
|
||||
.context("invalid Sidecar TUN configuration")?
|
||||
.insert(Value::from("enable"), Value::Bool(false));
|
||||
Ok(serde_yaml_ng::to_string(&config)?)
|
||||
Ok(serde_yaml_ng::to_string(&config)?.into())
|
||||
}
|
||||
|
||||
const SIDECAR_READINESS_ATTEMPTS: usize = 30;
|
||||
@@ -153,7 +154,7 @@ impl CoreManager {
|
||||
pub async fn get_clash_logs(&self) -> Result<Vec<String>> {
|
||||
match *self.get_running_mode() {
|
||||
RunningMode::Service => service::get_clash_logs_by_service().await,
|
||||
RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs()),
|
||||
RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs().into_iter().map(Into::into).collect()),
|
||||
RunningMode::NotRunning => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
@@ -257,7 +258,9 @@ impl CoreManager {
|
||||
match event {
|
||||
tauri_plugin_shell::process::CommandEvent::Stdout(line)
|
||||
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
|
||||
let message = String::from_utf8_lossy_owned(line);
|
||||
// from_utf8_lossy_owned exists only on the std String type; the tristr
|
||||
// alias shadows the bare name. LogRing stores std Strings.
|
||||
let message = std::string::String::from_utf8_lossy_owned(line);
|
||||
Logger::global().writer_sidecar_log(Level::Error, &message);
|
||||
CLASH_LOGGER.append_log(message);
|
||||
}
|
||||
@@ -265,12 +268,12 @@ impl CoreManager {
|
||||
let _ = terminated.send(());
|
||||
let manager = Self::global();
|
||||
let _ = manager.invalidate_core_readiness_if(core_readiness_generation);
|
||||
let message = if let Some(code) = term.code {
|
||||
format!("Process terminated with code: {}", code)
|
||||
let message: String = if let Some(code) = term.code {
|
||||
format!("Process terminated with code: {}", code).into()
|
||||
} else if let Some(signal) = term.signal {
|
||||
format!("Process terminated by signal: {}", signal)
|
||||
format!("Process terminated by signal: {}", signal).into()
|
||||
} else {
|
||||
String::from("Process terminated")
|
||||
"Process terminated".into()
|
||||
};
|
||||
Logger::global().writer_sidecar_log(Level::Info, &message);
|
||||
CLASH_LOGGER.clear_logs();
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use super::NoticeStatus;
|
||||
use crate::{APP_HANDLE, singleton};
|
||||
use smartstring::alias::String;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_mihomo::{Mihomo, MihomoExt as _};
|
||||
use tristr::alias::String;
|
||||
|
||||
use super::notification::{FrontendEvent, NotificationSystem};
|
||||
|
||||
@@ -73,7 +70,7 @@ impl Handle {
|
||||
Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid });
|
||||
}
|
||||
|
||||
pub fn notice(status: NoticeStatus, message: impl Into<Arc<str>>) {
|
||||
pub fn notice(status: NoticeStatus, message: impl Into<String>) {
|
||||
Self::send_event(FrontendEvent::NoticeMessage {
|
||||
status,
|
||||
message: message.into(),
|
||||
|
||||
@@ -3,11 +3,10 @@ use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::Mutex;
|
||||
#[cfg(test)]
|
||||
use serde_json::json;
|
||||
use smartstring::alias::String;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::{collections::HashMap, future::Future};
|
||||
use tauri::{AppHandle, Emitter as _, Manager as _, WebviewWindow};
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FrontendEvent<'a> {
|
||||
@@ -17,7 +16,7 @@ pub enum FrontendEvent<'a> {
|
||||
RefreshProxyConfig,
|
||||
NoticeMessage {
|
||||
status: NoticeStatus,
|
||||
message: Arc<str>,
|
||||
message: String,
|
||||
},
|
||||
ProfileChanged {
|
||||
current_profile_id: &'a String,
|
||||
@@ -77,10 +76,8 @@ impl FailedOperation {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PendingFailure {
|
||||
/// Stable code used as the table key.
|
||||
#[specta(type = std::string::String)]
|
||||
pub code: String,
|
||||
/// Full diagnostic context chain.
|
||||
#[specta(type = std::string::String)]
|
||||
pub detail: String,
|
||||
pub operation: FailedOperation,
|
||||
/// Monotonic identity for repeated failures under the same code.
|
||||
@@ -247,7 +244,6 @@ static PENDING_FAILURES: LazyLock<FailureTable> = LazyLock::new(FailureTable::de
|
||||
#[derive(Debug, Default, Clone, serde::Serialize, specta::Type)]
|
||||
pub struct SidecarFailureSnapshot {
|
||||
pub revision: u64,
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub detail: Option<String>,
|
||||
}
|
||||
|
||||
@@ -391,7 +387,7 @@ fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str,
|
||||
(
|
||||
FrontendEvent::NoticeMessage {
|
||||
status: NoticeStatus::Info,
|
||||
message: Arc::from("ok"),
|
||||
message: String::from("ok"),
|
||||
},
|
||||
"verge://notice-message",
|
||||
json!(["info", "ok"]),
|
||||
@@ -456,16 +452,16 @@ frontend_events! {
|
||||
refresh_verge: () = "verge://refresh-verge-config", FrontendEvent::RefreshVerge => ();
|
||||
refresh_profiles: () = "verge://refresh-profiles", FrontendEvent::RefreshProfiles => ();
|
||||
refresh_proxy: () = "verge://refresh-proxy-config", FrontendEvent::RefreshProxyConfig => ();
|
||||
notice: (NoticeStatus, std::string::String) = "verge://notice-message", FrontendEvent::NoticeMessage { status, ref message } => (status, message.to_string());
|
||||
profile_changed: std::string::String = "profile-changed", FrontendEvent::ProfileChanged { current_profile_id } => current_profile_id.to_string();
|
||||
timer_updated: std::string::String = "verge://timer-updated", FrontendEvent::TimerUpdated { profile_index } => profile_index.to_string();
|
||||
profile_started: ProfileUpdatePayload = "profile-update-started", FrontendEvent::ProfileUpdateStarted { uid } => ProfileUpdatePayload { uid: uid.to_string() };
|
||||
profile_completed: ProfileUpdatePayload = "profile-update-completed", FrontendEvent::ProfileUpdateCompleted { uid } => ProfileUpdatePayload { uid: uid.to_string() };
|
||||
notice: (NoticeStatus, String) = "verge://notice-message", FrontendEvent::NoticeMessage { status, ref message } => (status, message.clone());
|
||||
profile_changed: String = "profile-changed", FrontendEvent::ProfileChanged { current_profile_id } => current_profile_id.clone();
|
||||
timer_updated: String = "verge://timer-updated", FrontendEvent::TimerUpdated { profile_index } => profile_index.clone();
|
||||
profile_started: ProfileUpdatePayload = "profile-update-started", FrontendEvent::ProfileUpdateStarted { uid } => ProfileUpdatePayload { uid: uid.clone() };
|
||||
profile_completed: ProfileUpdatePayload = "profile-update-completed", FrontendEvent::ProfileUpdateCompleted { uid } => ProfileUpdatePayload { uid: uid.clone() };
|
||||
run_state: crate::core::runstate::RunStateView = "verge://run-state-changed", FrontendEvent::RunStateChanged { state } => state;
|
||||
pending_failures: () = "verge://pending-failures-changed", FrontendEvent::PendingFailuresChanged => ();
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, specta::Type)]
|
||||
pub struct ProfileUpdatePayload {
|
||||
pub uid: std::string::String,
|
||||
pub uid: String,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ use crate::utils::dirs;
|
||||
use anyhow::{Context as _, Result};
|
||||
use clash_verge_service_ipc::{OwnerCredentials, OwnerIdentity};
|
||||
use std::path::Path;
|
||||
#[cfg(windows)]
|
||||
use tristr::alias::String;
|
||||
|
||||
pub(crate) fn current_owner_credentials() -> Result<OwnerCredentials> {
|
||||
current_owner_credentials_for_root(&dirs::app_home_dir()?)
|
||||
@@ -39,16 +41,16 @@ pub(crate) fn current_owner_credentials_for_root(app_root: &Path) -> Result<Owne
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn windows_owner_credentials(app_data_root: &Path) -> Result<(OwnerIdentity, Option<String>)> {
|
||||
fn windows_owner_credentials(app_data_root: &Path) -> Result<(OwnerIdentity, Option<std::string::String>)> {
|
||||
let sid = windows_owner::current_sid()?;
|
||||
let token = windows_owner::load_or_create_token(app_data_root, &sid)?;
|
||||
Ok((OwnerIdentity::Windows { sid }, Some(token)))
|
||||
Ok((OwnerIdentity::Windows { sid: sid.into() }, Some(token.into())))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn windows_current_identity() -> Result<OwnerIdentity> {
|
||||
Ok(OwnerIdentity::Windows {
|
||||
sid: windows_owner::current_sid()?,
|
||||
sid: windows_owner::current_sid()?.into(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -79,7 +81,7 @@ pub(crate) fn repair_app_data_root_owner(app_data_root: &Path) -> Result<()> {
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn current_user_pipe_sddl() -> Result<String> {
|
||||
let sid = windows_owner::current_sid()?;
|
||||
Ok(format!("D:P(A;;GA;;;{sid})(A;;GA;;;SY)(A;;GA;;;BA)"))
|
||||
Ok(format!("D:P(A;;GA;;;{sid})(A;;GA;;;SY)(A;;GA;;;BA)").into())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -644,17 +646,19 @@ mod windows_owner {
|
||||
}
|
||||
let value = LocalWideString(value);
|
||||
let length = (0..).take_while(|index| unsafe { *value.0.add(*index) } != 0).count();
|
||||
String::from_utf16(unsafe { std::slice::from_raw_parts(value.0, length) })
|
||||
// from_utf16 exists only on the std String type; the tristr alias shadows the bare name.
|
||||
std::string::String::from_utf16(unsafe { std::slice::from_raw_parts(value.0, length) })
|
||||
.context("process SID is not valid UTF-16")
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
fn encode_token(token: &[u8; TOKEN_BYTES]) -> String {
|
||||
let mut encoded = String::with_capacity(TOKEN_BYTES * 2);
|
||||
let mut encoded = std::string::String::with_capacity(TOKEN_BYTES * 2);
|
||||
for byte in token {
|
||||
use std::fmt::Write as _;
|
||||
let _ = write!(encoded, "{byte:02x}");
|
||||
}
|
||||
encoded
|
||||
encoded.into()
|
||||
}
|
||||
|
||||
fn wide_path(path: &Path) -> Result<Vec<u16>> {
|
||||
|
||||
@@ -24,6 +24,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{sync::Mutex, time::timeout};
|
||||
use tristr::alias::String;
|
||||
|
||||
const CLEAR_ATTEMPTS: u32 = 3;
|
||||
const CLEAR_RETRY_DELAY: Duration = Duration::from_millis(100);
|
||||
@@ -320,7 +321,8 @@ fn truncate_utf8(value: &mut String, max_bytes: usize) {
|
||||
while !value.is_char_boundary(boundary) {
|
||||
boundary -= 1;
|
||||
}
|
||||
value.truncate(boundary);
|
||||
let truncated = value.as_str()[..boundary].into();
|
||||
*value = truncated;
|
||||
}
|
||||
|
||||
fn service_bypass(verge: &IVerge) -> Result<String> {
|
||||
@@ -328,11 +330,11 @@ fn service_bypass(verge: &IVerge) -> Result<String> {
|
||||
ensure!(!custom.contains('\0'), "system proxy bypass contains NUL");
|
||||
|
||||
let mut bypass = if custom.is_empty() {
|
||||
MACOS_DEFAULT_BYPASS.to_owned()
|
||||
MACOS_DEFAULT_BYPASS.into()
|
||||
} else if verge.use_default_bypass.unwrap_or(true) {
|
||||
format!("{MACOS_DEFAULT_BYPASS},{custom}")
|
||||
format!("{MACOS_DEFAULT_BYPASS},{custom}").into()
|
||||
} else {
|
||||
custom.to_owned()
|
||||
custom.into()
|
||||
};
|
||||
truncate_utf8(&mut bypass, MAX_SERVICE_BYPASS_LEN);
|
||||
Ok(bypass)
|
||||
@@ -353,7 +355,7 @@ fn service_proxy_config(verge: &IVerge, mixed_port: u16, pac_port: u16) -> Resul
|
||||
Ok(MacosProxyConfig::Global {
|
||||
host: LOOPBACK_HOST.to_owned(),
|
||||
port: mixed_port,
|
||||
bypass: service_bypass(verge)?,
|
||||
bypass: service_bypass(verge)?.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -368,7 +370,7 @@ fn service_apply_result(requested: &MacosProxyConfig, outcome: ProxyApplyOutcome
|
||||
// Direct fallback satisfies a disable request.
|
||||
ProxyApplyOutcome::DirectFallback { .. } if matches!(requested, MacosProxyConfig::Disabled) => Ok(()),
|
||||
ProxyApplyOutcome::DirectFallback { message } => {
|
||||
Err(SysproxyFailure::DirectFallback { detail: message }.into())
|
||||
Err(SysproxyFailure::DirectFallback { detail: message.into() }.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -644,7 +646,7 @@ const GUARD_FAILURES_BEFORE_STOPPING: u32 = 3;
|
||||
|
||||
async fn report_guard_stopped(captured_generation: u64, error: &anyhow::Error) {
|
||||
let stopped = anyhow::Error::new(SysproxyFailure::GuardStopped {
|
||||
detail: format!("{error:#}"),
|
||||
detail: format!("{error:#}").into(),
|
||||
});
|
||||
// Do not publish a late failure from a replaced guard.
|
||||
let recorded = SERVICE_PROXY_OPERATIONS
|
||||
@@ -1106,7 +1108,7 @@ mod tests {
|
||||
let requested = MacosProxyConfig::Global {
|
||||
host: "127.0.0.1".to_owned(),
|
||||
port: 7890,
|
||||
bypass: String::new(),
|
||||
bypass: std::string::String::new(),
|
||||
};
|
||||
|
||||
let error = service_apply_result(
|
||||
@@ -1146,8 +1148,8 @@ mod tests {
|
||||
let events = Mutex::new(Vec::new());
|
||||
super::settle_table(
|
||||
effect,
|
||||
|asked| events.lock().push(format!("retire:{asked:?}")),
|
||||
|operation, _| events.lock().push(format!("file:{operation:?}")),
|
||||
|asked| events.lock().push(format!("retire:{asked:?}").into()),
|
||||
|operation, _| events.lock().push(format!("file:{operation:?}").into()),
|
||||
);
|
||||
events.into_inner()
|
||||
}
|
||||
@@ -1276,7 +1278,7 @@ mod tests {
|
||||
let combined = rollback_failure(
|
||||
mappable(),
|
||||
anyhow::anyhow!("the service could not set it").context(SysproxyFailure::DirectFallback {
|
||||
detail: "fell back to direct".to_owned(),
|
||||
detail: "fell back to direct".into(),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use serde::{Serialize, Serializer, ser::SerializeMap as _};
|
||||
use serde::Serialize;
|
||||
use tauri_plugin_mihomo::models::{
|
||||
DelayHistory, Extra, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
|
||||
DelayHistory, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
pub struct ProxyViewInput {
|
||||
pub runtime_group_order: Vec<String>,
|
||||
@@ -57,28 +58,6 @@ pub struct ProxyCapabilities {
|
||||
pub smux: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, specta::Type)]
|
||||
struct ProxyLatencyHistory<'a> {
|
||||
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
||||
history: &'a [DelayHistory],
|
||||
}
|
||||
|
||||
fn serialize_proxy_latency_history<S: Serializer>(
|
||||
extra: &HashMap<String, Extra>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let mut map = serializer.serialize_map(Some(extra.len()))?;
|
||||
for (url, extra) in extra {
|
||||
map.serialize_entry(
|
||||
url,
|
||||
&ProxyLatencyHistory {
|
||||
history: &extra.history,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProxyGroupView {
|
||||
@@ -98,9 +77,6 @@ pub struct ProxyGroupView {
|
||||
pub test_url: Option<String>,
|
||||
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
||||
pub history: Vec<DelayHistory>,
|
||||
#[serde(serialize_with = "serialize_proxy_latency_history")]
|
||||
#[specta(type = HashMap<String, ProxyLatencyHistory<'static>>)]
|
||||
pub extra: HashMap<String, Extra>,
|
||||
#[serde(flatten)]
|
||||
pub capabilities: ProxyCapabilities,
|
||||
pub members: Vec<ProxyMemberRef>,
|
||||
@@ -116,9 +92,6 @@ pub struct ProxyNodeView {
|
||||
pub proxy_type: ProxyType,
|
||||
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
||||
pub history: Vec<DelayHistory>,
|
||||
#[serde(serialize_with = "serialize_proxy_latency_history")]
|
||||
#[specta(type = HashMap<String, ProxyLatencyHistory<'static>>)]
|
||||
pub extra: HashMap<String, Extra>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -284,7 +257,7 @@ impl ProxyViewBuilder {
|
||||
} else {
|
||||
ProxyViewProviderState::Unavailable
|
||||
};
|
||||
let (mut core_groups, core_nodes) = partition_core(proxies, providers.is_some());
|
||||
let (mut core_groups, core_nodes) = partition_core(proxies);
|
||||
let (mut records, core_node_ids) = build_core_records(core_nodes);
|
||||
let (providers, provider_candidates) = build_provider_records(providers, &mut records);
|
||||
let resolver = MemberResolver {
|
||||
@@ -298,7 +271,7 @@ impl ProxyViewBuilder {
|
||||
|
||||
let global = core_groups
|
||||
.remove("GLOBAL")
|
||||
.map(|proxy| build_group("GLOBAL".to_owned(), proxy, &resolver));
|
||||
.map(|proxy| build_group(String::from("GLOBAL"), proxy, &resolver));
|
||||
let (groups, order_source) = build_ordered_groups(core_groups, runtime_group_order, &resolver);
|
||||
let direct = core_node_ids.get("DIRECT").cloned();
|
||||
let standalone = build_standalone(&core_node_ids);
|
||||
@@ -317,14 +290,15 @@ impl ProxyViewBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
fn partition_core(proxies: Proxies, providers_available: bool) -> (BTreeMap<String, Proxy>, BTreeMap<String, Proxy>) {
|
||||
fn partition_core(proxies: Proxies) -> (BTreeMap<String, Proxy>, BTreeMap<String, Proxy>) {
|
||||
let mut groups = BTreeMap::new();
|
||||
let mut nodes = BTreeMap::new();
|
||||
|
||||
for (name, proxy) in proxies.proxies {
|
||||
let name = String::from(name);
|
||||
if proxy.all.is_some() {
|
||||
groups.insert(name, proxy);
|
||||
} else if !providers_available || proxy.provider_name.is_empty() {
|
||||
} else {
|
||||
nodes.insert(name, proxy);
|
||||
}
|
||||
}
|
||||
@@ -339,7 +313,7 @@ fn build_core_records(
|
||||
let mut ids = BTreeMap::new();
|
||||
|
||||
for (index, (name, proxy)) in core_nodes.into_iter().enumerate() {
|
||||
let record_id = format!("c:{index}");
|
||||
let record_id = String::from(format!("c:{index}"));
|
||||
ids.insert(name.clone(), record_id.clone());
|
||||
records.insert(
|
||||
record_id.clone(),
|
||||
@@ -360,7 +334,13 @@ fn build_provider_records(
|
||||
records: &mut BTreeMap<String, ProxyNodeView>,
|
||||
) -> (Vec<ProxyProviderView>, ProviderCandidates) {
|
||||
let providers = providers
|
||||
.map(|providers| providers.providers.into_iter().collect::<BTreeMap<_, _>>())
|
||||
.map(|providers| {
|
||||
providers
|
||||
.providers
|
||||
.into_iter()
|
||||
.map(|(name, provider)| (String::from(name), provider))
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut views = Vec::new();
|
||||
let mut candidates = BTreeMap::new();
|
||||
@@ -383,9 +363,9 @@ fn build_provider_records(
|
||||
let mut proxy_record_ids = Vec::new();
|
||||
|
||||
for (member_index, proxy) in proxies.into_iter().enumerate() {
|
||||
let record_id = format!("p:{provider_index}:{member_index}");
|
||||
let proxy_name = proxy.name.clone();
|
||||
let node_name = proxy.name.clone();
|
||||
let record_id = String::from(format!("p:{provider_index}:{member_index}"));
|
||||
let proxy_name = String::from(proxy.name.as_str());
|
||||
let node_name = String::from(proxy.name.as_str());
|
||||
candidates
|
||||
.entry(proxy_name.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
@@ -408,7 +388,7 @@ fn build_provider_records(
|
||||
views.push(ProxyProviderView {
|
||||
name: provider_name,
|
||||
vehicle_type,
|
||||
updated_at,
|
||||
updated_at: updated_at.map(String::from),
|
||||
subscription_info: subscription_info.map(|subscription_info| ProxySubscriptionInfo {
|
||||
upload: subscription_info.upload,
|
||||
download: subscription_info.download,
|
||||
@@ -431,7 +411,6 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
|
||||
now,
|
||||
test_url,
|
||||
history,
|
||||
extra,
|
||||
udp,
|
||||
xudp,
|
||||
tfo,
|
||||
@@ -446,6 +425,7 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|member| {
|
||||
let member = String::from(member);
|
||||
let (occurrence, scoped) = seen
|
||||
.entry(member.clone())
|
||||
.or_insert_with(|| (0, resolver.scoped_candidates(&name, &member)));
|
||||
@@ -458,13 +438,12 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
|
||||
ProxyGroupView {
|
||||
name,
|
||||
proxy_type,
|
||||
now,
|
||||
fixed,
|
||||
now: now.map(String::from),
|
||||
fixed: fixed.map(String::from),
|
||||
hidden,
|
||||
icon,
|
||||
test_url,
|
||||
icon: icon.map(String::from),
|
||||
test_url: test_url.map(String::from),
|
||||
history,
|
||||
extra,
|
||||
capabilities: ProxyCapabilities {
|
||||
udp,
|
||||
xudp,
|
||||
@@ -483,7 +462,6 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
|
||||
icon,
|
||||
test_url,
|
||||
history,
|
||||
extra,
|
||||
udp,
|
||||
xudp,
|
||||
tfo,
|
||||
@@ -498,11 +476,10 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
|
||||
name,
|
||||
proxy_type,
|
||||
history,
|
||||
extra,
|
||||
id,
|
||||
id: id.map(String::from),
|
||||
hidden,
|
||||
icon,
|
||||
test_url,
|
||||
icon: icon.map(String::from),
|
||||
test_url: test_url.map(String::from),
|
||||
capabilities: ProxyCapabilities {
|
||||
udp,
|
||||
xudp,
|
||||
@@ -552,62 +529,3 @@ fn build_standalone(core_node_ids: &BTreeMap<String, String>) -> Vec<String> {
|
||||
);
|
||||
standalone
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn provider_echo_in_proxies_does_not_override_group_membership() -> anyhow::Result<()> {
|
||||
let view = ProxyViewBuilder::build(ProxyViewInput {
|
||||
runtime_group_order: vec!["A".into(), "B".into()],
|
||||
group_scopes: ["A", "B"]
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
(
|
||||
name.into(),
|
||||
GroupScope {
|
||||
providers: vec![name.into()],
|
||||
exclude_types: vec![],
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
proxies: serde_json::from_value(json!({"proxies": {
|
||||
"A": {"all": ["node"], "type": "Selector"},
|
||||
"B": {"all": ["node"], "type": "Selector"},
|
||||
"node": {"name": "node", "provider-name": "A", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 32}]}
|
||||
}}))?,
|
||||
providers: Some(serde_json::from_value(json!({"providers": {
|
||||
"A": {"vehicleType": "Inline", "proxies": [{"name": "node", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 32}]}]},
|
||||
"B": {"vehicleType": "Inline", "proxies": [{"name": "node", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 123}]}]}
|
||||
}}))?),
|
||||
});
|
||||
for (group, expected) in view.groups.iter().zip([32, 123]) {
|
||||
let ProxyMemberRef::Node { record_id, .. } = &group.members[0] else {
|
||||
anyhow::bail!("Expected node")
|
||||
};
|
||||
let record = &view.records[record_id];
|
||||
assert_eq!(record.history.last().map(|entry| entry.delay), Some(expected));
|
||||
assert!(
|
||||
matches!(&record.source, ProxyNodeSource::Provider { provider_name, .. } if provider_name == &group.name)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn unavailable_provider_keeps_core_echo_interactable() -> anyhow::Result<()> {
|
||||
let view = ProxyViewBuilder::build(ProxyViewInput {
|
||||
runtime_group_order: vec!["A".into()],
|
||||
group_scopes: BTreeMap::new(),
|
||||
proxies: serde_json::from_value(json!({"proxies": {
|
||||
"A": {"all": ["node"], "type": "Selector"},
|
||||
"node": {"name": "node", "provider-name": "provider"}
|
||||
}}))?,
|
||||
providers: None,
|
||||
});
|
||||
assert!(matches!(&view.groups[0].members[0], ProxyMemberRef::Node { .. }));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ impl RunStateEnv for RealEnv {
|
||||
Ok(ServiceVersionReply {
|
||||
core,
|
||||
code: response.code,
|
||||
message: response.message,
|
||||
message: response.message.into(),
|
||||
protocol: response.data,
|
||||
})
|
||||
}
|
||||
@@ -123,6 +123,7 @@ mod fake {
|
||||
|
||||
use super::{PendingAction, RunState, RunStateEnv, ServiceVersionReply};
|
||||
use crate::core::manager::RunningMode;
|
||||
use tristr::alias::String;
|
||||
|
||||
/// A fail-closed scripted machine that records outbound effects.
|
||||
#[derive(Debug)]
|
||||
@@ -163,7 +164,7 @@ mod fake {
|
||||
self.with_evidence(true).always_replying(Ok(ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 0,
|
||||
message: "ok".to_owned(),
|
||||
message: "ok".into(),
|
||||
protocol: Some(ProtocolInfo::current()),
|
||||
}))
|
||||
}
|
||||
@@ -173,7 +174,7 @@ mod fake {
|
||||
self.with_evidence(true).always_replying(Ok(ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 0,
|
||||
message: "ok".to_owned(),
|
||||
message: "ok".into(),
|
||||
protocol: None,
|
||||
}))
|
||||
}
|
||||
@@ -181,12 +182,12 @@ mod fake {
|
||||
#[must_use]
|
||||
pub fn service_unreachable(self) -> Self {
|
||||
self.with_evidence(true)
|
||||
.always_replying(Err("ipc transport refused".to_owned()))
|
||||
.always_replying(Err("ipc transport refused".into()))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn evidence_unavailable(mut self) -> Self {
|
||||
self.evidence = Err("registry probe failed".to_owned());
|
||||
self.evidence = Err("registry probe failed".into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -236,7 +237,7 @@ mod fake {
|
||||
|
||||
#[must_use]
|
||||
pub fn privileged_operations_fail(self, reason: &str) -> Self {
|
||||
*self.privileged_outcome.lock() = Err(reason.to_owned());
|
||||
*self.privileged_outcome.lock() = Err(reason.into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -256,7 +257,7 @@ mod fake {
|
||||
replies
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Err("no service configured".to_owned()))
|
||||
.unwrap_or_else(|| Err("no service configured".into()))
|
||||
};
|
||||
reply.map_err(|error| anyhow!(error))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! Core lifecycle remains in `CoreManager`.
|
||||
|
||||
use crate::core::manager::RunningMode;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub enum ServiceHealth {
|
||||
|
||||
@@ -173,7 +173,7 @@ impl<E: RunStateEnv> RunStateStore<E> {
|
||||
Type::Service,
|
||||
"failed to inspect trusted service evidence: {error:#}"
|
||||
);
|
||||
return ServiceHealth::Unavailable(format!("service detection failed: {error:#}"));
|
||||
return ServiceHealth::Unavailable(format!("service detection failed: {error:#}").into());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -331,7 +331,7 @@ impl<E: RunStateEnv> RunStateStore<E> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn allow_sidecar_after_service_refusal(&self, reason: String) -> Result<()> {
|
||||
pub fn allow_sidecar_after_service_refusal(&self, reason: tristr::alias::String) -> Result<()> {
|
||||
let mut state = self.service.lock();
|
||||
if self.operation_running.load(Ordering::Acquire) || state.service.pending.is_some() {
|
||||
bail!("cannot fall back while a service operation is pending");
|
||||
@@ -496,7 +496,7 @@ mod tests {
|
||||
ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 0,
|
||||
message: "ok".to_owned(),
|
||||
message: "ok".into(),
|
||||
protocol: Some(clash_verge_service_ipc::ProtocolInfo::current()),
|
||||
}
|
||||
}
|
||||
@@ -713,7 +713,7 @@ mod tests {
|
||||
for health in [
|
||||
ServiceHealth::NotInstalled,
|
||||
ServiceHealth::VersionMismatch,
|
||||
ServiceHealth::Unavailable("boom".to_owned()),
|
||||
ServiceHealth::Unavailable("boom".into()),
|
||||
] {
|
||||
let store = with_env(FakeEnv::new());
|
||||
store.observe(health.clone());
|
||||
@@ -858,7 +858,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn awaiting_readiness_retries_a_silent_service_then_accepts_its_reply() {
|
||||
let store = with_env(FakeEnv::new().replying(vec![Err("ipc path not ready".to_owned()), Ok(ready_reply())]));
|
||||
let store = with_env(FakeEnv::new().replying(vec![Err("ipc path not ready".into()), Ok(ready_reply())]));
|
||||
|
||||
let state = store
|
||||
.await_ready(2, Duration::ZERO)
|
||||
|
||||
@@ -9,6 +9,7 @@ use clash_verge_service_ipc::{
|
||||
};
|
||||
|
||||
use super::health::ServiceHealth;
|
||||
use tristr::alias::String;
|
||||
|
||||
/// What the Service replied to a protocol-version query.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -50,7 +51,7 @@ pub fn classify_service_version_reply(reply: &ServiceVersionReply) -> ServiceVer
|
||||
return match &reply.core {
|
||||
Some(clash_verge_service_ipc::CoreAvailability::Ready) => ServiceVersionCheck::Ready,
|
||||
Some(clash_verge_service_ipc::CoreAvailability::Rejected { reason }) => {
|
||||
ServiceVersionCheck::CoreUnavailable(format!("{CORE_REJECTED_PREFIX}{reason}"))
|
||||
ServiceVersionCheck::CoreUnavailable(format!("{CORE_REJECTED_PREFIX}{reason}").into())
|
||||
}
|
||||
_ => ServiceVersionCheck::CoreUnavailable(
|
||||
"the selected approved core is missing or does not match; reinstall the service to repair it".into(),
|
||||
@@ -77,9 +78,12 @@ pub fn classify_service_version_reply(reply: &ServiceVersionReply) -> ServiceVer
|
||||
reply.code, reply.message, client.epoch
|
||||
)
|
||||
};
|
||||
ServiceVersionCheck::NeedsReinstall(format!(
|
||||
"Service helper version or protocol mismatch: {detail}. Choose Reinstall to uninstall the existing service and install the required version"
|
||||
))
|
||||
ServiceVersionCheck::NeedsReinstall(
|
||||
format!(
|
||||
"Service helper version or protocol mismatch: {detail}. Choose Reinstall to uninstall the existing service and install the required version"
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Classify a version reply into the probe outcome used by [`classify_service_health`].
|
||||
@@ -104,7 +108,7 @@ pub fn classify_service_health(
|
||||
match current {
|
||||
CurrentServiceProbe::Ready => ServiceHealth::Ready,
|
||||
CurrentServiceProbe::VersionMismatch => ServiceHealth::VersionMismatch,
|
||||
CurrentServiceProbe::Unavailable => ServiceHealth::Unavailable(unavailable_reason.to_owned()),
|
||||
CurrentServiceProbe::Unavailable => ServiceHealth::Unavailable(unavailable_reason.into()),
|
||||
CurrentServiceProbe::Missing if has_install_marker => ServiceHealth::VersionMismatch,
|
||||
CurrentServiceProbe::Missing => ServiceHealth::NotInstalled,
|
||||
}
|
||||
@@ -119,7 +123,7 @@ mod tests {
|
||||
ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 0,
|
||||
message: "ok".to_owned(),
|
||||
message: "ok".into(),
|
||||
protocol: Some(ProtocolInfo::current()),
|
||||
}
|
||||
}
|
||||
@@ -154,7 +158,7 @@ mod tests {
|
||||
let reply = ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 0,
|
||||
message: "ok".to_owned(),
|
||||
message: "ok".into(),
|
||||
protocol: None,
|
||||
};
|
||||
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
|
||||
@@ -172,7 +176,7 @@ mod tests {
|
||||
let reply = ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 7,
|
||||
message: "nope".to_owned(),
|
||||
message: "nope".into(),
|
||||
protocol: Some(ProtocolInfo::current()),
|
||||
};
|
||||
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
|
||||
@@ -188,7 +192,7 @@ mod tests {
|
||||
let reply = ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 0,
|
||||
message: "ok".to_owned(),
|
||||
message: "ok".into(),
|
||||
protocol: Some(info),
|
||||
};
|
||||
assert!(matches!(
|
||||
@@ -209,7 +213,7 @@ mod tests {
|
||||
let reply = ServiceVersionReply {
|
||||
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
|
||||
code: 0,
|
||||
message: "ok".to_owned(),
|
||||
message: "ok".into(),
|
||||
protocol: Some(info),
|
||||
};
|
||||
assert!(matches!(
|
||||
@@ -228,7 +232,7 @@ mod tests {
|
||||
let reply = ServiceVersionReply {
|
||||
core: None,
|
||||
code,
|
||||
message: "unsupported command".to_owned(),
|
||||
message: "unsupported command".into(),
|
||||
protocol: protocol.clone(),
|
||||
};
|
||||
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
|
||||
@@ -250,7 +254,7 @@ mod tests {
|
||||
fn an_unreachable_service_keeps_the_reason() {
|
||||
assert_eq!(
|
||||
classify_service_health(CurrentServiceProbe::Unavailable, true, "socket refused"),
|
||||
ServiceHealth::Unavailable("socket refused".to_owned())
|
||||
ServiceHealth::Unavailable("socket refused".into())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use serde_yaml_ng::{Mapping, Value};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use tristr::alias::String;
|
||||
|
||||
pub(crate) const GEO_ASSETS: &[&str] = &[
|
||||
"Country.mmdb",
|
||||
@@ -64,7 +65,7 @@ struct ProviderPathOwners {
|
||||
pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root: &Path) -> Result<()> {
|
||||
let config_root = std::fs::canonicalize(config_root)?;
|
||||
let mut paths = BTreeMap::<String, ProviderPathOwners>::new();
|
||||
let mut reserved: HashSet<String> = GEO_ASSETS.iter().map(|name| (*name).to_owned()).collect();
|
||||
let mut reserved: HashSet<String> = GEO_ASSETS.iter().map(|name| (*name).into()).collect();
|
||||
for section in ["proxy-providers", "rule-providers"] {
|
||||
let Some(providers) = config.get(section).and_then(Value::as_mapping) else {
|
||||
continue;
|
||||
@@ -89,7 +90,7 @@ pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root:
|
||||
match (is_remote, provider.get("url").and_then(Value::as_str)) {
|
||||
(true, Some(url)) => owners
|
||||
.remote
|
||||
.entry(url.to_owned())
|
||||
.entry(url.into())
|
||||
.or_default()
|
||||
.push((section, name.clone())),
|
||||
_ => owners.local = true,
|
||||
@@ -105,7 +106,7 @@ pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root:
|
||||
for (url, providers) in owners.remote {
|
||||
let replacement = allocate_provider_destination(&destination, &url, &mut reserved);
|
||||
for (section, name) in providers {
|
||||
config[section][&name]["path"] = Value::String(replacement.clone());
|
||||
config[section][&name]["path"] = Value::String(replacement.clone().into());
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
@@ -131,15 +132,16 @@ fn allocate_provider_destination(destination: &str, url: &str, reserved: &mut Ha
|
||||
let mut suffix = String::new();
|
||||
let mut sequence = 0;
|
||||
loop {
|
||||
let candidate = path
|
||||
let candidate: String = path
|
||||
.with_file_name(format!("cvr-{digest}{suffix}{extension}"))
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
.replace('\\', "/")
|
||||
.into();
|
||||
if reserved.insert(candidate.clone()) {
|
||||
return candidate;
|
||||
}
|
||||
sequence += 1;
|
||||
suffix = format!("-{sequence}");
|
||||
suffix = format!("-{sequence}").into();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +169,7 @@ fn collect_runtime_parts(mut config: Value, config_root: &Path) -> Result<Runtim
|
||||
)?;
|
||||
for filename in GEO_ASSETS {
|
||||
let source = config_root.join(filename);
|
||||
if source.is_file() && destinations.insert((*filename).to_string()) {
|
||||
if source.is_file() && destinations.insert((*filename).into()) {
|
||||
assets.push(RuntimeAsset {
|
||||
source: std::fs::canonicalize(&source)?.to_string_lossy().into_owned(),
|
||||
destination: (*filename).to_string(),
|
||||
@@ -228,9 +230,9 @@ fn collect_provider_assets(
|
||||
}
|
||||
remote_providers.push(RemoteProviderRef {
|
||||
section,
|
||||
name: name.as_str().unwrap_or_default().to_owned(),
|
||||
name: name.as_str().unwrap_or_default().into(),
|
||||
provider: RemoteProvider {
|
||||
destination: destination.clone(),
|
||||
destination: destination.clone().into(),
|
||||
url,
|
||||
},
|
||||
});
|
||||
@@ -259,13 +261,13 @@ fn collect_provider_assets(
|
||||
if destinations.insert(destination.clone()) {
|
||||
assets.push(RuntimeAsset {
|
||||
source: source.to_string_lossy().into_owned(),
|
||||
destination: destination.clone(),
|
||||
destination: destination.clone().into(),
|
||||
});
|
||||
}
|
||||
destination
|
||||
}
|
||||
};
|
||||
provider.insert(Value::String("path".to_owned()), Value::String(destination));
|
||||
provider.insert(Value::String("path".to_owned()), Value::String(destination.into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -339,7 +341,7 @@ fn normalized_destination(relative: &Path) -> Result<String> {
|
||||
if destination.as_os_str().is_empty() {
|
||||
bail!("local runtime provider destination is empty");
|
||||
}
|
||||
Ok(destination.to_string_lossy().replace('\\', "/"))
|
||||
Ok(destination.to_string_lossy().replace('\\', "/").into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -385,12 +387,12 @@ mod tests {
|
||||
for declared in readback {
|
||||
assert!(bundle.remote_providers.contains(&declared.provider));
|
||||
assert_eq!(
|
||||
config[declared.section][&declared.name]["path"],
|
||||
config[declared.section][declared.name.as_str()]["path"],
|
||||
declared.provider.destination
|
||||
);
|
||||
assert_eq!(
|
||||
config[declared.section][&declared.name]["url"],
|
||||
original[declared.section][&declared.name]["url"]
|
||||
config[declared.section][declared.name.as_str()]["url"],
|
||||
original[declared.section][declared.name.as_str()]["url"]
|
||||
);
|
||||
}
|
||||
std::fs::remove_dir_all(root)?;
|
||||
|
||||
@@ -36,6 +36,7 @@ use std::{
|
||||
sync::atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
static OWNER_MONITOR_GENERATION: AtomicU64 = AtomicU64::new(0);
|
||||
static ACTIVE_SERVICE_SESSION: Mutex<Option<ActiveServiceSession>> = Mutex::new(None);
|
||||
@@ -62,7 +63,7 @@ pub(crate) fn is_not_auto_started(reason: &str) -> bool {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn notify_service_fallback(reason: &str) {
|
||||
*PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(reason.to_owned());
|
||||
*PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(reason.into());
|
||||
Handle::notice(NoticeStatus::ServiceCoreSidecarFallback, "");
|
||||
}
|
||||
|
||||
@@ -91,7 +92,7 @@ fn app_data_owner_command(credentials: &OwnerCredentials) -> Option<String> {
|
||||
return None;
|
||||
};
|
||||
let path = credentials.app_data_dir.replace('\'', r"'\''");
|
||||
Some(format!("sudo chown -R {uid}:{gid} '{path}'"))
|
||||
Some(format!("sudo chown -R {uid}:{gid} '{path}'").into())
|
||||
}
|
||||
|
||||
/// Capabilities of the service session that owns the running Core.
|
||||
@@ -243,22 +244,24 @@ fn macos_service_install_markers() -> Vec<String> {
|
||||
format!(
|
||||
"/Library/LaunchDaemons/{}.plist",
|
||||
clash_verge_service_ipc::MACOS_SERVICE_ID
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
format!(
|
||||
"/Library/PrivilegedHelperTools/{}.bundle",
|
||||
clash_verge_service_ipc::MACOS_SERVICE_ID
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
"/Library/LaunchDaemons/io.github.clashverge.helper.plist".to_owned(),
|
||||
"/Library/LaunchDaemons/io.github.clashverge.helper.plist".into(),
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
"/Library/PrivilegedHelperTools/io.github.clashverge.helper".to_owned(),
|
||||
"/Library/PrivilegedHelperTools/io.github.clashverge.helper".into(),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn macos_service_install_marker_exists() -> std::io::Result<bool> {
|
||||
for marker in macos_service_install_markers() {
|
||||
if path_entry_exists_without_follow(Path::new(&marker))? {
|
||||
if path_entry_exists_without_follow(Path::new(marker.as_str()))? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
@@ -335,7 +338,8 @@ pub(crate) fn trusted_service_evidence() -> Result<bool> {
|
||||
output.status
|
||||
);
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim() != "not-found")
|
||||
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
|
||||
Ok(std::string::String::from_utf8_lossy(&output.stdout).trim() != "not-found")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -564,12 +568,12 @@ fn service_core_path(clash_core: &str, bin_ext: &str) -> Result<PathBuf> {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn escape_osascript_double_quoted_string(value: &str) -> String {
|
||||
value.replace('\\', "\\\\").replace('"', "\\\"")
|
||||
value.replace('\\', "\\\\").replace('"', "\\\"").into()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn shell_single_quote(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\'', r"'\''"))
|
||||
format!("'{}'", value.replace('\'', r"'\''")).into()
|
||||
}
|
||||
|
||||
fn packaged_service_tool_path(file_name: &str, packaged_path: impl FnOnce() -> Result<PathBuf>) -> Result<PathBuf> {
|
||||
@@ -641,7 +645,7 @@ fn uninstall_service() -> Result<()> {
|
||||
let status = if linux_running_as_root() {
|
||||
StdCommand::new(&uninstall_path).status()?
|
||||
} else {
|
||||
let result = StdCommand::new(&elevator)
|
||||
let result = StdCommand::new(elevator.as_str())
|
||||
.arg("--disable-internal-agent")
|
||||
.arg(&uninstall_path)
|
||||
.status()?;
|
||||
@@ -697,7 +701,7 @@ fn uninstall_service() -> Result<()> {
|
||||
}
|
||||
|
||||
let uninstall_path = macos_service_tool_path(&uninstall_path)?;
|
||||
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
|
||||
let uninstall_shell: String = uninstall_path.to_string_lossy().into();
|
||||
|
||||
// clash_verge_i18n::sync_locale(Config::verge().await.latest_arc().language.as_deref());
|
||||
|
||||
@@ -843,7 +847,7 @@ pub(super) async fn stage_runtime_by_service(config_file: &Path) -> Result<Stage
|
||||
if response.code > 0 {
|
||||
return Ok(StageRequest::Refused {
|
||||
code: response.code,
|
||||
message: response.message,
|
||||
message: response.message.into(),
|
||||
});
|
||||
}
|
||||
response
|
||||
@@ -879,7 +883,7 @@ fn record_service_start_refusal<E: RunStateEnv>(
|
||||
if store.state().mode == crate::core::manager::RunningMode::NotRunning
|
||||
&& refusal.code != ServiceErrorCode::ProxyClearFailed as u16
|
||||
{
|
||||
store.observe(ServiceHealth::Unavailable(refusal.to_string()));
|
||||
store.observe(ServiceHealth::Unavailable(refusal.to_string().into()));
|
||||
}
|
||||
refusal.into()
|
||||
}
|
||||
@@ -887,7 +891,7 @@ fn record_service_start_refusal<E: RunStateEnv>(
|
||||
/// Sidecar stays blocked until a reinstall replaces the residual helper, so ask for repair.
|
||||
pub(super) fn record_residual_service(error: &anyhow::Error) {
|
||||
if let Some(residual) = error.downcast_ref::<clash_verge_service_ipc::execution::ResidualServiceError>() {
|
||||
RUN_STATE.observe(ServiceHealth::Unavailable(residual.to_string()));
|
||||
RUN_STATE.observe(ServiceHealth::Unavailable(residual.to_string().into()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -901,7 +905,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
|
||||
let proposed_session_token = generate_service_session_token()?;
|
||||
let request = StartClashRequest {
|
||||
runtime,
|
||||
proposed_session_token: proposed_session_token.clone(),
|
||||
proposed_session_token: proposed_session_token.clone().into(),
|
||||
macos_proxy: None,
|
||||
};
|
||||
|
||||
@@ -939,8 +943,8 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
|
||||
&RUN_STATE,
|
||||
ServiceStartRefusal {
|
||||
code: response.code,
|
||||
core_path: request.runtime.core_path,
|
||||
message: err_msg,
|
||||
core_path: request.runtime.core_path.into(),
|
||||
message: err_msg.into(),
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -952,7 +956,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
|
||||
*ACTIVE_SERVICE_SESSION.lock() = Some(ActiveServiceSession {
|
||||
proof: OwnerSessionProof {
|
||||
generation: result.session.generation,
|
||||
token: proposed_session_token,
|
||||
token: proposed_session_token.into(),
|
||||
},
|
||||
supports_runtime_staging: capabilities.runtime_staging,
|
||||
supports_runtime_file_read: capabilities.runtime_file_read,
|
||||
@@ -1012,7 +1016,10 @@ pub(super) async fn get_clash_logs_by_service() -> Result<Vec<String>> {
|
||||
bail!(err_msg);
|
||||
}
|
||||
|
||||
Ok(response.data.unwrap_or_default())
|
||||
Ok(response
|
||||
.data
|
||||
.map(|logs| logs.into_iter().map(Into::into).collect())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_clash_log_snapshot_by_service() -> Result<String> {
|
||||
@@ -1030,7 +1037,8 @@ pub(crate) async fn get_clash_log_snapshot_by_service() -> Result<String> {
|
||||
}
|
||||
let encoded = response.data.context("服务未返回核心日志快照")?;
|
||||
let content = decode_hex(&encoded).context("服务返回了无效的核心日志快照")?;
|
||||
Ok(String::from_utf8_lossy_owned(content))
|
||||
// from_utf8_lossy_owned exists only on the std String type; the tristr alias shadows the bare name.
|
||||
Ok(std::string::String::from_utf8_lossy_owned(content).into())
|
||||
}
|
||||
|
||||
fn decode_hex(encoded: &str) -> Result<Vec<u8>> {
|
||||
@@ -1344,7 +1352,7 @@ async fn read_chunk(
|
||||
}
|
||||
Ok(Some(match response.data.context("服务未返回运行时文件")? {
|
||||
RuntimeFileOutcome::Absent => (None, 0, None),
|
||||
RuntimeFileOutcome::Chunk { hex, len, mtime_ns } => (Some(hex), len, mtime_ns),
|
||||
RuntimeFileOutcome::Chunk { hex, len, mtime_ns } => (Some(hex.into()), len, mtime_ns),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1356,11 +1364,11 @@ async fn provider_load_times() -> Result<ProviderLoadTimes> {
|
||||
let proxies = proxies.map_err(|error| anyhow!("failed to query proxy providers: {error}"))?;
|
||||
let mut loaded = ProviderLoadTimes::new();
|
||||
for (name, provider) in rules.providers {
|
||||
loaded.insert(("rule-providers", name), epoch_nanos(&provider.updated_at));
|
||||
loaded.insert(("rule-providers", name.into()), epoch_nanos(&provider.updated_at));
|
||||
}
|
||||
for (name, provider) in proxies.providers {
|
||||
loaded.insert(
|
||||
("proxy-providers", name),
|
||||
("proxy-providers", name.into()),
|
||||
provider.updated_at.as_deref().and_then(epoch_nanos),
|
||||
);
|
||||
}
|
||||
@@ -1586,7 +1594,7 @@ const fn owner_recovery_policy(reason: OwnerRecoveryReason, is_macos: bool) -> O
|
||||
fn mark_service_unavailable_after_owner_loss<E: RunStateEnv>(store: &RunStateStore<E>, reason: OwnerRecoveryReason) {
|
||||
if matches!(reason, OwnerRecoveryReason::TransportFailure) {
|
||||
store.observe(ServiceHealth::Unavailable(
|
||||
"service control IPC unavailable after sustained transport failure".to_owned(),
|
||||
"service control IPC unavailable after sustained transport failure".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1847,7 +1855,7 @@ async fn wait_for_service_ipc() -> Result<()> {
|
||||
}
|
||||
Err(ReadyWaitError::Unreachable(error)) => {
|
||||
tracing::Span::current().record("outcome", "unreachable");
|
||||
RUN_STATE.observe(ServiceHealth::Unavailable(format!("{CONTEXT}: {error:#}")));
|
||||
RUN_STATE.observe(ServiceHealth::Unavailable(format!("{CONTEXT}: {error:#}").into()));
|
||||
Err(error).context(CONTEXT)
|
||||
}
|
||||
Err(ReadyWaitError::Rejected(error)) => {
|
||||
@@ -2662,7 +2670,7 @@ mod tests {
|
||||
let outcome = super::run_action_restoring_sidecar(&store, was_allowed, async {
|
||||
store.perform(PendingAction::Install).await?;
|
||||
// Simulate `wait_for_service_ipc` recording health before it fails.
|
||||
store.observe(ServiceHealth::Unavailable("service never answered".to_owned()));
|
||||
store.observe(ServiceHealth::Unavailable("service never answered".into()));
|
||||
bail!("service IPC did not become available")
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -8,7 +8,6 @@ use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::RwLock;
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
@@ -18,6 +17,7 @@ use std::{
|
||||
};
|
||||
use sysproxy::{Autoproxy, GuardMonitor, GuardType, Sysproxy};
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum ProxyApplyStep {
|
||||
@@ -677,8 +677,8 @@ mod tests {
|
||||
(calls.into_inner(), result.is_ok())
|
||||
}
|
||||
|
||||
fn failure_text(result: anyhow::Result<()>) -> std::string::String {
|
||||
result.map_or_else(|error| format!("{error:#}"), |()| "no failure".to_owned())
|
||||
fn failure_text(result: anyhow::Result<()>) -> String {
|
||||
result.map_or_else(|error| format!("{error:#}").into(), |()| String::from("no failure"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
@@ -23,6 +22,7 @@ use tokio::{
|
||||
};
|
||||
use tokio_stream::StreamExt as _;
|
||||
use tokio_util::time::{DelayQueue, delay_queue::Key};
|
||||
use tristr::alias::String;
|
||||
|
||||
enum TimerCommand {
|
||||
Apply(HashMap<String, TaskSchedule>),
|
||||
@@ -502,7 +502,6 @@ mod tests {
|
||||
use super::{TaskSchedule, Timer, run_timer_profile_update_transition};
|
||||
use crate::config::{PrfItem, PrfOption};
|
||||
use parking_lot::Mutex;
|
||||
use smartstring::alias::String;
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
sync::{
|
||||
@@ -515,6 +514,7 @@ mod tests {
|
||||
use tokio::sync::Barrier;
|
||||
use tokio_stream::StreamExt as _;
|
||||
use tokio_util::time::DelayQueue;
|
||||
use tristr::alias::String;
|
||||
|
||||
struct CancellationProbe {
|
||||
cancelled: Arc<AtomicBool>,
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
use super::Tray;
|
||||
use crate::{
|
||||
Type, cmd,
|
||||
config::Config,
|
||||
core::{
|
||||
handle,
|
||||
notify::{Refresh, announce},
|
||||
proxy_view::{ProxyMemberRef, ProxyNodeSource},
|
||||
},
|
||||
logging,
|
||||
};
|
||||
|
||||
const TRAY_DELAY_TEST_CONCURRENCY: usize = 10;
|
||||
const TRAY_DEFAULT_LATENCY_URL: &str = "http://cp.cloudflare.com/generate_204";
|
||||
|
||||
fn is_latency_test_url(raw: &str) -> bool {
|
||||
let raw = raw.trim();
|
||||
raw.split_once("://")
|
||||
.is_some_and(|(scheme, _)| scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https"))
|
||||
&& tauri::Url::parse(raw).is_ok_and(|url| url.has_host() && matches!(url.scheme(), "http" | "https"))
|
||||
}
|
||||
|
||||
pub(super) fn test_url(
|
||||
group: &crate::core::proxy_view::ProxyGroupView,
|
||||
verge: &crate::config::IVerge,
|
||||
profiles: &crate::config::IProfiles,
|
||||
) -> String {
|
||||
let custom_url = profiles
|
||||
.current
|
||||
.as_ref()
|
||||
.and_then(|uid| profiles.get_item(uid).ok())
|
||||
.and_then(|profile| profile.latency_test_urls.as_ref())
|
||||
.and_then(|urls| urls.get(group.name.as_str()))
|
||||
.map(|url| url.as_str())
|
||||
.filter(|url| !url.trim().is_empty());
|
||||
let group_url = group.test_url.as_deref().filter(|url| !url.trim().is_empty());
|
||||
let default_url = verge
|
||||
.default_latency_test
|
||||
.as_deref()
|
||||
.filter(|url| !url.trim().is_empty());
|
||||
custom_url
|
||||
.or(group_url)
|
||||
.or(default_url)
|
||||
.filter(|&url| is_latency_test_url(url))
|
||||
.unwrap_or(TRAY_DEFAULT_LATENCY_URL)
|
||||
.trim()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
static RUNNING: parking_lot::Mutex<std::collections::BTreeSet<String>> =
|
||||
parking_lot::Mutex::new(std::collections::BTreeSet::new());
|
||||
|
||||
pub(super) fn is_running(group_name: &str) -> bool {
|
||||
RUNNING.lock().contains(group_name)
|
||||
}
|
||||
|
||||
pub(super) async fn test_proxy_group_delay(group_name: &str) {
|
||||
if !RUNNING.lock().insert(group_name.to_owned()) {
|
||||
return;
|
||||
}
|
||||
Tray::global().refresh_latency_item(group_name);
|
||||
let _running = scopeguard::guard(group_name, |name| {
|
||||
RUNNING.lock().remove(name);
|
||||
Tray::global().refresh_latency_item(name);
|
||||
});
|
||||
use futures::{StreamExt as _, stream};
|
||||
|
||||
let view = match cmd::proxy::get_proxy_view().await {
|
||||
Ok(view) => view,
|
||||
Err(err) => {
|
||||
logging!(warn, Type::Tray, "Failed to read proxies for tray latency test: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
|
||||
let group = view
|
||||
.groups
|
||||
.iter()
|
||||
.chain(view.global.iter())
|
||||
.find(|group| group.name.as_str() == group_name);
|
||||
let Some(group) = group else {
|
||||
logging!(warn, Type::Tray, "Tray latency test group not found: {group_name}");
|
||||
return;
|
||||
};
|
||||
|
||||
let profiles = Config::profiles().await.latest_arc();
|
||||
let url = test_url(group, &verge, &profiles);
|
||||
let timeout = verge
|
||||
.default_latency_timeout
|
||||
.filter(|timeout| *timeout > 0)
|
||||
.unwrap_or(10000) as u32;
|
||||
|
||||
let mut targets: Vec<(std::string::String, Option<std::string::String>)> = Vec::with_capacity(group.members.len());
|
||||
for member in &group.members {
|
||||
match member {
|
||||
ProxyMemberRef::Node { name, record_id } => match view.records.get(record_id).map(|node| &node.source) {
|
||||
Some(ProxyNodeSource::Provider {
|
||||
provider_name,
|
||||
proxy_name,
|
||||
}) => targets.push((proxy_name.clone(), Some(provider_name.clone()))),
|
||||
_ => targets.push((name.clone(), None)),
|
||||
},
|
||||
ProxyMemberRef::Group { name } => targets.push((name.clone(), None)),
|
||||
ProxyMemberRef::Unresolved { .. } => {}
|
||||
}
|
||||
}
|
||||
let mihomo = handle::Handle::mihomo();
|
||||
|
||||
stream::iter(targets.into_iter().map(move |(member, provider)| {
|
||||
let url = url.clone();
|
||||
async move {
|
||||
let result = match provider {
|
||||
Some(provider_name) => {
|
||||
mihomo
|
||||
.healthcheck_node_in_provider(&provider_name, &member, &url, timeout)
|
||||
.await
|
||||
}
|
||||
None => mihomo.delay_proxy_by_name(&member, &url, timeout).await,
|
||||
};
|
||||
if let Err(err) = result {
|
||||
logging!(debug, Type::Tray, "Tray latency test failed: {member} - {err}");
|
||||
}
|
||||
}
|
||||
}))
|
||||
.buffer_unordered(TRAY_DELAY_TEST_CONCURRENCY)
|
||||
.for_each(|()| std::future::ready(()))
|
||||
.await;
|
||||
|
||||
if let Err(err) = cmd::proxy::sync_tray_proxy_selection().await {
|
||||
logging!(warn, Type::Tray, "Failed to refresh tray after latency test: {err}");
|
||||
}
|
||||
|
||||
announce(Refresh::Proxies);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn latency_urls_require_a_valid_http_host_and_port() {
|
||||
for invalid in [
|
||||
"http://?",
|
||||
"http://host:invalid",
|
||||
"http://bad host/",
|
||||
"ftp://localhost",
|
||||
"http:localhost",
|
||||
] {
|
||||
assert!(!is_latency_test_url(invalid), "{invalid}");
|
||||
}
|
||||
for valid in ["http://localhost/", " https://example.com/ ", "HTTP://localhost:8080/"] {
|
||||
assert!(is_latency_test_url(valid), "{valid}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@ define_menu! {
|
||||
outbound_modes => OUTBOUND_MODES, "tray_outbound_modes", "tray.outboundModes",
|
||||
profiles => PROFILES, "tray_profiles", "tray.profiles",
|
||||
proxies => PROXIES, "tray_proxies", "tray.proxies",
|
||||
test_latency => TEST_LATENCY, "tray_test_latency", "tray.testLatency",
|
||||
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "tray.systemProxy",
|
||||
tun_mode => TUN_MODE, "tray_tun_mode", "tray.tunMode",
|
||||
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "tray.closeAllConnections",
|
||||
|
||||
+141
-363
@@ -1,6 +1,5 @@
|
||||
use crate::config::{IProfilePreview, IVerge};
|
||||
use crate::core::lightweight;
|
||||
use crate::core::proxy_view::{ProxyGroupView, ProxyMemberRef, ProxyNodeSource, ProxyViewProviderState, ProxyViewV1};
|
||||
use crate::core::tray::menu_def::TrayAction;
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::singleton;
|
||||
@@ -15,11 +14,11 @@ use crate::{
|
||||
use clash_verge_limiter::{Limiter, SystemClock, SystemLimiter};
|
||||
use clash_verge_logging::logging_error;
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
||||
use tauri_plugin_mihomo::models::Proxies;
|
||||
use tokio::fs;
|
||||
|
||||
use super::handle;
|
||||
use anyhow::Result;
|
||||
use smartstring::alias::String;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
@@ -27,8 +26,8 @@ use tauri::{
|
||||
AppHandle, Wry,
|
||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
};
|
||||
use tristr::alias::String;
|
||||
|
||||
mod latency;
|
||||
mod menu_def;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod speed_task;
|
||||
@@ -56,25 +55,8 @@ enum IconKind {
|
||||
Tun,
|
||||
}
|
||||
|
||||
struct LatencyMenuItem {
|
||||
item: MenuItem<Wry>,
|
||||
running: bool,
|
||||
proxies: Vec<ProxyLatencyItem>,
|
||||
provider_state: ProxyViewProviderState,
|
||||
}
|
||||
|
||||
struct ProxyLatencyItem {
|
||||
item: CheckMenuItem<Wry>,
|
||||
name: std::string::String,
|
||||
provider_name: Option<std::string::String>,
|
||||
text: std::string::String,
|
||||
}
|
||||
|
||||
pub struct Tray {
|
||||
limiter: SystemLimiter,
|
||||
proxy_hover_limiter: SystemLimiter,
|
||||
menu_update: tokio::sync::Mutex<()>,
|
||||
latency_items: parking_lot::Mutex<HashMap<std::string::String, LatencyMenuItem>>,
|
||||
#[cfg(target_os = "macos")]
|
||||
speed_controller: speed_task::TraySpeedController,
|
||||
}
|
||||
@@ -148,9 +130,6 @@ impl Default for Tray {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
limiter: Limiter::new(Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS), SystemClock),
|
||||
proxy_hover_limiter: Limiter::new(Duration::from_secs(1), SystemClock),
|
||||
menu_update: tokio::sync::Mutex::new(()),
|
||||
latency_items: parking_lot::Mutex::new(HashMap::new()),
|
||||
#[cfg(target_os = "macos")]
|
||||
speed_controller: speed_task::TraySpeedController::new(),
|
||||
}
|
||||
@@ -216,7 +195,6 @@ impl Tray {
|
||||
}
|
||||
|
||||
async fn update_menu_internal(&self, app_handle: &AppHandle, include_proxy_groups: bool) -> Result<()> {
|
||||
let _update = self.menu_update.lock().await;
|
||||
let Some(tray) = app_handle.tray_by_id(TRAY_ID) else {
|
||||
logging!(warn, Type::Tray, "Failed to update tray menu: tray not found");
|
||||
return Ok(());
|
||||
@@ -241,133 +219,29 @@ impl Tray {
|
||||
let profiles_preview = profiles_arc.profiles_preview().unwrap_or_default();
|
||||
let is_lightweight_mode = is_in_lightweight_mode();
|
||||
|
||||
let (menu, latency_items) = create_tray_menu(
|
||||
app_handle,
|
||||
Some(mode.as_str()),
|
||||
*system_proxy,
|
||||
*tun_mode,
|
||||
tun_mode_available,
|
||||
profiles_preview,
|
||||
TrayMenuOptions {
|
||||
is_lightweight_mode,
|
||||
include_proxy_groups,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
tray.set_menu(Some(menu))?;
|
||||
let groups: Vec<_> = latency_items.keys().cloned().collect();
|
||||
*self.latency_items.lock() = latency_items;
|
||||
for group in groups {
|
||||
self.refresh_latency_item(&group);
|
||||
}
|
||||
logging_error!(
|
||||
Type::Tray,
|
||||
tray.set_menu(Some(
|
||||
create_tray_menu(
|
||||
app_handle,
|
||||
Some(mode.as_str()),
|
||||
*system_proxy,
|
||||
*tun_mode,
|
||||
tun_mode_available,
|
||||
profiles_preview,
|
||||
TrayMenuOptions {
|
||||
is_lightweight_mode,
|
||||
include_proxy_groups,
|
||||
},
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
);
|
||||
|
||||
logging!(debug, Type::Tray, "托盘菜单更新成功");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn refresh_proxy_items(&'static self) {
|
||||
let Ok(update) = self.menu_update.try_lock() else {
|
||||
return;
|
||||
};
|
||||
if self.latency_items.lock().is_empty() || handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
let Ok(Ok(view)) = tokio::time::timeout(
|
||||
Duration::from_secs(1),
|
||||
cmd::proxy::proxy_view(Some(Duration::from_millis(750))),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if self
|
||||
.latency_items
|
||||
.lock()
|
||||
.values()
|
||||
.any(|entry| entry.provider_state != view.provider_state)
|
||||
{
|
||||
// Provider fallback changes member identities; rebuild through the existing queue.
|
||||
drop(update);
|
||||
logging_error!(Type::Tray, cmd::proxy::sync_tray_proxy_selection().await);
|
||||
return;
|
||||
}
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
let profiles = Config::profiles().await.latest_arc();
|
||||
logging_error!(
|
||||
Type::Tray,
|
||||
handle::Handle::app_handle().run_on_main_thread(move || {
|
||||
// Keep rebuilding serialized until these handles have received their snapshot.
|
||||
let _update = update;
|
||||
let mut items = self.latency_items.lock();
|
||||
for (group_name, entry) in items.iter_mut() {
|
||||
let Some(group) = find_group(&view, group_name) else {
|
||||
continue;
|
||||
};
|
||||
let url = latency::test_url(group, &verge, &profiles);
|
||||
let mut members = HashMap::with_capacity(group.members.len());
|
||||
for member in &group.members {
|
||||
members
|
||||
.entry((member_name(member), member_provider(&view, member)))
|
||||
.or_insert(member);
|
||||
}
|
||||
for proxy in &mut entry.proxies {
|
||||
let Some(member) = members.get(&(proxy.name.as_str(), proxy.provider_name.as_deref())) else {
|
||||
continue;
|
||||
};
|
||||
let text = proxy_item_text(&view, member, &url, &verge);
|
||||
let checked = group.now.as_ref() == Some(&proxy.name);
|
||||
if text != proxy.text {
|
||||
match proxy.item.set_text(&text) {
|
||||
Ok(()) => proxy.text = text,
|
||||
Err(err) => logging!(warn, Type::Tray, "Failed to refresh proxy latency: {err}"),
|
||||
}
|
||||
}
|
||||
match proxy.item.is_checked() {
|
||||
Ok(current) if current != checked => {
|
||||
logging_error!(Type::Tray, proxy.item.set_checked(checked))
|
||||
}
|
||||
Err(err) => logging!(warn, Type::Tray, "Failed to read proxy selection: {err}"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fn refresh_latency_item(&self, group_name: &str) {
|
||||
let group_name = group_name.to_owned();
|
||||
logging_error!(
|
||||
Type::Tray,
|
||||
handle::Handle::app_handle().run_on_main_thread(move || {
|
||||
Self::global().refresh_latency_item_on_main_thread(&group_name);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fn refresh_latency_item_on_main_thread(&self, group_name: &str) {
|
||||
let running = latency::is_running(group_name);
|
||||
let item = {
|
||||
let mut items = self.latency_items.lock();
|
||||
items.get_mut(group_name).and_then(|entry| {
|
||||
if entry.running == running {
|
||||
return None;
|
||||
}
|
||||
entry.running = running;
|
||||
Some(entry.item.clone())
|
||||
})
|
||||
};
|
||||
if let Some(item) = item {
|
||||
let label = if running {
|
||||
clash_verge_i18n::t!("tray.testingLatency")
|
||||
} else {
|
||||
clash_verge_i18n::t!("tray.testLatency")
|
||||
};
|
||||
logging_error!(Type::Tray, item.set_text(label));
|
||||
logging_error!(Type::Tray, item.set_enabled(!running));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_icon(&self, verge: &IVerge) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
|
||||
@@ -548,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,
|
||||
}
|
||||
@@ -583,175 +456,96 @@ fn create_profile_menu_item(
|
||||
.collect()
|
||||
}
|
||||
|
||||
const UNTESTED_DELAY_TEXT: &str = "-ms";
|
||||
const TIMEOUT_DELAY_TEXT: &str = "T/O";
|
||||
const TRAY_GROUP_DEPTH_LIMIT: usize = 8;
|
||||
|
||||
const fn member_name(member: &ProxyMemberRef) -> &str {
|
||||
match member {
|
||||
ProxyMemberRef::Node { name, .. }
|
||||
| ProxyMemberRef::Group { name }
|
||||
| ProxyMemberRef::Unresolved { name, .. } => name.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_group<'a>(view: &'a ProxyViewV1, name: &str) -> Option<&'a ProxyGroupView> {
|
||||
view.groups
|
||||
.iter()
|
||||
.chain(view.global.iter())
|
||||
.find(|group| group.name.as_str() == name)
|
||||
}
|
||||
|
||||
/// The delay recorded for `url`, falling back to the node's latest measurement under any URL, so a
|
||||
/// node the core tested on its own still shows a value.
|
||||
fn node_delay(view: &ProxyViewV1, record_id: &str, url: &str) -> Option<u16> {
|
||||
view.records
|
||||
.get(record_id)
|
||||
.map(|node| node.extra.get(url).map_or(&node.history, |extra| &extra.history))
|
||||
.and_then(|history| history.last())
|
||||
.map(|history| history.delay)
|
||||
}
|
||||
|
||||
/// A group row shows the delay of the node the group currently selects. The core never writes a
|
||||
/// history for the group itself, so `now` is followed until the chain reaches a node.
|
||||
fn selected_member_delay(view: &ProxyViewV1, group_name: &str, url: &str) -> Option<u16> {
|
||||
let mut current = group_name;
|
||||
for _ in 0..TRAY_GROUP_DEPTH_LIMIT {
|
||||
let Some(group) = find_group(view, current) else { break };
|
||||
let Some(now) = group.now.as_deref().filter(|now| !now.is_empty() && *now != current) else {
|
||||
break;
|
||||
};
|
||||
let Some(member) = group.members.iter().find(|member| member_name(member) == now) else {
|
||||
break;
|
||||
};
|
||||
match member {
|
||||
ProxyMemberRef::Node { record_id, .. } => return node_delay(view, record_id, url),
|
||||
ProxyMemberRef::Group { name } => current = name.as_str(),
|
||||
ProxyMemberRef::Unresolved { .. } => break,
|
||||
}
|
||||
}
|
||||
find_group(view, group_name)
|
||||
.map(|group| group.extra.get(url).map_or(&group.history, |extra| &extra.history))
|
||||
.and_then(|history| history.last())
|
||||
.map(|history| history.delay)
|
||||
}
|
||||
|
||||
fn member_provider<'a>(view: &'a ProxyViewV1, member: &ProxyMemberRef) -> Option<&'a str> {
|
||||
let ProxyMemberRef::Node { record_id, .. } = member else {
|
||||
return None;
|
||||
};
|
||||
match &view.records.get(record_id)?.source {
|
||||
ProxyNodeSource::Provider { provider_name, .. } => Some(provider_name),
|
||||
ProxyNodeSource::Core { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy_item_text(view: &ProxyViewV1, member: &ProxyMemberRef, url: &str, verge: &IVerge) -> std::string::String {
|
||||
let timeout = verge
|
||||
.default_latency_timeout
|
||||
.filter(|timeout| *timeout > 0)
|
||||
.unwrap_or(10000);
|
||||
let delay = match member {
|
||||
ProxyMemberRef::Node { record_id, .. } => node_delay(view, record_id, url),
|
||||
ProxyMemberRef::Group { name } => selected_member_delay(view, name, url),
|
||||
ProxyMemberRef::Unresolved { .. } => None,
|
||||
};
|
||||
let delay_text = delay.map_or_else(
|
||||
|| UNTESTED_DELAY_TEXT.to_owned(),
|
||||
|delay| {
|
||||
if delay == 0 || u32::from(delay) >= timeout as u32 {
|
||||
TIMEOUT_DELAY_TEXT.to_owned()
|
||||
} else {
|
||||
format!("{delay}ms")
|
||||
}
|
||||
},
|
||||
);
|
||||
format!("{} | {delay_text}", member_name(member))
|
||||
}
|
||||
|
||||
fn create_subcreate_proxy_menu_item(
|
||||
app_handle: &AppHandle,
|
||||
proxy_mode: &str,
|
||||
view: Option<ProxyViewV1>,
|
||||
latency_items: &mut HashMap<std::string::String, LatencyMenuItem>,
|
||||
texts: &MenuTexts,
|
||||
verge: &IVerge,
|
||||
profiles: &crate::config::IProfiles,
|
||||
proxy_group_order_map: Option<HashMap<String, usize>>,
|
||||
proxy_nodes_data: Option<Proxies>,
|
||||
) -> Vec<Submenu<Wry>> {
|
||||
let Some(view) = view else { return Vec::new() };
|
||||
view.groups
|
||||
.iter()
|
||||
.chain(view.global.iter())
|
||||
.filter_map(|group| {
|
||||
if group.hidden.unwrap_or_default() || (proxy_mode == "global") != (group.name == "GLOBAL") {
|
||||
return None;
|
||||
}
|
||||
let url = latency::test_url(group, verge, profiles);
|
||||
let proxies: Vec<ProxyLatencyItem> = group
|
||||
.members
|
||||
.iter()
|
||||
.filter_map(|member| {
|
||||
let name = member_name(member);
|
||||
let text = proxy_item_text(&view, member, &url, verge);
|
||||
let checked = group.now.as_deref() == Some(name);
|
||||
CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
format!("proxy_{}_{}", group.name, name),
|
||||
&text,
|
||||
true,
|
||||
checked,
|
||||
None::<&str>,
|
||||
)
|
||||
.map(|item| ProxyLatencyItem {
|
||||
item,
|
||||
name: name.to_owned(),
|
||||
provider_name: member_provider(&view, member).map(str::to_owned),
|
||||
text,
|
||||
let proxy_submenus: Vec<Submenu<Wry>> = {
|
||||
let mut submenus: Vec<(String, usize, Submenu<Wry>)> = Vec::new();
|
||||
|
||||
// TODO: 应用启动时,内核还未启动完全,无法获取代理节点信息
|
||||
if let Some(proxy_nodes_data) = proxy_nodes_data {
|
||||
for (group_name, group_data) in proxy_nodes_data.proxies.iter() {
|
||||
let should_show = match proxy_mode {
|
||||
"global" => group_name == "GLOBAL",
|
||||
_ => group_name != "GLOBAL",
|
||||
} && !group_data.hidden.unwrap_or_default();
|
||||
|
||||
if !should_show {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(all_proxies) = group_data.all.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let now_proxy = group_data.now.as_deref().unwrap_or_default();
|
||||
|
||||
let group_items: Vec<CheckMenuItem<Wry>> = all_proxies
|
||||
.iter()
|
||||
.filter_map(|proxy_str| {
|
||||
let is_selected = *proxy_str == now_proxy;
|
||||
let item_id = format!("proxy_{}_{}", group_name, proxy_str);
|
||||
|
||||
let delay_text = proxy_nodes_data
|
||||
.proxies
|
||||
.get(proxy_str)
|
||||
.and_then(|h| h.history.last())
|
||||
.map(|h| match h.delay {
|
||||
0 => "-ms".into(),
|
||||
delay if delay >= 10000 => "-ms".into(),
|
||||
_ => format!("{}ms", h.delay),
|
||||
})
|
||||
.unwrap_or_else(|| "-ms".into());
|
||||
|
||||
let display_text = format!("{} | {}", proxy_str, delay_text);
|
||||
|
||||
CheckMenuItem::with_id(app_handle, item_id, display_text, true, is_selected, None::<&str>)
|
||||
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e))
|
||||
.ok()
|
||||
})
|
||||
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {e}"))
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
let group_items: Vec<_> = proxies.iter().map(|proxy| proxy.item.clone()).collect();
|
||||
if group_items.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let running = latency::is_running(&group.name);
|
||||
let test_item = MenuItem::with_id(
|
||||
app_handle,
|
||||
format!("{}_{}", MenuIds::TEST_LATENCY, group.name),
|
||||
if running {
|
||||
clash_verge_i18n::t!("tray.testingLatency")
|
||||
.collect();
|
||||
|
||||
if group_items.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let group_display_name = group_name.to_string();
|
||||
|
||||
let group_items_refs: Vec<&dyn IsMenuItem<Wry>> =
|
||||
group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>).collect();
|
||||
|
||||
if let Ok(submenu) = Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
format!("proxy_group_{}", group_name),
|
||||
group_display_name,
|
||||
true,
|
||||
&group_items_refs,
|
||||
) {
|
||||
let insertion_index = submenus.len();
|
||||
submenus.push((group_name.into(), insertion_index, submenu));
|
||||
} else {
|
||||
texts.test_latency.clone()
|
||||
},
|
||||
!running,
|
||||
None::<&str>,
|
||||
)
|
||||
.ok()?;
|
||||
latency_items.insert(
|
||||
group.name.clone(),
|
||||
LatencyMenuItem {
|
||||
item: test_item.clone(),
|
||||
running,
|
||||
proxies,
|
||||
provider_state: view.provider_state,
|
||||
},
|
||||
);
|
||||
let separator = PredefinedMenuItem::separator(app_handle).ok()?;
|
||||
let mut items: Vec<&dyn IsMenuItem<Wry>> = vec![&test_item, &separator];
|
||||
items.extend(group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>));
|
||||
Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
format!("proxy_group_{}", group.name),
|
||||
&group.name,
|
||||
true,
|
||||
&items,
|
||||
)
|
||||
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy group submenu: {e}"))
|
||||
.ok()
|
||||
})
|
||||
.collect()
|
||||
logging!(warn, Type::Tray, "Failed to create proxy group submenu: {}", group_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(order_map) = proxy_group_order_map.as_ref() {
|
||||
submenus.sort_by(|(name_a, original_index_a, _), (name_b, original_index_b, _)| {
|
||||
match (order_map.get(name_a), order_map.get(name_b)) {
|
||||
(Some(index_a), Some(index_b)) => index_a.cmp(index_b),
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||
(None, None) => original_index_a.cmp(original_index_b),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submenus.into_iter().map(|(_, _, submenu)| submenu).collect()
|
||||
};
|
||||
proxy_submenus
|
||||
}
|
||||
|
||||
fn create_proxy_menu_item(
|
||||
@@ -798,38 +592,54 @@ async fn create_tray_menu(
|
||||
tun_mode_available: bool,
|
||||
profiles_preview: Vec<IProfilePreview<'_>>,
|
||||
options: TrayMenuOptions,
|
||||
) -> Result<(tauri::menu::Menu<Wry>, HashMap<std::string::String, LatencyMenuItem>)> {
|
||||
) -> Result<tauri::menu::Menu<Wry>> {
|
||||
let current_proxy_mode = mode.unwrap_or("");
|
||||
|
||||
let mut verge_settings = Config::verge().await.latest_arc();
|
||||
let fetch_proxy_groups =
|
||||
options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable");
|
||||
|
||||
// Proxy data must remain optional so a stopped Core cannot roll back unrelated settings.
|
||||
let proxy_view = if fetch_proxy_groups {
|
||||
tokio::time::timeout(
|
||||
Duration::from_millis(1000),
|
||||
cmd::proxy::proxy_view(Some(Duration::from_millis(750))),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(Result::ok)
|
||||
// TODO: should update tray menu again when it was timeout error
|
||||
let (proxy_nodes_data, runtime_proxy_groups_order) = if fetch_proxy_groups {
|
||||
let proxy_nodes_data =
|
||||
tokio::time::timeout(Duration::from_millis(1000), handle::Handle::mihomo().get_proxies())
|
||||
.await
|
||||
.map_or(None, |res| res.ok());
|
||||
|
||||
let runtime = Config::runtime().await.latest_arc();
|
||||
let runtime_proxy_groups_order = runtime.config.as_ref().map(|config| {
|
||||
config
|
||||
.get("proxy-groups")
|
||||
.and_then(|groups| groups.as_sequence())
|
||||
.map(|groups| {
|
||||
groups
|
||||
.iter()
|
||||
.filter_map(|group| group.get("name"))
|
||||
.filter_map(|name| name.as_str())
|
||||
.enumerate()
|
||||
.map(|(index, name)| (name.into(), index))
|
||||
.collect::<HashMap<String, usize>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
(proxy_nodes_data, runtime_proxy_groups_order)
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
};
|
||||
|
||||
if fetch_proxy_groups {
|
||||
verge_settings = Config::verge().await.latest_arc();
|
||||
}
|
||||
|
||||
let profiles_config = Config::profiles().await.latest_arc();
|
||||
|
||||
let tray_proxy_groups_display_mode = verge_settings
|
||||
.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 proxy_group_order_map = runtime_proxy_groups_order;
|
||||
|
||||
let show_outbound_modes_inline = verge_settings.tray_inline_outbound_modes.unwrap_or(false);
|
||||
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
@@ -909,17 +719,9 @@ async fn create_tray_menu(
|
||||
&profile_menu_items_refs,
|
||||
)?;
|
||||
|
||||
let mut latency_items = HashMap::new();
|
||||
let (proxies_menu, inline_proxy_items) = if include_proxy_groups {
|
||||
let proxy_sub_menus = create_subcreate_proxy_menu_item(
|
||||
app_handle,
|
||||
current_proxy_mode,
|
||||
proxy_view,
|
||||
&mut latency_items,
|
||||
&texts,
|
||||
&verge_settings,
|
||||
&profiles_config,
|
||||
);
|
||||
let proxy_sub_menus =
|
||||
create_subcreate_proxy_menu_item(app_handle, current_proxy_mode, proxy_group_order_map, proxy_nodes_data);
|
||||
|
||||
match tray_proxy_groups_display_mode {
|
||||
"default" => create_proxy_menu_item(app_handle, false, proxy_sub_menus, &texts.proxies)?,
|
||||
@@ -1063,25 +865,10 @@ async fn create_tray_menu(
|
||||
]);
|
||||
|
||||
let menu = tauri::menu::MenuBuilder::new(app_handle).items(&menu_items).build()?;
|
||||
Ok((menu, latency_items))
|
||||
Ok(menu)
|
||||
}
|
||||
|
||||
fn on_tray_icon_event(_tray_icon: &TrayIcon, tray_event: TrayIconEvent) {
|
||||
let refresh_proxies = match tray_event {
|
||||
TrayIconEvent::Enter { .. } => Tray::global().proxy_hover_limiter.check(),
|
||||
TrayIconEvent::Click {
|
||||
button: MouseButton::Left | MouseButton::Right,
|
||||
button_state: MouseButtonState::Down,
|
||||
..
|
||||
} => true,
|
||||
_ => false,
|
||||
};
|
||||
if refresh_proxies {
|
||||
AsyncHandler::spawn(|| async {
|
||||
Tray::global().refresh_proxy_items().await;
|
||||
});
|
||||
}
|
||||
|
||||
if matches!(
|
||||
tray_event,
|
||||
TrayIconEvent::Move { .. } | TrayIconEvent::Leave { .. } | TrayIconEvent::Enter { .. }
|
||||
@@ -1203,15 +990,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
};
|
||||
feat::toggle_proxy_profile(profile_index.into()).await;
|
||||
}
|
||||
id if id.starts_with(MenuIds::TEST_LATENCY) => {
|
||||
let group_name = id
|
||||
.strip_prefix(MenuIds::TEST_LATENCY)
|
||||
.and_then(|rest| rest.strip_prefix('_'));
|
||||
if let Some(group_name) = group_name {
|
||||
logging!(info, Type::Tray, "Tray menu: test group latency {}", group_name);
|
||||
latency::test_proxy_group_delay(group_name).await;
|
||||
}
|
||||
}
|
||||
id if id.starts_with("proxy_") => {
|
||||
let rest = match id.strip_prefix("proxy_") {
|
||||
Some(r) => r,
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::{
|
||||
};
|
||||
use tauri_plugin_updater::{Update, UpdaterExt as _};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tristr::alias::String;
|
||||
|
||||
pub struct SilentUpdater {
|
||||
update_ready: AtomicBool,
|
||||
@@ -55,8 +56,8 @@ impl SilentUpdater {
|
||||
.with_context(|| format!("failed to write update cache {}", bin_path.display()))?;
|
||||
|
||||
let meta = UpdateCacheMeta {
|
||||
version: version.to_string(),
|
||||
downloaded_at: Utc::now().to_rfc3339(),
|
||||
version: version.into(),
|
||||
downloaded_at: Utc::now().to_rfc3339().into(),
|
||||
};
|
||||
let meta_path = cache_dir.join("pending_update.json");
|
||||
std::fs::write(&meta_path, serde_json::to_string_pretty(&meta)?)
|
||||
@@ -133,9 +134,8 @@ fn verify_signature(app_handle: &tauri::AppHandle, bytes: &[u8], signature: &str
|
||||
use base64::Engine as _;
|
||||
|
||||
let decode = |value: &str| -> Result<String> {
|
||||
Ok(String::from_utf8(
|
||||
base64::engine::general_purpose::STANDARD.decode(value)?,
|
||||
)?)
|
||||
// from_utf8 exists only on the std String type; the tristr alias shadows the bare name.
|
||||
Ok(std::string::String::from_utf8(base64::engine::general_purpose::STANDARD.decode(value)?)?.into())
|
||||
};
|
||||
let config: tauri_plugin_updater::Config = serde_json::from_value(
|
||||
app_handle
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use scopeguard::defer;
|
||||
use serde::Serialize;
|
||||
use smartstring::alias::String;
|
||||
use std::{
|
||||
fmt,
|
||||
hash::{DefaultHasher, Hash as _, Hasher as _},
|
||||
@@ -10,6 +9,7 @@ use std::{
|
||||
};
|
||||
use tauri_plugin_shell::ShellExt as _;
|
||||
use tokio::fs;
|
||||
use tristr::alias::String;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::core::handle;
|
||||
@@ -114,14 +114,8 @@ impl fmt::Display for ValidationSkipReason {
|
||||
#[serde(tag = "status", rename_all = "camelCase")]
|
||||
pub enum ValidationOutcome {
|
||||
Valid,
|
||||
Invalid {
|
||||
kind: ValidationErrorKind,
|
||||
#[specta(type = std::string::String)]
|
||||
message: String,
|
||||
},
|
||||
Skipped {
|
||||
reason: ValidationSkipReason,
|
||||
},
|
||||
Invalid { kind: ValidationErrorKind, message: String },
|
||||
Skipped { reason: ValidationSkipReason },
|
||||
Busy,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::{
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use tokio::fs;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChainItem {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use tristr::alias::String;
|
||||
|
||||
pub const HANDLE_FIELDS: [&str; 12] = [
|
||||
"mode",
|
||||
|
||||
@@ -26,9 +26,9 @@ use anyhow::{Context as _, Result};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::Mutex;
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tokio::fs;
|
||||
use tristr::alias::String;
|
||||
|
||||
type ResultLog = Vec<(String, String)>;
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -6,8 +6,8 @@ use boa_engine::{Context, JsString, JsValue, Source, js_string, native_function:
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use parking_lot::Mutex;
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::sync::Arc;
|
||||
use tristr::alias::String;
|
||||
|
||||
const MAX_OUTPUTS: usize = 1000;
|
||||
const MAX_OUTPUT_SIZE: usize = 1024 * 1024; // 1MB
|
||||
@@ -16,7 +16,7 @@ const MAX_LOOP_ITERATIONS: u64 = 10_000_000;
|
||||
const SCRIPT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
type ScriptResult = (Mapping, Vec<String>, Vec<(String, String)>);
|
||||
type EvalOutcome = (Result<Mapping, std::string::String>, Vec<(String, String)>);
|
||||
type EvalOutcome = (Result<Mapping, String>, Vec<(String, String)>);
|
||||
|
||||
/// Never fails: exceptions are logged and the input config is returned unchanged.
|
||||
pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult {
|
||||
@@ -37,9 +37,9 @@ pub(super) async fn use_script(script: String, config: Mapping, name: String) ->
|
||||
let evaluated = AsyncHandler::spawn_blocking(move || eval_script(&script, &json, &name));
|
||||
let (outcome, mut logs) = match tokio::time::timeout(SCRIPT_TIMEOUT, evaluated).await {
|
||||
Ok(Ok(evaluated)) => evaluated,
|
||||
Ok(Err(join_err)) => (Err(format!("script task panicked: {join_err}")), vec![]),
|
||||
Ok(Err(join_err)) => (Err(format!("script task panicked: {join_err}").into()), vec![]),
|
||||
Err(_elapsed) => (
|
||||
Err(format!("script execution timed out after {SCRIPT_TIMEOUT:?}")),
|
||||
Err(format!("script execution timed out after {SCRIPT_TIMEOUT:?}").into()),
|
||||
vec![],
|
||||
),
|
||||
};
|
||||
@@ -59,7 +59,7 @@ pub(super) async fn use_script(script: String, config: Mapping, name: String) ->
|
||||
(result, changed_keys, logs)
|
||||
}
|
||||
Err(reason) => {
|
||||
logs.push(("exception".into(), reason.into()));
|
||||
logs.push(("exception".into(), reason));
|
||||
(config, vec![], logs)
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
|
||||
let outputs_clone = Arc::clone(&outputs);
|
||||
let total_size_clone = Arc::clone(&total_size);
|
||||
|
||||
let bail = |reason: std::string::String| (Err(reason), outputs.lock().to_vec());
|
||||
let bail = |reason: String| (Err(reason), outputs.lock().to_vec());
|
||||
|
||||
let _ = context.register_global_builtin_callable("__verge_log__".into(), 2, unsafe {
|
||||
NativeFunction::from_closure(move |_: &JsValue, args: &[JsValue], context: &mut Context| {
|
||||
@@ -137,13 +137,13 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
|
||||
JsValue::from(JsString::from(config_json)),
|
||||
Attribute::all(),
|
||||
) {
|
||||
return bail(format!("failed to bind config for script: {err}"));
|
||||
return bail(format!("failed to bind config for script: {err}").into());
|
||||
}
|
||||
|
||||
// 仅处理 name 参数中的特殊字符
|
||||
let safe_name = escape_js_string_for_single_quote(name);
|
||||
if safe_name.len() > 1024 {
|
||||
return bail("Name parameter too long".to_owned());
|
||||
return bail("Name parameter too long".into());
|
||||
}
|
||||
|
||||
let code = format!(
|
||||
@@ -157,19 +157,19 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
|
||||
|
||||
let result = match context.eval(Source::from_bytes(code.as_str())) {
|
||||
Ok(result) if result.is_string() => result,
|
||||
_ => return bail("main function should return object".to_owned()),
|
||||
_ => return bail("main function should return object".into()),
|
||||
};
|
||||
let result = match result.to_string(&mut context) {
|
||||
Ok(result) => result,
|
||||
Err(e) => return bail(format!("Failed to convert JS result to string: {e}")),
|
||||
Err(e) => return bail(format!("Failed to convert JS result to string: {e}").into()),
|
||||
};
|
||||
let result = match result.to_std_string() {
|
||||
Ok(result) => result,
|
||||
Err(_) => return bail("Failed to convert JS string to std string".to_owned()),
|
||||
Err(_) => return bail("Failed to convert JS string to std string".into()),
|
||||
};
|
||||
|
||||
if result.len() > MAX_JSON_SIZE {
|
||||
return bail("Script result exceeds maximum allowed size".to_owned());
|
||||
return bail("Script result exceeds maximum allowed size".into());
|
||||
}
|
||||
|
||||
match parse_json_safely(&result) {
|
||||
@@ -180,7 +180,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
|
||||
Type::Config,
|
||||
"Script execution error: {err:#}. Script name: {name}"
|
||||
);
|
||||
bail("Script execution failed".to_owned())
|
||||
bail("Script execution failed".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::{Mapping, Sequence, Value};
|
||||
use std::collections::HashSet;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SeqMap {
|
||||
@@ -12,8 +13,8 @@ pub struct SeqMap {
|
||||
fn collect_proxy_names(seq: &Sequence) -> Vec<String> {
|
||||
seq.iter()
|
||||
.filter_map(|item| match item {
|
||||
Value::Mapping(map) => map.get("name").and_then(Value::as_str).map(str::to_owned),
|
||||
Value::String(name) => Some(name.to_owned()),
|
||||
Value::Mapping(map) => map.get("name").and_then(Value::as_str).map(Into::into),
|
||||
Value::String(name) => Some(name.to_owned().into()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
@@ -58,10 +59,10 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||
.into_iter()
|
||||
.filter(|item| {
|
||||
if let Value::String(s) = item {
|
||||
!delete.contains(s)
|
||||
!delete.iter().any(|deleted| deleted == s)
|
||||
} else if let Value::Mapping(m) = item {
|
||||
if let Some(Value::String(name)) = m.get("name") {
|
||||
!delete.contains(name)
|
||||
!delete.iter().any(|deleted| deleted == name)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
@@ -99,7 +100,7 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||
.into_iter()
|
||||
.filter(|p| {
|
||||
if let Value::String(name) = p {
|
||||
!delete.contains(name)
|
||||
!delete.iter().any(|deleted| deleted == name)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
@@ -119,12 +120,12 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
|
||||
let mut seen_proxy_names = HashSet::new();
|
||||
for name in &added_proxy_names {
|
||||
if seen_proxy_names.insert(name.clone()) {
|
||||
merged_proxies.push(Value::String(name.clone()));
|
||||
merged_proxies.push(Value::String(name.clone().into()));
|
||||
}
|
||||
}
|
||||
for value in base_group_proxies {
|
||||
if let Value::String(name) = &value
|
||||
&& !seen_proxy_names.insert(name.to_owned())
|
||||
&& !seen_proxy_names.insert(name.to_owned().into())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -180,7 +181,7 @@ proxy-groups:
|
||||
let seq = SeqMap {
|
||||
prepend: Sequence::new(),
|
||||
append: Sequence::new(),
|
||||
delete: vec!["proxy1".to_string()],
|
||||
delete: vec!["proxy1".into()],
|
||||
};
|
||||
|
||||
config = use_seq(seq, config, "proxies");
|
||||
@@ -317,7 +318,7 @@ proxy-groups: "invalid"
|
||||
let seq = SeqMap {
|
||||
prepend: Sequence::new(),
|
||||
append: Sequence::new(),
|
||||
delete: vec!["proxy1".to_string()],
|
||||
delete: vec!["proxy1".into()],
|
||||
};
|
||||
|
||||
config = use_seq(seq, config, "proxies");
|
||||
|
||||
@@ -12,17 +12,14 @@ use chrono::Utc;
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use serde::Serialize;
|
||||
use smartstring::alias::String;
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
use tristr::alias::String;
|
||||
|
||||
#[derive(Debug, Serialize, specta::Type)]
|
||||
pub struct LocalBackupFile {
|
||||
#[specta(type = std::string::String)]
|
||||
pub filename: String,
|
||||
#[specta(type = std::string::String)]
|
||||
pub path: String,
|
||||
#[specta(type = std::string::String)]
|
||||
pub last_modified: String,
|
||||
pub content_length: u64,
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ use crate::{
|
||||
use bytes::BytesMut;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use tristr::alias::String;
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> = LazyLock::new(|| {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user