Compare commits

..
Author SHA1 Message Date
Tunglies 2fd6be642e refactor(services): drop the bespoke retry engine from the query wrapper
The custom onErrorRetry existed only to honor a function-form
retryDelay, which exactly one caller (the app data provider) used for
exponential backoff. Fall back to SWR's built-in error retry and
express that caller's cadence with constant options.
2026-10-28 10:52:18 +08:00
Tunglies 433230aedc refactor(hooks): trim dead surface from proxy selection
Drop the connection-cleanup option nobody ever disabled (behavior now
unconditional), the returned changeProxy no consumer called and the two
one-caller argument adapters; both call sites use the shared select
entry directly.
2026-10-28 10:43:01 +08:00
Tunglies 05475c7bb5 refactor(hooks): drop the write-only error channel and key-rotating refresh
The mihomo ws subscription's next callback accepted an error half that
baseNext silently discarded while traffic, memory and log consumers
kept constructing fallback payloads for it. Refresh minted a fresh
SWR key per call via a localStorage timestamp, abandoning old keys in
the cache forever. Reduce next to its data half and reset the snapshot
under a stable key instead.
2026-10-28 10:31:43 +08:00
Tunglies eb60c21c62 refactor(shared): replace the traffic error boundary with ErrorBoundary
TrafficErrorBoundary maintained a second boundary system beside
BaseErrorBoundary: a report builder that only console.logged constants
in the Tauri webview, an onError that re-logged what componentDidCatch
already logged, and a fallback wrapper with a single consumer. Fold
both mount points into react-error-boundary (the primitive behind
BaseErrorBoundary) with local fallbacks that keep the panel styling,
message text and retry reset, log each error once, and drop the retry
attempt cap and the stack-details collapse along with it.
2026-10-28 10:17:37 +08:00
Tunglies 4489c9a26f refactor(traffic): drop unreachable inline monitor fallback
The inline traffic monitor duplicated the worker's message loop,
snapshot throttling and aging logic but only runs when Worker is
undefined or its constructor throws, which no supported Tauri webview
or browser hits. Keep the worker as the single implementation and
remove the mode switch and the pending-message queue that could only
ever buffer the init message.
2026-10-28 09:59:44 +08:00
Tunglies 98a7932742 fix(services): make retries viable and align polling freshness window
Share no abort signal across retry attempts: after a timeout abort the
old signal stays dead and every later fetch rejects immediately, so
retries only burned backoff time. Also drop the system state query's
staleTime: the wrapper maps it to SWR's dedupingInterval, which
swallowed every other 3s poll tick while it exceeded the interval.
2026-10-28 09:42:04 +08:00
Tunglies c678d60964 fix(delay): write pending state once per node in batch checks
checkListDelay pre-marked every node as checking, help() re-marked it
when picked up and measureDelay marked it a third time on entry, so
each batch run queued three identical pending updates per node. Keep
the pre-mark (immediate feedback for queued nodes) and drop the
redundant marks along the chain.
2026-10-28 09:27:29 +08:00
Tunglies d5bc4ff58b fix(setting): mount only reachable proxy control viewers
ProxyControlSwitches unconditionally mounted both SysproxyViewer and
TunViewer while its switch rows are mutually exclusive, so every
instance carried a dialog that could never open, each firing
getEmbeddedServerPort or a clash query subscription on mount. Mount
each viewer under the same condition as the switch row that opens it.

SettingSystem also mounted its own SysproxyViewer and TunViewer whose
refs were never opened there; the reachable entries are the
ProxyControlSwitches rows, so the dead pair is removed. Viewer mounts
across the app drop from eight (setting-system: two own + two per
instance, proxy-tun-card: two) to three (one per rendered switch
variant).
2026-10-28 09:19:23 +08:00
Tunglies b81981162b fix(setting): build viewer form defaults from a single source
sysproxy, misc and tunnels viewers duplicated their form default
literals in useState and open(), and the two copies in misc-viewer had
already diverged (app log max size, app log max count, auto log clean).
Extract a buildInitialValues factory per viewer and unify on the open()
values, which are the only ones users could ever see.
2026-10-28 09:13:14 +08:00
Tunglies 6fa6d7664f fix(setting): write auto launch patch once per toggle
GuardState applies the optimistic onChange write before onGuard runs,
so repeating the same onChangeData patch inside the guard handler
duplicated every cache write and subscriber notification for the auto
launch switch.
2026-10-28 06:41:18 +08:00
Tunglies 4967ad347e fix(rules): refresh once per visibility trigger
The mount effect fired refreshRules and refreshRuleProviders
unconditionally and again under the page-visible flag, double-firing
both queries on mount and on every hide-to-show transition. Gate the
refresh on visibility once so each trigger performs exactly one
revalidation.
2026-10-28 06:36:33 +08:00
Tunglies 6394f347f5 refactor(types): remove dead provider view types superseded by bindings
Drop the handwritten IProxyProviderItem and IRuleProviderItem
interfaces: IProxyProviderItem is a stale duplicate of the generated
ProxyProviderView contract, and neither has any references left in
the tree.

Keep IProxyItem and IProxyGroupItem: IConfigData still declares
'proxy-groups'?: IProxyGroupItem[], which builds on IProxyItem.
2026-10-28 06:33:08 +08:00
Tunglies e56711b2db refactor(frontend): drop unused params, props and api surface
Remove the never-read _revalidate argument of mutateVerge and its 16
call-site placeholders, seven never-passed props of the split chip
editor, the unconsumed refreshDecorated context member, the now-orphaned
toggleFullscreen control, the unused translate argument of the notice
message handler, the reserved-but-unset ServiceConfig.timeout field and
unreachable branches in parseNoticeExtras.
2026-10-28 06:30:46 +08:00
Tunglies 3e28b287d1 refactor(frontend): remove dead states, refs and unreachable branches
Drop state that can only hold its initial value (profiles page disabled
flag, traffic graph currentFPS), a write-only window controls ref with
its unconsumed imperative handle, a no-op appendData ref method, an
unreachable gradient branch, an invisible fadeIn shell, constant-true
subexpressions in the system info card, an empty click handler and a
pointless useMemo.
2026-10-28 06:08:44 +08:00
Tunglies 51a4aa80dd docs(agents): add frontend liveness constraints
Add six liveness rules to the frontend guardrails covering dead state,
unused exposed surface, unreachable fallback paths, duplicate side
effects per interaction, unreachable mounts and unbounded resource
lifecycles. These target defect classes that the existing CI gates
(knip, eslint, arch:check) cannot detect mechanically.
2026-10-28 06:03:11 +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
145 changed files with 1204 additions and 1732 deletions
+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
+13 -103
View File
@@ -2,11 +2,23 @@ name: Frontend Check
on:
pull_request:
paths:
- "src/**"
- "**/*.js"
- "**/*.ts"
- "**/*.tsx"
- "**/*.css"
- "**/*.scss"
- "**/*.json"
- "**/*.md"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "vite.config.*"
- ".github/workflows/frontend-check.yml"
workflow_dispatch:
permissions:
contents: read
pull-requests: read
env:
HUSKY: 0
@@ -17,46 +29,17 @@ jobs:
steps:
- uses: actions/checkout@v7
- name: Check frontend changes
id: check_frontend
uses: dorny/paths-filter@v4
with:
filters: |
frontend:
- 'src/**'
- '**/*.js'
- '**/*.ts'
- '**/*.tsx'
- '**/*.css'
- '**/*.scss'
- '**/*.json'
- '**/*.md'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'knip.json'
- 'eslint.config.ts'
- 'tsconfig.json'
- 'vite.config.*'
- name: Skip if no frontend changes
if: steps.check_frontend.outputs.frontend != 'true'
run: echo "No frontend changes, skipping frontend checks."
- name: Install pnpm
if: steps.check_frontend.outputs.frontend == 'true'
uses: pnpm/action-setup@v6
with:
run_install: false
- uses: actions/setup-node@v7
if: steps.check_frontend.outputs.frontend == 'true'
with:
node-version: '24.22.1'
cache: 'pnpm'
- name: Restore pnpm cache
if: steps.check_frontend.outputs.frontend == 'true'
uses: actions/cache@v6
with:
path: ~/.pnpm-store
@@ -65,94 +48,21 @@ jobs:
pnpm-shared-stable-${{ runner.os }}-
- run: pnpm install --frozen-lockfile
if: steps.check_frontend.outputs.frontend == 'true'
- name: Run Prettier check
if: steps.check_frontend.outputs.frontend == 'true'
run: pnpm format:check
- name: Run ESLint
if: steps.check_frontend.outputs.frontend == 'true'
run: pnpm lint
- name: Run TypeScript typecheck
if: steps.check_frontend.outputs.frontend == 'true'
run: pnpm typecheck
- name: Run tests
if: steps.check_frontend.outputs.frontend == 'true'
run: pnpm test
- name: Run Knip
if: steps.check_frontend.outputs.frontend == 'true'
run: pnpm knip:check
- name: Architecture gates (listen/emit only in the event bus, invoke only in services)
if: steps.check_frontend.outputs.frontend == 'true'
run: node scripts/check-architecture.mjs listen emit invoke
contract-parity:
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@v7
- name: Check contract inputs
id: check_inputs
uses: dorny/paths-filter@v4
with:
filters: scripts/contract-inputs.json
- name: Skip if contract inputs unchanged
if: steps.check_inputs.outputs.contract != 'true'
run: echo "No contract inputs changed, skipping parity check."
- name: Install Rust Stable
if: steps.check_inputs.outputs.contract == 'true'
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Rust Cache
if: steps.check_inputs.outputs.contract == 'true'
uses: Swatinem/rust-cache@v2
with:
prefix-key: 'v1-rust'
key: 'rust-shared-stable-ubuntu-22.04-x86_64-unknown-linux-gnu-contract'
workspaces: |
. -> target
cache-all-crates: true
cache-workspace-crates: true
- name: Install dependencies
if: steps.check_inputs.outputs.contract == 'true'
run: |
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install pnpm
if: steps.check_inputs.outputs.contract == 'true'
uses: pnpm/action-setup@v6
with:
run_install: false
- uses: actions/setup-node@v7
if: steps.check_inputs.outputs.contract == 'true'
with:
node-version: '24.22.1'
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:
pull_request:
paths:
- "src-tauri/**"
- "crates/**"
- "Cargo.toml"
- "Cargo.lock"
- "rust-toolchain.toml"
- ".github/workflows/lint-clippy.yml"
workflow_dispatch:
env:
HUSKY: 0
@@ -23,28 +30,6 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Check src-tauri changes
if: github.event_name != 'workflow_dispatch'
id: check_changes
uses: dorny/paths-filter@v4
with:
filters: |
rust:
- 'src-tauri/**'
- name: Skip if src-tauri not changed
if: github.event_name != 'workflow_dispatch' && steps.check_changes.outputs.rust != 'true'
run: echo "No src-tauri changes, skipping clippy lint."
- name: Continue if src-tauri changed
if: github.event_name != 'workflow_dispatch' && steps.check_changes.outputs.rust == 'true'
run: echo "src-tauri changed, running clippy lint."
- name: Manual trigger - always run
if: github.event_name == 'workflow_dispatch'
run: |
echo "Manual trigger detected: skipping changes check and running clippy."
- name: Checkout Repository
uses: actions/checkout@v7
@@ -79,7 +64,7 @@ jobs:
run: cargo clippy-all
- name: Run Windows security tests
if: matrix.os == 'windows-latest' && (github.event_name == 'workflow_dispatch' || steps.check_changes.outputs.rust == 'true')
if: matrix.os == 'windows-latest'
working-directory: ./src-tauri
run: cargo test --all-features --target ${{ matrix.target }}
+4 -15
View File
@@ -6,6 +6,10 @@ name: Check Formatting
on:
pull_request:
paths:
- "src-tauri/**"
- "**/*.rs"
- ".github/workflows/rustfmt.yml"
permissions:
contents: read
@@ -19,25 +23,10 @@ jobs:
steps:
- uses: actions/checkout@v7
- name: Check Rust changes
id: check_rust
uses: dorny/paths-filter@v4
with:
filters: |
rust:
- 'src-tauri/**'
- '**/*.rs'
- name: Skip if no Rust changes
if: steps.check_rust.outputs.rust != 'true'
run: echo "No Rust changes, skipping rustfmt."
- name: install Rust stable and rustfmt
if: steps.check_rust.outputs.rust == 'true'
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: run cargo fmt
if: steps.check_rust.outputs.rust == 'true'
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
Generated
+13 -9
View File
@@ -1139,6 +1139,7 @@ dependencies = [
"serde_json",
"serde_yaml_ng",
"sha2 0.11.1",
"smartstring",
"socket2",
"specta",
"specta-typescript",
@@ -1168,7 +1169,6 @@ dependencies = [
"tokio-stream",
"tokio-util",
"tracing",
"tristr",
"warp",
"webpki-roots",
"windows 0.62.2",
@@ -6720,6 +6720,18 @@ dependencies = [
"thiserror 2.0.20",
]
[[package]]
name = "smartstring"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [
"autocfg",
"serde",
"static_assertions",
"version_check",
]
[[package]]
name = "socket2"
version = "0.6.5"
@@ -8493,14 +8505,6 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "tristr"
version = "0.1.0"
dependencies = [
"serde",
"specta",
]
[[package]]
name = "try-lock"
version = "0.2.5"
+2 -1
View File
@@ -85,8 +85,9 @@ reqwest = { version = "0.13.5", features = [
] }
rust_iso3166 = "0.2.0"
smartstring = { version = "1.0.1" }
serde = { version = "1.0.229" }
tristr = { path = "../tristr", default-features = false, features = ["serde", "specta"] }
serde_json = { version = "1.0.154" }
serde_yaml_ng = { version = "0.10.0" }
bitflags = { version = "2.13.2" }
+5
View File
@@ -3,10 +3,13 @@
<details>
<summary><strong> 🐞 修复问题 </strong></summary>
- 修复规则扩展包含空列表项时,「编辑规则」界面报错的问题
- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
- 修复 首页 网站测速失败后卡片一直显示测试中
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
- 修复 代理 在全局模式下无法展开节点组
- 修复 自定义 CSS 无法修改编辑器字体和字号的问题
**🐧 Linux**
@@ -18,6 +21,8 @@
<summary><strong> ✨ 新增功能 </strong></summary>
- 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为
- 新增「IP 信息」自动刷新开关,关闭后仍可手动刷新
- 新增 Muse AI 解锁检测,针对 Muse AI 特有的 Cloudflare 人机验证增加「Risky」状态标记
</details>
@@ -11,6 +11,7 @@ mod chatgpt;
mod claude;
mod disney_plus;
mod gemini;
mod muse;
mod netflix;
mod prime_video;
mod spotify;
@@ -30,6 +31,7 @@ enum UnlockCheck {
ChatGptWeb,
Claude,
Gemini,
MuseAi,
YouTubePremium,
BahamutAnime,
Netflix,
@@ -46,6 +48,7 @@ impl UnlockCheck {
Self::ChatGptWeb,
Self::Claude,
Self::Gemini,
Self::MuseAi,
Self::YouTubePremium,
Self::BahamutAnime,
Self::Netflix,
@@ -66,6 +69,7 @@ impl UnlockCheck {
Self::ChatGptWeb => chatgpt::CHATGPT_WEB_NAME,
Self::Claude => claude::CLAUDE_NAME,
Self::Gemini => gemini::GEMINI_NAME,
Self::MuseAi => muse::MUSE_AI_NAME,
Self::YouTubePremium => youtube::YOUTUBE_PREMIUM_NAME,
Self::BahamutAnime => bahamut::BAHAMUT_ANIME_NAME,
Self::Netflix => netflix::NETFLIX_NAME,
@@ -83,6 +87,7 @@ impl UnlockCheck {
Self::ChatGptWeb => chatgpt::check_chatgpt(client).await,
Self::Claude => claude::check_claude(client).await,
Self::Gemini => gemini::check_gemini(client).await,
Self::MuseAi => muse::check_muse_ai(client).await,
Self::YouTubePremium => youtube::check_youtube_premium(client).await,
Self::BahamutAnime => bahamut::check_bahamut_anime(client).await,
Self::Netflix => netflix::check_netflix(client).await,
@@ -0,0 +1,48 @@
use reqwest::Client;
use crate::utils::{classify_restricted_status, get_trace_location};
use super::UnlockItem;
pub(crate) const MUSE_AI_NAME: &str = "Muse AI";
const MUSE_URL: &str = "https://muse.ai";
const CLOUDFLARE_TRACE_URL: &str = "https://www.cloudflare.com/cdn-cgi/trace";
// The aymh ("Are You a Human") gateway answers plain HTTP requests with a
// generic Meta error page that always contains the Facebook logo asset.
const AYMH_ERROR_MARKERS: &[&str] = &["Sorry, something went wrong", "static.facebook.com/images/logos"];
pub(super) async fn check_muse_ai(client: &Client) -> UnlockItem {
// muse.ai itself is served by Vercel, so resolve the exit region through
// the generic Cloudflare trace endpoint instead of a service-specific one.
let region = get_trace_location(client, CLOUDFLARE_TRACE_URL)
.await
.map(|code| code.trim().to_ascii_uppercase())
.map(|code| UnlockItem::region_label(&code));
let Ok(response) = client.get(MUSE_URL).send().await else {
return UnlockItem::checked(MUSE_AI_NAME, "Failed (Network Connection)", region);
};
let status = response.status();
let landed_on_aymh = response.url().host_str() == Some("auth.muse.ai");
let body = response.text().await.unwrap_or_default();
// Every region is redirected to the aymh anti-bot gateway before the app
// loads, and a non-browser HTTP client cannot finish the JavaScript
// challenge. It returns the Meta error page, so reaching this point only
// proves the exit can connect to Muse's auth infrastructure; availability
// cannot be verified over plain HTTP and is reported as `Risky`.
let behind_anti_bot = landed_on_aymh || AYMH_ERROR_MARKERS.iter().any(|marker| body.contains(marker));
let result = if behind_anti_bot {
"Risky"
} else if status.is_success() {
"Yes"
} else {
classify_restricted_status(status).unwrap_or("Failed")
};
UnlockItem::checked(MUSE_AI_NAME, result, region)
}
+3 -2
View File
@@ -14,7 +14,7 @@
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
"tauri": "tauri",
"web:dev": "vite",
"web:build": "tsc --noEmit && vite build",
"web:build": "tsrs -p . --noEmit && vite build",
"web:serve": "vite preview",
"prebuild": "node scripts/prebuild.mjs",
"updater": "node scripts/updater.mjs",
@@ -32,7 +32,7 @@
"i18n:types": "node scripts/generate-i18n-keys.mjs",
"gen:contract": "node scripts/gen-contract.mjs",
"arch:check": "node scripts/check-architecture.mjs listen emit invoke",
"typecheck": "tsc --noEmit",
"typecheck": "tsrs -p . --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"knip:check": "knip",
@@ -128,6 +128,7 @@
"sass": "^1.104.1",
"tar": "^7.5.22",
"terser": "^5.51.2",
"tsrs": "0.20.0-ts7.1.0-beta",
"typescript": "^6.0.3",
"typescript-eslint": "^8.70.0",
"vite": "^8.3.0",
+88 -3
View File
@@ -416,6 +416,9 @@ importers:
terser:
specifier: ^5.51.2
version: 5.51.2
tsrs:
specifier: 0.20.0-ts7.1.0-beta
version: 0.20.0-ts7.1.0-beta
typescript:
specifier: ^6.0.3
version: 6.0.3
@@ -2218,6 +2221,52 @@ packages:
'@tauri-apps/plugin-updater@2.14.0':
resolution: {integrity: sha512-gdylJ9tger4hkyOn24QZd6MNOKVo2Eh53iw5th2Qj1vKTR/+u4PIwJWuiis5hohYvobn6iOvlnALfrqIPSzh7Q==}
'@tsrs/darwin-arm64@0.20.0-ts7.1.0-beta':
resolution: {integrity: sha512-mhHj8QGUmdJDBvJEnMXx5P/jvAKXYU3hy/z3qxrWBo8XHlbLES32PgC0ZpTRbJA960ReTpVqqvCOYaC6EL32/Q==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [darwin]
'@tsrs/darwin-x64@0.20.0-ts7.1.0-beta':
resolution: {integrity: sha512-CsRUnyYRLrhmChS7ol7LgmdqMXTu+mVlDU+7JgbW8RpkJRGLZ/zgEDEOeD8qrndt8rpP9oW1T1SJ36HSzB3/sg==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [darwin]
'@tsrs/linux-arm64-gnu@0.20.0-ts7.1.0-beta':
resolution: {integrity: sha512-VNwpdI/iICwQMMlS/MoSZCvKFByEn2+g7D0HREWG+3r3GRayXgbLv8iaNSHKT3QGcu8pXr3lH9170ghJQi2zRg==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tsrs/linux-arm64-musl@0.20.0-ts7.1.0-beta':
resolution: {integrity: sha512-cXnqPpmj5gzeGi4u1OV0ctd5df5t4ivwyeToCa4pBrKtjYodu0EeAo5Y76SHbJwggbiA1gzEJVj1vvuciQm5mQ==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tsrs/linux-x64-gnu@0.20.0-ts7.1.0-beta':
resolution: {integrity: sha512-jlhJSrO9lxNh0TF5EU1i+Frok5sRWsBvCuQhmdqMZKIN2pSHsDZl3CSmKUsJ4tNr50np9USCPpkkxwby2Eiqhg==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tsrs/linux-x64-musl@0.20.0-ts7.1.0-beta':
resolution: {integrity: sha512-pestGEPplhEPzIERc7zwuQ5zhh9ZwMSvAPNQy5TM0je/QuaizmM1zKoPYJ4NiaPzLh2M25HtP4HcpumTqmF3KQ==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tsrs/win32-x64@0.20.0-ts7.1.0-beta':
resolution: {integrity: sha512-sstxBfhjjOSYvSHFfSmROau2oGAVNLwAnj+H0h+ywLo7N7ueFKIDk14HAkIYa/F9iVboiICCpHDY/pmBO71sSg==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [win32]
'@tybys/wasm-util@0.10.4':
resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==}
@@ -4770,6 +4819,11 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsrs@0.20.0-ts7.1.0-beta:
resolution: {integrity: sha512-8NifAn2tcna3t/nluu1yFvp0fYE28vElDpnD4jiWik69+MDjiHgxdjH8sUYUEFmf/2hl6QIE5ExVGpJzQbBDPw==}
engines: {node: '>=16.20.0'}
hasBin: true
tunnel@0.0.6:
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
@@ -6882,6 +6936,27 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.12.4
'@tsrs/darwin-arm64@0.20.0-ts7.1.0-beta':
optional: true
'@tsrs/darwin-x64@0.20.0-ts7.1.0-beta':
optional: true
'@tsrs/linux-arm64-gnu@0.20.0-ts7.1.0-beta':
optional: true
'@tsrs/linux-arm64-musl@0.20.0-ts7.1.0-beta':
optional: true
'@tsrs/linux-x64-gnu@0.20.0-ts7.1.0-beta':
optional: true
'@tsrs/linux-x64-musl@0.20.0-ts7.1.0-beta':
optional: true
'@tsrs/win32-x64@0.20.0-ts7.1.0-beta':
optional: true
'@tybys/wasm-util@0.10.4':
dependencies:
tslib: 2.8.1
@@ -7222,7 +7297,7 @@ snapshots:
'@wdio/globals@9.31.3(expect-webdriverio@6.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))':
dependencies:
expect-webdriverio: 6.1.0(@wdio/globals@9.31.3)(@wdio/logger@10.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
expect-webdriverio: 6.1.0(@wdio/globals@9.31.3)(@wdio/logger@9.29.1)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
webdriverio: 10.1.0(supports-color@7.2.0)(yauzl@2.10.0)
'@wdio/logger@10.1.0':
@@ -8206,11 +8281,11 @@ snapshots:
expect-type@1.4.0: {}
expect-webdriverio@6.1.0(@wdio/globals@9.31.3)(@wdio/logger@10.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0)):
expect-webdriverio@6.1.0(@wdio/globals@9.31.3)(@wdio/logger@9.29.1)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0)):
dependencies:
'@vitest/snapshot': 4.1.11
'@wdio/globals': 9.31.3(expect-webdriverio@6.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
'@wdio/logger': 10.1.0
'@wdio/logger': 9.29.1
deep-eql: 5.0.2
expect: 30.5.2
jest-matcher-utils: 30.5.2
@@ -9865,6 +9940,16 @@ snapshots:
tslib@2.8.1: {}
tsrs@0.20.0-ts7.1.0-beta:
optionalDependencies:
'@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
tunnel@0.0.6: {}
type-check@0.4.0:
+8
View File
@@ -10,3 +10,11 @@ allowBuilds:
unrs-resolver: true
minimumReleaseAgeExclude:
- dayjs@1.11.21
- "@tsrs/darwin-arm64@0.20.0-ts7.1.0-beta"
- "@tsrs/darwin-x64@0.20.0-ts7.1.0-beta"
- "@tsrs/linux-arm64-gnu@0.20.0-ts7.1.0-beta"
- "@tsrs/linux-arm64-musl@0.20.0-ts7.1.0-beta"
- "@tsrs/linux-x64-gnu@0.20.0-ts7.1.0-beta"
- "@tsrs/linux-x64-musl@0.20.0-ts7.1.0-beta"
- "@tsrs/win32-x64@0.20.0-ts7.1.0-beta"
- tsrs@0.20.0-ts7.1.0-beta
+7
View File
@@ -79,6 +79,13 @@
"matchPackageNames": ["typescript"],
"allowedVersions": "<7.0.0 || >=7.1.0"
},
{
"description": "Review tsrs bumps alone; the version encodes the pinned TypeScript release",
"matchPackageNames": ["tsrs", "/^@tsrs//"],
"groupName": "tsrs",
"rangeStrategy": "pin",
"automerge": false
},
{
"description": "Prevent project from supply chain attack",
"matchDatasources": ["npm"],
+1 -1
View File
@@ -17,7 +17,7 @@
"pnpm-lock.yaml",
"pnpm-workspace.yaml",
"scripts/contract-inputs.json",
".github/workflows/frontend-check.yml",
".github/workflows/contract-parity.yml",
".husky/pre-push"
]
}
+1 -1
View File
@@ -52,9 +52,9 @@ anyhow = { workspace = true }
tokio = { workspace = true }
log = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tristr = { workspace = true }
serde_json = { workspace = true }
serde_yaml_ng = { workspace = true }
smartstring = { workspace = true, features = ["serde"] }
bitflags = { workspace = true }
socket2 = { workspace = true }
warp = { version = "0.4.3", features = ["server"] }
+14 -11
View File
@@ -5,8 +5,8 @@ use crate::{
feat,
utils::dirs,
};
use smartstring::alias::String;
use tauri::{AppHandle, Manager as _, ipc::Channel};
use tristr::alias::String;
#[tauri::command]
#[specta::specta]
@@ -59,10 +59,10 @@ pub async fn restart_app() -> CmdResult<()> {
#[specta::specta]
pub async fn install_update(
app_handle: AppHandle,
version: String,
version: std::string::String,
on_event: Channel<DownloadEvent>,
) -> CmdResult<bool> {
let version: String = version;
let version: String = version.into();
SilentUpdater::global()
.install_update(&app_handle, &version, |event| {
let _ = on_event.send(event);
@@ -79,22 +79,25 @@ pub fn cancel_update_download() {
#[tauri::command]
#[specta::specta]
pub fn get_app_dir() -> CmdResult<String> {
pub fn get_app_dir() -> CmdResult<std::string::String> {
let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into();
Ok(app_home_dir)
}
#[tauri::command]
#[specta::specta]
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
let url: String = url;
let name: String = name;
feat::download_icon_cache(url, name).await
pub async fn download_icon_cache(
url: std::string::String,
name: std::string::String,
) -> CmdResult<std::string::String> {
let url: String = url.into();
let name: String = name.into();
feat::download_icon_cache(url, name).await.map(Into::into)
}
#[tauri::command]
#[specta::specta]
pub async fn copy_icon_file(path: String, icon_info: feat::IconInfo) -> CmdResult<String> {
let path: String = path;
feat::copy_icon_file(path, icon_info).await
pub async fn copy_icon_file(path: std::string::String, icon_info: feat::IconInfo) -> CmdResult<std::string::String> {
let path: String = path.into();
feat::copy_icon_file(path, icon_info).await.map(Into::into)
}
+11 -11
View File
@@ -1,7 +1,7 @@
use super::CmdResult;
use crate::{cmd::StringifyErr as _, feat};
use feat::LocalBackupFile;
use tristr::alias::String;
use smartstring::alias::String;
#[tauri::command]
#[specta::specta]
@@ -17,15 +17,15 @@ pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
#[tauri::command]
#[specta::specta]
pub async fn delete_local_backup(filename: String) -> CmdResult<()> {
let filename: String = filename;
pub async fn delete_local_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
feat::delete_local_backup(filename).await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
let filename: String = filename;
pub async fn restore_local_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
feat::restore_local_backup(filename)
.await
.map_err(|error| super::proxy_aware_error(&error))
@@ -33,15 +33,15 @@ pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
#[tauri::command]
#[specta::specta]
pub async fn import_local_backup(source: String) -> CmdResult<String> {
let source: String = source;
feat::import_local_backup(source).await.stringify_err()
pub async fn import_local_backup(source: std::string::String) -> CmdResult<std::string::String> {
let source: String = source.into();
feat::import_local_backup(source).await.map(Into::into).stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
let filename: String = filename;
let destination: String = destination;
pub async fn export_local_backup(filename: std::string::String, destination: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
let destination: String = destination.into();
feat::export_local_backup(filename, destination).await.stringify_err()
}
+16 -16
View File
@@ -12,8 +12,8 @@ use crate::{
},
};
use clash_verge_logging::{Type, logging, logging_error};
use smartstring::alias::String;
use tokio::fs;
use tristr::alias::String;
#[tauri::command]
#[specta::specta]
@@ -37,8 +37,8 @@ pub async fn patch_clash_config(payload: crate::ipc::YamlMapping) -> CmdResult {
#[tauri::command]
#[specta::specta]
pub async fn patch_clash_mode(payload: String) -> CmdResult {
let payload: String = payload;
pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult {
let payload: String = payload.into();
feat::change_clash_mode(payload)
.await
.with_error_code("CLASH_MODE_UPDATE_FAILED")
@@ -47,14 +47,14 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult {
/// Reads the saved mode without depending on strict mihomo `/configs` deserialization.
#[tauri::command]
#[specta::specta]
pub async fn get_clash_mode() -> CmdResult<Option<String>> {
pub async fn get_clash_mode() -> CmdResult<Option<std::string::String>> {
Ok(Config::clash().await.data_arc().get_mode())
}
#[tauri::command]
#[specta::specta]
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<CommandFailure>> {
let clash_core: String = clash_core;
pub async fn change_clash_core(clash_core: std::string::String) -> CmdResult<Option<CommandFailure>> {
let clash_core: String = clash_core.into();
logging!(info, Type::Config, "changing core to {clash_core}");
match CoreManager::global().change_core(&clash_core).await {
@@ -112,8 +112,8 @@ pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeRepor
#[tauri::command]
#[specta::specta]
pub async fn test_delay(url: String) -> CmdResult<u32> {
let url: String = url;
pub async fn test_delay(url: std::string::String) -> CmdResult<u32> {
let url: String = url.into();
let result = match feat::test_delay(url).await {
Ok(delay) => delay,
Err(e) => {
@@ -148,12 +148,12 @@ pub fn take_dns_override_notice() -> bool {
#[tauri::command]
#[specta::specta]
pub async fn set_dns_override(
profile_uid: String,
profile_uid: std::string::String,
enabled: bool,
confirmation: Option<String>,
confirmation: Option<std::string::String>,
) -> CmdResult<feat::DnsOverrideOutcome> {
let profile_uid: String = profile_uid;
let confirmation = confirmation;
let profile_uid: String = profile_uid.into();
let confirmation = confirmation.map(String::from);
feat::set_dns_override(profile_uid, enabled, confirmation)
.await
.map_err(|error| proxy_aware_coded_error(&error, "DNS_OVERRIDE_UPDATE_FAILED"))
@@ -212,14 +212,14 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
#[tauri::command]
#[specta::specta]
pub async fn get_dns_config_content() -> CmdResult<Option<String>> {
pub async fn get_dns_config_content() -> CmdResult<Option<std::string::String>> {
use crate::utils::dirs;
use tokio::fs;
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
match fs::read_to_string(&dns_path).await {
Ok(content) => Ok(Some(content.into())),
Ok(content) => Ok(Some(content)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(CommandFailure::plain(error)),
}
@@ -244,7 +244,7 @@ pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
#[tauri::command]
#[specta::specta]
pub async fn get_clash_logs() -> CmdResult<Vec<String>> {
pub async fn get_clash_logs() -> CmdResult<Vec<std::string::String>> {
let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default();
Ok(logs.into_iter().collect())
Ok(logs)
}
@@ -2,7 +2,6 @@ pub use clash_verge_media_unlock::UnlockItem;
use reqwest::{Client, Proxy};
use std::time::Duration;
use tauri::{command, ipc::Channel};
use tristr::alias::String;
#[command]
#[specta::specta]
@@ -24,7 +23,7 @@ async fn create_client() -> Result<Client, String> {
.cookie_store(true)
.proxy(proxy)
.build()
.map_err(|error| format!("创建HTTP客户端失败: {error}").into())
.map_err(|error| format!("创建HTTP客户端失败: {error}"))
}
#[command]
@@ -41,7 +40,5 @@ pub async fn check_media_unlock(on_complete: Channel<UnlockItem>) -> Result<Vec<
#[specta::specta]
pub async fn check_media_unlock_item(name: String) -> Result<UnlockItem, String> {
let client = create_client().await?;
clash_verge_media_unlock::check_media_unlock_item(&client, &name)
.await
.map_err(Into::into)
clash_verge_media_unlock::check_media_unlock_item(&client, &name).await
}
+16 -6
View File
@@ -1,5 +1,5 @@
use anyhow::Result;
use tristr::alias::String;
use smartstring::alias::String;
pub type CmdResult<T = ()> = Result<T, CommandFailure>;
@@ -8,9 +8,9 @@ pub type CmdResult<T = ()> = Result<T, CommandFailure>;
pub struct CommandFailure {
/// Stable frontend identifier, when classified.
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
pub code: Option<std::string::String>,
/// Human-readable diagnostic detail.
pub detail: String,
pub detail: std::string::String,
/// Requested operation when known by the command.
#[serde(skip_serializing_if = "Option::is_none")]
pub operation: Option<crate::core::notification::FailedOperation>,
@@ -20,8 +20,8 @@ impl CommandFailure {
/// A failure with a stable code.
pub fn coded(code: &str, detail: impl std::fmt::Display) -> Self {
Self {
code: Some(code.into()),
detail: detail.to_string().into(),
code: Some(code.to_owned()),
detail: detail.to_string(),
operation: None,
}
}
@@ -30,7 +30,7 @@ impl CommandFailure {
pub fn plain(detail: impl std::fmt::Display) -> Self {
Self {
code: None,
detail: detail.to_string().into(),
detail: detail.to_string(),
operation: None,
}
}
@@ -50,6 +50,16 @@ impl From<&str> for CommandFailure {
}
}
impl From<std::string::String> for CommandFailure {
fn from(detail: std::string::String) -> Self {
Self {
code: None,
detail,
operation: None,
}
}
}
impl From<String> for CommandFailure {
fn from(detail: String) -> Self {
Self::plain(detail)
+6 -13
View File
@@ -5,7 +5,6 @@ use crate::core::{proxy_control, sysopt::Sysopt};
use gethostname::gethostname;
use sysproxy::{Autoproxy, Sysproxy};
use tauri_plugin_clash_verge_sysinfo;
use tristr::alias::String;
#[tauri::command]
#[specta::specta]
@@ -25,8 +24,8 @@ pub async fn get_sys_proxy() -> CmdResult<SystemProxy> {
Ok(SystemProxy {
enable,
server: format!("{host}:{port}").into(),
bypass: bypass.into(),
server: format!("{host}:{port}"),
bypass,
})
}
@@ -40,10 +39,7 @@ pub async fn get_auto_proxy() -> CmdResult<AutoProxy> {
};
let Autoproxy { enable, url } = auto_proxy;
Ok(AutoProxy {
enable,
url: url.into(),
})
Ok(AutoProxy { enable, url })
}
#[tauri::command]
@@ -56,10 +52,10 @@ pub fn get_embedded_server_port() -> CmdResult<u16> {
#[specta::specta]
pub fn get_system_hostname() -> String {
match gethostname().into_string() {
Ok(name) => name.into(),
Ok(name) => name,
Err(os_string) => {
let fallback = format!("{os_string:?}");
fallback.trim_matches('"').into()
fallback.trim_matches('"').to_string()
}
}
}
@@ -68,9 +64,6 @@ pub fn get_system_hostname() -> String {
#[specta::specta]
pub fn get_network_interfaces() -> Vec<String> {
tauri_plugin_clash_verge_sysinfo::list_network_interfaces()
.into_iter()
.map(Into::into)
.collect()
}
#[tauri::command]
@@ -84,7 +77,7 @@ pub fn get_network_interfaces_info() -> CmdResult<Vec<crate::ipc::NetworkInterfa
let mut result = Vec::new();
for interface in interfaces {
if names.iter().any(|name| name == &interface.name) {
if names.contains(&interface.name) {
result.push(interface.into());
}
}
+31 -21
View File
@@ -19,17 +19,17 @@ use crate::{
use clash_verge_draft::{Draft, SharedDraft};
use clash_verge_logging::{Type, logging, logging_error};
use scopeguard::defer;
use smartstring::alias::String;
use std::sync::atomic::{AtomicBool, Ordering};
use tristr::alias::String;
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
fn profile_import_error(err: &anyhow::Error) -> String {
fn profile_import_error(err: &anyhow::Error) -> std::string::String {
if let Some(cause) = err.chain().find(|cause| cause.to_string().contains("TLS 1.0/1.1")) {
return cause.to_string().into();
return cause.to_string();
}
format!("导入订阅失败: {err:#}").into()
format!("导入订阅失败: {err:#}")
}
#[tauri::command]
@@ -68,7 +68,7 @@ pub async fn enhance_profiles() -> CmdResult<ValidationOutcome> {
#[tauri::command]
#[tracing::instrument(skip_all, level = "info", fields(url = %help::mask_url(&url), uid = tracing::field::Empty))]
#[specta::specta]
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
pub async fn import_profile(url: std::string::String, option: Option<PrfOption>) -> CmdResult {
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
Ok(it) => it,
Err(e) => {
@@ -98,9 +98,9 @@ pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult
#[tauri::command]
#[specta::specta]
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
let active_id: String = active_id;
let over_id: String = over_id;
pub async fn reorder_profile(active_id: std::string::String, over_id: std::string::String) -> CmdResult {
let active_id: String = active_id.into();
let over_id: String = over_id.into();
match profiles_reorder_safe(&active_id, &over_id).await {
Ok(_) => {
logging!(info, Type::Cmd, "重新排序配置文件: {} -> {}", active_id, over_id);
@@ -121,9 +121,9 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
#[tauri::command]
#[specta::specta]
pub async fn create_profile(item: ProfileCreate, file_data: Option<String>) -> CmdResult {
pub async fn create_profile(item: ProfileCreate, file_data: Option<std::string::String>) -> CmdResult {
let item: PrfItem = item.into();
let file_data = file_data;
let file_data = file_data.map(String::from);
match profiles_append_item_with_filedata_safe(&item, file_data).await {
Ok(_) => {
profiles_save_file_safe()
@@ -138,8 +138,8 @@ pub async fn create_profile(item: ProfileCreate, file_data: Option<String>) -> C
#[tauri::command]
#[specta::specta]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
let index: String = index;
pub async fn update_profile(index: std::string::String, option: Option<PrfOption>) -> CmdResult {
let index: String = index.into();
match feat::update_profile(&index, option.as_ref(), true).await {
Ok(_) => Ok(()),
Err(e) => {
@@ -151,8 +151,8 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
#[tauri::command]
#[specta::specta]
pub async fn delete_profile(index: String) -> CmdResult {
let index: String = index;
pub async fn delete_profile(index: std::string::String) -> CmdResult {
let index: String = index.into();
let profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
let profiles = Config::profiles().await;
@@ -325,6 +325,7 @@ async fn perform_config_update(
#[derive(Default, serde::Deserialize, specta::Type)]
#[serde(default)]
pub struct ProfileSelection {
#[specta(type = Option<std::string::String>)]
pub current: Option<String>,
}
@@ -332,9 +333,13 @@ pub struct ProfileSelection {
#[serde(default)]
pub struct ProfileCreate {
#[serde(rename = "type")]
#[specta(type = Option<std::string::String>)]
pub itype: Option<String>,
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub desc: Option<String>,
#[specta(type = Option<std::string::String>)]
pub url: Option<String>,
pub option: Option<PrfOption>,
}
@@ -356,10 +361,15 @@ impl From<ProfileCreate> for PrfItem {
#[serde(default)]
pub struct ProfilePatch {
#[serde(rename = "type")]
#[specta(type = Option<std::string::String>)]
pub itype: Option<String>,
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub desc: Option<String>,
#[specta(type = Option<std::string::String>)]
pub file: Option<String>,
#[specta(type = Option<std::string::String>)]
pub url: Option<String>,
pub selected: Option<Vec<crate::config::PrfSelected>>,
pub extra: Option<crate::config::PrfExtra>,
@@ -426,9 +436,9 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
#[tauri::command]
#[specta::specta]
pub async fn patch_profile(index: String, profile: ProfilePatch) -> CmdResult {
pub async fn patch_profile(index: std::string::String, profile: ProfilePatch) -> CmdResult {
let profile: PrfItem = profile.into();
let index: String = index;
let index: String = index.into();
let profiles = Config::profiles().await;
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
&& let Some(new_option) = profile.option.as_ref()
@@ -469,7 +479,7 @@ pub async fn patch_profile(index: String, profile: ProfilePatch) -> CmdResult {
#[tauri::command]
#[specta::specta]
pub async fn view_profile(index: String) -> CmdResult {
pub async fn view_profile(index: std::string::String) -> CmdResult {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
let file = profiles_ref
@@ -494,7 +504,7 @@ pub async fn view_profile(index: String) -> CmdResult {
#[tauri::command]
#[specta::specta]
pub async fn read_profile_file(index: String) -> CmdResult<String> {
pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::string::String> {
let item = {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
@@ -514,7 +524,7 @@ pub async fn read_profile_file(index: String) -> CmdResult<String> {
.join(file.as_str());
match tokio::fs::try_exists(&path).await {
Ok(true) => {}
Ok(false) => return Ok(String::new()),
Ok(false) => return Ok(std::string::String::new()),
Err(err) => {
return Err(coded_error(
"PROFILE_READ_FAILED",
@@ -525,12 +535,12 @@ pub async fn read_profile_file(index: String) -> CmdResult<String> {
}
let data = item.read_file().await.with_error_code("PROFILE_READ_FAILED")?;
Ok(data)
Ok(data.into())
}
#[tauri::command]
#[specta::specta]
pub async fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
pub async fn get_next_update_time(uid: std::string::String) -> CmdResult<Option<i64>> {
let timer = Timer::global();
let next_time = timer.get_next_update_time(&uid).await;
Ok(next_time)
+5 -6
View File
@@ -15,7 +15,6 @@ use std::{
collections::{BTreeMap, HashSet},
sync::atomic::{AtomicBool, Ordering},
};
use tristr::alias::String;
/// Merges one selection pair into fresh backend state to avoid stale-list overwrites.
#[tauri::command]
@@ -49,7 +48,7 @@ fn runtime_group_order(config: Option<&Mapping>) -> Vec<String> {
.filter_map(|name| name.as_str())
.filter(|name| !name.is_empty() && *name != "GLOBAL")
.filter(|name| seen.insert(*name))
.map(String::from)
.map(str::to_owned)
.collect()
}
@@ -64,7 +63,7 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
.into_iter()
.flat_map(|providers| providers.keys())
.filter_map(|name| name.as_str())
.map(String::from)
.map(str::to_owned)
.collect::<Vec<_>>();
all_providers.sort();
let flag = |group: &Mapping, key: &str| group.get(key).and_then(|value| value.as_bool()) == Some(true);
@@ -76,7 +75,7 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
.flatten()
.filter_map(|group| group.as_mapping())
.filter_map(|group| {
let name: String = group.get("name")?.as_str()?.into();
let name = group.get("name")?.as_str()?.to_owned();
let providers = if flag(group, "include-all") || flag(group, "include-all-providers") {
all_providers.clone()
} else {
@@ -86,14 +85,14 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
.into_iter()
.flatten()
.filter_map(|provider| provider.as_str())
.map(String::from)
.map(str::to_owned)
.collect()
};
let exclude_types = group
.get("exclude-type")
.and_then(|types| types.as_str())
.filter(|types| !types.is_empty())
.map(|types| types.split('|').map(String::from).collect())
.map(|types| types.split('|').map(str::to_owned).collect())
.unwrap_or_default();
Some((
name,
+11 -6
View File
@@ -1,8 +1,8 @@
use super::CmdResult;
use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager, utils::yaml_emitter};
use anyhow::{Context as _, anyhow};
use smartstring::alias::String;
use std::collections::HashMap;
use tristr::alias::String;
#[tauri::command]
#[specta::specta]
@@ -17,7 +17,7 @@ pub async fn get_runtime_config() -> CmdResult<Option<crate::ipc::YamlMapping>>
#[tauri::command]
#[specta::specta]
pub async fn get_runtime_yaml() -> CmdResult<String> {
pub async fn get_runtime_yaml() -> CmdResult<std::string::String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_arc();
@@ -36,17 +36,22 @@ pub async fn get_runtime_logs() -> CmdResult<RuntimeLogs> {
#[derive(serde::Serialize, specta::Type)]
#[serde(transparent)]
pub struct RuntimeLogs(HashMap<String, Vec<(String, String)>>);
pub struct RuntimeLogs(
#[specta(type = HashMap<std::string::String, Vec<(std::string::String, std::string::String)>>)]
HashMap<String, Vec<(String, String)>>,
);
#[tauri::command]
#[specta::specta]
pub fn take_discarded_keys_notice() -> Option<String> {
crate::enhance::take_discarded_keys_notice()
pub fn take_discarded_keys_notice() -> Option<std::string::String> {
crate::enhance::take_discarded_keys_notice().map(Into::into)
}
#[tauri::command]
#[specta::specta]
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
pub async fn get_runtime_proxy_chain_config(
proxy_chain_exit_node: std::string::String,
) -> CmdResult<std::string::String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_arc();
+10 -7
View File
@@ -1,4 +1,4 @@
use super::{CmdResult, CommandFailure};
use super::CmdResult;
use crate::{
cmd::StringifyErr as _,
cmd::validate::{ValidationNoticeTarget, handle_validation_notice},
@@ -11,15 +11,18 @@ use crate::{
utils::dirs,
};
use clash_verge_logging::{Type, logging, logging_error};
use smartstring::alias::String;
use tokio::fs;
use tristr::alias::String;
/// 保存profiles的配置
#[tauri::command]
#[specta::specta]
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult<ValidationOutcome> {
let index: String = index;
let file_data = file_data;
pub async fn save_profile_file(
index: std::string::String,
file_data: Option<std::string::String>,
) -> CmdResult<ValidationOutcome> {
let index: String = index.into();
let file_data = file_data.map(String::from);
let file_data = match file_data {
Some(d) => d,
None => return Ok(ValidationOutcome::Valid),
@@ -150,7 +153,7 @@ async fn handle_saved_profile_file(
Err(e) => {
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {e:#}");
restore_original(file_path, original_content, original_existed).await?;
return Err(CommandFailure::plain(e));
return Err(e.to_string().into());
}
}
@@ -178,7 +181,7 @@ async fn handle_saved_profile_file(
Err(err) => {
logging!(error, Type::Config, "[cmd配置save] 运行时配置应用错误: {err:#}");
restore_original(file_path, original_content, original_existed).await?;
Err(CommandFailure::plain(err))
Err(err.to_string().into())
}
}
}
+3 -4
View File
@@ -7,7 +7,6 @@ use crate::{
service::{SERVICE_MANAGER, ServiceStatus, request_runtime_provider_sync},
},
};
use tristr::alias::String;
#[derive(Debug, PartialEq, Eq, serde::Serialize, specta::Type)]
#[serde(tag = "status", rename_all = "camelCase")]
@@ -75,7 +74,7 @@ where
if reasons.is_empty() {
return Err(proxy_aware_coded_error(&error, error_code));
}
let reason: String = reasons.join("\n").into();
let reason = reasons.join("\n");
fallback(reason.clone())
.await
.map_err(|error| proxy_aware_coded_error(&error, "SERVICE_SIDECAR_FAILED"))?;
@@ -180,7 +179,7 @@ mod tests {
let error = match availability {
Some(availability) => anyhow::Error::new(InstallationVerificationError {
status: InstallationStatus {
service_sha256: std::string::String::new(),
service_sha256: String::new(),
protocol: ProtocolInfo::current(),
cores: vec![CoreInspection {
name: "verge-mihomo.exe".into(),
@@ -204,7 +203,7 @@ mod tests {
assert_eq!(
result,
Ok(ServiceInstallOutcome::Sidecar {
reason: format!("verge-mihomo.exe: {reason}").into()
reason: format!("verge-mihomo.exe: {reason}")
})
);
} else {
-1
View File
@@ -2,7 +2,6 @@ use crate::core::{
notification::{self, PendingFailure},
runstate::{RUN_STATE, RunStateView},
};
use tristr::alias::String;
/// Returns one coherent core/service snapshot instead of independently refreshed state.
#[tauri::command]
+13 -9
View File
@@ -4,14 +4,18 @@ use crate::{
config::{Config, IVerge},
core, feat,
};
use tristr::alias::String;
use smartstring::alias::String;
#[tauri::command]
#[specta::specta]
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
let url: String = url;
let username: String = username;
let password: String = password;
pub async fn save_webdav_config(
url: std::string::String,
username: std::string::String,
password: std::string::String,
) -> CmdResult<()> {
let url: String = url.into();
let username: String = username.into();
let password: String = password.into();
let patch = IVerge {
webdav_url: Some(url),
webdav_username: Some(username),
@@ -44,15 +48,15 @@ pub async fn list_webdav_backup() -> CmdResult<Vec<crate::ipc::WebDavFile>> {
#[tauri::command]
#[specta::specta]
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
let filename: String = filename;
pub async fn delete_webdav_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
feat::delete_webdav_backup(filename).await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
let filename: String = filename;
pub async fn restore_webdav_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
feat::restore_webdav_backup(filename)
.await
.map_err(|error| super::proxy_aware_error(&error))
+11 -18
View File
@@ -10,7 +10,6 @@ use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
str::FromStr as _,
};
use tristr::alias::String;
#[derive(Default, Debug, Clone)]
pub struct IClashTemp(pub Mapping);
@@ -85,12 +84,12 @@ impl IClashTemp {
#[cfg(unix)]
map.insert(
"external-controller-unix".into(),
Self::guard_external_controller_ipc().as_str().into(),
Self::guard_external_controller_ipc().into(),
);
#[cfg(windows)]
map.insert(
"external-controller-pipe".into(),
Self::guard_external_controller_ipc().as_str().into(),
Self::guard_external_controller_ipc().into(),
);
map.insert("tun".into(), tun_config.into());
cors_map.insert("allow-private-network".into(), true.into());
@@ -135,18 +134,12 @@ impl IClashTemp {
config.insert("mixed-port".into(), mixed_port.into());
config.insert("socks-port".into(), socks_port.into());
config.insert("port".into(), port.into());
config.insert("external-controller".into(), ctrl.as_str().into());
config.insert("external-controller".into(), ctrl.into());
#[cfg(unix)]
config.insert(
"external-controller-unix".into(),
external_controller_unix.as_str().into(),
);
config.insert("external-controller-unix".into(), external_controller_unix.into());
#[cfg(windows)]
config.insert(
"external-controller-pipe".into(),
external_controller_pipe.as_str().into(),
);
config.insert("external-controller-pipe".into(), external_controller_pipe.into());
config
}
@@ -173,9 +166,9 @@ impl IClashTemp {
port: Self::guard_port(config),
server: Self::guard_client_ctrl(config),
secret: config.get("secret").and_then(|value| match value {
Value::String(val_str) => Some(val_str.clone().into()),
Value::Bool(val_bool) => Some(val_bool.to_string().into()),
Value::Number(val_num) => Some(val_num.to_string().into()),
Value::String(val_str) => Some(val_str.clone()),
Value::Bool(val_bool) => Some(val_bool.to_string()),
Value::Number(val_num) => Some(val_num.to_string()),
_ => None,
}),
}
@@ -187,7 +180,7 @@ impl IClashTemp {
/// 的严格 `BaseConfig` 反序列化,因此即使核心返回的字段与插件结构体不匹配也能取到。
pub fn get_mode(&self) -> Option<String> {
self.0.get("mode").and_then(|value| match value {
Value::String(val_str) => Some(val_str.clone().into()),
Value::String(val_str) => Some(val_str.clone()),
_ => None,
})
}
@@ -283,7 +276,7 @@ impl IClashTemp {
false => Cow::Borrowed(val_str),
};
SocketAddr::from_str(&val).ok().map(|s| s.to_string().into())
SocketAddr::from_str(&val).ok().map(|s| s.to_string())
}
None => None,
})
@@ -297,7 +290,7 @@ impl IClashTemp {
if socket.ip().is_unspecified() {
socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
}
socket.to_string().into()
socket.to_string()
}
Err(_) => "127.0.0.1:9097".into(),
}
+3 -3
View File
@@ -20,10 +20,10 @@ use backon::{ExponentialBuilder, Retryable as _};
use clash_verge_draft::Draft;
use clash_verge_logging::{Type, logging, logging_error};
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::{collections::HashSet, path::PathBuf};
use tokio::sync::{Mutex, MutexGuard, OnceCell};
use tokio::time::sleep;
use tristr::alias::String;
pub(crate) struct Config {
clash_config: Draft<IClashTemp>,
@@ -205,7 +205,7 @@ impl Config {
Self::write_runtime_file(&yaml).await
}
pub(crate) async fn runtime_config_yaml() -> Result<String> {
pub(crate) async fn runtime_config_yaml() -> Result<std::string::String> {
let runtime = Self::runtime().await;
AsyncHandler::spawn_blocking(move || {
let runtime_lastest = runtime.latest_arc();
@@ -216,7 +216,7 @@ impl Config {
.or_else(|| runtime_data.config.as_ref())
.ok_or_else(|| anyhow!("failed to generate runtime config, might need to restart application"))?;
let yaml_str = serde_yaml_ng::to_string(config)?;
Ok(format!("# Generated by Clash Verge\n\n{}", yaml_str).into())
Ok(format!("# Generated by Clash Verge\n\n{}", yaml_str))
})
.await
.map_err(|join| anyhow!("runtime serialization task failed: {join}"))?
+2 -1
View File
@@ -4,8 +4,8 @@ use clash_verge_draft::DraftTransaction;
use serde::{Deserialize, Serialize};
use serde_yaml_ng::{Mapping, Value};
use sha2::{Digest as _, Sha256};
use smartstring::alias::String;
use std::sync::atomic::{AtomicBool, Ordering};
use tristr::alias::String;
use super::{Config, IVerge};
use crate::core::handle::Handle;
@@ -110,6 +110,7 @@ impl DnsOverrideState {
pub struct ProfileDnsSettings {
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub confirmation: Option<String>,
}
+2 -6
View File
@@ -7,7 +7,6 @@ use base64::{Engine as _, engine::general_purpose::STANDARD};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::cell::Cell;
use std::future::Future;
use tristr::alias::String;
const NONCE_LENGTH: usize = 12;
@@ -29,7 +28,7 @@ pub fn encrypt_data(data: &str) -> Result<String, Box<dyn std::error::Error>> {
let mut combined = nonce.to_vec();
combined.extend(ciphertext);
Ok(STANDARD.encode(combined).into())
Ok(STANDARD.encode(combined))
}
pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error>> {
@@ -47,10 +46,7 @@ pub fn decrypt_data(encrypted: &str) -> Result<String, Box<dyn std::error::Error
.decrypt(nonce.into(), ciphertext)
.map_err(|e| format!("Decryption failed: {e}"))?;
// from_utf8 exists only on the std String type; the tristr alias shadows the bare name.
std::string::String::from_utf8(plaintext)
.map_err(|e| e.into())
.map(Into::into)
String::from_utf8(plaintext).map_err(|e| e.into())
}
pub fn serialize_encrypted<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
+2 -3
View File
@@ -18,7 +18,6 @@ use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
use serde_yaml_ng::Value;
use std::{collections::HashSet, net::SocketAddr, str::FromStr as _};
use tristr::alias::String;
#[derive(Clone, Copy)]
struct MixedPortFallback {
@@ -142,8 +141,8 @@ impl Config {
}
pub(crate) fn block_startup_core(error: &anyhow::Error) {
*STARTUP_CORE_BLOCK_REASON.lock() = Some(error.to_string().into());
report_fallback_error(error.to_string().into());
*STARTUP_CORE_BLOCK_REASON.lock() = Some(error.to_string());
report_fallback_error(error.to_string());
}
pub(crate) fn startup_core_block_reason() -> Option<String> {
+29 -9
View File
@@ -9,9 +9,9 @@ use crate::{
use anyhow::{Context as _, Result, bail};
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::time::Duration;
use tokio::fs;
use tristr::alias::String;
// TODO, use other re-export
use reqwest_dav::re_exports::url::form_urlencoded;
use tauri::Url;
@@ -30,18 +30,24 @@ pub(super) fn normalize_profile_home_url(raw: &str) -> Option<String> {
#[derive(Debug, Clone, Deserialize, Serialize, Default, specta::Type)]
#[serde(default)]
pub struct PrfItem {
#[specta(type = Option<std::string::String>)]
pub uid: Option<String>,
/// enum value: remote | local | script | merge
#[serde(rename = "type")]
#[specta(type = Option<std::string::String>)]
pub itype: Option<String>,
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub desc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -56,16 +62,20 @@ pub struct PrfItem {
pub option: Option<PrfOption>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub home: Option<String>,
#[serde(skip)]
#[specta(type = Option<std::string::String>)]
pub file_data: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[serde(default)]
pub struct PrfSelected {
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub now: Option<String>,
}
@@ -81,6 +91,7 @@ pub struct PrfExtra {
#[serde(default)]
pub struct PrfOption {
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub user_agent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -103,10 +114,15 @@ pub struct PrfOption {
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_auto_update: Option<bool>,
#[specta(type = Option<std::string::String>)]
pub merge: Option<String>,
#[specta(type = Option<std::string::String>)]
pub script: Option<String>,
#[specta(type = Option<std::string::String>)]
pub rules: Option<String>,
#[specta(type = Option<std::string::String>)]
pub proxies: Option<String>,
#[specta(type = Option<std::string::String>)]
pub groups: Option<String>,
}
@@ -171,7 +187,7 @@ impl PrfItem {
file_data: Option<String>,
option: Option<&PrfOption>,
) -> Result<Self> {
let uid = help::get_uid("L");
let uid = help::get_uid("L").into();
let file = format!("{uid}.yaml").into();
let opt_ref = option.as_ref();
let update_interval = opt_ref.and_then(|o| o.update_interval);
@@ -344,11 +360,11 @@ impl PrfItem {
.and_then(|value| value.to_str().ok())
.and_then(normalize_profile_home_url);
let uid = help::get_uid("R");
let uid = help::get_uid("R").into();
let file = format!("{uid}.yaml").into();
let name = name
.map(|s| s.to_owned())
.unwrap_or_else(|| filename.unwrap_or_else(|| "Remote File".into()));
.unwrap_or_else(|| filename.map(|s| s.into()).unwrap_or_else(|| "Remote File".into()));
let data = resp.text();
let data = data.trim_start_matches('\u{feff}');
@@ -414,7 +430,7 @@ impl PrfItem {
let (id, template) = if let Some(uid) = uid {
(uid, tmpl::ITEM_MERGE.into())
} else {
(help::get_uid("m"), tmpl::ITEM_MERGE_EMPTY.into())
(help::get_uid("m").into(), tmpl::ITEM_MERGE_EMPTY.into())
};
let file = format!("{id}.yaml").into();
@@ -429,7 +445,11 @@ impl PrfItem {
}
pub(super) fn from_script(uid: Option<String>) -> Self {
let id = if let Some(uid) = uid { uid } else { help::get_uid("s") };
let id = if let Some(uid) = uid {
uid
} else {
help::get_uid("s").into()
};
let file = format!("{id}.js").into(); // js ext
Self {
uid: Some(id),
@@ -442,7 +462,7 @@ impl PrfItem {
}
fn from_rules() -> Self {
let uid = help::get_uid("r");
let uid = help::get_uid("r").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Self {
@@ -456,7 +476,7 @@ impl PrfItem {
}
fn from_proxies() -> Self {
let uid = help::get_uid("p");
let uid = help::get_uid("p").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Self {
@@ -470,7 +490,7 @@ impl PrfItem {
}
fn from_groups() -> Self {
let uid = help::get_uid("g");
let uid = help::get_uid("g").into();
let file = format!("{uid}.yaml").into(); // yaml ext
Self {
+3 -2
View File
@@ -17,6 +17,7 @@ use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::num::NonZeroUsize;
use std::{
collections::{HashMap, HashSet},
@@ -29,7 +30,6 @@ use std::{
};
use tauri_plugin_mihomo::models::{Proxies, ProxyType};
use tokio::{fs, task::JoinHandle};
use tristr::alias::String;
pub(crate) static PROFILE_WRITE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
@@ -51,6 +51,7 @@ const SELECTED_NODES_FIRST_PASS_BUDGET: Duration = Duration::from_secs(3);
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
#[serde(default)]
pub struct IProfiles {
#[specta(type = Option<std::string::String>)]
pub current: Option<String>,
pub items: Option<Vec<PrfItem>>,
@@ -114,7 +115,7 @@ impl IProfiles {
let mut home_changed = false;
for item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d"));
item.uid = Some(help::get_uid("d").into());
}
if item.itype.as_deref() == Some("remote") {
item.option
+1 -1
View File
@@ -1,6 +1,6 @@
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::collections::{HashMap, HashSet};
use tristr::alias::String;
use crate::enhance::field::use_keys;
+1 -1
View File
@@ -8,7 +8,7 @@ use anyhow::Result;
use clash_verge_logging::{Type, logging};
use log::LevelFilter;
use serde::{Deserialize, Serialize};
use tristr::alias::String;
use smartstring::alias::String;
/// ### `verge.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
+3 -4
View File
@@ -20,7 +20,6 @@ use tokio::{
fs,
sync::{Mutex, watch},
};
use tristr::alias::String;
const DEFAULT_INTERVAL_HOURS: u64 = 24;
const MIN_INTERVAL_HOURS: u64 = 1;
@@ -212,7 +211,7 @@ impl AutoBackupManager {
return Ok(());
}
let file_name = create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug())).await?;
let file_name = create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug()).into()).await?;
self.last_backup.store(Local::now().timestamp(), Ordering::Release);
if let Err(err) = cleanup_auto_backups().await {
@@ -240,8 +239,8 @@ impl AutoBackupManager {
fn append_auto_suffix(file_name: &str, slug: &str) -> String {
match file_name.rsplit_once('.') {
Some((stem, ext)) => format!("{stem}{AUTO_MARKER}{slug}.{ext}").into(),
None => format!("{file_name}{AUTO_MARKER}{slug}").into(),
Some((stem, ext)) => format!("{stem}{AUTO_MARKER}{slug}.{ext}"),
None => format!("{file_name}{AUTO_MARKER}{slug}"),
}
}
+1 -1
View File
@@ -5,6 +5,7 @@ use arc_swap::{ArcSwap, ArcSwapOption};
use backon::{ConstantBuilder, Retryable as _};
use clash_verge_logging::{Type, logging};
use reqwest_dav::list_cmd::{ListEntity, ListFile, ListMultiStatus};
use smartstring::alias::String;
use std::{
collections::HashMap,
env::{consts::OS, temp_dir},
@@ -14,7 +15,6 @@ use std::{
time::Duration,
};
use tokio::{fs, time::timeout};
use tristr::alias::String;
use zip::write::SimpleFileOptions;
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
+1 -1
View File
@@ -7,6 +7,7 @@ use crate::{config::Config, core::handle, core::lightweight::entry_lightweight_m
use anyhow::{Result, bail};
use arc_swap::ArcSwap;
use clash_verge_logging::{Type, logging};
use smartstring::alias::String;
use std::{
collections::HashMap,
fmt,
@@ -17,7 +18,6 @@ use std::{
},
};
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt as _, ShortcutState};
use tristr::alias::String;
/// Enum representing all available hotkey functions
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+10 -13
View File
@@ -10,7 +10,6 @@ use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
str::FromStr as _,
};
use tristr::alias::String;
/// The config key of the Mixed Port, named once so callers stop spelling it out.
pub(crate) const MIXED_PORT_KEY: &str = "mixed-port";
@@ -164,7 +163,7 @@ pub(crate) fn probe_listener(request: &ListenerProbe) -> ListenerProbeOutcome {
Ok(address) => address,
Err(error) => {
return ListenerProbeOutcome::Invalid {
message: error.to_string().into(),
message: error.to_string(),
};
}
};
@@ -202,7 +201,7 @@ pub(crate) fn probe_proxy_port_change(
Ok(claims) => claims,
Err(error) => {
return ListenerProbeOutcome::Invalid {
message: format!("current proxy listener configuration is invalid: {error:#}").into(),
message: format!("current proxy listener configuration is invalid: {error:#}"),
};
}
}
@@ -213,7 +212,7 @@ pub(crate) fn probe_proxy_port_change(
Ok(claims) => claims,
Err(error) => {
return ListenerProbeOutcome::Invalid {
message: format!("candidate proxy listener configuration is invalid: {error:#}").into(),
message: format!("candidate proxy listener configuration is invalid: {error:#}"),
};
}
};
@@ -402,8 +401,7 @@ fn probe_claims(claims: &[BindClaim]) -> ListenerProbeOutcome {
claim.socket_addr(),
transport_name(claim.transport),
error
)
.into(),
),
};
}
Err(error) => {
@@ -414,8 +412,7 @@ fn probe_claims(claims: &[BindClaim]) -> ListenerProbeOutcome {
claim.socket_addr(),
transport_name(claim.transport),
error
)
.into(),
),
};
}
}
@@ -742,7 +739,7 @@ mod tests {
let port = listener.local_addr()?.port();
assert_eq!(
probe_listener(&ListenerProbe {
address: format!("0.0.0.0:{port}").into(),
address: format!("0.0.0.0:{port}"),
transports: vec![ListenerTransport::Tcp],
}),
ListenerProbeOutcome::Conflict {
@@ -758,7 +755,7 @@ mod tests {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
let port = listener.local_addr()?.port();
let outcome = probe_listener(&ListenerProbe {
address: format!("127.0.0.1:{port}").into(),
address: format!("127.0.0.1:{port}"),
transports: vec![ListenerTransport::Tcp],
});
assert_eq!(
@@ -776,7 +773,7 @@ mod tests {
let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, 0))?;
let port = listener.local_addr()?.port();
let outcome = probe_listener(&ListenerProbe {
address: format!("127.0.0.1:{port}").into(),
address: format!("127.0.0.1:{port}"),
transports: vec![ListenerTransport::Tcp],
});
assert_eq!(
@@ -812,7 +809,7 @@ mod tests {
assert_eq!(
probe_listener(&ListenerProbe {
address: format!("127.0.0.1:{port}").into(),
address: format!("127.0.0.1:{port}"),
transports: vec![ListenerTransport::Tcp],
}),
ListenerProbeOutcome::Available
@@ -825,7 +822,7 @@ mod tests {
let socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))?;
let port = socket.local_addr()?.port();
let outcome = probe_listener(&ListenerProbe {
address: format!("127.0.0.1:{port}").into(),
address: format!("127.0.0.1:{port}"),
transports: vec![ListenerTransport::Udp],
});
assert_eq!(
+2 -2
View File
@@ -15,6 +15,7 @@ use clash_verge_draft::DraftTransaction;
use clash_verge_logging::{Type, logging};
use clash_verge_service_ipc::StageRuntimeOutcome;
use scopeguard::defer;
use smartstring::alias::String;
use std::{
collections::HashSet,
path::PathBuf,
@@ -22,7 +23,6 @@ use std::{
time::{Duration, Instant},
};
use tauri_plugin_mihomo::Error as MihomoError;
use tristr::alias::String;
/// A staging result separated from the policy that acts on it.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -92,7 +92,7 @@ async fn ask_to_stage(path: &std::path::Path) -> StageAttempt {
match crate::core::service::stage_runtime_by_service(path).await {
Ok(StageRequest::Answered(outcome)) => StageAttempt::Answered(outcome),
Ok(StageRequest::Refused { code, message }) => {
let message = message;
let message = message.into();
if StageRequest::is_about_the_bundle(code) {
StageAttempt::RefusedTheBundle(message)
} else {
+7 -7
View File
@@ -7,10 +7,10 @@ use crate::core::service::{SERVICE_MANAGER, ServiceStatus};
use anyhow::Result;
use clash_verge_logging::{Type, logging};
use scopeguard::defer;
use smartstring::alias::String;
use std::path::Path;
#[cfg(target_os = "windows")]
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
use tristr::alias::String;
#[cfg(any(target_os = "windows", test))]
const fn should_wait_for_service(tun_enabled: bool, service_ready: bool, is_admin: bool) -> bool {
@@ -194,7 +194,7 @@ async fn run_service_start_with_sidecar_fallback<Start, StartFuture, Check, Chec
start_service: Start,
confirm_idle: Check,
start_sidecar: Fallback,
record_unavailable: impl FnOnce(String),
record_unavailable: impl FnOnce(std::string::String),
) -> Result<()>
where
Start: FnOnce() -> StartFuture,
@@ -217,7 +217,7 @@ where
// A lost start response can hide a running core; the shared check must resolve occupancy.
if let Err(probe_error) = confirm_idle().await {
let error = error.context(format!("Sidecar fallback was not safe: {probe_error:#}"));
record_unavailable(format!("{error:#}").into());
record_unavailable(format!("{error:#}"));
return Err(error);
}
logging!(
@@ -650,7 +650,7 @@ impl CoreManager {
|error| self.start_sidecar_after_service_failure(error),
|reason| {
crate::core::runstate::RUN_STATE
.observe(crate::core::runstate::ServiceHealth::Unavailable(reason.into()));
.observe(crate::core::runstate::ServiceHealth::Unavailable(reason));
},
)
.await
@@ -688,7 +688,7 @@ impl CoreManager {
}
let reason = match state.health {
ServiceHealth::Unavailable(reason) => reason,
ServiceHealth::VersionMismatch => "registered service is unavailable or incompatible".into(),
ServiceHealth::VersionMismatch => "registered service is unavailable or incompatible".to_owned(),
_ => return Ok(()),
};
if let Err(error) = clash_verge_service_ipc::execution::check_sidecar_available()
@@ -712,7 +712,7 @@ impl CoreManager {
if Handle::global().is_exiting() {
return Err(error.context("application exited before Sidecar fallback"));
}
let reason: String = format!("{error:#}").into();
let reason = format!("{error:#}");
RUN_STATE.allow_sidecar_after_service_refusal(reason.clone())?;
logging!(warn, Type::Core, "Starting Sidecar with Service unavailable: {reason}");
let result = self.start_core_by_sidecar().await;
@@ -1590,7 +1590,7 @@ mod tests {
started.store(true, Ordering::SeqCst);
Ok(())
},
|reason| store.observe(ServiceHealth::Unavailable(reason.into())),
|reason| store.observe(ServiceHealth::Unavailable(reason)),
)
.await;
+8 -11
View File
@@ -16,7 +16,6 @@ use std::num::NonZeroUsize;
use std::path::Path;
use tauri_plugin_mihomo::MihomoExt as _;
use tauri_plugin_shell::ShellExt as _;
use tristr::alias::String;
pub(super) fn explain_core_occupancy(error: anyhow::Error) -> anyhow::Error {
let detail = error.to_string();
@@ -28,7 +27,7 @@ pub(super) fn explain_core_occupancy(error: anyhow::Error) -> anyhow::Error {
}
#[cfg(target_os = "windows")]
fn sidecar_config_without_tun(yaml: &str) -> Result<String> {
fn sidecar_config_without_tun(yaml: &str) -> Result<std::string::String> {
use serde_yaml_ng::{Mapping, Value};
let mut config: Mapping = serde_yaml_ng::from_str(yaml)?;
@@ -41,7 +40,7 @@ fn sidecar_config_without_tun(yaml: &str) -> Result<String> {
tun.as_mapping_mut()
.context("invalid Sidecar TUN configuration")?
.insert(Value::from("enable"), Value::Bool(false));
Ok(serde_yaml_ng::to_string(&config)?.into())
Ok(serde_yaml_ng::to_string(&config)?)
}
const SIDECAR_READINESS_ATTEMPTS: usize = 30;
@@ -154,7 +153,7 @@ impl CoreManager {
pub async fn get_clash_logs(&self) -> Result<Vec<String>> {
match *self.get_running_mode() {
RunningMode::Service => service::get_clash_logs_by_service().await,
RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs().into_iter().map(Into::into).collect()),
RunningMode::Sidecar => Ok(CLASH_LOGGER.get_logs()),
RunningMode::NotRunning => Ok(Vec::new()),
}
}
@@ -258,9 +257,7 @@ impl CoreManager {
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(line)
| tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
// from_utf8_lossy_owned exists only on the std String type; the tristr
// alias shadows the bare name. LogRing stores std Strings.
let message = std::string::String::from_utf8_lossy_owned(line);
let message = String::from_utf8_lossy_owned(line);
Logger::global().writer_sidecar_log(Level::Error, &message);
CLASH_LOGGER.append_log(message);
}
@@ -268,12 +265,12 @@ impl CoreManager {
let _ = terminated.send(());
let manager = Self::global();
let _ = manager.invalidate_core_readiness_if(core_readiness_generation);
let message: String = if let Some(code) = term.code {
format!("Process terminated with code: {}", code).into()
let message = if let Some(code) = term.code {
format!("Process terminated with code: {}", code)
} else if let Some(signal) = term.signal {
format!("Process terminated by signal: {}", signal).into()
format!("Process terminated by signal: {}", signal)
} else {
"Process terminated".into()
String::from("Process terminated")
};
Logger::global().writer_sidecar_log(Level::Info, &message);
CLASH_LOGGER.clear_logs();
+6 -3
View File
@@ -1,9 +1,12 @@
use super::NoticeStatus;
use crate::{APP_HANDLE, singleton};
use std::sync::atomic::{AtomicBool, Ordering};
use smartstring::alias::String;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use tauri::AppHandle;
use tauri_plugin_mihomo::{Mihomo, MihomoExt as _};
use tristr::alias::String;
use super::notification::{FrontendEvent, NotificationSystem};
@@ -70,7 +73,7 @@ impl Handle {
Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid });
}
pub fn notice(status: NoticeStatus, message: impl Into<String>) {
pub fn notice(status: NoticeStatus, message: impl Into<Arc<str>>) {
Self::send_event(FrontendEvent::NoticeMessage {
status,
message: message.into(),
+13 -9
View File
@@ -3,10 +3,11 @@ use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
#[cfg(test)]
use serde_json::json;
use smartstring::alias::String;
use std::sync::Arc;
use std::sync::LazyLock;
use std::{collections::HashMap, future::Future};
use tauri::{AppHandle, Emitter as _, Manager as _, WebviewWindow};
use tristr::alias::String;
#[derive(Debug, Clone)]
pub enum FrontendEvent<'a> {
@@ -16,7 +17,7 @@ pub enum FrontendEvent<'a> {
RefreshProxyConfig,
NoticeMessage {
status: NoticeStatus,
message: String,
message: Arc<str>,
},
ProfileChanged {
current_profile_id: &'a String,
@@ -76,8 +77,10 @@ impl FailedOperation {
#[serde(rename_all = "camelCase")]
pub struct PendingFailure {
/// Stable code used as the table key.
#[specta(type = std::string::String)]
pub code: String,
/// Full diagnostic context chain.
#[specta(type = std::string::String)]
pub detail: String,
pub operation: FailedOperation,
/// Monotonic identity for repeated failures under the same code.
@@ -244,6 +247,7 @@ static PENDING_FAILURES: LazyLock<FailureTable> = LazyLock::new(FailureTable::de
#[derive(Debug, Default, Clone, serde::Serialize, specta::Type)]
pub struct SidecarFailureSnapshot {
pub revision: u64,
#[specta(type = Option<std::string::String>)]
pub detail: Option<String>,
}
@@ -387,7 +391,7 @@ fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str,
(
FrontendEvent::NoticeMessage {
status: NoticeStatus::Info,
message: String::from("ok"),
message: Arc::from("ok"),
},
"verge://notice-message",
json!(["info", "ok"]),
@@ -452,16 +456,16 @@ frontend_events! {
refresh_verge: () = "verge://refresh-verge-config", FrontendEvent::RefreshVerge => ();
refresh_profiles: () = "verge://refresh-profiles", FrontendEvent::RefreshProfiles => ();
refresh_proxy: () = "verge://refresh-proxy-config", FrontendEvent::RefreshProxyConfig => ();
notice: (NoticeStatus, String) = "verge://notice-message", FrontendEvent::NoticeMessage { status, ref message } => (status, message.clone());
profile_changed: String = "profile-changed", FrontendEvent::ProfileChanged { current_profile_id } => current_profile_id.clone();
timer_updated: String = "verge://timer-updated", FrontendEvent::TimerUpdated { profile_index } => profile_index.clone();
profile_started: ProfileUpdatePayload = "profile-update-started", FrontendEvent::ProfileUpdateStarted { uid } => ProfileUpdatePayload { uid: uid.clone() };
profile_completed: ProfileUpdatePayload = "profile-update-completed", FrontendEvent::ProfileUpdateCompleted { uid } => ProfileUpdatePayload { uid: uid.clone() };
notice: (NoticeStatus, std::string::String) = "verge://notice-message", FrontendEvent::NoticeMessage { status, ref message } => (status, message.to_string());
profile_changed: std::string::String = "profile-changed", FrontendEvent::ProfileChanged { current_profile_id } => current_profile_id.to_string();
timer_updated: std::string::String = "verge://timer-updated", FrontendEvent::TimerUpdated { profile_index } => profile_index.to_string();
profile_started: ProfileUpdatePayload = "profile-update-started", FrontendEvent::ProfileUpdateStarted { uid } => ProfileUpdatePayload { uid: uid.to_string() };
profile_completed: ProfileUpdatePayload = "profile-update-completed", FrontendEvent::ProfileUpdateCompleted { uid } => ProfileUpdatePayload { uid: uid.to_string() };
run_state: crate::core::runstate::RunStateView = "verge://run-state-changed", FrontendEvent::RunStateChanged { state } => state;
pending_failures: () = "verge://pending-failures-changed", FrontendEvent::PendingFailuresChanged => ();
}
#[derive(serde::Serialize, specta::Type)]
pub struct ProfileUpdatePayload {
pub uid: String,
pub uid: std::string::String,
}
+7 -11
View File
@@ -2,8 +2,6 @@ use crate::utils::dirs;
use anyhow::{Context as _, Result};
use clash_verge_service_ipc::{OwnerCredentials, OwnerIdentity};
use std::path::Path;
#[cfg(windows)]
use tristr::alias::String;
pub(crate) fn current_owner_credentials() -> Result<OwnerCredentials> {
current_owner_credentials_for_root(&dirs::app_home_dir()?)
@@ -41,16 +39,16 @@ pub(crate) fn current_owner_credentials_for_root(app_root: &Path) -> Result<Owne
}
#[cfg(windows)]
fn windows_owner_credentials(app_data_root: &Path) -> Result<(OwnerIdentity, Option<std::string::String>)> {
fn windows_owner_credentials(app_data_root: &Path) -> Result<(OwnerIdentity, Option<String>)> {
let sid = windows_owner::current_sid()?;
let token = windows_owner::load_or_create_token(app_data_root, &sid)?;
Ok((OwnerIdentity::Windows { sid: sid.into() }, Some(token.into())))
Ok((OwnerIdentity::Windows { sid }, Some(token)))
}
#[cfg(windows)]
fn windows_current_identity() -> Result<OwnerIdentity> {
Ok(OwnerIdentity::Windows {
sid: windows_owner::current_sid()?.into(),
sid: windows_owner::current_sid()?,
})
}
@@ -81,7 +79,7 @@ pub(crate) fn repair_app_data_root_owner(app_data_root: &Path) -> Result<()> {
#[cfg(windows)]
pub(crate) fn current_user_pipe_sddl() -> Result<String> {
let sid = windows_owner::current_sid()?;
Ok(format!("D:P(A;;GA;;;{sid})(A;;GA;;;SY)(A;;GA;;;BA)").into())
Ok(format!("D:P(A;;GA;;;{sid})(A;;GA;;;SY)(A;;GA;;;BA)"))
}
#[cfg(windows)]
@@ -646,19 +644,17 @@ mod windows_owner {
}
let value = LocalWideString(value);
let length = (0..).take_while(|index| unsafe { *value.0.add(*index) } != 0).count();
// from_utf16 exists only on the std String type; the tristr alias shadows the bare name.
std::string::String::from_utf16(unsafe { std::slice::from_raw_parts(value.0, length) })
String::from_utf16(unsafe { std::slice::from_raw_parts(value.0, length) })
.context("process SID is not valid UTF-16")
.map(Into::into)
}
fn encode_token(token: &[u8; TOKEN_BYTES]) -> String {
let mut encoded = std::string::String::with_capacity(TOKEN_BYTES * 2);
let mut encoded = String::with_capacity(TOKEN_BYTES * 2);
for byte in token {
use std::fmt::Write as _;
let _ = write!(encoded, "{byte:02x}");
}
encoded.into()
encoded
}
fn wide_path(path: &Path) -> Result<Vec<u16>> {
+11 -13
View File
@@ -24,7 +24,6 @@ use std::{
time::Duration,
};
use tokio::{sync::Mutex, time::timeout};
use tristr::alias::String;
const CLEAR_ATTEMPTS: u32 = 3;
const CLEAR_RETRY_DELAY: Duration = Duration::from_millis(100);
@@ -321,8 +320,7 @@ fn truncate_utf8(value: &mut String, max_bytes: usize) {
while !value.is_char_boundary(boundary) {
boundary -= 1;
}
let truncated = value.as_str()[..boundary].into();
*value = truncated;
value.truncate(boundary);
}
fn service_bypass(verge: &IVerge) -> Result<String> {
@@ -330,11 +328,11 @@ fn service_bypass(verge: &IVerge) -> Result<String> {
ensure!(!custom.contains('\0'), "system proxy bypass contains NUL");
let mut bypass = if custom.is_empty() {
MACOS_DEFAULT_BYPASS.into()
MACOS_DEFAULT_BYPASS.to_owned()
} else if verge.use_default_bypass.unwrap_or(true) {
format!("{MACOS_DEFAULT_BYPASS},{custom}").into()
format!("{MACOS_DEFAULT_BYPASS},{custom}")
} else {
custom.into()
custom.to_owned()
};
truncate_utf8(&mut bypass, MAX_SERVICE_BYPASS_LEN);
Ok(bypass)
@@ -355,7 +353,7 @@ fn service_proxy_config(verge: &IVerge, mixed_port: u16, pac_port: u16) -> Resul
Ok(MacosProxyConfig::Global {
host: LOOPBACK_HOST.to_owned(),
port: mixed_port,
bypass: service_bypass(verge)?.into(),
bypass: service_bypass(verge)?,
})
}
}
@@ -370,7 +368,7 @@ fn service_apply_result(requested: &MacosProxyConfig, outcome: ProxyApplyOutcome
// Direct fallback satisfies a disable request.
ProxyApplyOutcome::DirectFallback { .. } if matches!(requested, MacosProxyConfig::Disabled) => Ok(()),
ProxyApplyOutcome::DirectFallback { message } => {
Err(SysproxyFailure::DirectFallback { detail: message.into() }.into())
Err(SysproxyFailure::DirectFallback { detail: message }.into())
}
}
}
@@ -646,7 +644,7 @@ const GUARD_FAILURES_BEFORE_STOPPING: u32 = 3;
async fn report_guard_stopped(captured_generation: u64, error: &anyhow::Error) {
let stopped = anyhow::Error::new(SysproxyFailure::GuardStopped {
detail: format!("{error:#}").into(),
detail: format!("{error:#}"),
});
// Do not publish a late failure from a replaced guard.
let recorded = SERVICE_PROXY_OPERATIONS
@@ -1108,7 +1106,7 @@ mod tests {
let requested = MacosProxyConfig::Global {
host: "127.0.0.1".to_owned(),
port: 7890,
bypass: std::string::String::new(),
bypass: String::new(),
};
let error = service_apply_result(
@@ -1148,8 +1146,8 @@ mod tests {
let events = Mutex::new(Vec::new());
super::settle_table(
effect,
|asked| events.lock().push(format!("retire:{asked:?}").into()),
|operation, _| events.lock().push(format!("file:{operation:?}").into()),
|asked| events.lock().push(format!("retire:{asked:?}")),
|operation, _| events.lock().push(format!("file:{operation:?}")),
);
events.into_inner()
}
@@ -1278,7 +1276,7 @@ mod tests {
let combined = rollback_failure(
mappable(),
anyhow::anyhow!("the service could not set it").context(SysproxyFailure::DirectFallback {
detail: "fell back to direct".into(),
detail: "fell back to direct".to_owned(),
}),
);
+14 -23
View File
@@ -4,7 +4,6 @@ use serde::Serialize;
use tauri_plugin_mihomo::models::{
DelayHistory, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
};
use tristr::alias::String;
pub struct ProxyViewInput {
pub runtime_group_order: Vec<String>,
@@ -271,7 +270,7 @@ impl ProxyViewBuilder {
let global = core_groups
.remove("GLOBAL")
.map(|proxy| build_group(String::from("GLOBAL"), proxy, &resolver));
.map(|proxy| build_group("GLOBAL".to_owned(), proxy, &resolver));
let (groups, order_source) = build_ordered_groups(core_groups, runtime_group_order, &resolver);
let direct = core_node_ids.get("DIRECT").cloned();
let standalone = build_standalone(&core_node_ids);
@@ -295,7 +294,6 @@ fn partition_core(proxies: Proxies) -> (BTreeMap<String, Proxy>, BTreeMap<String
let mut nodes = BTreeMap::new();
for (name, proxy) in proxies.proxies {
let name = String::from(name);
if proxy.all.is_some() {
groups.insert(name, proxy);
} else {
@@ -313,7 +311,7 @@ fn build_core_records(
let mut ids = BTreeMap::new();
for (index, (name, proxy)) in core_nodes.into_iter().enumerate() {
let record_id = String::from(format!("c:{index}"));
let record_id = format!("c:{index}");
ids.insert(name.clone(), record_id.clone());
records.insert(
record_id.clone(),
@@ -334,13 +332,7 @@ fn build_provider_records(
records: &mut BTreeMap<String, ProxyNodeView>,
) -> (Vec<ProxyProviderView>, ProviderCandidates) {
let providers = providers
.map(|providers| {
providers
.providers
.into_iter()
.map(|(name, provider)| (String::from(name), provider))
.collect::<BTreeMap<_, _>>()
})
.map(|providers| providers.providers.into_iter().collect::<BTreeMap<_, _>>())
.unwrap_or_default();
let mut views = Vec::new();
let mut candidates = BTreeMap::new();
@@ -363,9 +355,9 @@ fn build_provider_records(
let mut proxy_record_ids = Vec::new();
for (member_index, proxy) in proxies.into_iter().enumerate() {
let record_id = String::from(format!("p:{provider_index}:{member_index}"));
let proxy_name = String::from(proxy.name.as_str());
let node_name = String::from(proxy.name.as_str());
let record_id = format!("p:{provider_index}:{member_index}");
let proxy_name = proxy.name.clone();
let node_name = proxy.name.clone();
candidates
.entry(proxy_name.clone())
.or_insert_with(Vec::new)
@@ -388,7 +380,7 @@ fn build_provider_records(
views.push(ProxyProviderView {
name: provider_name,
vehicle_type,
updated_at: updated_at.map(String::from),
updated_at,
subscription_info: subscription_info.map(|subscription_info| ProxySubscriptionInfo {
upload: subscription_info.upload,
download: subscription_info.download,
@@ -425,7 +417,6 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
.unwrap_or_default()
.into_iter()
.map(|member| {
let member = String::from(member);
let (occurrence, scoped) = seen
.entry(member.clone())
.or_insert_with(|| (0, resolver.scoped_candidates(&name, &member)));
@@ -438,11 +429,11 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
ProxyGroupView {
name,
proxy_type,
now: now.map(String::from),
fixed: fixed.map(String::from),
now,
fixed,
hidden,
icon: icon.map(String::from),
test_url: test_url.map(String::from),
icon,
test_url,
history,
capabilities: ProxyCapabilities {
udp,
@@ -476,10 +467,10 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
name,
proxy_type,
history,
id: id.map(String::from),
id,
hidden,
icon: icon.map(String::from),
test_url: test_url.map(String::from),
icon,
test_url,
capabilities: ProxyCapabilities {
udp,
xudp,
+7 -8
View File
@@ -64,7 +64,7 @@ impl RunStateEnv for RealEnv {
Ok(ServiceVersionReply {
core,
code: response.code,
message: response.message.into(),
message: response.message,
protocol: response.data,
})
}
@@ -123,7 +123,6 @@ mod fake {
use super::{PendingAction, RunState, RunStateEnv, ServiceVersionReply};
use crate::core::manager::RunningMode;
use tristr::alias::String;
/// A fail-closed scripted machine that records outbound effects.
#[derive(Debug)]
@@ -164,7 +163,7 @@ mod fake {
self.with_evidence(true).always_replying(Ok(ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".into(),
message: "ok".to_owned(),
protocol: Some(ProtocolInfo::current()),
}))
}
@@ -174,7 +173,7 @@ mod fake {
self.with_evidence(true).always_replying(Ok(ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".into(),
message: "ok".to_owned(),
protocol: None,
}))
}
@@ -182,12 +181,12 @@ mod fake {
#[must_use]
pub fn service_unreachable(self) -> Self {
self.with_evidence(true)
.always_replying(Err("ipc transport refused".into()))
.always_replying(Err("ipc transport refused".to_owned()))
}
#[must_use]
pub fn evidence_unavailable(mut self) -> Self {
self.evidence = Err("registry probe failed".into());
self.evidence = Err("registry probe failed".to_owned());
self
}
@@ -237,7 +236,7 @@ mod fake {
#[must_use]
pub fn privileged_operations_fail(self, reason: &str) -> Self {
*self.privileged_outcome.lock() = Err(reason.into());
*self.privileged_outcome.lock() = Err(reason.to_owned());
self
}
@@ -257,7 +256,7 @@ mod fake {
replies
.first()
.cloned()
.unwrap_or_else(|| Err("no service configured".into()))
.unwrap_or_else(|| Err("no service configured".to_owned()))
};
reply.map_err(|error| anyhow!(error))
}
-1
View File
@@ -2,7 +2,6 @@
//! Core lifecycle remains in `CoreManager`.
use crate::core::manager::RunningMode;
use tristr::alias::String;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum ServiceHealth {
+5 -5
View File
@@ -173,7 +173,7 @@ impl<E: RunStateEnv> RunStateStore<E> {
Type::Service,
"failed to inspect trusted service evidence: {error:#}"
);
return ServiceHealth::Unavailable(format!("service detection failed: {error:#}").into());
return ServiceHealth::Unavailable(format!("service detection failed: {error:#}"));
}
};
@@ -331,7 +331,7 @@ impl<E: RunStateEnv> RunStateStore<E> {
}
#[cfg(target_os = "windows")]
pub fn allow_sidecar_after_service_refusal(&self, reason: tristr::alias::String) -> Result<()> {
pub fn allow_sidecar_after_service_refusal(&self, reason: String) -> Result<()> {
let mut state = self.service.lock();
if self.operation_running.load(Ordering::Acquire) || state.service.pending.is_some() {
bail!("cannot fall back while a service operation is pending");
@@ -496,7 +496,7 @@ mod tests {
ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".into(),
message: "ok".to_owned(),
protocol: Some(clash_verge_service_ipc::ProtocolInfo::current()),
}
}
@@ -713,7 +713,7 @@ mod tests {
for health in [
ServiceHealth::NotInstalled,
ServiceHealth::VersionMismatch,
ServiceHealth::Unavailable("boom".into()),
ServiceHealth::Unavailable("boom".to_owned()),
] {
let store = with_env(FakeEnv::new());
store.observe(health.clone());
@@ -858,7 +858,7 @@ mod tests {
#[tokio::test]
async fn awaiting_readiness_retries_a_silent_service_then_accepts_its_reply() {
let store = with_env(FakeEnv::new().replying(vec![Err("ipc path not ready".into()), Ok(ready_reply())]));
let store = with_env(FakeEnv::new().replying(vec![Err("ipc path not ready".to_owned()), Ok(ready_reply())]));
let state = store
.await_ready(2, Duration::ZERO)
+12 -16
View File
@@ -9,7 +9,6 @@ use clash_verge_service_ipc::{
};
use super::health::ServiceHealth;
use tristr::alias::String;
/// What the Service replied to a protocol-version query.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -51,7 +50,7 @@ pub fn classify_service_version_reply(reply: &ServiceVersionReply) -> ServiceVer
return match &reply.core {
Some(clash_verge_service_ipc::CoreAvailability::Ready) => ServiceVersionCheck::Ready,
Some(clash_verge_service_ipc::CoreAvailability::Rejected { reason }) => {
ServiceVersionCheck::CoreUnavailable(format!("{CORE_REJECTED_PREFIX}{reason}").into())
ServiceVersionCheck::CoreUnavailable(format!("{CORE_REJECTED_PREFIX}{reason}"))
}
_ => ServiceVersionCheck::CoreUnavailable(
"the selected approved core is missing or does not match; reinstall the service to repair it".into(),
@@ -78,12 +77,9 @@ pub fn classify_service_version_reply(reply: &ServiceVersionReply) -> ServiceVer
reply.code, reply.message, client.epoch
)
};
ServiceVersionCheck::NeedsReinstall(
format!(
"Service helper version or protocol mismatch: {detail}. Choose Reinstall to uninstall the existing service and install the required version"
)
.into(),
)
ServiceVersionCheck::NeedsReinstall(format!(
"Service helper version or protocol mismatch: {detail}. Choose Reinstall to uninstall the existing service and install the required version"
))
}
/// Classify a version reply into the probe outcome used by [`classify_service_health`].
@@ -108,7 +104,7 @@ pub fn classify_service_health(
match current {
CurrentServiceProbe::Ready => ServiceHealth::Ready,
CurrentServiceProbe::VersionMismatch => ServiceHealth::VersionMismatch,
CurrentServiceProbe::Unavailable => ServiceHealth::Unavailable(unavailable_reason.into()),
CurrentServiceProbe::Unavailable => ServiceHealth::Unavailable(unavailable_reason.to_owned()),
CurrentServiceProbe::Missing if has_install_marker => ServiceHealth::VersionMismatch,
CurrentServiceProbe::Missing => ServiceHealth::NotInstalled,
}
@@ -123,7 +119,7 @@ mod tests {
ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".into(),
message: "ok".to_owned(),
protocol: Some(ProtocolInfo::current()),
}
}
@@ -158,7 +154,7 @@ mod tests {
let reply = ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".into(),
message: "ok".to_owned(),
protocol: None,
};
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
@@ -176,7 +172,7 @@ mod tests {
let reply = ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 7,
message: "nope".into(),
message: "nope".to_owned(),
protocol: Some(ProtocolInfo::current()),
};
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
@@ -192,7 +188,7 @@ mod tests {
let reply = ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".into(),
message: "ok".to_owned(),
protocol: Some(info),
};
assert!(matches!(
@@ -213,7 +209,7 @@ mod tests {
let reply = ServiceVersionReply {
core: Some(clash_verge_service_ipc::CoreAvailability::Ready),
code: 0,
message: "ok".into(),
message: "ok".to_owned(),
protocol: Some(info),
};
assert!(matches!(
@@ -232,7 +228,7 @@ mod tests {
let reply = ServiceVersionReply {
core: None,
code,
message: "unsupported command".into(),
message: "unsupported command".to_owned(),
protocol: protocol.clone(),
};
let ServiceVersionCheck::NeedsReinstall(detail) = classify_service_version_reply(&reply) else {
@@ -254,7 +250,7 @@ mod tests {
fn an_unreachable_service_keeps_the_reason() {
assert_eq!(
classify_service_health(CurrentServiceProbe::Unavailable, true, "socket refused"),
ServiceHealth::Unavailable("socket refused".into())
ServiceHealth::Unavailable("socket refused".to_owned())
);
}
+15 -17
View File
@@ -5,7 +5,6 @@ use serde_yaml_ng::{Mapping, Value};
use sha2::{Digest as _, Sha256};
use std::collections::{BTreeMap, HashSet};
use std::path::{Component, Path, PathBuf};
use tristr::alias::String;
pub(crate) const GEO_ASSETS: &[&str] = &[
"Country.mmdb",
@@ -65,7 +64,7 @@ struct ProviderPathOwners {
pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root: &Path) -> Result<()> {
let config_root = std::fs::canonicalize(config_root)?;
let mut paths = BTreeMap::<String, ProviderPathOwners>::new();
let mut reserved: HashSet<String> = GEO_ASSETS.iter().map(|name| (*name).into()).collect();
let mut reserved: HashSet<String> = GEO_ASSETS.iter().map(|name| (*name).to_owned()).collect();
for section in ["proxy-providers", "rule-providers"] {
let Some(providers) = config.get(section).and_then(Value::as_mapping) else {
continue;
@@ -90,7 +89,7 @@ pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root:
match (is_remote, provider.get("url").and_then(Value::as_str)) {
(true, Some(url)) => owners
.remote
.entry(url.into())
.entry(url.to_owned())
.or_default()
.push((section, name.clone())),
_ => owners.local = true,
@@ -106,7 +105,7 @@ pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root:
for (url, providers) in owners.remote {
let replacement = allocate_provider_destination(&destination, &url, &mut reserved);
for (section, name) in providers {
config[section][&name]["path"] = Value::String(replacement.clone().into());
config[section][&name]["path"] = Value::String(replacement.clone());
logging!(
warn,
Type::Config,
@@ -132,16 +131,15 @@ fn allocate_provider_destination(destination: &str, url: &str, reserved: &mut Ha
let mut suffix = String::new();
let mut sequence = 0;
loop {
let candidate: String = path
let candidate = path
.with_file_name(format!("cvr-{digest}{suffix}{extension}"))
.to_string_lossy()
.replace('\\', "/")
.into();
.replace('\\', "/");
if reserved.insert(candidate.clone()) {
return candidate;
}
sequence += 1;
suffix = format!("-{sequence}").into();
suffix = format!("-{sequence}");
}
}
@@ -169,7 +167,7 @@ fn collect_runtime_parts(mut config: Value, config_root: &Path) -> Result<Runtim
)?;
for filename in GEO_ASSETS {
let source = config_root.join(filename);
if source.is_file() && destinations.insert((*filename).into()) {
if source.is_file() && destinations.insert((*filename).to_string()) {
assets.push(RuntimeAsset {
source: std::fs::canonicalize(&source)?.to_string_lossy().into_owned(),
destination: (*filename).to_string(),
@@ -230,9 +228,9 @@ fn collect_provider_assets(
}
remote_providers.push(RemoteProviderRef {
section,
name: name.as_str().unwrap_or_default().into(),
name: name.as_str().unwrap_or_default().to_owned(),
provider: RemoteProvider {
destination: destination.clone().into(),
destination: destination.clone(),
url,
},
});
@@ -261,13 +259,13 @@ fn collect_provider_assets(
if destinations.insert(destination.clone()) {
assets.push(RuntimeAsset {
source: source.to_string_lossy().into_owned(),
destination: destination.clone().into(),
destination: destination.clone(),
});
}
destination
}
};
provider.insert(Value::String("path".to_owned()), Value::String(destination.into()));
provider.insert(Value::String("path".to_owned()), Value::String(destination));
}
Ok(())
}
@@ -341,7 +339,7 @@ fn normalized_destination(relative: &Path) -> Result<String> {
if destination.as_os_str().is_empty() {
bail!("local runtime provider destination is empty");
}
Ok(destination.to_string_lossy().replace('\\', "/").into())
Ok(destination.to_string_lossy().replace('\\', "/"))
}
#[cfg(test)]
@@ -387,12 +385,12 @@ mod tests {
for declared in readback {
assert!(bundle.remote_providers.contains(&declared.provider));
assert_eq!(
config[declared.section][declared.name.as_str()]["path"],
config[declared.section][&declared.name]["path"],
declared.provider.destination
);
assert_eq!(
config[declared.section][declared.name.as_str()]["url"],
original[declared.section][declared.name.as_str()]["url"]
config[declared.section][&declared.name]["url"],
original[declared.section][&declared.name]["url"]
);
}
std::fs::remove_dir_all(root)?;
+27 -35
View File
@@ -36,7 +36,6 @@ use std::{
sync::atomic::{AtomicBool, AtomicU64, Ordering},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tristr::alias::String;
static OWNER_MONITOR_GENERATION: AtomicU64 = AtomicU64::new(0);
static ACTIVE_SERVICE_SESSION: Mutex<Option<ActiveServiceSession>> = Mutex::new(None);
@@ -63,7 +62,7 @@ pub(crate) fn is_not_auto_started(reason: &str) -> bool {
#[cfg(target_os = "windows")]
pub(crate) fn notify_service_fallback(reason: &str) {
*PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(reason.into());
*PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(reason.to_owned());
Handle::notice(NoticeStatus::ServiceCoreSidecarFallback, "");
}
@@ -92,7 +91,7 @@ fn app_data_owner_command(credentials: &OwnerCredentials) -> Option<String> {
return None;
};
let path = credentials.app_data_dir.replace('\'', r"'\''");
Some(format!("sudo chown -R {uid}:{gid} '{path}'").into())
Some(format!("sudo chown -R {uid}:{gid} '{path}'"))
}
/// Capabilities of the service session that owns the running Core.
@@ -244,24 +243,22 @@ fn macos_service_install_markers() -> Vec<String> {
format!(
"/Library/LaunchDaemons/{}.plist",
clash_verge_service_ipc::MACOS_SERVICE_ID
)
.into(),
),
format!(
"/Library/PrivilegedHelperTools/{}.bundle",
clash_verge_service_ipc::MACOS_SERVICE_ID
)
.into(),
),
#[cfg(not(feature = "verge-dev"))]
"/Library/LaunchDaemons/io.github.clashverge.helper.plist".into(),
"/Library/LaunchDaemons/io.github.clashverge.helper.plist".to_owned(),
#[cfg(not(feature = "verge-dev"))]
"/Library/PrivilegedHelperTools/io.github.clashverge.helper".into(),
"/Library/PrivilegedHelperTools/io.github.clashverge.helper".to_owned(),
]
}
#[cfg(target_os = "macos")]
fn macos_service_install_marker_exists() -> std::io::Result<bool> {
for marker in macos_service_install_markers() {
if path_entry_exists_without_follow(Path::new(marker.as_str()))? {
if path_entry_exists_without_follow(Path::new(&marker))? {
return Ok(true);
}
}
@@ -338,8 +335,7 @@ pub(crate) fn trusted_service_evidence() -> Result<bool> {
output.status
);
}
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
Ok(std::string::String::from_utf8_lossy(&output.stdout).trim() != "not-found")
Ok(String::from_utf8_lossy(&output.stdout).trim() != "not-found")
}
#[cfg(target_os = "macos")]
@@ -568,12 +564,12 @@ fn service_core_path(clash_core: &str, bin_ext: &str) -> Result<PathBuf> {
#[cfg(target_os = "macos")]
fn escape_osascript_double_quoted_string(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"").into()
value.replace('\\', "\\\\").replace('"', "\\\"")
}
#[cfg(target_os = "macos")]
fn shell_single_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', r"'\''")).into()
format!("'{}'", value.replace('\'', r"'\''"))
}
fn packaged_service_tool_path(file_name: &str, packaged_path: impl FnOnce() -> Result<PathBuf>) -> Result<PathBuf> {
@@ -645,7 +641,7 @@ fn uninstall_service() -> Result<()> {
let status = if linux_running_as_root() {
StdCommand::new(&uninstall_path).status()?
} else {
let result = StdCommand::new(elevator.as_str())
let result = StdCommand::new(&elevator)
.arg("--disable-internal-agent")
.arg(&uninstall_path)
.status()?;
@@ -701,7 +697,7 @@ fn uninstall_service() -> Result<()> {
}
let uninstall_path = macos_service_tool_path(&uninstall_path)?;
let uninstall_shell: String = uninstall_path.to_string_lossy().into();
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
// clash_verge_i18n::sync_locale(Config::verge().await.latest_arc().language.as_deref());
@@ -847,7 +843,7 @@ pub(super) async fn stage_runtime_by_service(config_file: &Path) -> Result<Stage
if response.code > 0 {
return Ok(StageRequest::Refused {
code: response.code,
message: response.message.into(),
message: response.message,
});
}
response
@@ -883,7 +879,7 @@ fn record_service_start_refusal<E: RunStateEnv>(
if store.state().mode == crate::core::manager::RunningMode::NotRunning
&& refusal.code != ServiceErrorCode::ProxyClearFailed as u16
{
store.observe(ServiceHealth::Unavailable(refusal.to_string().into()));
store.observe(ServiceHealth::Unavailable(refusal.to_string()));
}
refusal.into()
}
@@ -891,7 +887,7 @@ fn record_service_start_refusal<E: RunStateEnv>(
/// Sidecar stays blocked until a reinstall replaces the residual helper, so ask for repair.
pub(super) fn record_residual_service(error: &anyhow::Error) {
if let Some(residual) = error.downcast_ref::<clash_verge_service_ipc::execution::ResidualServiceError>() {
RUN_STATE.observe(ServiceHealth::Unavailable(residual.to_string().into()));
RUN_STATE.observe(ServiceHealth::Unavailable(residual.to_string()));
}
}
@@ -905,7 +901,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
let proposed_session_token = generate_service_session_token()?;
let request = StartClashRequest {
runtime,
proposed_session_token: proposed_session_token.clone().into(),
proposed_session_token: proposed_session_token.clone(),
macos_proxy: None,
};
@@ -943,8 +939,8 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
&RUN_STATE,
ServiceStartRefusal {
code: response.code,
core_path: request.runtime.core_path.into(),
message: err_msg.into(),
core_path: request.runtime.core_path,
message: err_msg,
},
));
}
@@ -956,7 +952,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
*ACTIVE_SERVICE_SESSION.lock() = Some(ActiveServiceSession {
proof: OwnerSessionProof {
generation: result.session.generation,
token: proposed_session_token.into(),
token: proposed_session_token,
},
supports_runtime_staging: capabilities.runtime_staging,
supports_runtime_file_read: capabilities.runtime_file_read,
@@ -1016,10 +1012,7 @@ pub(super) async fn get_clash_logs_by_service() -> Result<Vec<String>> {
bail!(err_msg);
}
Ok(response
.data
.map(|logs| logs.into_iter().map(Into::into).collect())
.unwrap_or_default())
Ok(response.data.unwrap_or_default())
}
pub(crate) async fn get_clash_log_snapshot_by_service() -> Result<String> {
@@ -1037,8 +1030,7 @@ pub(crate) async fn get_clash_log_snapshot_by_service() -> Result<String> {
}
let encoded = response.data.context("服务未返回核心日志快照")?;
let content = decode_hex(&encoded).context("服务返回了无效的核心日志快照")?;
// from_utf8_lossy_owned exists only on the std String type; the tristr alias shadows the bare name.
Ok(std::string::String::from_utf8_lossy_owned(content).into())
Ok(String::from_utf8_lossy_owned(content))
}
fn decode_hex(encoded: &str) -> Result<Vec<u8>> {
@@ -1352,7 +1344,7 @@ async fn read_chunk(
}
Ok(Some(match response.data.context("服务未返回运行时文件")? {
RuntimeFileOutcome::Absent => (None, 0, None),
RuntimeFileOutcome::Chunk { hex, len, mtime_ns } => (Some(hex.into()), len, mtime_ns),
RuntimeFileOutcome::Chunk { hex, len, mtime_ns } => (Some(hex), len, mtime_ns),
}))
}
@@ -1364,11 +1356,11 @@ async fn provider_load_times() -> Result<ProviderLoadTimes> {
let proxies = proxies.map_err(|error| anyhow!("failed to query proxy providers: {error}"))?;
let mut loaded = ProviderLoadTimes::new();
for (name, provider) in rules.providers {
loaded.insert(("rule-providers", name.into()), epoch_nanos(&provider.updated_at));
loaded.insert(("rule-providers", name), epoch_nanos(&provider.updated_at));
}
for (name, provider) in proxies.providers {
loaded.insert(
("proxy-providers", name.into()),
("proxy-providers", name),
provider.updated_at.as_deref().and_then(epoch_nanos),
);
}
@@ -1594,7 +1586,7 @@ const fn owner_recovery_policy(reason: OwnerRecoveryReason, is_macos: bool) -> O
fn mark_service_unavailable_after_owner_loss<E: RunStateEnv>(store: &RunStateStore<E>, reason: OwnerRecoveryReason) {
if matches!(reason, OwnerRecoveryReason::TransportFailure) {
store.observe(ServiceHealth::Unavailable(
"service control IPC unavailable after sustained transport failure".into(),
"service control IPC unavailable after sustained transport failure".to_owned(),
));
}
}
@@ -1855,7 +1847,7 @@ async fn wait_for_service_ipc() -> Result<()> {
}
Err(ReadyWaitError::Unreachable(error)) => {
tracing::Span::current().record("outcome", "unreachable");
RUN_STATE.observe(ServiceHealth::Unavailable(format!("{CONTEXT}: {error:#}").into()));
RUN_STATE.observe(ServiceHealth::Unavailable(format!("{CONTEXT}: {error:#}")));
Err(error).context(CONTEXT)
}
Err(ReadyWaitError::Rejected(error)) => {
@@ -2670,7 +2662,7 @@ mod tests {
let outcome = super::run_action_restoring_sidecar(&store, was_allowed, async {
store.perform(PendingAction::Install).await?;
// Simulate `wait_for_service_ipc` recording health before it fails.
store.observe(ServiceHealth::Unavailable("service never answered".into()));
store.observe(ServiceHealth::Unavailable("service never answered".to_owned()));
bail!("service IPC did not become available")
})
.await;
+3 -3
View File
@@ -8,6 +8,7 @@ use anyhow::Result;
use clash_verge_logging::{Type, logging};
use parking_lot::RwLock;
use scopeguard::defer;
use smartstring::alias::String;
use std::{
sync::{
Arc,
@@ -17,7 +18,6 @@ use std::{
};
use sysproxy::{Autoproxy, GuardMonitor, GuardType, Sysproxy};
use tokio::sync::Mutex as TokioMutex;
use tristr::alias::String;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ProxyApplyStep {
@@ -677,8 +677,8 @@ mod tests {
(calls.into_inner(), result.is_ok())
}
fn failure_text(result: anyhow::Result<()>) -> String {
result.map_or_else(|error| format!("{error:#}").into(), |()| String::from("no failure"))
fn failure_text(result: anyhow::Result<()>) -> std::string::String {
result.map_or_else(|error| format!("{error:#}"), |()| "no failure".to_owned())
}
#[test]
+2 -2
View File
@@ -8,6 +8,7 @@ use crate::{
use anyhow::Result;
use clash_verge_logging::{Type, logging, logging_error};
use parking_lot::{Mutex, RwLock};
use smartstring::alias::String;
use std::{
collections::HashMap,
sync::{
@@ -22,7 +23,6 @@ use tokio::{
};
use tokio_stream::StreamExt as _;
use tokio_util::time::{DelayQueue, delay_queue::Key};
use tristr::alias::String;
enum TimerCommand {
Apply(HashMap<String, TaskSchedule>),
@@ -502,6 +502,7 @@ mod tests {
use super::{TaskSchedule, Timer, run_timer_profile_update_transition};
use crate::config::{PrfItem, PrfOption};
use parking_lot::Mutex;
use smartstring::alias::String;
use std::collections::HashMap;
use std::{
sync::{
@@ -514,7 +515,6 @@ mod tests {
use tokio::sync::Barrier;
use tokio_stream::StreamExt as _;
use tokio_util::time::DelayQueue;
use tristr::alias::String;
struct CancellationProbe {
cancelled: Arc<AtomicBool>,
+1 -1
View File
@@ -19,6 +19,7 @@ use tokio::fs;
use super::handle;
use anyhow::Result;
use smartstring::alias::String;
use std::borrow::Cow;
use std::collections::HashMap;
use std::time::Duration;
@@ -26,7 +27,6 @@ use tauri::{
AppHandle, Wry,
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
};
use tristr::alias::String;
mod menu_def;
#[cfg(target_os = "macos")]
+5 -5
View File
@@ -11,7 +11,6 @@ use std::{
};
use tauri_plugin_updater::{Update, UpdaterExt as _};
use tokio_util::sync::CancellationToken;
use tristr::alias::String;
pub struct SilentUpdater {
update_ready: AtomicBool,
@@ -56,8 +55,8 @@ impl SilentUpdater {
.with_context(|| format!("failed to write update cache {}", bin_path.display()))?;
let meta = UpdateCacheMeta {
version: version.into(),
downloaded_at: Utc::now().to_rfc3339().into(),
version: version.to_string(),
downloaded_at: Utc::now().to_rfc3339(),
};
let meta_path = cache_dir.join("pending_update.json");
std::fs::write(&meta_path, serde_json::to_string_pretty(&meta)?)
@@ -134,8 +133,9 @@ fn verify_signature(app_handle: &tauri::AppHandle, bytes: &[u8], signature: &str
use base64::Engine as _;
let decode = |value: &str| -> Result<String> {
// from_utf8 exists only on the std String type; the tristr alias shadows the bare name.
Ok(std::string::String::from_utf8(base64::engine::general_purpose::STANDARD.decode(value)?)?.into())
Ok(String::from_utf8(
base64::engine::general_purpose::STANDARD.decode(value)?,
)?)
};
let config: tauri_plugin_updater::Config = serde_json::from_value(
app_handle
+9 -3
View File
@@ -1,6 +1,7 @@
use anyhow::Result;
use scopeguard::defer;
use serde::Serialize;
use smartstring::alias::String;
use std::{
fmt,
hash::{DefaultHasher, Hash as _, Hasher as _},
@@ -9,7 +10,6 @@ use std::{
};
use tauri_plugin_shell::ShellExt as _;
use tokio::fs;
use tristr::alias::String;
use crate::config::Config;
use crate::core::handle;
@@ -114,8 +114,14 @@ impl fmt::Display for ValidationSkipReason {
#[serde(tag = "status", rename_all = "camelCase")]
pub enum ValidationOutcome {
Valid,
Invalid { kind: ValidationErrorKind, message: String },
Skipped { reason: ValidationSkipReason },
Invalid {
kind: ValidationErrorKind,
#[specta(type = std::string::String)]
message: String,
},
Skipped {
reason: ValidationSkipReason,
},
Busy,
}
+1 -1
View File
@@ -4,8 +4,8 @@ use crate::{
utils::{dirs, help},
};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use tokio::fs;
use tristr::alias::String;
#[derive(Debug, Clone)]
pub struct ChainItem {
+1 -1
View File
@@ -1,5 +1,5 @@
use serde_yaml_ng::{Mapping, Value};
use tristr::alias::String;
use smartstring::alias::String;
pub const HANDLE_FIELDS: [&str; 12] = [
"mode",
+1 -1
View File
@@ -26,9 +26,9 @@ use anyhow::{Context as _, Result};
use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::collections::{HashMap, HashSet};
use tokio::fs;
use tristr::alias::String;
type ResultLog = Vec<(String, String)>;
#[derive(Debug)]
+13 -13
View File
@@ -6,8 +6,8 @@ use boa_engine::{Context, JsString, JsValue, Source, js_string, native_function:
use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::sync::Arc;
use tristr::alias::String;
const MAX_OUTPUTS: usize = 1000;
const MAX_OUTPUT_SIZE: usize = 1024 * 1024; // 1MB
@@ -16,7 +16,7 @@ const MAX_LOOP_ITERATIONS: u64 = 10_000_000;
const SCRIPT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
type ScriptResult = (Mapping, Vec<String>, Vec<(String, String)>);
type EvalOutcome = (Result<Mapping, String>, Vec<(String, String)>);
type EvalOutcome = (Result<Mapping, std::string::String>, Vec<(String, String)>);
/// Never fails: exceptions are logged and the input config is returned unchanged.
pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult {
@@ -37,9 +37,9 @@ pub(super) async fn use_script(script: String, config: Mapping, name: String) ->
let evaluated = AsyncHandler::spawn_blocking(move || eval_script(&script, &json, &name));
let (outcome, mut logs) = match tokio::time::timeout(SCRIPT_TIMEOUT, evaluated).await {
Ok(Ok(evaluated)) => evaluated,
Ok(Err(join_err)) => (Err(format!("script task panicked: {join_err}").into()), vec![]),
Ok(Err(join_err)) => (Err(format!("script task panicked: {join_err}")), vec![]),
Err(_elapsed) => (
Err(format!("script execution timed out after {SCRIPT_TIMEOUT:?}").into()),
Err(format!("script execution timed out after {SCRIPT_TIMEOUT:?}")),
vec![],
),
};
@@ -59,7 +59,7 @@ pub(super) async fn use_script(script: String, config: Mapping, name: String) ->
(result, changed_keys, logs)
}
Err(reason) => {
logs.push(("exception".into(), reason));
logs.push(("exception".into(), reason.into()));
(config, vec![], logs)
}
}
@@ -79,7 +79,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
let outputs_clone = Arc::clone(&outputs);
let total_size_clone = Arc::clone(&total_size);
let bail = |reason: String| (Err(reason), outputs.lock().to_vec());
let bail = |reason: std::string::String| (Err(reason), outputs.lock().to_vec());
let _ = context.register_global_builtin_callable("__verge_log__".into(), 2, unsafe {
NativeFunction::from_closure(move |_: &JsValue, args: &[JsValue], context: &mut Context| {
@@ -137,13 +137,13 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
JsValue::from(JsString::from(config_json)),
Attribute::all(),
) {
return bail(format!("failed to bind config for script: {err}").into());
return bail(format!("failed to bind config for script: {err}"));
}
// 仅处理 name 参数中的特殊字符
let safe_name = escape_js_string_for_single_quote(name);
if safe_name.len() > 1024 {
return bail("Name parameter too long".into());
return bail("Name parameter too long".to_owned());
}
let code = format!(
@@ -157,19 +157,19 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
let result = match context.eval(Source::from_bytes(code.as_str())) {
Ok(result) if result.is_string() => result,
_ => return bail("main function should return object".into()),
_ => return bail("main function should return object".to_owned()),
};
let result = match result.to_string(&mut context) {
Ok(result) => result,
Err(e) => return bail(format!("Failed to convert JS result to string: {e}").into()),
Err(e) => return bail(format!("Failed to convert JS result to string: {e}")),
};
let result = match result.to_std_string() {
Ok(result) => result,
Err(_) => return bail("Failed to convert JS string to std string".into()),
Err(_) => return bail("Failed to convert JS string to std string".to_owned()),
};
if result.len() > MAX_JSON_SIZE {
return bail("Script result exceeds maximum allowed size".into());
return bail("Script result exceeds maximum allowed size".to_owned());
}
match parse_json_safely(&result) {
@@ -180,7 +180,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
Type::Config,
"Script execution error: {err:#}. Script name: {name}"
);
bail("Script execution failed".into())
bail("Script execution failed".to_owned())
}
}
}
+9 -10
View File
@@ -1,7 +1,6 @@
use serde::{Deserialize, Serialize};
use serde_yaml_ng::{Mapping, Sequence, Value};
use std::collections::HashSet;
use tristr::alias::String;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SeqMap {
@@ -13,8 +12,8 @@ pub struct SeqMap {
fn collect_proxy_names(seq: &Sequence) -> Vec<String> {
seq.iter()
.filter_map(|item| match item {
Value::Mapping(map) => map.get("name").and_then(Value::as_str).map(Into::into),
Value::String(name) => Some(name.to_owned().into()),
Value::Mapping(map) => map.get("name").and_then(Value::as_str).map(str::to_owned),
Value::String(name) => Some(name.to_owned()),
_ => None,
})
.collect()
@@ -59,10 +58,10 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
.into_iter()
.filter(|item| {
if let Value::String(s) = item {
!delete.iter().any(|deleted| deleted == s)
!delete.contains(s)
} else if let Value::Mapping(m) = item {
if let Some(Value::String(name)) = m.get("name") {
!delete.iter().any(|deleted| deleted == name)
!delete.contains(name)
} else {
true
}
@@ -100,7 +99,7 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
.into_iter()
.filter(|p| {
if let Value::String(name) = p {
!delete.iter().any(|deleted| deleted == name)
!delete.contains(name)
} else {
true
}
@@ -120,12 +119,12 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
let mut seen_proxy_names = HashSet::new();
for name in &added_proxy_names {
if seen_proxy_names.insert(name.clone()) {
merged_proxies.push(Value::String(name.clone().into()));
merged_proxies.push(Value::String(name.clone()));
}
}
for value in base_group_proxies {
if let Value::String(name) = &value
&& !seen_proxy_names.insert(name.to_owned().into())
&& !seen_proxy_names.insert(name.to_owned())
{
continue;
}
@@ -181,7 +180,7 @@ proxy-groups:
let seq = SeqMap {
prepend: Sequence::new(),
append: Sequence::new(),
delete: vec!["proxy1".into()],
delete: vec!["proxy1".to_string()],
};
config = use_seq(seq, config, "proxies");
@@ -318,7 +317,7 @@ proxy-groups: "invalid"
let seq = SeqMap {
prepend: Sequence::new(),
append: Sequence::new(),
delete: vec!["proxy1".into()],
delete: vec!["proxy1".to_string()],
};
config = use_seq(seq, config, "proxies");
+4 -1
View File
@@ -12,14 +12,17 @@ use chrono::Utc;
use clash_verge_logging::{Type, logging, logging_error};
use reqwest_dav::list_cmd::ListFile;
use serde::Serialize;
use smartstring::alias::String;
use std::path::PathBuf;
use tokio::fs;
use tristr::alias::String;
#[derive(Debug, Serialize, specta::Type)]
pub struct LocalBackupFile {
#[specta(type = std::string::String)]
pub filename: String,
#[specta(type = std::string::String)]
pub path: String,
#[specta(type = std::string::String)]
pub last_modified: String,
pub content_length: u64,
}
+1 -1
View File
@@ -10,8 +10,8 @@ use crate::{
use bytes::BytesMut;
use clash_verge_logging::{Type, logging};
use serde_yaml_ng::{Mapping, Value};
use smartstring::alias::String;
use std::sync::{Arc, LazyLock};
use tristr::alias::String;
#[allow(clippy::expect_used)]
static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> = LazyLock::new(|| {
+10 -13
View File
@@ -21,7 +21,6 @@ use std::{
process::Command,
sync::atomic::{AtomicU64, Ordering},
};
use tristr::alias::String;
const RELEASE_VERSION_URL: &str = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
const ALPHA_BASE_URL: &str = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha";
@@ -51,7 +50,7 @@ pub async fn upgrade_core(force: bool) -> Result<CoreUpgradeReport> {
// A core already broken by the in-place updater cannot report a version; upgrading is the repair.
let installed = read_core_version(&target).unwrap_or_else(|error| {
logging!(warn, Type::Core, "core upgrade: unreadable installed core: {error:#}");
String::new()
std::string::String::new()
});
let (proxy, latest) = resolve_latest_version(alpha).await?;
@@ -173,15 +172,14 @@ fn managed_core_path(core: &str) -> Result<PathBuf> {
}
/// Pins the package to the resolved version, so a release moving on mid-upgrade cannot 404.
fn package_url(alpha: bool, version: &str) -> Result<String> {
fn package_url(alpha: bool, version: &str) -> Result<std::string::String> {
let asset = asset_base_name(alpha)?;
let extension = if cfg!(windows) { "zip" } else { "gz" };
Ok(if alpha {
format!("{ALPHA_BASE_URL}/{asset}-{version}.{extension}")
} else {
format!("{RELEASE_DOWNLOAD_URL}/{version}/{asset}-{version}.{extension}")
}
.into())
})
}
/// Mirrors the asset map in `scripts/prebuild.mjs` so an upgrade keeps the build variant
@@ -220,11 +218,11 @@ fn asset_base_name(alpha: bool) -> Result<&'static str> {
/// Returns the proxy that reached GitHub so the package download reuses it.
#[tracing::instrument(skip_all, level = "debug", fields(alpha))]
async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, String)> {
async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, std::string::String)> {
let url = if alpha {
String::from(format!("{ALPHA_BASE_URL}/version.txt"))
format!("{ALPHA_BASE_URL}/version.txt")
} else {
String::from(RELEASE_VERSION_URL)
RELEASE_VERSION_URL.to_owned()
};
let mut last_error = None;
@@ -236,7 +234,7 @@ async fn resolve_latest_version(alpha: bool) -> Result<(ProxyType, String)> {
match attempt {
Ok(response) if response.status().is_success() => {
// An error page answered with 200 must not become a version, nor reach the URL.
let version = String::from(response.text().trim());
let version = response.text().trim().to_owned();
if !is_usable_version(&version) {
logging!(
warn,
@@ -448,7 +446,7 @@ fn new_command(program: impl AsRef<OsStr>) -> Command {
}
/// Parses the version out of `Mihomo Meta v1.19.30 darwin arm64 with go1.22.12 ...`.
fn read_core_version(path: &Path) -> Result<String> {
fn read_core_version(path: &Path) -> Result<std::string::String> {
let output = new_command(path)
.arg("-v")
.output()
@@ -458,12 +456,11 @@ fn read_core_version(path: &Path) -> Result<String> {
bail!("{} -v exited with {}", path.display(), output.status);
}
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
let stdout = std::string::String::from_utf8_lossy(&output.stdout);
let stdout = String::from_utf8_lossy(&output.stdout);
stdout
.split_whitespace()
.nth(2)
.map(String::from)
.map(str::to_owned)
.with_context(|| format!("unexpected version output from {}: {stdout}", path.display()))
}
+5 -2
View File
@@ -2,7 +2,7 @@ use crate::core::notify::{Refresh, announce};
use anyhow::{Result, ensure};
use clash_verge_draft::DraftTransaction;
use serde::Serialize;
use tristr::alias::String;
use smartstring::alias::String;
use crate::{
config::{
@@ -19,7 +19,10 @@ use crate::{
#[serde(tag = "status", rename_all = "snake_case")]
pub enum DnsOverrideOutcome {
Applied,
ConfirmationRequired { source: String },
ConfirmationRequired {
#[specta(type = std::string::String)]
source: String,
},
}
pub async fn set_dns_override(
+9 -10
View File
@@ -1,17 +1,20 @@
use crate::{
cmd::{CmdResult, CommandFailure, StringifyErr as _},
cmd::{CmdResult, StringifyErr as _},
utils::dirs::{self, PathBufExec as _},
};
use clash_verge_logging::{Type, logging};
use smartstring::alias::String;
use std::path::{Component, Path, PathBuf};
use tokio::fs;
use tokio::io::AsyncWriteExt as _;
use tristr::alias::String;
#[derive(Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct IconInfo {
#[specta(type = std::string::String)]
name: String,
#[specta(type = std::string::String)]
previous_t: String,
#[specta(type = std::string::String)]
current_t: String,
}
@@ -40,7 +43,7 @@ fn ensure_icon_cache_target(icon_cache_dir: &Path, file_name: &str) -> CmdResult
Ok(icon_path)
}
fn normalized_text_prefix(content: &[u8]) -> String {
fn normalized_text_prefix(content: &[u8]) -> std::string::String {
let content = content.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(content);
let start = content
.iter()
@@ -48,8 +51,7 @@ fn normalized_text_prefix(content: &[u8]) -> String {
.unwrap_or(content.len());
let end = content.len().min(start.saturating_add(2048));
let prefix = &content[start..end];
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
std::string::String::from_utf8_lossy(prefix).to_ascii_lowercase().into()
std::string::String::from_utf8_lossy(prefix).to_ascii_lowercase()
}
fn looks_like_html(content: &[u8]) -> bool {
@@ -93,10 +95,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
if !is_supported_icon_content(&content) {
let _ = temp_path.remove_if_exists().await;
return Err(CommandFailure::plain(format!(
"Downloaded content is not a valid image: {}",
url.as_str()
)));
return Err(format!("Downloaded content is not a valid image: {}", url.as_str()).into());
}
{
@@ -165,7 +164,7 @@ pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<Stri
match fs::copy(file_path, &dest_path).await {
Ok(_) => Ok(dest_path.to_string_lossy().into()),
Err(err) => Err(CommandFailure::plain(err)),
Err(err) => Err(err.to_string().into()),
}
} else {
Err("file not found".into())
+2 -2
View File
@@ -8,7 +8,7 @@ use crate::{
};
use anyhow::{Result, bail};
use clash_verge_logging::{Type, logging, logging_error};
use tristr::alias::String;
use smartstring::alias::String;
/// Toggle proxy profile
pub async fn toggle_proxy_profile(profile_index: String) {
@@ -231,7 +231,7 @@ pub async fn update_profile(uid: &String, option: Option<&PrfOption>, is_mannual
};
let message = mask_err(&message);
let message = if profile_persisted {
format!("订阅已保存,但未能确认已应用到内核: {message}").into()
format!("订阅已保存,但未能确认已应用到内核: {message}")
} else {
message
};
+5 -6
View File
@@ -1,5 +1,4 @@
use crate::cmd;
use tristr::alias::String;
pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
tauri_specta::Builder::new()
@@ -122,7 +121,7 @@ pub struct WebDavFile {
impl From<reqwest_dav::list_cmd::ListFile> for WebDavFile {
fn from(file: reqwest_dav::list_cmd::ListFile) -> Self {
Self {
href: file.href.into(),
href: file.href,
last_modified: file.last_modified,
}
}
@@ -149,20 +148,20 @@ pub struct IpAddress {
impl From<network_interface::NetworkInterface> for NetworkInterfaceView {
fn from(interface: network_interface::NetworkInterface) -> Self {
Self {
name: interface.name.into(),
name: interface.name,
addr: interface
.addr
.into_iter()
.map(|address| match address {
network_interface::Addr::V4(address) => InterfaceAddress::V4(IpAddress {
ip: address.ip.to_string().into(),
ip: address.ip.to_string(),
}),
network_interface::Addr::V6(address) => InterfaceAddress::V6(IpAddress {
ip: address.ip.to_string().into(),
ip: address.ip.to_string(),
}),
})
.collect(),
mac_addr: interface.mac_addr.map(Into::into),
mac_addr: interface.mac_addr,
}
}
}
+14 -18
View File
@@ -11,7 +11,6 @@ use std::{
str::FromStr,
};
use tokio::io::AsyncWriteExt as _;
use tristr::alias::String;
pub async fn read_yaml<T: DeserializeOwned>(path: &Path) -> Result<T> {
if !tokio::fs::try_exists(path).await.unwrap_or(false) {
@@ -35,13 +34,12 @@ pub async fn read_mapping(path: &Path) -> Result<Mapping> {
let bytes = tokio::fs::read(path)
.await
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
// from_utf8 exists only on the std String type; the tristr alias shadows the bare name.
let yaml_str = std::string::String::from_utf8(bytes)
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
let yaml_str =
String::from_utf8(bytes).with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
let label = path.to_path_buf();
// Parsing a multi-megabyte profile is pure CPU; keep it off the async worker.
AsyncHandler::spawn_blocking(move || parse_mapping(yaml_str.into(), label))
AsyncHandler::spawn_blocking(move || parse_mapping(yaml_str, label))
.await
.map_err(|join| anyhow!("yaml parse task failed: {join}"))?
}
@@ -141,7 +139,7 @@ const ALPHABET: [char; 62] = [
pub fn get_uid(prefix: &str) -> String {
let id = nanoid!(11, &ALPHABET);
format!("{prefix}{id}").into()
format!("{prefix}{id}")
}
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
@@ -173,7 +171,7 @@ pub fn mask_url(url: &str) -> String {
let scheme_and_host = &path_part[..host_end];
let path = &path_part[host_end..]; // starts with '/' or empty
let mut result: String = scheme_and_host.into();
let mut result = scheme_and_host.to_owned();
if !path.is_empty() {
let masked: Vec<&str> = path
@@ -188,8 +186,8 @@ pub fn mask_url(url: &str) -> String {
let masked_query: Vec<String> = query
.split('&')
.map(|param| match param.find('=') {
Some(eq) => format!("{}=***", &param[..eq]).into(),
None => param.into(),
Some(eq) => format!("{}=***", &param[..eq]),
None => param.to_owned(),
})
.collect();
result.push_str(&masked_query.join("&"));
@@ -200,8 +198,7 @@ pub fn mask_url(url: &str) -> String {
/// Masks every HTTP(S) URL embedded in a log string while preserving surrounding text.
pub fn mask_err(err: &str) -> String {
// with_capacity exists only on the std String type; the tristr alias shadows the bare name.
let mut result = std::string::String::with_capacity(err.len());
let mut result = String::with_capacity(err.len());
let mut remaining = err;
loop {
@@ -227,7 +224,7 @@ pub fn mask_err(err: &str) -> String {
remaining = &remaining[url_end..];
}
result.into()
result
}
pub fn get_last_part_and_decode(url: &str) -> Option<String> {
@@ -238,8 +235,7 @@ pub fn get_last_part_and_decode(url: &str) -> Option<String> {
Some(
percent_encoding::percent_decode_str(last_segment)
.decode_utf8_lossy()
.to_string()
.into(),
.to_string(),
)
}
@@ -281,15 +277,15 @@ pub fn linux_elevator() -> String {
Ok(output) => {
if !output.stdout.is_empty() {
if let Ok(path) = std::str::from_utf8(&output.stdout) {
path.trim().into()
path.trim().to_string()
} else {
"sudo".into()
"sudo".to_string()
}
} else {
"sudo".into()
"sudo".to_string()
}
}
Err(_) => "sudo".into(),
Err(_) => "sudo".to_string(),
}
}
+14 -15
View File
@@ -11,7 +11,6 @@ use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::PathBuf;
use tristr::alias::String;
pub fn ensure_mimeapps_entries(desktop_file: &str, schemes: &[&str]) -> Result<()> {
let Some(path) = mimeapps_list_path() else {
@@ -47,19 +46,19 @@ pub fn ensure_mimeapps_entries(desktop_file: &str, schemes: &[&str]) -> Result<(
if trimmed.eq_ignore_ascii_case("[Default Applications]") {
default_present = true;
current_section = Some(SectionKind::DefaultApplications);
output_lines.push("[Default Applications]".into());
output_lines.push("[Default Applications]".to_string());
continue;
} else if trimmed.eq_ignore_ascii_case("[Added Associations]") {
current_section = Some(SectionKind::AddedAssociations);
output_lines.push("[Added Associations]".into());
output_lines.push("[Added Associations]".to_string());
continue;
}
}
if current_section.is_some() {
section_buffer.push(line.into());
section_buffer.push(line.to_string());
} else {
output_lines.push(line.into());
output_lines.push(line.to_string());
}
}
@@ -79,9 +78,9 @@ pub fn ensure_mimeapps_entries(desktop_file: &str, schemes: &[&str]) -> Result<(
if output_lines.last().is_some_and(|line| !line.is_empty()) {
output_lines.push(String::new());
}
output_lines.push("[Default Applications]".into());
output_lines.push("[Default Applications]".to_string());
for &scheme in schemes {
output_lines.push(format!("x-scheme-handler/{scheme}={desktop_file};").into());
output_lines.push(format!("x-scheme-handler/{scheme}={desktop_file};"));
}
}
@@ -172,7 +171,7 @@ fn flush_section(
.split(';')
.filter_map(|value| {
let trimmed = value.trim();
(!trimmed.is_empty()).then(|| trimmed.into())
(!trimmed.is_empty()).then(|| trimmed.to_string())
})
.collect();
@@ -188,7 +187,7 @@ fn flush_section(
.split(';')
.filter_map(|value| {
let trimmed = value.trim();
(!trimmed.is_empty()).then(|| trimmed.into())
(!trimmed.is_empty()).then(|| trimmed.to_string())
})
.collect();
@@ -204,10 +203,10 @@ fn flush_section(
merged_values.insert(0, moved);
}
} else {
merged_values.insert(0, desktop_file.into());
merged_values.insert(0, desktop_file.to_string());
}
let mut merged_line: String = format!("{existing_prefix}x-scheme-handler/{scheme}=").into();
let mut merged_line = format!("{existing_prefix}x-scheme-handler/{scheme}=");
merged_line.push_str(&merged_values.join(";"));
merged_line.push(';');
@@ -223,16 +222,16 @@ fn flush_section(
if let Some(pos) = values.iter().position(|value| value == desktop_file) {
if pos != 0 {
values.remove(pos);
values.insert(0, desktop_file.into());
values.insert(0, desktop_file.to_string());
*changed = true;
}
} else {
values.insert(0, desktop_file.into());
values.insert(0, desktop_file.to_string());
*changed = true;
}
let prefix = line.chars().take_while(|c| c.is_whitespace()).collect::<String>();
let mut new_line: String = format!("{prefix}x-scheme-handler/{scheme}=").into();
let mut new_line = format!("{prefix}x-scheme-handler/{scheme}=");
new_line.push_str(&values.join(";"));
new_line.push(';');
@@ -254,7 +253,7 @@ fn flush_section(
if ensure_all {
for &scheme in schemes {
if !seen.contains_key(scheme) {
processed.push(format!("x-scheme-handler/{scheme}={desktop_file};").into());
processed.push(format!("x-scheme-handler/{scheme}={desktop_file};"));
*changed = true;
}
}
+6 -6
View File
@@ -5,10 +5,10 @@ use reqwest::{
Client, Proxy, StatusCode,
header::{HeaderMap, HeaderValue, USER_AGENT},
};
use smartstring::alias::String;
use std::{sync::Arc, time::Duration};
use sysproxy::Sysproxy;
use tauri::Url;
use tristr::alias::String;
#[derive(Debug)]
pub struct HttpResponse {
@@ -57,7 +57,7 @@ impl NetworkManager {
fn build_client(
&self,
proxy_url: Option<String>,
proxy_url: Option<std::string::String>,
default_headers: HeaderMap,
accept_invalid_certs: bool,
timeout_secs: Option<u64>,
@@ -76,7 +76,7 @@ impl NetworkManager {
// 设置代理
if let Some(proxy_str) = proxy_url {
let proxy = Proxy::all(proxy_str.as_str())?;
let proxy = Proxy::all(proxy_str)?;
builder = builder.proxy(proxy);
} else {
builder = builder.no_proxy();
@@ -226,15 +226,15 @@ impl NetworkManager {
accept_invalid_certs: bool,
tls_root_mode: TlsRootMode,
) -> Result<Client> {
let proxy_url: Option<String> = match proxy_type {
let proxy_url: Option<std::string::String> = match proxy_type {
ProxyType::None => None,
ProxyType::Localhost => {
let port = MixedPort::desired().await;
Some(format!("http://127.0.0.1:{port}").into())
Some(format!("http://127.0.0.1:{port}"))
}
ProxyType::System => {
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
Some(format!("http://{}:{}", p.host, p.port).into())
Some(format!("http://{}:{}", p.host, p.port))
} else {
None
}
+7 -7
View File
@@ -1,8 +1,8 @@
use crate::core::notify::NoticeStatus;
use anyhow::Result;
use percent_encoding::percent_decode_str;
use smartstring::alias::String;
use tauri::Url;
use tristr::alias::String;
use crate::{
config::{Config, PrfItem, profiles},
@@ -39,7 +39,7 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
Ok(())
}
fn extract_subscription_info(link_parsed: &Url) -> Option<(String, Option<String>)> {
fn extract_subscription_info(link_parsed: &Url) -> Option<(std::string::String, Option<String>)> {
if !matches!(link_parsed.scheme(), "clash" | "clash-verge") {
return None;
}
@@ -52,7 +52,7 @@ fn extract_subscription_info(link_parsed: &Url) -> Option<(String, Option<String
Some((url, name))
}
fn extract_subscription_url(link_parsed: &Url) -> Option<String> {
fn extract_subscription_url(link_parsed: &Url) -> Option<std::string::String> {
let query = link_parsed.query()?;
let prefix = "url=";
let pos = query.find(prefix)?;
@@ -60,15 +60,15 @@ fn extract_subscription_url(link_parsed: &Url) -> Option<String> {
Some(decode_subscription_url(raw_url))
}
fn decode_subscription_url(raw_url: &str) -> String {
fn decode_subscription_url(raw_url: &str) -> std::string::String {
// Avoid double-decoding nested subscription URLs; decode only when needed.
if Url::parse(raw_url).is_ok() {
return String::from(raw_url);
return raw_url.to_string();
}
let mut candidate = String::from(raw_url);
let mut candidate = raw_url.to_string();
for _ in 0..2 {
let next = String::from(percent_decode_str(&candidate).decode_utf8_lossy());
let next = percent_decode_str(&candidate).decode_utf8_lossy().to_string();
if next == candidate {
break;
}
+12 -17
View File
@@ -5,7 +5,6 @@ use std::fs;
use std::os::windows::process::CommandExt as _;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use tristr::alias::String;
use windows::Win32::Globalization::{GetACP, GetOEMCP, MULTI_BYTE_TO_WIDE_CHAR_FLAGS, MultiByteToWideChar};
const CREATE_NO_WINDOW: u32 = 0x08000000;
@@ -73,11 +72,11 @@ fn get_task_user_id() -> Result<String> {
if let Some(domain) = domain {
let domain = domain.trim();
if !domain.is_empty() {
return Ok(format!("{domain}\\{username}").into());
return Ok(format!("{domain}\\{username}"));
}
}
Ok(username.into())
Ok(username.to_string())
}
fn get_startup_dir() -> Result<PathBuf> {
@@ -119,8 +118,7 @@ fn task_xml_path(mode: TaskMode) -> Result<PathBuf> {
}
fn xml_escape(value: &str) -> String {
// Per-char pushes must stay on the std String: TriStr growth re-materializes per push.
let mut escaped = std::string::String::with_capacity(value.len());
let mut escaped = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'&' => escaped.push_str("&amp;"),
@@ -131,7 +129,7 @@ fn xml_escape(value: &str) -> String {
_ => escaped.push(ch),
}
}
escaped.into()
escaped
}
fn build_task_xml(mode: TaskMode) -> Result<String> {
@@ -184,8 +182,7 @@ fn build_task_xml(mode: TaskMode) -> Result<String> {
user_id,
mode.xml_run_level(),
exe_path
)
.into())
))
}
fn encode_utf16le_with_bom(content: &str) -> Vec<u8> {
@@ -229,13 +226,12 @@ fn decode_with_code_page(bytes: &[u8], code_page: u32) -> Option<String> {
}
wide.truncate(written as usize);
// from_utf16_lossy exists only on the std String type; the tristr alias shadows the bare name.
Some(std::string::String::from_utf16_lossy(&wide).into())
Some(String::from_utf16_lossy(&wide))
}
fn decode_console_output(bytes: &[u8]) -> String {
if let Ok(text) = std::str::from_utf8(bytes) {
return text.into();
return text.to_string();
}
let oem = unsafe { GetOEMCP() };
@@ -248,8 +244,7 @@ fn decode_console_output(bytes: &[u8]) -> String {
return text;
}
// from_utf8_lossy exists only on the std String type; the tristr alias shadows the bare name.
std::string::String::from_utf8_lossy(bytes).into()
String::from_utf8_lossy(bytes).to_string()
}
fn output_message(output: &Output) -> String {
@@ -259,10 +254,10 @@ fn output_message(output: &Output) -> String {
let stderr = stderr.trim();
match (stdout.is_empty(), stderr.is_empty()) {
(true, true) => "unknown error".into(),
(false, true) => stdout.into(),
(true, false) => stderr.into(),
(false, false) => format!("{stdout} | {stderr}").into(),
(true, true) => "unknown error".to_string(),
(false, true) => stdout.to_string(),
(true, false) => stderr.to_string(),
(false, false) => format!("{stdout} | {stderr}"),
}
}
+1 -2
View File
@@ -16,7 +16,6 @@ use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::sync::oneshot;
use tristr::alias::String;
use warp::Filter as _;
const INSTANCE_TOKEN_HEADER: &str = "x-instance-token";
@@ -142,7 +141,7 @@ async fn notify_existing_instance(record: &InstanceRecord) -> bool {
let request = client.get(format!("http://127.0.0.1:{}/commands/visible", record.port));
request
.header(INSTANCE_TOKEN_HEADER, record.token.as_str())
.header(INSTANCE_TOKEN_HEADER, &record.token)
.send()
.await
.is_ok_and(|response| response.status().is_success())
+4 -6
View File
@@ -1,6 +1,5 @@
use serde::Serialize;
use serde_yaml_ng::Result;
use tristr::alias::String;
pub fn to_mihomo_config_string<T: Serialize>(data: &T) -> Result<String> {
let yaml = serde_yaml_ng::to_string(data)?;
@@ -8,8 +7,7 @@ pub fn to_mihomo_config_string<T: Serialize>(data: &T) -> Result<String> {
}
fn quote_fake_ip_filter_wildcards(yaml: &str) -> String {
// with_capacity exists only on the std String type; the tristr alias shadows the bare name.
let mut result = std::string::String::with_capacity(yaml.len());
let mut result = String::with_capacity(yaml.len());
let mut in_fake_ip_filter = false;
let mut fake_ip_filter_indent = 0;
@@ -41,7 +39,7 @@ fn quote_fake_ip_filter_wildcards(yaml: &str) -> String {
result.push('\n');
}
result.into()
result
}
fn leading_spaces(line: &str) -> usize {
@@ -52,12 +50,12 @@ fn quote_wildcard_item(item: &str) -> String {
if !item.starts_with('\"') && !item.starts_with('\'') && contains_wildcard(item) {
quote_string(item)
} else {
item.into()
item.to_string()
}
}
fn quote_string(s: &str) -> String {
("\'".to_string() + s.replace('\'', "''").as_str() + "\'").into()
"\'".to_string() + s.replace('\'', "''").as_str() + "\'"
}
fn contains_wildcard(value: &str) -> bool {
+24
View File
@@ -21,3 +21,27 @@ These rules supplement the repository's root `AGENTS.md`.
- **Command ownership.** `services` owns backend command invocation and transport
wrappers. Components and hooks use those boundaries; plugin mutations follow
the same intent ownership as backend commands.
- **State liveness.** Every `useState`/`useRef` member has a reachable write
that differs from its initial value and a read that observes the difference.
A value that can only ever hold its initial value is a constant: declare the
constant and delete the state. A ref nothing reads is deleted, not kept.
- **Surface liveness.** Anything a module exposes — props, parameters, context
members, imperative-handle methods — ships with at least one external caller
in the same change. "Reserved for future use" fields, `_`-prefixed params
that callers still pass, and error or callback channels that every receiver
ignores are removed, not maintained.
- **Path liveness.** A fallback or branch names the concrete runtime condition
that reaches it. If no supported runtime can reach it, the branch is not
added — or it replaces the primary path. Constant subexpressions and cleanup
of resources that cannot exist yet are simplified away.
- **Effect economy.** One user interaction produces one state write and one
command. A handler does not re-apply a value its wrapper already applied,
and an effect does not re-run a refresh another branch of the same effect
just ran.
- **Mount economy.** Components that issue commands or subscriptions on mount
are mounted only where their UI is reachable. A dialog no rendered control
can open stays unmounted, not mounted-and-idle.
- **Resource bounds.** Every cache key, timer and subscription a feature
allocates is reused or released on refresh; refresh never allocates an
unbounded sequence of new keys. Retries re-create per-attempt resources so
a retry can actually succeed.
+13 -30
View File
@@ -22,23 +22,16 @@ interface BaseSplitChipEditorProps {
error?: boolean
helperText?: ReactNode
placeholder?: string
rows?: number
separator?: string
splitPattern?: RegExp
defaultMode?: BaseSplitChipEditorMode
showModeToggle?: boolean
ariaLabel?: string
addLabel?: ReactNode
emptyLabel?: ReactNode
modeLabels?: Partial<Record<BaseSplitChipEditorMode, ReactNode>>
renderHeader?: (modeToggle: ReactNode) => ReactNode
}
const DEFAULT_SPLIT_PATTERN = /[,\n;\r]+/
const splitValue = (value: string, splitPattern: RegExp) =>
const splitValue = (value: string) =>
value
.split(splitPattern)
.split(DEFAULT_SPLIT_PATTERN)
.map((item) => item.trim())
.filter(Boolean)
@@ -49,35 +42,25 @@ export const BaseSplitChipEditor = ({
error = false,
helperText,
placeholder,
rows = 4,
separator = ',',
splitPattern = DEFAULT_SPLIT_PATTERN,
defaultMode = 'visual',
showModeToggle = true,
ariaLabel,
addLabel,
emptyLabel,
modeLabels,
renderHeader,
}: BaseSplitChipEditorProps) => {
const { t } = useTranslation()
const [mode, setMode] = useState<BaseSplitChipEditorMode>(defaultMode)
const [mode, setMode] = useState<BaseSplitChipEditorMode>('visual')
const [draft, setDraft] = useState('')
const resolvedLabels = useMemo(
() => ({
visual: modeLabels?.visual ?? t('shared.editorModes.visualization'),
advanced: modeLabels?.advanced ?? t('shared.editorModes.advanced'),
add: addLabel ?? t('shared.actions.new'),
empty: emptyLabel ?? t('shared.statuses.empty'),
visual: t('shared.editorModes.visualization'),
advanced: t('shared.editorModes.advanced'),
add: t('shared.actions.new'),
empty: t('shared.statuses.empty'),
}),
[t, modeLabels, addLabel, emptyLabel],
[t],
)
const values = useMemo(
() => splitValue(value, splitPattern),
[value, splitPattern],
)
const values = useMemo(() => splitValue(value), [value])
const items = useMemo(() => {
const counts = new Map<string, number>()
@@ -92,7 +75,7 @@ export const BaseSplitChipEditor = ({
}, [values])
const handleAddDraft = () => {
const nextValues = splitValue(draft, splitPattern)
const nextValues = splitValue(draft)
if (!nextValues.length) {
return
}
@@ -113,7 +96,7 @@ export const BaseSplitChipEditor = ({
const resolvedAriaLabel =
ariaLabel ?? (typeof toggleLabel === 'string' ? toggleLabel : undefined)
const modeToggle = showModeToggle ? (
const modeToggle = (
<Tooltip title={toggleLabel}>
<IconButton
size="small"
@@ -128,7 +111,7 @@ export const BaseSplitChipEditor = ({
<ToggleIcon fontSize="small" />
</IconButton>
</Tooltip>
) : null
)
return (
<>
@@ -202,7 +185,7 @@ export const BaseSplitChipEditor = ({
disabled={disabled}
size="small"
multiline
rows={rows}
rows={4}
sx={{ width: '100%' }}
value={value}
helperText={helperText}
+3 -9
View File
@@ -596,7 +596,7 @@ export const CurrentProxyCard = () => {
[getProfileStorageKey],
)
const { handleSelectChange } = useProxySelection({
const { select } = useProxySelection({
onSuccess: () => {
refreshProxy()
},
@@ -766,15 +766,9 @@ export const CurrentProxyCard = () => {
const previousProxy = selectedGroup.now
const nextName = option.member.ref.name
// The profile selection is the durable source across core restarts and run modes.
handleSelectChange(
selectedGroup.name,
previousProxy!,
selectedGroup.fixed!,
)({
target: { value: nextName },
})
select(selectedGroup.name, nextName, previousProxy!, selectedGroup.fixed!)
},
[handleSelectChange, isDirectMode, selectedGroup, unsortedProxyOptions],
[isDirectMode, select, selectedGroup, unsortedProxyOptions],
)
const goToProxies = useCallback(() => {
@@ -14,7 +14,6 @@ import { useTranslation } from 'react-i18next'
import { useTrafficGraphDataEnhanced } from '@/hooks/use-traffic-monitor'
import { useVerge } from '@/hooks/use-verge'
import { debugLog } from '@/utils/debug'
import parseTraffic from '@/utils/parse-traffic'
import {
formatTrafficHourMinute,
@@ -23,14 +22,7 @@ import {
isSameTrafficData,
} from '@/utils/traffic-sampler'
interface ITrafficItem {
up: number
down: number
timestamp?: number
}
export interface EnhancedCanvasTrafficGraphRef {
appendData: (data: ITrafficItem) => void
toggleStyle: () => void
}
@@ -142,7 +134,6 @@ export const EnhancedCanvasTrafficGraph = memo(
[],
)
const debounceTimeoutRef = useRef<number | null>(null)
const [currentFPS, setCurrentFPS] = useState(GRAPH_CONFIG.targetFPS)
const colors = useMemo(
() => ({
@@ -183,8 +174,6 @@ export const EnhancedCanvasTrafficGraph = memo(
if (displayData.length === 0) {
lastDataTimestampRef.current = 0
dataStaleRef.current = false
// eslint-disable-next-line @eslint-react/set-state-in-effect
setCurrentFPS(GRAPH_CONFIG.targetFPS)
return
}
@@ -200,18 +189,11 @@ export const EnhancedCanvasTrafficGraph = memo(
}
}, [displayData])
const handleFocusStateChange = useCallback(
(focused: boolean) => {
isWindowFocusedRef.current = focused
const handleFocusStateChange = useCallback((focused: boolean) => {
isWindowFocusedRef.current = focused
if (focused || !pause_render_traffic_stats_on_blur) {
setCurrentFPS(GRAPH_CONFIG.targetFPS)
}
scheduleDrawGraphRef.current()
},
[pause_render_traffic_stats_on_blur],
)
scheduleDrawGraphRef.current()
}, [])
useEffect(() => {
if (typeof window === 'undefined' || typeof document === 'undefined') {
@@ -684,19 +666,13 @@ export const EnhancedCanvasTrafficGraph = memo(
ctx.beginPath()
ctx.moveTo(getX(0), getY(0))
if (chartStyle === 'bezier') {
for (let i = 1; i < data.length; i++) {
const currentX = getX(i)
const currentY = getY(i)
const nextIndex = Math.min(i + 1, lastIndex)
const controlX = (currentX + getX(nextIndex)) / 2
const controlY = (currentY + getY(nextIndex)) / 2
ctx.quadraticCurveTo(currentX, currentY, controlX, controlY)
}
} else {
for (let i = 1; i < data.length; i++) {
ctx.lineTo(getX(i), getY(i))
}
for (let i = 1; i < data.length; i++) {
const currentX = getX(i)
const currentY = getY(i)
const nextIndex = Math.min(i + 1, lastIndex)
const controlX = (currentX + getX(nextIndex)) / 2
const controlY = (currentY + getY(nextIndex)) / 2
ctx.quadraticCurveTo(currentX, currentY, controlX, controlY)
}
ctx.lineTo(getX(lastIndex), height - padding.bottom)
@@ -984,21 +960,7 @@ export const EnhancedCanvasTrafficGraph = memo(
setChartStyle((prev) => (prev === 'bezier' ? 'line' : 'bezier'))
}, [])
const appendData = useCallback((data: ITrafficItem) => {
debugLog(
'[EnhancedCanvasTrafficGraphV2] appendData called (using global data):',
data,
)
}, [])
useImperativeHandle(
ref,
() => ({
appendData,
toggleStyle,
}),
[appendData, toggleStyle],
)
useImperativeHandle(ref, () => ({ toggleStyle }), [toggleStyle])
const getTimeRangeText = useCallback(() => {
return t('home.components.traffic.patterns.minutes', {
@@ -1142,7 +1104,7 @@ export const EnhancedCanvasTrafficGraph = memo(
{t('home.components.traffic.diagnostics', {
points: displayData.length,
compressed: samplerStats.compressedBufferSize,
fps: currentFPS,
fps: GRAPH_CONFIG.targetFPS,
})}
</Box>
+55 -6
View File
@@ -3,10 +3,14 @@ import {
ArrowUpwardRounded,
CloudDownloadRounded,
CloudUploadRounded,
ErrorOutlineRounded,
LinkRounded,
MemoryRounded,
} from '@mui/icons-material'
import {
Alert,
Box,
Button,
Grid,
PaletteColor,
Paper,
@@ -15,9 +19,9 @@ import {
useTheme,
} from '@mui/material'
import { ReactNode, memo, useMemo, useRef } from 'react'
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
import { useTranslation } from 'react-i18next'
import { TrafficErrorBoundary } from '@/components/shared/traffic-error-boundary'
import { useConnectionSummaryData } from '@/hooks/use-connection-data'
import { useMemoryData } from '@/hooks/use-memory-data'
import { useTrafficData } from '@/hooks/use-traffic-data'
@@ -127,6 +131,50 @@ const CompactStatCard = memo(
// 添加显示名称
CompactStatCard.displayName = 'CompactStatCard'
const fallbackSx = {
p: 2,
minHeight: 200,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
border: '1px dashed',
borderColor: 'error.main',
borderRadius: 2,
bgcolor: 'error.light',
}
const reloadPage = () => window.location.reload()
const TrafficFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
const { t } = useTranslation()
return (
<Box sx={fallbackSx}>
<ErrorOutlineRounded color="error" sx={{ fontSize: 48, mb: 2 }} />
<Typography variant="h6" gutterBottom>
{t('shared.feedback.errors.trafficStats')}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t('shared.feedback.errors.trafficStatsDescription')}
</Typography>
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
{error instanceof Error
? error.message
: t('shared.feedback.errors.unknown')}
</Alert>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button variant="contained" size="small" onClick={resetErrorBoundary}>
{t('shared.actions.retry')}
</Button>
<Button variant="outlined" size="small" onClick={reloadPage}>
{t('shared.actions.refreshPage')}
</Button>
</Box>
</Box>
)
}
export const EnhancedTrafficStats = () => {
const { t } = useTranslation()
const theme = useTheme()
@@ -254,10 +302,11 @@ export const EnhancedTrafficStats = () => {
}, [t, parsedData, displayMemory])
return (
<TrafficErrorBoundary
onError={(error, errorInfo) => {
console.error('[EnhancedTrafficStats] 组件错误:', error, errorInfo)
}}
<ErrorBoundary
FallbackComponent={TrafficFallback}
onError={(error, errorInfo) =>
console.error('[EnhancedTrafficStats]', error, errorInfo)
}
>
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
{trafficGraph && (
@@ -273,6 +322,6 @@ export const EnhancedTrafficStats = () => {
</Grid>
))}
</Grid>
</TrafficErrorBoundary>
</ErrorBoundary>
)
}
+37 -9
View File
@@ -4,10 +4,19 @@ import {
VisibilityOffOutlined,
VisibilityOutlined,
} from '@mui/icons-material'
import { Box, Button, IconButton, Skeleton, Typography } from '@mui/material'
import {
Box,
Button,
FormControlLabel,
IconButton,
Skeleton,
Switch,
Typography,
} from '@mui/material'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { useEffect } from 'foxact/use-abortable-effect'
import { useIntersection } from 'foxact/use-intersection'
import { useLocalStorage } from 'foxact/use-local-storage'
import type { XOR } from 'foxts/ts-xor'
import {
forwardRef,
@@ -99,9 +108,13 @@ const IPInfoCardContainer = forwardRef<HTMLElement, React.PropsWithChildren>(
export const IpInfoCard = () => {
const { t } = useTranslation()
const [showIp, setShowIp] = useState(false)
const [autoRefresh, setAutoRefresh] = useLocalStorage(
'ip-info-auto-refresh',
true,
)
const appWindow = useMemo(() => getCurrentWebviewWindow(), [])
// Once intersected, refreshes stay enabled until unmount.
// Once intersected, the card stays eligible for refresh until unmount.
const [containerRef, hasIntersected, _resetIntersected] = useIntersection({
rootMargin: '0px',
})
@@ -151,6 +164,8 @@ export const IpInfoCard = () => {
})
useEffect(() => {
if (!autoRefresh) return
let timer: number | null = null
// Prefer DOM intersection state; Tauri visibility is unreliable on some platforms.
@@ -190,7 +205,7 @@ export const IpInfoCard = () => {
if (timer != null) clearInterval(timer)
document.removeEventListener('visibilitychange', onVisibilityChange)
}
}, [hasIntersected])
}, [autoRefresh, hasIntersected])
const toggleShowIp = useCallback(() => {
setShowIp((prev) => !prev)
@@ -364,12 +379,25 @@ export const IpInfoCard = () => {
fontSize: '0.7rem',
}}
>
<Typography variant="caption">
{t('home.components.ipInfo.labels.autoRefresh')}
{countdown.type === 'countdown'
? `: ${countdown.remainingSeconds}s`
: '...'}
</Typography>
<FormControlLabel
sx={{ m: 0 }}
control={
<Switch
size="small"
checked={autoRefresh}
onChange={(_, checked) => setAutoRefresh(checked)}
/>
}
label={
<Typography variant="caption">
{t('home.components.ipInfo.labels.autoRefresh')}
{autoRefresh &&
(countdown.type === 'countdown'
? `: ${countdown.remainingSeconds}s`
: '...')}
</Typography>
}
/>
<Typography
variant="caption"
sx={{
+6 -13
View File
@@ -86,16 +86,11 @@ export const SystemInfoCard = () => {
}, [verge, patchVerge])
const handleRunningModeClick = useCallback(async () => {
if (isSidecarMode || (isAdminMode && isSidecarMode)) {
if (isSidecarMode) {
await installServiceAndRestartCore()
await mutateSystemState()
}
}, [
isSidecarMode,
isAdminMode,
installServiceAndRestartCore,
mutateSystemState,
])
}, [isSidecarMode, installServiceAndRestartCore, mutateSystemState])
const onCheckUpdate = useLockFn(async () => {
try {
@@ -122,18 +117,16 @@ export const SystemInfoCard = () => {
const runningModeStyle = useMemo(
() => ({
cursor:
isSidecarMode || (isAdminMode && isSidecarMode) ? 'pointer' : 'default',
textDecoration:
isSidecarMode || (isAdminMode && isSidecarMode) ? 'underline' : 'none',
cursor: isSidecarMode ? 'pointer' : 'default',
textDecoration: isSidecarMode ? 'underline' : 'none',
display: 'flex',
alignItems: 'center',
gap: 0.5,
'&:hover': {
opacity: isSidecarMode || (isAdminMode && isSidecarMode) ? 0.7 : 1,
opacity: isSidecarMode ? 0.7 : 1,
},
}),
[isSidecarMode, isAdminMode],
[isSidecarMode],
)
const getModeIcon = () => {
+3 -3
View File
@@ -87,7 +87,7 @@ export const TestCard = () => {
x.uid === uid ? { ...x, ...patch } : x,
)
mutateVerge({ ...verge, test_list: newList }, false)
mutateVerge({ ...verge, test_list: newList })
},
[testList, verge, mutateVerge],
)
@@ -96,7 +96,7 @@ export const TestCard = () => {
(uid: string) => {
const newList = testList.filter((x) => x.uid !== uid)
patchVerge({ test_list: newList })
mutateVerge({ ...verge, test_list: newList }, false)
mutateVerge({ ...verge, test_list: newList })
},
[testList, verge, patchVerge, mutateVerge],
)
@@ -129,7 +129,7 @@ export const TestCard = () => {
newList.splice(newIndex, 0, removed)
// 优化:先本地更新,再异步 patch,避免UI卡死
mutateVerge({ ...verge, test_list: newList }, false)
mutateVerge({ ...verge, test_list: newList })
const patchFn = () => {
try {
patchVerge({ test_list: newList })
+1 -4
View File
@@ -35,10 +35,7 @@ export const LayoutSidebar = (props: LayoutSidebarProps) => {
const handleMenuOrderOptimisticUpdate = useCallback(
(order: string[]) => {
mutateVerge(
(prev) => (prev ? { ...prev, menu_order: order } : prev),
false,
)
mutateVerge((prev) => (prev ? { ...prev, menu_order: order } : prev))
},
[mutateVerge],
)
+33 -4
View File
@@ -1,14 +1,15 @@
import {
ArrowDownwardRounded,
ArrowUpwardRounded,
ErrorOutlineRounded,
MemoryRounded,
} from '@mui/icons-material'
import type { BoxProps, SvgIconProps, TypographyProps } from '@mui/material'
import { Box, Typography } from '@mui/material'
import { useEffect, useRef } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { useTranslation } from 'react-i18next'
import { LightweightTrafficErrorBoundary } from '@/components/shared/traffic-error-boundary'
import { useMemoryData } from '@/hooks/use-memory-data'
import { useTrafficData } from '@/hooks/use-traffic-data'
import { useVerge } from '@/hooks/use-verge'
@@ -17,6 +18,30 @@ import parseTraffic from '@/utils/parse-traffic'
import { TrafficGraph, type TrafficRef } from './traffic-graph'
const unavailableSx = {
p: 1,
minHeight: 60,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'error.light',
borderRadius: 1,
color: 'error.contrastText',
}
const TrafficUnavailableFallback = () => {
const { t } = useTranslation()
return (
<Box sx={unavailableSx}>
<ErrorOutlineRounded sx={{ mr: 1, fontSize: 20 }} />
<Typography variant="caption">
{t('shared.feedback.errors.trafficUnavailable')}
</Typography>
</Box>
)
}
export const LayoutTraffic = () => {
const { t } = useTranslation()
const { verge } = useVerge()
@@ -75,7 +100,12 @@ export const LayoutTraffic = () => {
}
return (
<LightweightTrafficErrorBoundary>
<ErrorBoundary
FallbackComponent={TrafficUnavailableFallback}
onError={(error, errorInfo) =>
console.error('[LayoutTraffic]', error, errorInfo)
}
>
<Box sx={{ position: 'relative' }}>
{trafficGraph && pageVisible && (
<div
@@ -130,7 +160,6 @@ export const LayoutTraffic = () => {
cursor: 'auto',
}}
color={'disabled'}
onClick={async () => {}}
>
<MemoryRounded {...iconStyle} />
<Typography {...valStyle}>{inuse}</Typography>
@@ -139,6 +168,6 @@ export const LayoutTraffic = () => {
)}
</Box>
</Box>
</LightweightTrafficErrorBoundary>
</ErrorBoundary>
)
}
+4 -36
View File
@@ -1,11 +1,6 @@
import { Close, CropSquare, FilterNone, Minimize } from '@mui/icons-material'
import { Box, IconButton } from '@mui/material'
import {
forwardRef,
type PointerEvent,
useCallback,
useImperativeHandle,
} from 'react'
import { type PointerEvent, useCallback } from 'react'
import { useWindowControls } from '@/hooks/use-window'
import getSystem from '@/utils/get-system'
@@ -63,36 +58,9 @@ export const WindowResizeHandles = () => {
)
}
export const WindowControls = forwardRef(function WindowControls(props, ref) {
export const WindowControls = () => {
const OS = getSystem()
const {
currentWindow,
maximized,
minimize,
close,
toggleFullscreen,
toggleMaximize,
} = useWindowControls()
useImperativeHandle(
ref,
() => ({
currentWindow,
maximized,
minimize,
close,
toggleFullscreen,
toggleMaximize,
}),
[
currentWindow,
maximized,
minimize,
close,
toggleFullscreen,
toggleMaximize,
],
)
const { maximized, minimize, close, toggleMaximize } = useWindowControls()
// 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。
// 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准。
@@ -186,4 +154,4 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) {
)}
</Box>
)
})
}
@@ -397,9 +397,9 @@ export const RulesEditorViewer = (props: Props) => {
return
}
setPrependSeq(obj?.prepend || [])
setAppendSeq(obj?.append || [])
setDeleteSeq(obj?.delete || [])
setPrependSeq((obj?.prepend || []).filter((rule) => rule !== null))
setAppendSeq((obj?.append || []).filter((rule) => rule !== null))
setDeleteSeq((obj?.delete || []).filter((rule) => rule !== null))
hasLoadedSeqConfigRef.current = true
}, [property])
@@ -417,9 +417,9 @@ export const RulesEditorViewer = (props: Props) => {
hasLoadedSeqConfigRef.current = true
startTransition(() => {
setPrependSeq(obj?.prepend ?? [])
setAppendSeq(obj?.append ?? [])
setDeleteSeq(obj?.delete ?? [])
setPrependSeq((obj?.prepend ?? []).filter((rule) => rule !== null))
setAppendSeq((obj?.append ?? []).filter((rule) => rule !== null))
setDeleteSeq((obj?.delete ?? []).filter((rule) => rule !== null))
})
setVisualization(true)
}
+4 -4
View File
@@ -462,7 +462,7 @@ function NormalProxyGroups(props: { mode: string }) {
}
}, [handleScroll])
const { handleProxyGroupChange } = useProxySelection({
const { select } = useProxySelection({
onSuccess: () => {
onProxies()
},
@@ -477,9 +477,9 @@ function NormalProxyGroups(props: { mode: string }) {
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return
if (!isInteractableMember(member)) return
handleProxyGroupChange(group, { name: member.ref.name })
select(group.name, member.ref.name, group.now!, group.fixed!)
},
[handleProxyGroupChange],
[select],
)
const handleLocation = useStableCallback((group: ProxyGroupView) => {
@@ -608,7 +608,7 @@ function NormalProxyGroups(props: { mode: string }) {
/>
</DragDropProvider>
{mode === 'rule' && (
{(mode === 'rule' || mode === 'global') && (
<ProxyGroupNavigator
proxyGroupNames={proxyGroupNames}
onGroupLocation={handleGroupLocationByName}
@@ -30,10 +30,8 @@ export const useProxyGroupHeaderLayout = () => {
if (canceled || !operation.source || !operation.target) return
const position = toolsOnLeft ? 'right' : 'left'
mutateVerge(
(prev) =>
prev ? { ...prev, proxy_group_tools_position: position } : prev,
false,
mutateVerge((prev) =>
prev ? { ...prev, proxy_group_tools_position: position } : prev,
)
try {
await patchVerge({ proxy_group_tools_position: position })
+2 -2
View File
@@ -211,7 +211,7 @@ export const useRenderList = (
if (!proxyView) return []
if (isChainMode)
return selectedGroup ? [selectedGroup] : [CHAIN_DELAY_GROUP]
return mode === 'rule' || mode === 'script'
return mode === 'rule' || mode === 'global'
? proxyView.groups.map(({ name }) => name)
: proxyView.global
? [proxyView.global.name]
@@ -257,7 +257,7 @@ export const useRenderList = (
}))
}
const useRule = mode === 'rule' || mode === 'script'
const useRule = mode === 'rule' || mode === 'global'
const renderGroups = useRule
? proxyView.groups
: proxyView.global === null
@@ -113,17 +113,15 @@ export const BackupConfigViewer = memo(
errorNotice: false,
},
)
await mutateVerge(
(current) =>
current
? {
...current,
webdav_url: trimmedUrl,
webdav_username: trimmedUsername,
webdav_password: data.password,
}
: current,
false,
await mutateVerge((current) =>
current
? {
...current,
webdav_url: trimmedUrl,
webdav_username: trimmedUsername,
webdav_password: data.password,
}
: current,
)
setWebdavStatus(signature, 'unknown')
showNotice.success('settings.modals.backup.messages.webdavConfigSaved')
@@ -107,7 +107,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
showNotice.error(err)
}
const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false)
mutateVerge({ ...verge, ...patch })
}
return (
+18 -29
View File
@@ -15,44 +15,33 @@ import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
import { useVerge } from '@/hooks/use-verge'
import { showNotice } from '@/services/notice-service'
const buildInitialValues = (verge?: IVergeConfig | null) => ({
appLogLevel: verge?.app_log_level ?? 'warn',
appLogMaxSize: verge?.app_log_max_size ?? 128,
appLogMaxCount: verge?.app_log_max_count ?? 8,
autoCloseConnection: verge?.auto_close_connection ?? true,
autoCheckUpdate: verge?.auto_check_update ?? true,
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
proxyLayoutColumn: verge?.proxy_layout_column || 6,
enableAutoDelayDetection: verge?.enable_auto_delay_detection ?? false,
autoDelayDetectionIntervalMinutes:
verge?.auto_delay_detection_interval_minutes ?? 5,
defaultLatencyTest: verge?.default_latency_test || '',
autoLogClean: verge?.auto_log_clean || 0,
defaultLatencyTimeout: verge?.default_latency_timeout || 10000,
})
export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation()
const { verge, patchVerge } = useVerge()
const [open, setOpen] = useState(false)
const [values, setValues] = useState({
appLogLevel: 'warn',
appLogMaxSize: 8,
appLogMaxCount: 12,
autoCloseConnection: true,
autoCheckUpdate: true,
enableBuiltinEnhanced: true,
proxyLayoutColumn: 6,
enableAutoDelayDetection: false,
autoDelayDetectionIntervalMinutes: 5,
defaultLatencyTest: '',
autoLogClean: 2,
defaultLatencyTimeout: 10000,
})
const [values, setValues] = useState(() => buildInitialValues(null))
useImperativeHandle(ref, () => ({
open: () => {
setOpen(true)
setValues({
appLogLevel: verge?.app_log_level ?? 'warn',
appLogMaxSize: verge?.app_log_max_size ?? 128,
appLogMaxCount: verge?.app_log_max_count ?? 8,
autoCloseConnection: verge?.auto_close_connection ?? true,
autoCheckUpdate: verge?.auto_check_update ?? true,
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
proxyLayoutColumn: verge?.proxy_layout_column || 6,
enableAutoDelayDetection: verge?.enable_auto_delay_detection ?? false,
autoDelayDetectionIntervalMinutes:
verge?.auto_delay_detection_interval_minutes ?? 5,
defaultLatencyTest: verge?.default_latency_test || '',
autoLogClean: verge?.auto_log_clean || 0,
defaultLatencyTimeout: verge?.default_latency_timeout || 10000,
})
setValues(buildInitialValues(verge))
},
close: () => setOpen(false),
}))
+14 -21
View File
@@ -87,6 +87,17 @@ const splitBypass = (value?: string) =>
.map((item) => item.trim())
.filter(Boolean)
const buildInitialValues = (verge?: IVergeConfig | null) => ({
guard: verge?.enable_proxy_guard,
enable_bypass_check: verge?.enable_bypass_check ?? true,
bypass: verge?.system_proxy_bypass,
duration: verge?.proxy_guard_duration ?? 10,
use_default: verge?.use_default_bypass ?? true,
pac: verge?.proxy_auto_config,
pac_content: verge?.pac_file_content ?? DEFAULT_PAC,
proxy_host: verge?.proxy_host ?? '127.0.0.1',
})
export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation()
const systemName = getSystem()
@@ -116,16 +127,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
proxy_host,
} = verge ?? {}
const [value, setValue] = useState({
guard: enable_proxy_guard,
enable_bypass_check: enable_bypass_check ?? true,
bypass: system_proxy_bypass,
duration: proxy_guard_duration ?? 10,
use_default: use_default_bypass ?? true,
pac: proxy_auto_config,
pac_content: pac_file_content ?? DEFAULT_PAC,
proxy_host: proxy_host ?? '127.0.0.1',
})
const [value, setValue] = useState(() => buildInitialValues(null))
const [embeddedServerPort, setEmbeddedServerPort] = useState<number | null>(
null,
)
@@ -192,16 +194,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
useImperativeHandle(ref, () => ({
open: () => {
setOpen(true)
setValue({
guard: enable_proxy_guard,
enable_bypass_check: enable_bypass_check ?? true,
bypass: system_proxy_bypass,
duration: proxy_guard_duration ?? 10,
use_default: use_default_bypass ?? true,
pac: proxy_auto_config,
pac_content: pac_file_content ?? DEFAULT_PAC,
proxy_host: proxy_host ?? '127.0.0.1',
})
setValue(buildInitialValues(verge))
fetchNetworkInterfaces()
},
close: () => setOpen(false),
@@ -339,7 +332,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
Promise.resolve().then(async () => {
try {
if (Object.keys(patch).length > 0) {
mutateVerge({ ...verge, ...patch }, false)
mutateVerge({ ...verge, ...patch })
}
if (Object.keys(patch).length > 0) {
await patchVerge(patch)
+12 -18
View File
@@ -76,34 +76,28 @@ const proxyOptionToken = ({ memberIndex, member }: TunnelProxyOption) =>
member.kind === 'node' ? member.node.recordId : member.ref.name
}`
const buildInitialValues = (): TunnelFormValues => ({
localAddr: '',
localPort: '',
targetAddr: '',
targetPort: '',
network: 'tcp+udp',
group: '',
proxy: null,
})
export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
const { t } = useTranslation()
const { clash, mutateClash, patchClash } = useClash()
const [open, setOpen] = useState(false)
const [expanded, setExpanded] = useState(false)
const [values, setValues] = useState<TunnelFormValues>({
localAddr: '',
localPort: '',
targetAddr: '',
targetPort: '',
network: 'tcp+udp',
group: '',
proxy: null,
})
const [values, setValues] = useState<TunnelFormValues>(buildInitialValues)
const [draftTunnels, setDraftTunnels] = useState<TunnelEntry[]>([])
useImperativeHandle(ref, () => ({
open: () => {
setValues(() => ({
localAddr: '',
localPort: '',
targetAddr: '',
targetPort: '',
network: 'tcp+udp',
group: '',
proxy: null,
}))
setValues(buildInitialValues())
setDraftTunnels(() => clash?.tunnels ?? [])
setOpen(true)
// 如果没有隧道,则自动展开
@@ -50,21 +50,21 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
const handleAdd = useLockFn(async (value: string) => {
const newList = [...webUIList, value]
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false)
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old))
await patchVerge({ web_ui_list: newList })
})
const handleChange = useLockFn(async (index: number, value?: string) => {
const newList = [...webUIList]
newList[index] = value ?? ''
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false)
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old))
await patchVerge({ web_ui_list: newList })
})
const handleDelete = useLockFn(async (index: number) => {
const newList = [...webUIList]
newList.splice(index, 1)
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false)
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old))
await patchVerge({ web_ui_list: newList })
})

Some files were not shown because too many files have changed in this diff Show More