Compare commits

...
Author SHA1 Message Date
Tunglies acef575935 refactor(enhance): call the hysteria alpn builtin directly
builtin() existed to feed the uid-dispatch loop; with a single
builtin the vec and match are ceremony. apply_builtin_scripts calls
builtin_hy_alpn directly and to_script goes away; tests construct
ChainItem literals the way the merge items already did.
2026-10-28 09:15:46 +08:00
Tunglies f489c7afa6 refactor(enhance): drop unreachable mode script guard
merge_default_config overwrites the profile's mode with the verge-owned
clash config, whose mode is always present (template seeds rule and
patch semantics never clear it) and restricted to rule/global/direct;
change_clash_mode is rejected by the core before persisting anything
else. mode:script can therefore only arrive through a hand-crafted
patch_clash call, and for that path an explicit invalid-mode validation
error is more honest than silently rewriting to rule. mihomo rejects
mode:script outright (invalid mode), and user merge/script chains run
after the builtins, so the guard never covered them anyway.
2026-10-28 09:15:46 +08:00
Tunglies 238d4de328 refactor(enhance): drop builtin core filtering that never discriminates
Both valid cores matched a different ChainSupport variant, yet each
variant registered the same uid pair, so the filtered builtin list was
identical for stable and alpha. Invalid cores are coerced at startup
(validate_and_fix_config / get_valid_clash_core) and the None arm of
is_support was unreachable. builtin() now returns the items directly
and the clash_core threading through ConfigValues goes away; re-add
core discrimination when a builtin actually differs per core.
2026-10-28 09:15:46 +08:00
Tunglies ab0e1b957a refactor: replace remaining clash meta naming with mihomo
Doc comment, generated bindings and the prebuild sidecar helpers stop
referring to the legacy Clash Meta name; installer legacy-process
cleanup keeps the old executable names on purpose.
2026-10-28 09:15:46 +08:00
Tunglies 1313692b3d refactor(sysopt): name the proxy state lock for its full role
The lock now owns the (Sysproxy, Autoproxy) state, so proxy_state_lock
says what it guards; the guard locals follow. constants::proxy held a
single sysproxy constant and is renamed sysproxy.
2026-10-28 09:15:46 +08:00
Tunglies a5d0b8dacb refactor(config): name prfitem constructor for what it builds
new_seq_item states that a fresh item is built from a template; the
from_ prefix wrongly implied an existing source. append_companion_item
returns the uid directly: append_item bails on a missing uid, so the
Option was always Some on the Ok path.
2026-10-28 09:15:46 +08:00
Tunglies 1a5094caf0 refactor(enhance): lowercase takes a reference and chain support is copy
use_lowercase borrows the mapping and clones only the values; call
sites no longer clone whole configs. ChainSupport is a fieldless enum
and now Copy; builtin() registers each support pair without clones.
2026-10-28 09:15:46 +08:00
Tunglies 93f70f4c36 refactor(enhance): rename ChainSupport variants to Mihomo names
Clash is a legacy name; the variants match the verge-mihomo and
verge-mihomo-alpha cores they gate.
2026-10-28 09:15:46 +08:00
Tunglies e5b691b47a refactor(enhance): fold lowercase helpers into single owned function
use_lowercase_owned disappears; the surviving use_lowercase takes the
owned Mapping and borrowed callers clone at the call site.
2026-10-28 09:15:45 +08:00
Tunglies 7e9fa61f52 refactor(core): dedupe window listeners, elevated runners, backup settings
- Collapse the close/focus window listener pairs in lightweight.rs onto a shared WindowListener with per-listener event and log label.
- Extract the windows runas-or-direct execution into service::run_maybe_elevated, shared by win_uwp and the service uninstaller, and factor the common uninstaller path resolution and status check shared across platforms.
- Make the auto-backup watch channel the single settings source, replacing the duplicate Arc<RwLock> copy with a stored receiver.
2026-10-28 09:15:45 +08:00
Tunglies acde039820 refactor(sysopt): share macos bypass formatting
The macOS default bypass list and its three-way composition (empty ->
default, default + custom, custom only) were maintained twice: in
sysopt's format_bypass and inline in proxy_control's service_bypass.
Keep the literal once in constants and compose both paths through
format_bypass; service_bypass keeps its NUL check and UTF-8 truncation.
2026-10-28 09:15:45 +08:00
Tunglies 0474c9cd0c refactor(sysopt): drop redundant lock and busy-wait
The proxy target pair sat behind an Arc<RwLock> with no readers and an
Arc that was never cloned; both writers already serialize on
update_lock, so store the pair inside that mutex and delete the RwLock.

refresh_guard waited for the guard to become ready with a yield_now spin
at full CPU while holding guard_operation_lock; a 10 ms sleep keeps the
same wait condition and lock scope without the spin.
2026-10-28 09:15:45 +08:00
Tunglies c82719af45 refactor(backup): share restore finalize path
restore_webdav_backup and restore_local_backup duplicated the
credential extraction (backup creation strips the WebDAV credentials,
so both re-inject the live ones), the archive extraction and the
finalize tail. Move that tail into a private restore_backup_archive
helper so the two entries keep only their download/locate logic; the
config-write lock scope and the off-runtime extraction land in one
place. The webdav entry keeps its downloaded-file cleanup.
2026-10-28 09:15:45 +08:00
Tunglies 808f27abb4 refactor(app): extract common exit cleanup
restart_app and quit duplicate the whole exit preparation: set the
exiting flag, persist configs, run interactive cleanup, and on a failed
core stop clear the flag, restore public DNS under the lifecycle lock
on macOS, and send the stop-failed notice.

Move that flow into run_exit_cleanup_flow next to clean_async, taking
the notice variant as a parameter and returning the CleanupResult so
each caller keeps its own tail: restart aborts before shutdown, quit
always shuts down and exits with the cleanup code.
2026-10-28 09:14:31 +08:00
Tunglies ad0035edb9 refactor(net): reuse webpki tls config and fallback
Extract build_webpki_rooted_tls_config so the delay-test TLS connector
and the reqwest clients share one static-webpki-roots builder; ALPN
stays caller-specific (only reqwest offers h2).

Collapse the duplicated platform-verifier -> static-webpki-roots retry
in get/get_bytes into request_with_static_webpki_fallback, gated by the
caller's certificate-verification setting.
2026-10-28 09:14:31 +08:00
Tunglies 9063181468 refactor(enhance): merge chain processing and drop unused builtin js payload
- collapse the identical process_global_items/process_profile_items into
  one parameterized process_override_items (entry uid sources unchanged)
- fold the identical rules/proxies/groups arms of ChainItem::from_async
  into a single arm with an inner dispatch
- stop embedding builtin JS payloads never executed by any path: the
  enhance loop dispatches builtin items by uid to native implementations,
  so ChainItem::builtin now carries empty script data (uids unchanged)
- source ProfileItems fallback literals from Default instead of a second
  hand-written copy in collect_profile_items
2026-10-28 09:14:31 +08:00
Tunglies bfb50ac0d7 refactor(config): share patch macro and profiles helpers
IVerge::patch_config, IProfiles::patch_item and PrfOption::merge each
hand-rolled the same copy-Some-overwrite pattern; they now share one
patch_opt_field macro (the or-else merge form is field-for-field
equivalent for Option fields). The four profiles *_safe wrappers that
inlined Config::profiles().with_data_modify boilerplate delegate to a
with_profiles_edit helper, and record/forget_selected_node share the
current-item lookup prelude via current_item_mut.
2026-10-28 09:14:31 +08:00
Tunglies b79c5615a5 refactor(config): dedupe prfitem factories and companion seeding
from_rules/from_proxies/from_groups differed only in uid prefix, type
string and template, so they now delegate to a shared from_seq_item.
The five current_* accessors collapse into a local macro. The identical
five-block companion seeding in from_local and from_url moves into
ensure_companion_items, preserving seeding order and error propagation.
2026-10-28 09:14:31 +08:00
Tunglies 8702779375 refactor(config): fold port guards into single helper
The five IClashTemp port guards duplicated the same read/parse/default
logic with drifting defaults: some hardcoded 7895/7898/7899 while others
referenced network::ports constants. Route them all through one
guard_port_value helper and use the constants everywhere, keeping each
guard's name, key and cfg attributes.
2026-10-28 09:14:31 +08:00
Tunglies a6d680236f refactor(core): drop unreachable hotkey dtor and dedupe tray predicate
Remove impl Drop for Hotkey: the instance lives in a OnceLock static via
the singleton! macro and is never dropped; exit-time unregistration is
already handled by Hotkey::reset().

Spell the include_proxy_groups/display-mode predicate identically at
both evaluation sites in tray menu building; behavior is unchanged
(Option != Some("disable") is equivalent to unwrap_or("default") != "disable").
2026-10-28 09:14:31 +08:00
Tunglies 4c778a93db refactor(enhance): simplify tun mode check and lowercase helpers
- Drop the always-true second clause in use_tun's enhanced-mode check:
  the key is absent only when the unwrap_or default already makes
  current_mode == "fake-ip" true.
- Collapse use_lowercase into a delegation to use_lowercase_owned.
2026-10-28 09:14:31 +08:00
Tunglies dbaee8a5e4 refactor(profile): remove identical blind retry when switching node
The second select_node_for_group call in switch_proxy_node was a
zero-interval retry with identical arguments. Its success path was a
degraded copy of the first (missing the Refresh::Proxies announcement
ever since the old force-refresh emit was removed), and its distinct
"回退/最终失败" log wording misled maintainers. Keep a single call,
the primary success aftermath, and one error log.
2026-10-28 09:14:30 +08:00
Tunglies 3bb708233e refactor(utils): drop dead reset helper and tidy init_resources
- remove resolve_reset_async, which had no callers
- drop the duplicated time.*.com entry from the default fake-ip-filter
- init_resources no longer recreates app_home (ensure_directories during
  init_config already created it) and now propagates res_dir creation errors
2026-10-28 09:14:30 +08:00
Shubham Padkonde 3953e125b2 fix(profile): ignore null entries in the rules editor (#8432) 2026-10-27 17:39:05 +00:00
nrenvo cbd5ed73a5 fix(theme): allow injected CSS to style Monaco view containers (#8385)
`@scope`'s `to` bound excludes the boundary elements themselves, so rules
targeting `.view-lines` never matched. Move the bound to its children
with `> *`: the container becomes styleable, its internals stay excluded.
2026-10-27 15:13:58 +00:00
Tunglies 02e5e0e4a6 ci: gate frontend and rust checks by changed paths (#8433)
The paths-filter output in lint-clippy.yml only gated two echo steps;
checkout, toolchain setup and clippy itself ran unconditionally, so
frontend-only PRs still burned a 3-runner matrix on a full clippy build
plus the logging-check tool install.

- lint-clippy/rustfmt/frontend-check: move path filters to workflow
  level so irrelevant PRs spawn no runner at all
- split contract-parity into its own workflow; its trigger set spans
  both sides and workflow paths cannot vary per job
- clippy paths now cover crates/** and workspace Cargo files, which the
  old src-tauri/** filter missed

dev has no required status checks, so path-filtered workflows cannot
get stuck in "Expected".
2026-10-27 16:36:49 +08:00
0498ba3e4f fix(proxies): render configured groups in global mode (#8402)
* fix(proxies): render configured groups in global mode

Global mode previously only rendered the synthetic GLOBAL group and
sliced off its header, which flattened every user-defined group into
a non-expandable proxy row. Configured groups could no longer be
opened or inspected. Render the same group list as rule/script mode
so every group keeps its own header and expand toggle.

Fixes #8376

* style: keep useRule assignment on one line

* fix & changelog

* fix: hover when global proxy

---------

Co-authored-by: susu-Sugar <214122492+susu-Sugar@users.noreply.github.com>
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2026-10-27 01:41:19 +00:00
Tunglies f39644ee6b chore(typecheck): replace tsc with tsrs as the typecheck gate (#8428)
tsrs (Rust port of the TypeScript 7 checker) replaces tsc --noEmit in
the typecheck script, web:build, and CI. Measured on this repository:
~0.4s vs ~3.5s wall time and ~280 MB vs ~750 MB peak RSS, with
identical zero-diagnostic results on the current tree. The typescript
package stays installed: typescript-eslint projectService and
scripts/cleanup-unused-i18n.mjs consume its compiler API.
2026-10-27 01:34:29 +00:00
488f022c05 feat(media-unlock): add Muse AI (muse.ai) detection (#8411)
* feat(media-unlock): add Muse AI detection

muse.ai redirects every region to Meta's aymh anti-bot gateway before the app loads, and a plain HTTP client cannot finish the JavaScript challenge. Report the result as a new Risky status (exit reachable, availability unverifiable) together with the exit region instead of a misleading Yes/No.

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>

* style(media-unlock): apply rustfmt and biome formatting

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>

* docs(changelog): Update changelog

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>

---------

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
Co-authored-by: susu-Sugar <214122492+susu-Sugar@users.noreply.github.com>
Co-authored-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
2026-10-26 17:17:09 +00:00
Ekko82 eefd025806 feat(home): add an IP auto-refresh toggle (#8403) 2026-10-26 02:04:55 +00:00
❤是纱雾酱哟~andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 10ca0a5d0c chore(deps): update npm dependencies (#8421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-25 16:24:26 +00:00
❤是纱雾酱哟~andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 324c8c9016 chore(deps): update dependency pnpm to v12.15.0 (#8418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-25 15:35:18 +00:00
oomeow 02520d7392 fix(tray): skip Win-key accelerators on Windows (#8400) 2026-10-25 09:34:09 +00:00
Tunglies 5b8edbaead feat(tun): expose mihomo tun dns-mode in TUN settings
Support the dns-mode field introduced in mihomo v1.19.33. A
button group below the stack selector offers
Auto/Disabled/Native/Hijack; Auto writes an empty string, which
the core treats as the legacy behavior. Adding dns-mode to
GUI_KEYS makes the value GUI-authoritative once saved, matching
the other TUN dialog fields.
2026-10-25 16:04:59 +08:00
Tunglies 1ed8050947 fix(perf): auto-derive WebDriver window rect scale in harness 2026-10-25 13:14:14 +08:00
❤是纱雾酱哟~ 3d9a79a3fb fix(windows): patch executable for common-controls-v6 (#8378)
- Enhance patch logic, also patch for "example" executables

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
2026-10-22 09:59:10 +00:00
❤是纱雾酱哟~andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 8b88ddb7f2 chore(deps): update node.js to v24.22.1 (#8389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-21 17:25:15 +00:00
Tunglies 02bd739ab6 docs: clarify issue and PR intake rules 2026-10-21 19:36:50 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Tunglies
b6c7af82a7 chore(deps): update tauri (#8346)
* chore(deps): update tauri

* fix(deps): pin rust-ini to 0.21.1 for tauri-plugin-deep-link 2.6.2

tauri-plugin-deep-link 2.6.2 constructs ini::ParseOption with an
exhaustive struct literal, which fails to compile against rust-ini
0.21.2+ (new required fields enabled_indented_mutiline_value and
enabled_preserve_key_leading_whitespace, E0063 on linux). Upstream
fixed it in tauri-apps/plugins-workspace#3711 with ..Default::default()
but no release contains the fix yet, so downgrade rust-ini in Cargo.lock
until deep-link 2.6.3 ships.

---------

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