Compare commits

...
Author SHA1 Message Date
Tunglies 067a9f7cad fix(webdav): allow clearing saved configuration
Close #8495
2026-11-02 03:26:36 +08:00
Tunglies 07ee0a602a ci(release): unify RC and stable publication through dev 2026-11-02 00:46:06 +08:00
❤是纱雾酱哟~andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 693f2f7ac7 chore(deps): update npm dependencies (#8490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-01 13:08:26 +00:00
nrenvo 633f166e46 ci: trigger rust checks when their configuration changes (#8487) 2026-11-01 12:40:42 +00:00
Tunglies 261178b6d8 fix(updater): render release notes without remark-gfm
Remove GFM extensions that require unsupported regular expressions on older WebKit. Keep native Markdown links, existing release-note styling, and HTML sanitization.

Closes #8079
Closes #8082
2026-11-01 16:35:01 +08:00
Tunglies 0cae907de6 fix(config): wait for pending configuration writes
Serialize Verge patches behind the active configuration writer so toggling the system proxy after TUN does not fail immediately on lock contention.

CLose: #6613, #8481, #8357
2026-11-01 16:13:11 +08:00
nrenvo 94942b9105 fix(backup): publish scheduler settings with send_replace (#8386)
`watch::Sender::send` drops the value when there are no receivers at
startup, leaving the scheduler on the default disabled settings.
`send_replace` stores it for the runner that subscribes right after.
2026-11-01 07:43:42 +00:00
Tunglies 12f842d038 fix(macos): prevent menu bar flicker when closing the window
Keep the application activation policy unchanged when hiding the main window. Lightweight mode still hides the Dock icon.

Closes #8319
2026-11-01 15:33:26 +08:00
Tunglies 8e9832712a doc: clash-verge-rev with mihomo 1.19.33 2026-11-01 14:59:14 +08:00
Tunglies 719f63b57f chore: update tao and wry 2026-11-01 14:30:28 +08:00
heeeex100 ef784e459d docs: document complete development setup (#8457)
* docs: document complete development setup

* docs: follow review feedback on toolchain setup
2026-10-30 15:48:33 +00:00
❤是纱雾酱哟~andCopilot c236f5094e deps: upgrade updater to remove vulnerable zip (#8476)
* fix(deps): update updater to patched zip release

Upgrade tauri-plugin-updater to 2.14.1 and resolve the lockfile without vulnerable zip 4.6.1.

Assisted-by: Copilot SDK

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

* chore(deps): align updater package versions

Upgrade the PNPM updater plugin to match the Rust updater crate at 2.14.1.

Assisted-by: Copilot SDK

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

---------

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-30 15:04:34 +00:00
❤是纱雾酱哟~andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> bdea31a7ca chore(deps): update dependency pnpm to v12.16.1 (#8475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-30 15:04:11 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 44d50eb67d chore(deps): update rust crate zip to v9.1.3 [security] (#8464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-30 03:52:54 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> afd9d4b5a0 chore(deps): update rust crate zip to v9.1.2 [security] (#8461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-29 22:45:12 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 8bf20d199c chore(deps): update rust crate zip to v9.1.1 [security] (#8460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-29 17:26:16 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> e156ff09ae chore(deps): update rust crate zip to v9.1.0 [security] (#8458)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-29 13:39:08 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> df70146b02 chore(deps): update npm dependencies (#8453)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-29 10:37:26 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> de22706f3b chore(deps): update rust crate zip to v9.0.5 [security] (#8454)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-29 08:16:28 +00:00
UddooandTunglies 4b87634da9 fix(macos): restore Big Sur home rendering without Array.at (#8358)
* fix(macos): restore Big Sur home rendering without Array.at

Include the Array.prototype.at polyfill for the declared Safari 14 target so delay and traffic rendering can read their latest records.

Fixes clash-verge-rev/clash-verge-rev#8333

AI assistance: Codex generated the change and ran validation; the implementation model identifier and effort are not exposed. Independent review used GPT-6.1-Sol at medium effort.

* build(web): auto-detect modern polyfills instead of a manual list

modernPolyfills: true lets @vitejs/plugin-legacy derive the polyfill
set from the emitted bundle, dependencies included, so post-Safari-14
API coverage no longer depends on remembering to extend the list
(#8333, and the dependency-side gaps behind #1126 and #6582).

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2026-10-29 08:04:33 +00:00
❤是纱雾酱哟~andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> c9a6cf240d chore(deps): update node.js to v26 (#8450)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-29 04:21:49 +00:00
❤是纱雾酱哟~andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 8128227302 chore(deps): update dependency react-i18next to v17.1.9 (#8448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-28 17:33:10 +00: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
55 changed files with 1577 additions and 691 deletions
+2 -2
View File
@@ -4,8 +4,8 @@
"features": { "features": {
"ghcr.io/devcontainers/features/node:2": { "ghcr.io/devcontainers/features/node:2": {
"version": "24.22.1", "version": "26.13.0",
"pnpmVersion": "12.15.0" "pnpmVersion": "12.16.1"
}, },
"ghcr.io/devcontainers/features/rust:1": { "ghcr.io/devcontainers/features/rust:1": {
"version": "1.99.1", "version": "1.99.1",
+5 -5
View File
@@ -51,7 +51,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -185,7 +185,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
cache: 'pnpm' cache: 'pnpm'
- name: Pnpm Cache - name: Pnpm Cache
@@ -282,7 +282,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
cache: 'pnpm' cache: 'pnpm'
- name: Pnpm Cache - name: Pnpm Cache
@@ -426,7 +426,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
cache: 'pnpm' cache: 'pnpm'
- name: Pnpm Cache - name: Pnpm Cache
@@ -524,7 +524,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
- uses: pnpm/action-setup@v6.1.0 - uses: pnpm/action-setup@v6.1.0
name: Install pnpm name: Install pnpm
+80
View File
@@ -0,0 +1,80 @@
name: Contract Parity
on:
pull_request:
paths:
- "src-tauri/**/*.rs"
- "crates/**/*.rs"
- "Cargo.toml"
- "Cargo.lock"
- "src-tauri/Cargo.toml"
- "crates/**/Cargo.toml"
- "rust-toolchain.toml"
- "src-tauri/tauri.conf*.json"
- "src/services/bindings.ts"
- "scripts/gen-contract.mjs"
- "scripts/check-contract-changes.mjs"
- "biome.json"
- "biome.jsonc"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "scripts/contract-inputs.json"
- ".github/workflows/contract-parity.yml"
- ".husky/pre-push"
workflow_dispatch:
permissions:
contents: read
env:
HUSKY: 0
jobs:
contract-parity:
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@v7
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
prefix-key: 'v1-rust'
key: 'rust-shared-stable-ubuntu-22.04-x86_64-unknown-linux-gnu-contract'
workspaces: |
. -> target
cache-all-crates: true
cache-workspace-crates: true
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install pnpm
uses: pnpm/action-setup@v6
with:
run_install: false
- uses: actions/setup-node@v7
with:
node-version: '26.13.0'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Provision sidecar placeholders for tauri-build
run: |
mkdir -p src-tauri/sidecar src-tauri/resources dist
touch dist/index.html
for bin in clash-verge-service clash-verge-service-install clash-verge-service-uninstall verge-mihomo verge-mihomo-alpha; do
touch "src-tauri/sidecar/${bin}-x86_64-unknown-linux-gnu"
done
- name: Check generated contract without changing the checkout
run: pnpm gen:contract --check
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
- uses: pnpm/action-setup@v6 - uses: pnpm/action-setup@v6
name: Install pnpm name: Install pnpm
+1 -1
View File
@@ -103,7 +103,7 @@ jobs:
if: github.event.inputs[matrix.input] == 'true' if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
cache: 'pnpm' cache: 'pnpm'
- name: Pnpm Cache - name: Pnpm Cache
+14 -104
View File
@@ -2,11 +2,23 @@ name: Frontend Check
on: on:
pull_request: pull_request:
paths:
- "src/**"
- "**/*.js"
- "**/*.ts"
- "**/*.tsx"
- "**/*.css"
- "**/*.scss"
- "**/*.json"
- "**/*.md"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "vite.config.*"
- ".github/workflows/frontend-check.yml"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
contents: read contents: read
pull-requests: read
env: env:
HUSKY: 0 HUSKY: 0
@@ -17,46 +29,17 @@ jobs:
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
- name: Check frontend changes
id: check_frontend
uses: dorny/paths-filter@v4
with:
filters: |
frontend:
- 'src/**'
- '**/*.js'
- '**/*.ts'
- '**/*.tsx'
- '**/*.css'
- '**/*.scss'
- '**/*.json'
- '**/*.md'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'knip.json'
- 'eslint.config.ts'
- 'tsconfig.json'
- 'vite.config.*'
- name: Skip if no frontend changes
if: steps.check_frontend.outputs.frontend != 'true'
run: echo "No frontend changes, skipping frontend checks."
- name: Install pnpm - name: Install pnpm
if: steps.check_frontend.outputs.frontend == 'true'
uses: pnpm/action-setup@v6 uses: pnpm/action-setup@v6
with: with:
run_install: false run_install: false
- uses: actions/setup-node@v7 - uses: actions/setup-node@v7
if: steps.check_frontend.outputs.frontend == 'true'
with: with:
node-version: '24.22.1' node-version: '26.13.0'
cache: 'pnpm' cache: 'pnpm'
- name: Restore pnpm cache - name: Restore pnpm cache
if: steps.check_frontend.outputs.frontend == 'true'
uses: actions/cache@v6 uses: actions/cache@v6
with: with:
path: ~/.pnpm-store path: ~/.pnpm-store
@@ -65,94 +48,21 @@ jobs:
pnpm-shared-stable-${{ runner.os }}- pnpm-shared-stable-${{ runner.os }}-
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
if: steps.check_frontend.outputs.frontend == 'true'
- name: Run Prettier check - name: Run Prettier check
if: steps.check_frontend.outputs.frontend == 'true'
run: pnpm format:check run: pnpm format:check
- name: Run ESLint - name: Run ESLint
if: steps.check_frontend.outputs.frontend == 'true'
run: pnpm lint run: pnpm lint
- name: Run TypeScript typecheck - name: Run TypeScript typecheck
if: steps.check_frontend.outputs.frontend == 'true'
run: pnpm typecheck run: pnpm typecheck
- name: Run tests - name: Run tests
if: steps.check_frontend.outputs.frontend == 'true'
run: pnpm test run: pnpm test
- name: Run Knip - name: Run Knip
if: steps.check_frontend.outputs.frontend == 'true'
run: pnpm knip:check run: pnpm knip:check
- name: Architecture gates (listen/emit only in the event bus, invoke only in services) - name: Architecture gates (listen/emit only in the event bus, invoke only in services)
if: steps.check_frontend.outputs.frontend == 'true'
run: node scripts/check-architecture.mjs listen emit invoke run: node scripts/check-architecture.mjs listen emit invoke
contract-parity:
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@v7
- name: Check contract inputs
id: check_inputs
uses: dorny/paths-filter@v4
with:
filters: scripts/contract-inputs.json
- name: Skip if contract inputs unchanged
if: steps.check_inputs.outputs.contract != 'true'
run: echo "No contract inputs changed, skipping parity check."
- name: Install Rust Stable
if: steps.check_inputs.outputs.contract == 'true'
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Rust Cache
if: steps.check_inputs.outputs.contract == 'true'
uses: Swatinem/rust-cache@v2
with:
prefix-key: 'v1-rust'
key: 'rust-shared-stable-ubuntu-22.04-x86_64-unknown-linux-gnu-contract'
workspaces: |
. -> target
cache-all-crates: true
cache-workspace-crates: true
- name: Install dependencies
if: steps.check_inputs.outputs.contract == 'true'
run: |
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install pnpm
if: steps.check_inputs.outputs.contract == 'true'
uses: pnpm/action-setup@v6
with:
run_install: false
- uses: actions/setup-node@v7
if: steps.check_inputs.outputs.contract == 'true'
with:
node-version: '24.22.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
+9 -23
View File
@@ -2,6 +2,14 @@ name: Clippy Lint
on: on:
pull_request: pull_request:
paths:
- "src-tauri/**"
- "crates/**"
- "Cargo.toml"
- "Cargo.lock"
- "rust-toolchain.toml"
- ".cargo/config.toml"
- ".github/workflows/lint-clippy.yml"
workflow_dispatch: workflow_dispatch:
env: env:
HUSKY: 0 HUSKY: 0
@@ -23,28 +31,6 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Check src-tauri changes
if: github.event_name != 'workflow_dispatch'
id: check_changes
uses: dorny/paths-filter@v4
with:
filters: |
rust:
- 'src-tauri/**'
- name: Skip if src-tauri not changed
if: github.event_name != 'workflow_dispatch' && steps.check_changes.outputs.rust != 'true'
run: echo "No src-tauri changes, skipping clippy lint."
- name: Continue if src-tauri changed
if: github.event_name != 'workflow_dispatch' && steps.check_changes.outputs.rust == 'true'
run: echo "src-tauri changed, running clippy lint."
- name: Manual trigger - always run
if: github.event_name == 'workflow_dispatch'
run: |
echo "Manual trigger detected: skipping changes check and running clippy."
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v7 uses: actions/checkout@v7
@@ -79,7 +65,7 @@ jobs:
run: cargo clippy-all run: cargo clippy-all
- name: Run Windows security tests - name: Run Windows security tests
if: matrix.os == 'windows-latest' && (github.event_name == 'workflow_dispatch' || steps.check_changes.outputs.rust == 'true') if: matrix.os == 'windows-latest'
working-directory: ./src-tauri working-directory: ./src-tauri
run: cargo test --all-features --target ${{ matrix.target }} run: cargo test --all-features --target ${{ matrix.target }}
+79
View File
@@ -0,0 +1,79 @@
name: Prepare Release
on:
pull_request_target:
types: [edited, closed]
branches: [dev]
workflow_dispatch:
inputs:
channel:
description: Release channel
required: true
type: choice
options: [rc, stable]
default: rc
release_tag:
description: 'Optional tag: vX.Y.Z-rc[.N] for RC, vX.Y.Z for stable'
required: false
type: string
permissions:
contents: write
pull-requests: write
concurrency:
group: prepare-release
cancel-in-progress: false
jobs:
prepare:
if: >-
${{ github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.login == 'github-actions[bot]' &&
((startsWith(github.event.pull_request.head.ref, 'release/v') &&
((github.event.action == 'closed' && github.event.pull_request.merged == true) ||
(github.event.changes.body && github.event.pull_request.state == 'open' &&
contains(github.event.pull_request.body, '[x] Refresh this PR from latest dev')))) ||
(startsWith(github.event.pull_request.head.ref, 'next-dev/v') &&
github.event.changes.body && github.event.pull_request.state == 'open' &&
(contains(github.event.pull_request.body, '[x] Refresh this PR from latest dev') ||
contains(github.event.pull_request.body, '[x] Use the next minor version') !=
contains(github.event.changes.body.from, '[x] Use the next minor version'))))) }}
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Require the dev branch
if: github.event_name == 'workflow_dispatch'
env:
SOURCE_REF: ${{ github.ref }}
run: |
if [[ "$SOURCE_REF" != 'refs/heads/dev' ]]; then
echo '::error::Run Prepare Release from the dev branch.'
exit 1
fi
- uses: actions/checkout@v7
with:
ref: refs/heads/dev
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: '26.13.0'
- uses: pnpm/action-setup@v6
if: github.event.action != 'closed'
with:
run_install: false
- run: pnpm install --frozen-lockfile --ignore-scripts
if: github.event.action != 'closed'
- name: Create or refresh version or publication PR
env:
RELEASE_TAG: ${{ inputs.release_tag }}
RELEASE_CHANNEL: ${{ inputs.channel }}
RELEASE_COMMAND: ${{ github.event.action == 'closed' && 'publish-pr' || startsWith(github.event.pull_request.head.ref, 'next-dev/') && 'next-dev' || 'prepare' }}
run: node scripts-workflow/release.mjs "$RELEASE_COMMAND"
+123 -76
View File
@@ -1,6 +1,9 @@
name: Release Build name: Release Build
on: on:
pull_request:
types: [edited]
branches: [main]
push: push:
# -rc tag 时预览发布, 跳过 telegram 通知、跳过 winget 提交、跳过 latest.json 文件更新 # -rc tag 时预览发布, 跳过 telegram 通知、跳过 winget 提交、跳过 latest.json 文件更新
tags: tags:
@@ -11,75 +14,59 @@ env:
RUST_BACKTRACE: short RUST_BACKTRACE: short
HUSKY: 0 HUSKY: 0
concurrency: concurrency:
group: '${{ github.workflow }} - ${{ github.head_ref || github.ref }}' group: release-publication
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} cancel-in-progress: false
jobs: jobs:
check_tag_version: prepare_release:
name: Check Release Tag and package.json Version Consistency name: Approve Snapshot and Prepare Draft Release
if: >-
${{ github.event_name == 'push' ||
(github.event.changes.body &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.login == 'github-actions[bot]' &&
startsWith(github.event.pull_request.head.ref, 'publish/v') &&
contains(github.event.pull_request.body, '[x] Publish this approved release')) }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
outputs:
release_tag: ${{ steps.source.outputs.release_tag }}
source_sha: ${{ steps.source.outputs.source_sha }}
channel: ${{ steps.source.outputs.channel }}
release_id: ${{ steps.draft.outputs.id }}
steps: steps:
- name: Checkout repository - name: Checkout trusted release tooling
uses: actions/checkout@v7 uses: actions/checkout@v7
with: with:
ref: refs/heads/dev
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-node@v7
- name: Check if tag is from main branch
run: |
TAG_REF="${GITHUB_REF##*/}"
echo "Checking if tag $TAG_REF is from main branch..."
TAG_COMMIT=$(git rev-list -n 1 $TAG_REF)
MAIN_COMMITS=$(git rev-list origin/main)
if echo "$MAIN_COMMITS" | grep -q "$TAG_COMMIT"; then
echo "✅ Tag $TAG_REF is from main branch"
else
echo "❌ Tag $TAG_REF is not from main branch"
echo "This release workflow only accepts tags from main branch."
exit 1
fi
- name: Check tag and package.json version
run: |
TAG_REF="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}"
echo "Current tag: $TAG_REF"
PKG_VERSION=$(jq -r .version package.json)
echo "package.json version: $PKG_VERSION"
EXPECTED_TAG="v$PKG_VERSION"
if [[ "$TAG_REF" != "$EXPECTED_TAG" ]]; then
echo "❌ Version mismatch:"
echo " Git tag : $TAG_REF"
echo " package.json : $EXPECTED_TAG"
exit 1
fi
echo "✅ Tag and package.json version are consistent."
prepare_release:
name: Prepare Draft Release
runs-on: ubuntu-latest
needs: [check_tag_version]
outputs:
release_id: ${{ steps.prepare_release.outputs.id }}
steps:
- name: Create or update draft release
id: prepare_release
uses: softprops/action-gh-release@v3
with: with:
tag_name: ${{ github.ref_name }} node-version: '26.13.0'
target_commitish: ${{ github.sha }} - name: Validate source and publish the approved snapshot
name: 'Clash Verge Rev ${{ github.ref_name }}' id: source
body: 'Draft release, will be updated later.' env:
token: ${{ secrets.GITHUB_TOKEN }} RELEASE_COMMAND: ${{ github.event_name == 'push' && 'validate-tag' || 'publish' }}
draft: true run: node scripts-workflow/release.mjs "$RELEASE_COMMAND"
prerelease: ${{ contains(github.ref_name, '-rc') }}
- name: Create or reuse draft release
id: draft
env:
RELEASE_TAG: ${{ steps.source.outputs.release_tag }}
SOURCE_SHA: ${{ steps.source.outputs.source_sha }}
run: node scripts-workflow/release.mjs prepare-draft
update_tag: update_tag:
name: Update tag name: Publish Completed Release
env:
RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }}
outputs:
source_sha: ${{ needs.prepare_release.outputs.source_sha }}
channel: ${{ needs.prepare_release.outputs.channel }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
[ [
@@ -91,6 +78,8 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
with:
ref: ${{ needs.prepare_release.outputs.source_sha }}
- name: Group changelog platforms - name: Group changelog platforms
run: bash ./scripts-workflow/group_platforms.sh run: bash ./scripts-workflow/group_platforms.sh
@@ -103,7 +92,7 @@ jobs:
- name: Set Env - name: Set Env
run: | run: |
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
TAG_REF="${GITHUB_REF##*/}" TAG_REF="$RELEASE_TAG"
echo "TAG_NAME=$TAG_REF" >> $GITHUB_ENV echo "TAG_NAME=$TAG_REF" >> $GITHUB_ENV
VERSION=$(echo "$TAG_REF" | sed 's/^v//') VERSION=$(echo "$TAG_REF" | sed 's/^v//')
echo "VERSION=$VERSION" >> $GITHUB_ENV echo "VERSION=$VERSION" >> $GITHUB_ENV
@@ -149,10 +138,16 @@ jobs:
Created at ${{ env.BUILDTIME }}. Created at ${{ env.BUILDTIME }}.
EOF EOF
- name: Verify release tag still points to the approved source
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SOURCE_SHA: ${{ needs.prepare_release.outputs.source_sha }}
run: node scripts-workflow/release.mjs verify-target
- name: Publish Release - name: Publish Release
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PRERELEASE: ${{ contains(github.ref_name, '-rc') }} PRERELEASE: ${{ contains(env.RELEASE_TAG, '-rc') }}
run: | run: |
release_id="${{ needs.prepare_release.outputs.release_id }}" release_id="${{ needs.prepare_release.outputs.release_id }}"
@@ -181,6 +176,8 @@ jobs:
release: release:
name: Release Build name: Release Build
needs: [prepare_release] needs: [prepare_release]
env:
RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -200,6 +197,8 @@ jobs:
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v7 uses: actions/checkout@v7
with:
ref: ${{ needs.prepare_release.outputs.source_sha }}
- name: Install Rust Stable - name: Install Rust Stable
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
@@ -230,7 +229,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
- uses: pnpm/action-setup@v6 - uses: pnpm/action-setup@v6
name: Install pnpm name: Install pnpm
@@ -249,7 +248,6 @@ jobs:
echo "Rust target ${{ matrix.target }} installed." echo "Rust target ${{ matrix.target }} installed."
- name: Tauri build - name: Tauri build
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
uses: tauri-apps/tauri-action@v1.0.0 uses: tauri-apps/tauri-action@v1.0.0
env: env:
NODE_OPTIONS: '--max_old_space_size=4096' NODE_OPTIONS: '--max_old_space_size=4096'
@@ -264,11 +262,14 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with: with:
releaseId: ${{ needs.prepare_release.outputs.release_id }} releaseId: ${{ needs.prepare_release.outputs.release_id }}
tagName: ${{ github.ref_name }} tagName: ${{ env.RELEASE_TAG }}
releaseBody: 'Draft release, will be updated later.' releaseBody: 'Draft release, will be updated later.'
releaseDraft: true
prerelease: ${{ needs.prepare_release.outputs.channel == 'rc' }}
releaseCommitish: ${{ needs.prepare_release.outputs.source_sha }}
tauriScript: pnpm tauriScript: pnpm
args: --target ${{ matrix.target }} args: --target ${{ matrix.target }}
includeUpdaterJson: true uploadUpdaterJson: ${{ needs.prepare_release.outputs.channel == 'stable' }}
- name: Attest Windows bundles - name: Attest Windows bundles
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
@@ -293,6 +294,8 @@ jobs:
release-for-linux-arm: release-for-linux-arm:
name: Release Build for Linux ARM name: Release Build for Linux ARM
needs: [prepare_release] needs: [prepare_release]
env:
RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -307,6 +310,8 @@ jobs:
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v7 uses: actions/checkout@v7
with:
ref: ${{ needs.prepare_release.outputs.source_sha }}
- name: Install Rust Stable - name: Install Rust Stable
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
@@ -331,7 +336,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v6 uses: pnpm/action-setup@v6
@@ -420,7 +425,7 @@ jobs:
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
gh release upload "${{ github.ref_name }}" \ gh release upload "$RELEASE_TAG" \
target/${{ matrix.target }}/release/bundle/deb/*.deb \ target/${{ matrix.target }}/release/bundle/deb/*.deb \
target/${{ matrix.target }}/release/bundle/rpm/*.rpm \ target/${{ matrix.target }}/release/bundle/rpm/*.rpm \
--clobber --clobber
@@ -428,6 +433,8 @@ jobs:
release-for-fixed-webview2: release-for-fixed-webview2:
name: Release Build for Fixed WebView2 name: Release Build for Fixed WebView2
needs: [prepare_release] needs: [prepare_release]
env:
RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -442,6 +449,8 @@ jobs:
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v7 uses: actions/checkout@v7
with:
ref: ${{ needs.prepare_release.outputs.source_sha }}
- name: Install Rust Stable - name: Install Rust Stable
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
@@ -466,7 +475,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
- uses: pnpm/action-setup@v6 - uses: pnpm/action-setup@v6
name: Install pnpm name: Install pnpm
@@ -532,22 +541,55 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
$files = Get-ChildItem "target\${{ matrix.target }}\release\bundle\nsis\*setup*" | Select-Object -ExpandProperty FullName $files = Get-ChildItem "target\${{ matrix.target }}\release\bundle\nsis\*setup*" | Select-Object -ExpandProperty FullName
gh release upload "${{ github.ref_name }}" $files --clobber gh release upload "$env:RELEASE_TAG" $files --clobber
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
prepare-next-dev:
name: Prepare Next Dev Version
if: ${{ needs.update_tag.outputs.channel == 'stable' }}
needs: [prepare_release, update_tag]
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
concurrency:
group: prepare-release
cancel-in-progress: false
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }}
SOURCE_SHA: ${{ needs.prepare_release.outputs.source_sha }}
steps:
- uses: actions/checkout@v7
with:
ref: refs/heads/dev
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: '26.13.0'
- uses: pnpm/action-setup@v6
with:
run_install: false
- name: Prepare version and changelog PR
run: |
pnpm install --frozen-lockfile --ignore-scripts
node scripts-workflow/release.mjs next-dev
release-update: release-update:
if: ${{ !contains(github.ref_name, '-rc') }} if: ${{ needs.update_tag.outputs.channel == 'stable' }}
name: Release Update name: Release Update
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [update_tag] needs: [update_tag]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
with:
ref: ${{ needs.update_tag.outputs.source_sha }}
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
- uses: pnpm/action-setup@v6 - uses: pnpm/action-setup@v6
name: Install pnpm name: Install pnpm
@@ -563,17 +605,19 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-update-for-fixed-webview2: release-update-for-fixed-webview2:
if: ${{ !contains(github.ref_name, '-rc') }} if: ${{ needs.update_tag.outputs.channel == 'stable' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [update_tag] needs: [update_tag]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
with:
ref: ${{ needs.update_tag.outputs.source_sha }}
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
- uses: pnpm/action-setup@v6 - uses: pnpm/action-setup@v6
name: Install pnpm name: Install pnpm
@@ -589,7 +633,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
submit-to-winget: submit-to-winget:
if: ${{ !contains(github.ref_name, '-rc') }} if: ${{ needs.update_tag.outputs.channel == 'stable' }}
name: Submit to Winget name: Submit to Winget
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [update_tag, release-update] needs: [update_tag, release-update]
@@ -597,6 +641,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
with: with:
ref: ${{ needs.update_tag.outputs.source_sha }}
fetch-depth: 0 fetch-depth: 0
- name: Get Version - name: Get Version
run: | run: |
@@ -613,7 +658,7 @@ jobs:
token: ${{ secrets.WINGET_TOKEN }} token: ${{ secrets.WINGET_TOKEN }}
notify-telegram: notify-telegram:
if: ${{ !contains(github.ref_name, '-rc') }} if: ${{ needs.update_tag.outputs.channel == 'stable' }}
name: Notify Telegram name: Notify Telegram
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
@@ -626,6 +671,8 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
with:
ref: ${{ needs.update_tag.outputs.source_sha }}
- name: Group changelog platforms - name: Group changelog platforms
run: bash ./scripts-workflow/group_platforms.sh run: bash ./scripts-workflow/group_platforms.sh
@@ -638,7 +685,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
- uses: pnpm/action-setup@v6 - uses: pnpm/action-setup@v6
name: Install pnpm name: Install pnpm
+6 -15
View File
@@ -6,6 +6,12 @@ name: Check Formatting
on: on:
pull_request: pull_request:
paths:
- "src-tauri/**"
- "**/*.rs"
- "rustfmt.toml"
- "rust-toolchain.toml"
- ".github/workflows/rustfmt.yml"
permissions: permissions:
contents: read contents: read
@@ -19,25 +25,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
- name: Check Rust changes
id: check_rust
uses: dorny/paths-filter@v4
with:
filters: |
rust:
- 'src-tauri/**'
- '**/*.rs'
- name: Skip if no Rust changes
if: steps.check_rust.outputs.rust != 'true'
run: echo "No Rust changes, skipping rustfmt."
- name: install Rust stable and rustfmt - name: install Rust stable and rustfmt
if: steps.check_rust.outputs.rust == 'true'
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
components: rustfmt components: rustfmt
- name: run cargo fmt - name: run cargo fmt
if: steps.check_rust.outputs.rust == 'true'
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v6 uses: pnpm/action-setup@v6
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
- uses: pnpm/action-setup@v6 - uses: pnpm/action-setup@v6
name: Install pnpm name: Install pnpm
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
- uses: pnpm/action-setup@v6 - uses: pnpm/action-setup@v6
name: Install pnpm name: Install pnpm
@@ -39,7 +39,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
node-version: '24.22.1' node-version: '26.13.0'
- uses: pnpm/action-setup@v6 - uses: pnpm/action-setup@v6
name: Install pnpm name: Install pnpm
+2 -2
View File
@@ -1,3 +1,3 @@
nodejs 24.22.1 nodejs 26.13.0
pnpm 12.15.0 pnpm 12.16.1
rust 1.99.1 rust 1.99.1
+19 -6
View File
@@ -28,7 +28,7 @@ Before contributing, you need to set up your development environment. Follow the
### Prerequisites ### Prerequisites
1. **Install Rust and Node.js** 1. **Install Rust and Node.js**
Our project requires both Rust and Node.js. Follow the official installation instructions [here](https://tauri.app/start/prerequisites/). Follow the [Tauri prerequisites](https://tauri.app/start/prerequisites/) for your operating system. Use the Node.js version in [`.tool-versions`](.tool-versions); [`rust-toolchain.toml`](rust-toolchain.toml) selects the Rust version and required components when you run Cargo from this repository.
### Windows Users ### Windows Users
@@ -51,18 +51,21 @@ rustup set default-host x86_64-pc-windows-msvc
### Install Node.js Package Manager ### Install Node.js Package Manager
Enable `corepack`: [Corepack is no longer bundled with Node.js starting with Node.js 25](https://nodejs.org/api/corepack.html). Install it globally if it is not already available, then enable it:
```bash ```bash
npm install --global corepack
corepack enable corepack enable
``` ```
Run pnpm from this repository so Corepack selects the version pinned by `packageManager` in [`package.json`](package.json).
### Install Project Dependencies ### Install Project Dependencies
Node.js dependencies: Node.js dependencies:
```bash ```bash
pnpm install pnpm install --frozen-lockfile
``` ```
Ubuntu-only system packages: Ubuntu-only system packages:
@@ -78,14 +81,24 @@ pnpm run prebuild
pnpm run prebuild --force # Re-download and overwrite Mihomo core and service binaries pnpm run prebuild --force # Re-download and overwrite Mihomo core and service binaries
``` ```
### Prepare the Development Service Source
`pnpm dev` builds the development service from a sibling checkout of [`clash-verge-service-ipc`](https://github.com/clash-verge-rev/clash-verge-service-ipc). The binaries downloaded by `pnpm prebuild` do not replace this source checkout.
Check the `clash_verge_service_ipc` dependency in [`src-tauri/Cargo.toml`](src-tauri/Cargo.toml), then clone the service repository alongside `clash-verge-rev` and check out the matching tag. If the sibling directory already exists, reuse it and check its version rather than overwriting it. Without this checkout, `pnpm dev` fails when it tries to access `../clash-verge-service-ipc/Cargo.toml`.
### Run the Development Server ### Run the Development Server
Run these commands from the root of `clash-verge-rev`:
```bash ```bash
pnpm dev # Standard pnpm dev # Build the development service and start the desktop app
pnpm dev:diff # If an app instance already exists pnpm dev:diff # Alias of pnpm dev
pnpm dev:tauri # Run Tauri development mode pnpm dev:tauri # Run Tauri directly with the tauri-dev feature
``` ```
For frontend-only development, use `pnpm web:dev`. It does not start the Rust backend or build the development service.
### Build the Project ### Build the Project
Standard build: Standard build:
Generated
+38 -69
View File
@@ -33,6 +33,7 @@ dependencies = [
"cipher", "cipher",
"cpubits", "cpubits",
"cpufeatures 0.3.0", "cpufeatures 0.3.0",
"zeroize",
] ]
[[package]] [[package]]
@@ -135,15 +136,6 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]] [[package]]
name = "arboard" name = "arboard"
version = "3.6.1" version = "3.6.1"
@@ -1175,7 +1167,7 @@ dependencies = [
"windows-service", "windows-service",
"windows-sys 0.61.2", "windows-sys 0.61.2",
"winreg 0.56.0", "winreg 0.56.0",
"zip 9.0.4", "zip",
] ]
[[package]] [[package]]
@@ -1342,7 +1334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -1870,17 +1862,6 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "2.1.1" version = "2.1.1"
@@ -2034,7 +2015,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users 0.5.2", "redox_users 0.5.2",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -2293,7 +2274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -2809,7 +2790,7 @@ dependencies = [
"gobject-sys 0.21.5", "gobject-sys 0.21.5",
"libc", "libc",
"system-deps 7.0.8", "system-deps 7.0.8",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -3363,7 +3344,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"wasm-bindgen", "wasm-bindgen",
"windows-core 0.62.2", "windows-core 0.61.2",
] ]
[[package]] [[package]]
@@ -4051,9 +4032,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "lzma-rust2" name = "lzma-rust2"
version = "0.22.0" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7277c9742229dc25243236d578b6471a798c554512c6f6a04365ec2e87c2a3bb" checksum = "f1c46f3fcd2e888afdb01a07efc071cb21300e07c68d32c3e8b01b5c8cb1fcaa"
dependencies = [ dependencies = [
"sha2 0.11.1", "sha2 0.11.1",
] ]
@@ -4234,7 +4215,7 @@ dependencies = [
"png 0.18.1", "png 0.18.1",
"serde", "serde",
"thiserror 2.0.20", "thiserror 2.0.20",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -4401,7 +4382,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -4878,7 +4859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -5543,7 +5524,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -6047,7 +6028,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.4.15", "linux-raw-sys 0.4.15",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -6060,7 +6041,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.12.1", "linux-raw-sys 0.12.1",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -6119,7 +6100,7 @@ dependencies = [
"security-framework", "security-framework",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs", "webpki-root-certs",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -6739,7 +6720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -7088,9 +7069,9 @@ checksum = "c0e973b34477b7823833469eb0f5a3a60370fef7a453e02d751b59180d0a5a05"
[[package]] [[package]]
name = "tao" name = "tao"
version = "0.37.1" version = "0.37.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400" checksum = "d5777b06fe6435126ae8d816eba269e050e1e10d302cf5461e9c49eccabc3548"
dependencies = [ dependencies = [
"bitflags 2.13.2", "bitflags 2.13.2",
"block2", "block2",
@@ -7575,11 +7556,11 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-updater" name = "tauri-plugin-updater"
version = "2.14.0" version = "2.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7259989d4fe11b2bf51f7d671642249b1837c4ae2da1430dc713ae38f98ceeec" checksum = "d957abbcb3f9f6c76d22a3a0f012f30880640d0bccb5634d2b12a9937e4c872b"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.23.1",
"dirs 7.0.0", "dirs 7.0.0",
"flate2", "flate2",
"futures-util", "futures-util",
@@ -7604,7 +7585,7 @@ dependencies = [
"tokio", "tokio",
"url", "url",
"windows-sys 0.61.2", "windows-sys 0.61.2",
"zip 4.6.1", "zip",
] ]
[[package]] [[package]]
@@ -7805,10 +7786,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.4.3", "getrandom 0.3.4",
"once_cell", "once_cell",
"rustix 1.1.4", "rustix 1.1.4",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -8474,7 +8455,7 @@ dependencies = [
"png 0.18.1", "png 0.18.1",
"serde", "serde",
"thiserror 2.0.20", "thiserror 2.0.20",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -8553,9 +8534,9 @@ dependencies = [
[[package]] [[package]]
name = "typed-path" name = "typed-path"
version = "0.12.3" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e"
[[package]] [[package]]
name = "typeid" name = "typeid"
@@ -8583,7 +8564,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [ dependencies = [
"memoffset", "memoffset",
"tempfile", "tempfile",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -9125,7 +9106,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -9146,7 +9127,7 @@ dependencies = [
"objc2-foundation", "objc2-foundation",
"objc2-quartz-core", "objc2-quartz-core",
"raw-window-handle", "raw-window-handle",
"windows-sys 0.61.2", "windows-sys 0.60.2",
"windows-version", "windows-version",
] ]
@@ -9721,7 +9702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -9762,11 +9743,11 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]] [[package]]
name = "wry" name = "wry"
version = "0.57.0" version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0" checksum = "759c50ddad0ef9913bb5496cb9d509a50433a3b78cb0238d61105526589cf0e9"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.23.1",
"block2", "block2",
"cookie", "cookie",
"crossbeam-channel", "crossbeam-channel",
@@ -9787,7 +9768,6 @@ dependencies = [
"objc2-foundation", "objc2-foundation",
"objc2-ui-kit", "objc2-ui-kit",
"objc2-web-kit", "objc2-web-kit",
"once_cell",
"percent-encoding", "percent-encoding",
"raw-window-handle", "raw-window-handle",
"sha2 0.10.9", "sha2 0.10.9",
@@ -10054,21 +10034,9 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "4.6.1" version = "9.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.14.0",
"memchr",
]
[[package]]
name = "zip"
version = "9.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a954114b47a54df49f83d2722814e86b23d4bb1b54a1b90fb7d92b41619ddc22"
dependencies = [ dependencies = [
"aes", "aes",
"bzip2", "bzip2",
@@ -10087,6 +10055,7 @@ dependencies = [
"sha1 0.11.0", "sha1 0.11.0",
"time", "time",
"typed-path", "typed-path",
"windows-sys 0.61.2",
"zeroize", "zeroize",
"zopfli", "zopfli",
"zstd", "zstd",
+13
View File
@@ -1,12 +1,23 @@
## v2.5.9 ## v2.5.9
- **Mihomo(Meta) 内核升级至 v1.19.33**
<details> <details>
<summary><strong> 🐞 修复问题 </strong></summary> <summary><strong> 🐞 修复问题 </strong></summary>
- 修复规则扩展包含空列表项时,「编辑规则」界面报错的问题
- 修复 重启应用后定时自动备份不执行的问题
- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口 - 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置 - 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
- 修复 首页 网站测速失败后卡片一直显示测试中 - 修复 首页 网站测速失败后卡片一直显示测试中
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新 - 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
- 修复 代理 在全局模式下无法展开节点组
- 修复 自定义 CSS 无法修改编辑器字体和字号的问题
- 修复 WebDAV 设置清空后无法保存
**🍎 macOS**
- macOS:修复 Bigsur 上首页测速和流量图无法显示的问题
**🐧 Linux** **🐧 Linux**
@@ -18,6 +29,8 @@
<summary><strong> ✨ 新增功能 </strong></summary> <summary><strong> ✨ 新增功能 </strong></summary>
- 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为 - 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为
- 新增「IP 信息」自动刷新开关,关闭后仍可手动刷新
- 新增 Muse AI 解锁检测,针对 Muse AI 特有的 Cloudflare 人机验证增加「Risky」状态标记
</details> </details>
@@ -11,6 +11,7 @@ mod chatgpt;
mod claude; mod claude;
mod disney_plus; mod disney_plus;
mod gemini; mod gemini;
mod muse;
mod netflix; mod netflix;
mod prime_video; mod prime_video;
mod spotify; mod spotify;
@@ -30,6 +31,7 @@ enum UnlockCheck {
ChatGptWeb, ChatGptWeb,
Claude, Claude,
Gemini, Gemini,
MuseAi,
YouTubePremium, YouTubePremium,
BahamutAnime, BahamutAnime,
Netflix, Netflix,
@@ -46,6 +48,7 @@ impl UnlockCheck {
Self::ChatGptWeb, Self::ChatGptWeb,
Self::Claude, Self::Claude,
Self::Gemini, Self::Gemini,
Self::MuseAi,
Self::YouTubePremium, Self::YouTubePremium,
Self::BahamutAnime, Self::BahamutAnime,
Self::Netflix, Self::Netflix,
@@ -66,6 +69,7 @@ impl UnlockCheck {
Self::ChatGptWeb => chatgpt::CHATGPT_WEB_NAME, Self::ChatGptWeb => chatgpt::CHATGPT_WEB_NAME,
Self::Claude => claude::CLAUDE_NAME, Self::Claude => claude::CLAUDE_NAME,
Self::Gemini => gemini::GEMINI_NAME, Self::Gemini => gemini::GEMINI_NAME,
Self::MuseAi => muse::MUSE_AI_NAME,
Self::YouTubePremium => youtube::YOUTUBE_PREMIUM_NAME, Self::YouTubePremium => youtube::YOUTUBE_PREMIUM_NAME,
Self::BahamutAnime => bahamut::BAHAMUT_ANIME_NAME, Self::BahamutAnime => bahamut::BAHAMUT_ANIME_NAME,
Self::Netflix => netflix::NETFLIX_NAME, Self::Netflix => netflix::NETFLIX_NAME,
@@ -83,6 +87,7 @@ impl UnlockCheck {
Self::ChatGptWeb => chatgpt::check_chatgpt(client).await, Self::ChatGptWeb => chatgpt::check_chatgpt(client).await,
Self::Claude => claude::check_claude(client).await, Self::Claude => claude::check_claude(client).await,
Self::Gemini => gemini::check_gemini(client).await, Self::Gemini => gemini::check_gemini(client).await,
Self::MuseAi => muse::check_muse_ai(client).await,
Self::YouTubePremium => youtube::check_youtube_premium(client).await, Self::YouTubePremium => youtube::check_youtube_premium(client).await,
Self::BahamutAnime => bahamut::check_bahamut_anime(client).await, Self::BahamutAnime => bahamut::check_bahamut_anime(client).await,
Self::Netflix => netflix::check_netflix(client).await, Self::Netflix => netflix::check_netflix(client).await,
@@ -0,0 +1,48 @@
use reqwest::Client;
use crate::utils::{classify_restricted_status, get_trace_location};
use super::UnlockItem;
pub(crate) const MUSE_AI_NAME: &str = "Muse AI";
const MUSE_URL: &str = "https://muse.ai";
const CLOUDFLARE_TRACE_URL: &str = "https://www.cloudflare.com/cdn-cgi/trace";
// The aymh ("Are You a Human") gateway answers plain HTTP requests with a
// generic Meta error page that always contains the Facebook logo asset.
const AYMH_ERROR_MARKERS: &[&str] = &["Sorry, something went wrong", "static.facebook.com/images/logos"];
pub(super) async fn check_muse_ai(client: &Client) -> UnlockItem {
// muse.ai itself is served by Vercel, so resolve the exit region through
// the generic Cloudflare trace endpoint instead of a service-specific one.
let region = get_trace_location(client, CLOUDFLARE_TRACE_URL)
.await
.map(|code| code.trim().to_ascii_uppercase())
.map(|code| UnlockItem::region_label(&code));
let Ok(response) = client.get(MUSE_URL).send().await else {
return UnlockItem::checked(MUSE_AI_NAME, "Failed (Network Connection)", region);
};
let status = response.status();
let landed_on_aymh = response.url().host_str() == Some("auth.muse.ai");
let body = response.text().await.unwrap_or_default();
// Every region is redirected to the aymh anti-bot gateway before the app
// loads, and a non-browser HTTP client cannot finish the JavaScript
// challenge. It returns the Meta error page, so reaching this point only
// proves the exit can connect to Muse's auth infrastructure; availability
// cannot be verified over plain HTTP and is reported as `Risky`.
let behind_anti_bot = landed_on_aymh || AYMH_ERROR_MARKERS.iter().any(|marker| body.contains(marker));
let result = if behind_anti_bot {
"Risky"
} else if status.is_success() {
"Yes"
} else {
classify_restricted_status(status).unwrap_or("Failed")
};
UnlockItem::checked(MUSE_AI_NAME, result, region)
}
+8
View File
@@ -129,6 +129,7 @@ export default defineConfig([
'eslint.config.ts', 'eslint.config.ts',
`vite.config.mts`, `vite.config.mts`,
'scripts/*.mjs', 'scripts/*.mjs',
'scripts-workflow/*.mjs',
'tests/*.ts', 'tests/*.ts',
'src/polyfills/*.js', 'src/polyfills/*.js',
'tests/*.mjs', 'tests/*.mjs',
@@ -238,6 +239,13 @@ export default defineConfig([
}, },
}, },
}, },
{
files: ['scripts-workflow/*.mjs'],
languageOptions: {
globals: globals.node,
},
rules: { 'es-x/no-object-groupby': 'off' },
},
{ {
files: ['scripts/perf/*.{mjs,mts}'], files: ['scripts/perf/*.{mjs,mts}'],
languageOptions: { languageOptions: {
+10 -10
View File
@@ -14,7 +14,7 @@
"build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release", "build:fast": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build -- --profile fast-release",
"tauri": "tauri", "tauri": "tauri",
"web:dev": "vite", "web:dev": "vite",
"web:build": "tsc --noEmit && vite build", "web:build": "tsrs -p . --noEmit && vite build",
"web:serve": "vite preview", "web:serve": "vite preview",
"prebuild": "node scripts/prebuild.mjs", "prebuild": "node scripts/prebuild.mjs",
"updater": "node scripts/updater.mjs", "updater": "node scripts/updater.mjs",
@@ -32,7 +32,7 @@
"i18n:types": "node scripts/generate-i18n-keys.mjs", "i18n:types": "node scripts/generate-i18n-keys.mjs",
"gen:contract": "node scripts/gen-contract.mjs", "gen:contract": "node scripts/gen-contract.mjs",
"arch:check": "node scripts/check-architecture.mjs listen emit invoke", "arch:check": "node scripts/check-architecture.mjs listen emit invoke",
"typecheck": "tsc --noEmit", "typecheck": "tsrs -p . --noEmit",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",
"knip:check": "knip", "knip:check": "knip",
@@ -60,7 +60,7 @@
"@tauri-apps/plugin-fs": "2.7.1", "@tauri-apps/plugin-fs": "2.7.1",
"@tauri-apps/plugin-http": "2.8.3", "@tauri-apps/plugin-http": "2.8.3",
"@tauri-apps/plugin-opener": "2.7.2", "@tauri-apps/plugin-opener": "2.7.2",
"@tauri-apps/plugin-updater": "2.14.0", "@tauri-apps/plugin-updater": "2.14.1",
"ahooks": "^3.10.0", "ahooks": "^3.10.0",
"cidr-block": "^2.3.0", "cidr-block": "^2.3.0",
"compare-versions": "^6.1.1", "compare-versions": "^6.1.1",
@@ -79,12 +79,11 @@
"react-dom": "19.3.1", "react-dom": "19.3.1",
"react-error-boundary": "6.1.6", "react-error-boundary": "6.1.6",
"react-hook-form": "^7.88.0", "react-hook-form": "^7.88.0",
"react-i18next": "17.1.8", "react-i18next": "17.1.9",
"react-markdown": "10.1.0", "react-markdown": "10.1.0",
"react-router": "^8.4.0", "react-router": "^8.4.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0", "rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"swr": "^2.5.1", "swr": "^2.5.1",
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#main", "tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#main",
"types-pac": "^1.0.3", "types-pac": "^1.0.3",
@@ -97,7 +96,7 @@
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@tauri-apps/cli": "2.12.3", "@tauri-apps/cli": "2.12.3",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^24.13.6", "@types/node": "^26.0.0",
"@types/react": "19.3.0", "@types/react": "19.3.0",
"@types/react-dom": "19.3.0", "@types/react-dom": "19.3.0",
"@types/validator": "^13.15.10", "@types/validator": "^13.15.10",
@@ -118,23 +117,24 @@
"eslint-plugin-react-refresh": "^0.5.7", "eslint-plugin-react-refresh": "^0.5.7",
"eslint-plugin-sukka": "^10.2.0", "eslint-plugin-sukka": "^10.2.0",
"eslint-plugin-unused-imports": "^4.4.1", "eslint-plugin-unused-imports": "^4.4.1",
"glob": "^13.0.6", "glob": "^14.0.0",
"globals": "^17.12.0", "globals": "^17.12.0",
"https-proxy-agent": "^9.1.0", "https-proxy-agent": "^9.1.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"jiti": "^2.7.0", "jiti": "^2.7.0",
"knip": "6.43.0", "knip": "6.44.1",
"lint-staged": "^17.5.1", "lint-staged": "^17.5.1",
"sass": "^1.104.1", "sass": "^1.104.1",
"tar": "^7.5.22", "tar": "^7.5.22",
"terser": "^5.51.2", "terser": "^5.51.2",
"tsrs": "0.20.0-ts7.1.0-beta",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"typescript-eslint": "^8.70.0", "typescript-eslint": "^8.70.0",
"vite": "^8.3.0", "vite": "^8.3.0",
"vite-plugin-svgr": "^5.2.0", "vite-plugin-svgr": "^5.2.0",
"vitest": "^5.0.1", "vitest": "^5.0.1",
"webdriverio": "10.1.0", "webdriverio": "10.1.0",
"ws": "8.22.0" "ws": "8.22.1"
}, },
"lint-staged": { "lint-staged": {
"*.{ts,tsx,js,mjs}": [ "*.{ts,tsx,js,mjs}": [
@@ -146,5 +146,5 @@
] ]
}, },
"type": "module", "type": "module",
"packageManager": "pnpm@12.15.0" "packageManager": "pnpm@12.16.1"
} }
+239 -322
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -10,3 +10,11 @@ allowBuilds:
unrs-resolver: true unrs-resolver: true
minimumReleaseAgeExclude: minimumReleaseAgeExclude:
- dayjs@1.11.21 - dayjs@1.11.21
- "@tsrs/darwin-arm64@0.20.0-ts7.1.0-beta"
- "@tsrs/darwin-x64@0.20.0-ts7.1.0-beta"
- "@tsrs/linux-arm64-gnu@0.20.0-ts7.1.0-beta"
- "@tsrs/linux-arm64-musl@0.20.0-ts7.1.0-beta"
- "@tsrs/linux-x64-gnu@0.20.0-ts7.1.0-beta"
- "@tsrs/linux-x64-musl@0.20.0-ts7.1.0-beta"
- "@tsrs/win32-x64@0.20.0-ts7.1.0-beta"
- tsrs@0.20.0-ts7.1.0-beta
+7
View File
@@ -79,6 +79,13 @@
"matchPackageNames": ["typescript"], "matchPackageNames": ["typescript"],
"allowedVersions": "<7.0.0 || >=7.1.0" "allowedVersions": "<7.0.0 || >=7.1.0"
}, },
{
"description": "Review tsrs bumps alone; the version encodes the pinned TypeScript release",
"matchPackageNames": ["tsrs", "/^@tsrs//"],
"groupName": "tsrs",
"rangeStrategy": "pin",
"automerge": false
},
{ {
"description": "Prevent project from supply chain attack", "description": "Prevent project from supply chain attack",
"matchDatasources": ["npm"], "matchDatasources": ["npm"],
+761
View File
@@ -0,0 +1,761 @@
import { execFileSync } from 'node:child_process'
import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'
const env = process.env
const repository = env.GITHUB_REPOSITORY
const refreshLabel = 'Refresh this PR from latest dev'
const publishLabel = 'Publish this approved release'
const minorLabel = 'Use the next minor version'
const checked = (body, label = refreshLabel) =>
(body || '')
.split('\n')
.some((line) => line.toLowerCase() === `- [x] ${label}`.toLowerCase())
const versionPattern = '(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)'
const rcPattern = new RegExp(`^v${versionPattern}-rc(?:\\.(0|[1-9][0-9]*))?$`)
const stablePattern = new RegExp(`^v${versionPattern}$`)
const versionFiles = [
'package.json',
'src-tauri/tauri.conf.json',
'src-tauri/Cargo.toml',
'Cargo.lock',
]
const read = (path) => readFileSync(path, 'utf8')
const json = (path) => JSON.parse(read(path))
const run = (command, args, options = {}) =>
execFileSync(command, args, { encoding: 'utf8', ...options }).trim()
const git = (...args) => run('git', args)
const gitFile = (sha, path) =>
execFileSync('git', ['show', `${sha}:${path}`], { encoding: 'utf8' })
const isRc = (tag) => tag.match(rcPattern)?.[0] === tag
const isRelease = (tag) => isRc(tag) || stablePattern.test(tag)
const titleFor = (tag) => `${isRc(tag) ? 'Pre-Release' : 'Release'}: ${tag}`
const branchTag = (branch, prefix = 'release/') =>
branch.startsWith(prefix) && isRelease(branch.slice(prefix.length))
? branch.slice(prefix.length)
: undefined
const latest = (tags) =>
tags.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })).at(-1)
const summary = (text) => appendFileSync(env.GITHUB_STEP_SUMMARY, `${text}\n`)
const endpoint = (path) =>
path === 'graphql' ? path : `repos/${repository}/${path}`
const list = (path) =>
JSON.parse(run('gh', ['api', '--paginate', '--slurp', endpoint(path)])).flat()
function api(path, body, method = 'POST') {
const response = run(
'gh',
['api', '--method', method, endpoint(path), '--input', '-'],
{
input: JSON.stringify(body),
},
)
const result = response ? JSON.parse(response) : null
if (result?.errors) throw new Error(JSON.stringify(result.errors))
return result
}
const get = (path) => JSON.parse(run('gh', ['api', endpoint(path)]))
const canWrite = (login) =>
['write', 'maintain', 'admin'].includes(
get(`collaborators/${login}/permission`).permission,
)
function output(values) {
appendFileSync(
env.GITHUB_OUTPUT,
Object.entries(values)
.map(([key, value]) => `${key}=${value}\n`)
.join(''),
)
}
function versionPatternFor(path) {
if (path.endsWith('.json')) return /("version"\s*:\s*")[^"]+/
return path === 'Cargo.lock'
? /(\[\[package\]\]\r?\nname = "clash-verge"\r?\nversion = ")[^"]+/
: /(^version\s*=\s*")[^"]+/m
}
function validateVersions(tag, sha) {
for (const path of versionFiles) {
const content = sha ? gitFile(sha, path) : read(path)
const version = path.endsWith('.json')
? JSON.parse(content).version
: content.match(versionPatternFor(path))?.[0].split('"').at(-1)
if (`v${version}` !== tag)
throw new Error(`${path} version does not match ${tag}.`)
}
}
function validateVersionCommit(tag, sha) {
validateVersions(tag, sha)
const commit = get(`commits/${sha}`)
if (!commit.commit.verification.verified || commit.parents.length !== 1)
throw new Error('The version commit must be signed and have one parent.')
const title = commit.commit.message.split('\n')[0]
if (title.replace(/ \(#\d+\)$/, '') !== titleFor(tag))
throw new Error('The final version commit has an unexpected title.')
const parent = commit.parents[0].sha
const changed = git('diff', '--name-only', parent, sha)
.split('\n')
.filter(Boolean)
if (
changed.some((path) => !versionFiles.includes(path)) ||
git('diff', '--summary', parent, sha)
)
throw new Error(
'The version commit changes files outside the four version files.',
)
for (const path of versionFiles) {
const before = gitFile(parent, path)
const after = gitFile(sha, path)
if (
before.replace(versionPatternFor(path), '$1VERSION') !==
after.replace(versionPatternFor(path), '$1VERSION')
)
throw new Error(`${path} contains changes other than its version.`)
}
}
function remoteRefs() {
return git('ls-remote', '--heads', '--tags', '--refs', 'origin')
.split('\n')
.filter(Boolean)
.map((line) => line.split('\t'))
}
function fetchBranches() {
git(
'fetch',
'--no-tags',
'origin',
'+refs/heads/dev:refs/remotes/origin/dev',
'+refs/heads/main:refs/remotes/origin/main',
)
}
function prepare() {
let refresh
if (env.GITHUB_EVENT_NAME === 'pull_request_target') {
const event = json(env.GITHUB_EVENT_PATH)
if (checked(event.changes?.body?.from) || !checked(event.pull_request.body))
return
if (!canWrite(event.sender.login))
throw new Error('Refreshing a version PR requires write access.')
refresh = get(`pulls/${event.pull_request.number}`)
if (refresh.state !== 'open' || !checked(refresh.body)) return
requireBotPr(refresh, 'dev', 'release/')
}
fetchBranches()
git('checkout', '--detach', 'origin/dev')
const channel = refresh
? isRc(branchTag(refresh.head.ref))
? 'rc'
: 'stable'
: env.RELEASE_CHANNEL || 'rc'
if (!['rc', 'stable'].includes(channel))
throw new Error('Unknown release channel.')
const requested = refresh
? branchTag(refresh.head.ref)
: env.RELEASE_TAG || ''
if (
requested &&
!(channel === 'rc' ? isRc(requested) : stablePattern.test(requested))
)
throw new Error(
channel === 'rc'
? 'Expected vX.Y.Z-rc or vX.Y.Z-rc.N.'
: 'Expected vX.Y.Z.',
)
const existing =
!refresh &&
list('pulls?state=open&base=dev&per_page=100').find(
(pr) =>
pr.head.repo?.full_name === repository &&
pr.user.login === 'github-actions[bot]' &&
branchTag(pr.head.ref) &&
isRc(branchTag(pr.head.ref)) === (channel === 'rc') &&
(!requested || branchTag(pr.head.ref) === requested),
)
if (existing) {
api(`issues/${existing.number}/labels`, { labels: ['release'] })
output({ existing_pr: existing.html_url })
summary(
`Existing version PR: ${existing.html_url}\nClose it before preparing a replacement; its candidate is unchanged.`,
)
return
}
const base = (requested || `v${json('package.json').version}`).split(
/[-+]/,
)[0]
if (!stablePattern.test(base))
throw new Error(`Invalid base version: ${base}`)
const refs = remoteRefs().map(([, ref]) => ref)
const tags = refs
.filter((ref) => ref.startsWith('refs/tags/'))
.map((ref) => ref.slice(10))
const reserved = refs
.filter((ref) => ref.startsWith('refs/heads/'))
.filter((ref) => !refresh || ref !== `refs/heads/${refresh.head.ref}`)
.map(
(ref) => branchTag(ref.slice(11)) || branchTag(ref.slice(11), 'publish/'),
)
.filter(Boolean)
const sameBase = (tag) => isRc(tag) && tag.startsWith(`${base}-rc`)
const occupied = [...tags, ...reserved]
const last = latest(occupied.filter(sameBase))
const tag =
requested ||
(channel === 'stable'
? base
: last
? `${base}-rc.${BigInt(last.match(rcPattern)[4] ?? 0) + 1n}`
: `${base}-rc`)
if (occupied.includes(tag))
throw new Error(`Tag ${tag} already exists or is reserved.`)
const sha = git('rev-parse', 'HEAD')
const body = releaseBody(tag, sha, tags, false)
summary(body)
if (
remoteRefs().some(
([, ref]) =>
ref === `refs/tags/${tag}` || ref === `refs/heads/publish/${tag}`,
)
)
throw new Error(`Tag ${tag} already exists or is reserved for publication.`)
if (json('package.json').version !== tag.slice(1))
run('node', ['scripts/release-version.mjs', tag])
validateVersions(tag)
const title = titleFor(tag)
const branch = refresh
? `release-refresh/${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}`
: `release/${tag}`
writeVersionPr({ sha, body, refresh, title, branch, files: versionFiles })
}
function releaseBody(tag, sha, tags, publishing) {
const previous =
(isRc(tag) &&
latest(
tags.filter(
(value) =>
isRc(value) &&
value.startsWith(`${tag.split('-')[0]}-rc`) &&
value !== tag,
),
)) ||
latest(tags.filter((value) => stablePattern.test(value) && value !== tag))
const url = `${env.GITHUB_SERVER_URL}/${repository}`
let range = sha
let comparison = 'No previous release tag'
if (previous) {
git('fetch', '--no-tags', 'origin', `refs/tags/${previous}`)
range = `${git('rev-parse', 'FETCH_HEAD^{commit}')}..${sha}`
comparison = `Changes since **${previous}** · [Compare changes](${url}/compare/${previous}...${sha})`
}
comparison += ` · **${git('rev-list', '--count', range)} commits**`
const commits = git('log', '--format=%h %s', range)
const groups = Object.groupBy(commits ? commits.split('\n') : [], (line) => {
if (/^\S+ chore\(deps\)!?: /i.test(line)) return 'deps'
const type = line.match(/^\S+ (\w+)(?:\([^)]*\))?!?: /)?.[1]?.toLowerCase()
return type === 'doc' ? 'docs' : type || 'other'
})
const labels = {
fix: 'Fixes',
feat: 'Features',
perf: 'Performance',
revert: 'Reverts',
refactor: 'Refactoring',
build: 'Build',
deps: 'Dependencies',
ci: 'CI',
docs: 'Documentation',
test: 'Tests',
chore: 'Maintenance',
style: 'Style',
other: 'Other',
}
const names = {
macos: 'macOS',
windows: 'Windows',
ui: 'UI',
ci: 'CI',
tun: 'TUN',
}
const order = Object.keys(labels)
const rank = (type) =>
order.includes(type) ? order.indexOf(type) : order.length
const scope =
Object.entries(groups)
.sort(([a], [b]) => rank(a) - rank(b) || a.localeCompare(b))
.map(([type, lines]) => {
const entries = lines.map((line) => {
const hash = line.slice(0, line.indexOf(' '))
const title = line
.slice(hash.length + 1)
.replace(/\s*\((?:[\w.-]+\/[\w.-]+)?#\d+\)/g, '')
.replace(/(?:[\w.-]+\/[\w.-]+)?#\d+\b/g, '')
.replace(/\s{2,}/g, ' ')
.trim()
.replace(
/^\w+(?:\(([^)]*)\))?(!)?:\s*/,
(_, scope, breaking) =>
`${breaking ? 'Breaking: ' : ''}${scope ? `${Object.hasOwn(names, scope) ? names[scope] : scope}: ` : ''}`,
)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replace(/[\\`*_[\]]/g, '\\$&')
return `- ${title} [#${hash}](${url}/commit/${hash})`
})
return `### ${Object.hasOwn(labels, type) ? labels[type] : type} · ${lines.length}\n\n${entries.join('\n')}`
})
.join('\n\n') || 'No new commits.'
return `## ${titleFor(tag)}
Source: \`dev\` · [${sha}](${url}/commit/${sha}) \n${comparison}
${
publishing
? `<!-- release-source:${sha} -->
After approval, select the checkbox below to publish.
Do not use the normal Squash or Rebase merge buttons.
- [ ] ${publishLabel}`
: `
- [ ] ${refreshLabel}
Refreshing requires a new approval.`
}
## Changes
${scope}
`
}
function writeVersionPr({ sha, body, refresh, title, branch, files }) {
// Create-only ref and expectedHeadOid prevent replacing another candidate.
api('git/refs', { ref: `refs/heads/${branch}`, sha })
let pr
try {
const commit = api('graphql', {
query: `mutation($input: CreateCommitOnBranchInput!) {
createCommitOnBranch(input: $input) { commit { oid } }
}`,
variables: {
input: {
branch: { repositoryNameWithOwner: repository, branchName: branch },
expectedHeadOid: sha,
message: { headline: title },
fileChanges: {
additions: files.map((path) => ({
path,
contents: readFileSync(path).toString('base64'),
})),
},
},
},
})
if (!commit.data?.createCommitOnBranch?.commit?.oid)
throw new Error('Signed version commit failed.')
if (refresh) {
const current = get(`pulls/${refresh.number}`)
if (
current.state !== 'open' ||
current.head.sha !== refresh.head.sha ||
current.base.ref !== refresh.base.ref ||
current.body !== refresh.body
)
throw new Error(
'The version pull request changed while preparing the update.',
)
const oid = commit.data.createCommitOnBranch.commit.oid
updateRefs(refresh.base.repo.node_id, [
{
name: `refs/heads/${refresh.head.ref}`,
beforeOid: refresh.head.sha,
afterOid: oid,
force: true,
},
{
name: `refs/heads/${branch}`,
beforeOid: oid,
afterOid: '0'.repeat(40),
},
])
for (const review of list(
`pulls/${refresh.number}/reviews?per_page=100`,
)) {
if (review.state === 'APPROVED')
api(
`pulls/${refresh.number}/reviews/${review.id}/dismissals`,
{
message:
'The version PR was refreshed from dev; please review again.',
},
'PUT',
)
}
pr = api(`pulls/${refresh.number}`, { title, body }, 'PATCH')
} else {
pr = api('pulls', {
head: branch,
base: 'dev',
title,
body,
})
}
} catch (error) {
if (refresh) {
try {
api(`git/refs/heads/${branch}`, {}, 'DELETE')
} catch {}
}
throw error
}
api(`issues/${pr.number}/labels`, { labels: ['release'] })
summary(`Version pull request: ${pr.html_url}`)
}
function updateRefs(repositoryId, refUpdates) {
api('graphql', {
query: `mutation($input: UpdateRefsInput!) { updateRefs(input: $input) { clientMutationId } }`,
variables: { input: { repositoryId, refUpdates } },
})
}
function requireBotPr(pr, base, prefix) {
const tag = branchTag(pr.head.ref, prefix)
if (
!tag ||
pr.base.ref !== base ||
pr.base.repo.full_name !== repository ||
pr.head.repo?.full_name !== repository ||
pr.user.login !== 'github-actions[bot]'
)
throw new Error(
'Expected a release PR created by this repository workflow.',
)
return tag
}
function publishPr() {
const event = json(env.GITHUB_EVENT_PATH)
const pr = get(`pulls/${event.pull_request.number}`)
const tag = requireBotPr(pr, 'dev', 'release/')
if (!pr.merged || pr.merge_commit_sha !== event.pull_request.merge_commit_sha)
throw new Error('Expected the final merged version commit on dev.')
const sha = pr.merge_commit_sha
fetchBranches()
git('fetch', '--no-tags', 'origin', sha)
git('merge-base', '--is-ancestor', sha, 'origin/dev')
validateVersionCommit(tag, sha)
const branch = `publish/${tag}`
const existing = list(
`pulls?state=all&base=main&head=${repository.split('/')[0]}:${branch}&per_page=100`,
)
if (existing.length) {
const publication = existing[0]
requireBotPr(publication, 'main', 'publish/')
if (publication.head.sha !== sha)
throw new Error(
'This tag is already reserved by a different publication snapshot.',
)
summary(`Existing publication PR: ${publication.html_url}`)
return
}
const refs = remoteRefs()
if (refs.some(([, ref]) => ref === `refs/tags/${tag}`))
throw new Error(`Tag ${tag} already exists.`)
git('merge-base', '--is-ancestor', 'origin/main', sha)
const head = refs.find(([, ref]) => ref === `refs/heads/${branch}`)?.[0]
if (head && head !== sha)
throw new Error('Publication branch already points to another commit.')
if (!head) api('git/refs', { ref: `refs/heads/${branch}`, sha })
const body = releaseBody(
tag,
sha,
refs
.filter(([, ref]) => ref.startsWith('refs/tags/'))
.map(([, ref]) => ref.slice(10)),
true,
)
const publication = api('pulls', {
head: branch,
base: 'main',
title: titleFor(tag),
body,
})
api(`issues/${publication.number}/labels`, { labels: ['release'] })
summary(`Publication PR: ${publication.html_url}\n\n${body}`)
}
function requireApproval(pr, sha) {
const reviews = new Map()
for (const review of list(`pulls/${pr.number}/reviews?per_page=100`)) {
if (['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED'].includes(review.state))
reviews.set(review.user.login, review)
}
let approved = false
for (const review of reviews.values()) {
if (!canWrite(review.user.login)) continue
if (review.state === 'CHANGES_REQUESTED')
throw new Error('A reviewer with write access has requested changes.')
if (review.state === 'APPROVED' && review.commit_id === sha) approved = true
}
if (!approved)
throw new Error(
'The current PR head needs approval from a reviewer with write access.',
)
}
function releaseOutput(tag, sha) {
output({
release_tag: tag,
source_sha: sha,
channel: isRc(tag) ? 'rc' : 'stable',
})
}
function publish() {
const event = json(env.GITHUB_EVENT_PATH)
const tag = requireBotPr(event.pull_request, 'main', 'publish/')
if (
checked(event.changes?.body?.from, publishLabel) ||
!checked(event.pull_request.body, publishLabel)
)
throw new Error('Publishing requires a new checkbox selection.')
if (!canWrite(event.sender.login))
throw new Error('Publishing requires write access.')
const sha = event.pull_request.head.sha
const pr = get(`pulls/${event.pull_request.number}`)
requireBotPr(pr, 'main', 'publish/')
if (
pr.head.sha !== sha ||
!checked(pr.body, publishLabel) ||
!pr.body.includes(`<!-- release-source:${sha} -->`)
)
throw new Error(
'The publication snapshot changed after the checkbox selection.',
)
fetchBranches()
git('fetch', '--no-tags', 'origin', sha)
git('merge-base', '--is-ancestor', sha, 'origin/dev')
validateVersionCommit(tag, sha)
const marker = `Release workflow run: ${env.GITHUB_RUN_ID}\nSource: ${sha}\nPR: ${pr.number}`
const existing = remoteRefs().find(([, ref]) => ref === `refs/tags/${tag}`)
if (existing) {
const ref = get(`git/ref/tags/${tag}`)
const object =
ref.object.type === 'tag' && get(`git/tags/${ref.object.sha}`)
if (
!object ||
object.object.type !== 'commit' ||
object.object.sha !== sha ||
object.message.trim() !== marker ||
Number(env.GITHUB_RUN_ATTEMPT) <= 1
)
throw new Error(
'An existing tag can only be recovered by its original release run.',
)
git('merge-base', '--is-ancestor', sha, 'origin/main')
summary(`Resuming ${tag} at ${sha} from this release run.`)
} else {
if (pr.state !== 'open' || pr.draft)
throw new Error('Expected an open, ready publication PR.')
requireApproval(pr, sha)
const main = git('rev-parse', 'origin/main')
git('merge-base', '--is-ancestor', main, sha)
const object = api('git/tags', {
tag,
message: marker,
object: sha,
type: 'commit',
})
// The tag records run ownership in the same transaction as the fast-forward.
updateRefs(pr.base.repo.node_id, [
{ name: 'refs/heads/main', beforeOid: main, afterOid: sha, force: false },
{
name: `refs/heads/${pr.head.ref}`,
beforeOid: sha,
afterOid: sha,
force: false,
},
{
name: `refs/tags/${tag}`,
beforeOid: '0'.repeat(40),
afterOid: object.sha,
force: false,
},
])
summary(`Fast-forwarded main and tagged ${tag} at approved commit ${sha}.`)
}
releaseOutput(tag, sha)
}
function validateTag() {
const tag = env.GITHUB_REF_NAME
if (!isRelease(tag)) throw new Error('Expected a stable or RC release tag.')
fetchBranches()
const sha = git('rev-parse', `${env.GITHUB_SHA}^{commit}`)
const ref = get(`git/ref/tags/${tag}`)
const object = ref.object.type === 'tag' && get(`git/tags/${ref.object.sha}`)
if (object && object.message.includes('Release workflow run:'))
throw new Error(
'Resume the original publication run for workflow-owned tags.',
)
git('fetch', '--no-tags', 'origin', `refs/tags/${tag}`)
if (git('rev-parse', 'FETCH_HEAD^{commit}') !== sha)
throw new Error('The tag moved after this run started.')
for (const branch of ['main', 'dev'])
git('merge-base', '--is-ancestor', sha, `origin/${branch}`)
validateVersions(tag, sha)
releaseOutput(tag, sha)
}
function prepareDraft() {
const tag = env.RELEASE_TAG
verifyTarget()
const release = list('releases?per_page=100').find(
(release) => release.tag_name === tag,
)
if (release && !release.draft)
throw new Error('This release is already public.')
const draft =
release ||
api('releases', {
tag_name: tag,
target_commitish: env.SOURCE_SHA,
name: `Clash Verge Rev ${tag}`,
body: 'Draft release, will be updated later.',
draft: true,
prerelease: isRc(tag),
})
output({ id: draft.id })
}
function verifyTarget(tag = env.RELEASE_TAG, sha = env.SOURCE_SHA) {
const ref = get(`git/ref/tags/${tag}`)
const target =
ref.object.type === 'tag'
? get(`git/tags/${ref.object.sha}`).object
: ref.object
if (target.type !== 'commit' || target.sha !== sha)
throw new Error('The release tag no longer points to the validated source.')
}
function nextDev() {
let refresh
let tag = env.RELEASE_TAG
let source = env.SOURCE_SHA
let minor = false
if (env.GITHUB_EVENT_NAME === 'pull_request_target') {
const event = json(env.GITHUB_EVENT_PATH)
const oldBody = event.changes?.body?.from
const body = event.pull_request.body
if (
checked(oldBody, minorLabel) === checked(body, minorLabel) &&
(checked(oldBody) || !checked(body))
)
return
if (!canWrite(event.sender.login))
throw new Error('Changing the next dev version requires write access.')
refresh = get(`pulls/${event.pull_request.number}`)
if (refresh.state !== 'open') return
tag = requireBotPr(refresh, 'dev', 'next-dev/')
if (
refresh.head.sha !== event.pull_request.head.sha ||
refresh.body !== body
)
throw new Error(
'The version update PR changed after the checkbox selection.',
)
minor = checked(body, minorLabel)
source = body.match(/<!-- next-dev-source:([a-f0-9]{40}) -->/)?.[1]
}
if (!stablePattern.test(tag) || !/^[a-f0-9]{40}$/.test(source))
throw new Error('The next dev version requires a stable release snapshot.')
verifyTarget(tag, source)
const release = get(`releases/tags/${tag}`)
if (release.draft || release.prerelease)
throw new Error('The stable release must be public before preparing dev.')
const branch = `next-dev/${tag}`
if (!refresh) {
const existing = list(
`pulls?state=all&base=dev&head=${repository.split('/')[0]}:${branch}&per_page=100`,
)[0]
if (existing) {
requireBotPr(existing, 'dev', 'next-dev/')
summary(`Existing dev version PR: ${existing.html_url}`)
return
}
}
fetchBranches()
git('fetch', '--no-tags', 'origin', source)
git('merge-base', '--is-ancestor', source, 'origin/main')
git('merge-base', '--is-ancestor', source, 'origin/dev')
git('checkout', '--detach', 'origin/dev')
const sha = git('rev-parse', 'HEAD')
validateVersions(tag, source)
validateVersions(tag, sha)
if (read('Changelog.md') !== gitFile(source, 'Changelog.md'))
throw new Error(
'Changelog.md changed after this release. Preserve the unreleased entries and prepare the dev update manually.',
)
if (
!read('Changelog.md').split('\n').includes(`## ${tag}`) ||
read('docs/Changelog.history.md').split('\n').includes(`## ${tag}`)
)
throw new Error('The released changelog is missing or already archived.')
const [major, minorVersion, patch] = tag.slice(1).split('.').map(BigInt)
const patchTag = `v${major}.${minorVersion}.${patch + 1n}`
const minorTag = `v${major}.${minorVersion + 1n}.0`
const nextTag = minor ? minorTag : patchTag
run('pnpm', ['release-version', nextTag])
run('bash', ['scripts-workflow/bump_changelog.sh'])
const changelog = read('Changelog.md')
if (!changelog.includes('## v(Version Goes Here)'))
throw new Error('The changelog template has no version placeholder.')
writeFileSync(
'Changelog.md',
changelog.replace('## v(Version Goes Here)', `## ${nextTag}`),
)
validateVersions(nextTag)
const title = `Dev: ${nextTag}`
const body = `## ${title}
After [${tag}](${env.GITHUB_SERVER_URL}/${repository}/releases/tag/${tag}).
<!-- next-dev-source:${source} -->
Patch: \`${patchTag}\` · Minor: \`${minorTag}\`
- [${minor ? 'x' : ' '}] ${minorLabel}
- [ ] ${refreshLabel}
Changing the version or refreshing requires a new approval.
`
writeVersionPr({
sha,
body,
refresh,
title,
branch: refresh
? `next-dev-refresh/${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}`
: branch,
files: [...versionFiles, 'Changelog.md', 'docs/Changelog.history.md'],
})
}
const commands = {
prepare,
'next-dev': nextDev,
'publish-pr': publishPr,
publish,
'validate-tag': validateTag,
'prepare-draft': prepareDraft,
'verify-target': verifyTarget,
}
const command = commands[process.argv[2]]
if (!command) throw new Error('Unknown release command.')
command()
+1 -1
View File
@@ -17,7 +17,7 @@
"pnpm-lock.yaml", "pnpm-lock.yaml",
"pnpm-workspace.yaml", "pnpm-workspace.yaml",
"scripts/contract-inputs.json", "scripts/contract-inputs.json",
".github/workflows/frontend-check.yml", ".github/workflows/contract-parity.yml",
".husky/pre-push" ".husky/pre-push"
] ]
} }
+2 -2
View File
@@ -79,7 +79,7 @@ tauri-plugin-fs = "2.7.1"
tauri-plugin-process = "2.4.0" tauri-plugin-process = "2.4.0"
tauri-plugin-deep-link = "2.6.2" tauri-plugin-deep-link = "2.6.2"
tauri-plugin-window-state = "2.5.0" tauri-plugin-window-state = "2.5.0"
zip = "9.0.4" zip = "9.1.3"
flate2 = "1.1.10" flate2 = "1.1.10"
reqwest_dav = "0.3.3" reqwest_dav = "0.3.3"
aes-gcm = { version = "0.11.1" } aes-gcm = { version = "0.11.1" }
@@ -154,7 +154,7 @@ windows-sys.workspace = true
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.7.0" tauri-plugin-autostart = "2.7.0"
tauri-plugin-global-shortcut = "2.4.1" tauri-plugin-global-shortcut = "2.4.1"
tauri-plugin-updater = "2.14.0" tauri-plugin-updater = "2.14.1"
[dev-dependencies] [dev-dependencies]
criterion = { workspace = true } criterion = { workspace = true }
+2 -2
View File
@@ -109,7 +109,7 @@ impl AutoBackupManager {
{ {
*self.settings.write() = settings; *self.settings.write() = settings;
} }
let _ = self.settings_tx.send(settings); let _ = self.settings_tx.send_replace(settings);
self.maybe_start_runner(settings); self.maybe_start_runner(settings);
Ok(()) Ok(())
} }
@@ -119,7 +119,7 @@ impl AutoBackupManager {
{ {
*self.settings.write() = settings; *self.settings.write() = settings;
} }
let _ = self.settings_tx.send(settings); let _ = self.settings_tx.send_replace(settings);
self.maybe_start_runner(settings); self.maybe_start_runner(settings);
Ok(()) Ok(())
} }
+1 -1
View File
@@ -27,7 +27,7 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
/// Apply a patch without post-update reconciliation. /// Apply a patch without post-update reconciliation.
pub(super) async fn apply_verge_patch(patch: &IVerge, not_save_file: bool) -> Result<()> { pub(super) async fn apply_verge_patch(patch: &IVerge, not_save_file: bool) -> Result<()> {
let config_write = Config::try_lock_config_write()?; let config_write = Config::lock_config_write().await;
apply_verge_patch_locked(&config_write, patch, not_save_file).await apply_verge_patch_locked(&config_write, patch, not_save_file).await
} }
-1
View File
@@ -316,7 +316,6 @@ pub async fn hide() {
{ {
let _ = window.hide(); let _ = window.hide();
} }
handle::Handle::global().set_activation_policy_accessory();
} }
#[cfg(test)] #[cfg(test)]
-3
View File
@@ -289,9 +289,6 @@ pub fn run() -> std::process::ExitCode {
} }
pub fn handle_window_close(api: &tauri::WindowEvent) { pub fn handle_window_close(api: &tauri::WindowEvent) {
#[cfg(target_os = "macos")]
handle::Handle::global().set_activation_policy_accessory();
if core::handle::Handle::global().is_exiting() { if core::handle::Handle::global().is_exiting() {
return; return;
} }
+33 -5
View File
@@ -4,10 +4,19 @@ import {
VisibilityOffOutlined, VisibilityOffOutlined,
VisibilityOutlined, VisibilityOutlined,
} from '@mui/icons-material' } from '@mui/icons-material'
import { Box, Button, IconButton, Skeleton, Typography } from '@mui/material' import {
Box,
Button,
FormControlLabel,
IconButton,
Skeleton,
Switch,
Typography,
} from '@mui/material'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { useEffect } from 'foxact/use-abortable-effect' import { useEffect } from 'foxact/use-abortable-effect'
import { useIntersection } from 'foxact/use-intersection' import { useIntersection } from 'foxact/use-intersection'
import { useLocalStorage } from 'foxact/use-local-storage'
import type { XOR } from 'foxts/ts-xor' import type { XOR } from 'foxts/ts-xor'
import { import {
forwardRef, forwardRef,
@@ -99,9 +108,13 @@ const IPInfoCardContainer = forwardRef<HTMLElement, React.PropsWithChildren>(
export const IpInfoCard = () => { export const IpInfoCard = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [showIp, setShowIp] = useState(false) const [showIp, setShowIp] = useState(false)
const [autoRefresh, setAutoRefresh] = useLocalStorage(
'ip-info-auto-refresh',
true,
)
const appWindow = useMemo(() => getCurrentWebviewWindow(), []) const appWindow = useMemo(() => getCurrentWebviewWindow(), [])
// Once intersected, refreshes stay enabled until unmount. // Once intersected, the card stays eligible for refresh until unmount.
const [containerRef, hasIntersected, _resetIntersected] = useIntersection({ const [containerRef, hasIntersected, _resetIntersected] = useIntersection({
rootMargin: '0px', rootMargin: '0px',
}) })
@@ -151,6 +164,8 @@ export const IpInfoCard = () => {
}) })
useEffect(() => { useEffect(() => {
if (!autoRefresh) return
let timer: number | null = null let timer: number | null = null
// Prefer DOM intersection state; Tauri visibility is unreliable on some platforms. // Prefer DOM intersection state; Tauri visibility is unreliable on some platforms.
@@ -190,7 +205,7 @@ export const IpInfoCard = () => {
if (timer != null) clearInterval(timer) if (timer != null) clearInterval(timer)
document.removeEventListener('visibilitychange', onVisibilityChange) document.removeEventListener('visibilitychange', onVisibilityChange)
} }
}, [hasIntersected]) }, [autoRefresh, hasIntersected])
const toggleShowIp = useCallback(() => { const toggleShowIp = useCallback(() => {
setShowIp((prev) => !prev) setShowIp((prev) => !prev)
@@ -364,12 +379,25 @@ export const IpInfoCard = () => {
fontSize: '0.7rem', fontSize: '0.7rem',
}} }}
> >
<FormControlLabel
sx={{ m: 0 }}
control={
<Switch
size="small"
checked={autoRefresh}
onChange={(_, checked) => setAutoRefresh(checked)}
/>
}
label={
<Typography variant="caption"> <Typography variant="caption">
{t('home.components.ipInfo.labels.autoRefresh')} {t('home.components.ipInfo.labels.autoRefresh')}
{countdown.type === 'countdown' {autoRefresh &&
(countdown.type === 'countdown'
? `: ${countdown.remainingSeconds}s` ? `: ${countdown.remainingSeconds}s`
: '...'} : '...')}
</Typography> </Typography>
}
/>
<Typography <Typography
variant="caption" variant="caption"
sx={{ sx={{
@@ -397,9 +397,9 @@ export const RulesEditorViewer = (props: Props) => {
return return
} }
setPrependSeq(obj?.prepend || []) setPrependSeq((obj?.prepend || []).filter((rule) => rule !== null))
setAppendSeq(obj?.append || []) setAppendSeq((obj?.append || []).filter((rule) => rule !== null))
setDeleteSeq(obj?.delete || []) setDeleteSeq((obj?.delete || []).filter((rule) => rule !== null))
hasLoadedSeqConfigRef.current = true hasLoadedSeqConfigRef.current = true
}, [property]) }, [property])
@@ -417,9 +417,9 @@ export const RulesEditorViewer = (props: Props) => {
hasLoadedSeqConfigRef.current = true hasLoadedSeqConfigRef.current = true
startTransition(() => { startTransition(() => {
setPrependSeq(obj?.prepend ?? []) setPrependSeq((obj?.prepend ?? []).filter((rule) => rule !== null))
setAppendSeq(obj?.append ?? []) setAppendSeq((obj?.append ?? []).filter((rule) => rule !== null))
setDeleteSeq(obj?.delete ?? []) setDeleteSeq((obj?.delete ?? []).filter((rule) => rule !== null))
}) })
setVisualization(true) setVisualization(true)
} }
+1 -1
View File
@@ -608,7 +608,7 @@ function NormalProxyGroups(props: { mode: string }) {
/> />
</DragDropProvider> </DragDropProvider>
{mode === 'rule' && ( {(mode === 'rule' || mode === 'global') && (
<ProxyGroupNavigator <ProxyGroupNavigator
proxyGroupNames={proxyGroupNames} proxyGroupNames={proxyGroupNames}
onGroupLocation={handleGroupLocationByName} onGroupLocation={handleGroupLocationByName}
+2 -2
View File
@@ -211,7 +211,7 @@ export const useRenderList = (
if (!proxyView) return [] if (!proxyView) return []
if (isChainMode) if (isChainMode)
return selectedGroup ? [selectedGroup] : [CHAIN_DELAY_GROUP] return selectedGroup ? [selectedGroup] : [CHAIN_DELAY_GROUP]
return mode === 'rule' || mode === 'script' return mode === 'rule' || mode === 'global'
? proxyView.groups.map(({ name }) => name) ? proxyView.groups.map(({ name }) => name)
: proxyView.global : proxyView.global
? [proxyView.global.name] ? [proxyView.global.name]
@@ -257,7 +257,7 @@ export const useRenderList = (
})) }))
} }
const useRule = mode === 'rule' || mode === 'script' const useRule = mode === 'rule' || mode === 'global'
const renderGroups = useRule const renderGroups = useRule
? proxyView.groups ? proxyView.groups
: proxyView.global === null : proxyView.global === null
@@ -40,9 +40,9 @@ export const BackupConfigViewer = memo(
const { register, handleSubmit, watch } = useForm<IWebDavConfig>({ const { register, handleSubmit, watch } = useForm<IWebDavConfig>({
defaultValues: { defaultValues: {
url: webdav_url!, url: webdav_url ?? '',
username: webdav_username!, username: webdav_username ?? '',
password: webdav_password!, password: webdav_password ?? '',
}, },
}) })
const url = watch('url') const url = watch('url')
@@ -50,9 +50,9 @@ export const BackupConfigViewer = memo(
const password = watch('password') const password = watch('password')
const webdavChanged = const webdavChanged =
webdav_url !== url || (webdav_url ?? '') !== url ||
webdav_username !== username || (webdav_username ?? '') !== username ||
webdav_password !== password (webdav_password ?? '') !== password
const webdavSignature = buildWebdavSignature(verge) const webdavSignature = buildWebdavSignature(verge)
const webdavStatus = getWebdavStatus(webdavSignature) const webdavStatus = getWebdavStatus(webdavSignature)
@@ -69,9 +69,9 @@ export const BackupConfigViewer = memo(
}, [webdav_url, webdav_username, webdav_password, onInit, shouldAutoInit]) }, [webdav_url, webdav_username, webdav_password, onInit, shouldAutoInit])
const checkForm = () => { const checkForm = () => {
const username = usernameRef.current?.value const username = usernameRef.current?.value.trim()
const password = passwordRef.current?.value const password = passwordRef.current?.value
const url = urlRef.current?.value const url = urlRef.current?.value.trim()
if (!url) { if (!url) {
urlRef.current?.focus() urlRef.current?.focus()
@@ -95,14 +95,15 @@ export const BackupConfigViewer = memo(
} }
const save = useLockFn(async (data: IWebDavConfig) => { const save = useLockFn(async (data: IWebDavConfig) => {
checkForm() const trimmedUrl = data.url.trim()
const trimmedUsername = data.username.trim()
const isClearing = !trimmedUrl && !trimmedUsername && !data.password
if (!isClearing) checkForm()
const signature = buildWebdavSignature({ const signature = buildWebdavSignature({
webdav_url: data.url, webdav_url: data.url,
webdav_username: data.username, webdav_username: data.username,
webdav_password: data.password, webdav_password: data.password,
}) })
const trimmedUrl = data.url.trim()
const trimmedUsername = data.username.trim()
try { try {
setLoading(true) setLoading(true)
@@ -127,7 +128,7 @@ export const BackupConfigViewer = memo(
) )
setWebdavStatus(signature, 'unknown') setWebdavStatus(signature, 'unknown')
showNotice.success('settings.modals.backup.messages.webdavConfigSaved') showNotice.success('settings.modals.backup.messages.webdavConfigSaved')
await onSaveSuccess(signature) if (!isClearing) await onSaveSuccess(signature)
} catch (error) { } catch (error) {
showNotice.error( showNotice.error(
'settings.modals.backup.messages.webdavConfigSaveFailed', 'settings.modals.backup.messages.webdavConfigSaveFailed',
@@ -223,9 +224,9 @@ export const BackupConfigViewer = memo(
}} }}
> >
{webdavChanged || {webdavChanged ||
webdav_url === undefined || !webdav_url ||
webdav_username === undefined || !webdav_username ||
webdav_password === undefined ? ( !webdav_password ? (
<Button <Button
variant="contained" variant="contained"
color={'primary'} color={'primary'}
@@ -72,12 +72,10 @@ const LazyReactMarkdown = lazy(async () => {
{ default: ReactMarkdown }, { default: ReactMarkdown },
{ default: rehypeRaw }, { default: rehypeRaw },
{ default: rehypeSanitize, defaultSchema }, { default: rehypeSanitize, defaultSchema },
{ default: remarkGfm },
] = await Promise.all([ ] = await Promise.all([
import('react-markdown'), import('react-markdown'),
import('rehype-raw'), import('rehype-raw'),
import('rehype-sanitize'), import('rehype-sanitize'),
import('remark-gfm'),
]) ])
const sanitizeSchema = { const sanitizeSchema = {
@@ -104,7 +102,6 @@ const LazyReactMarkdown = lazy(async () => {
default: (props: ReactMarkdownOptions) => ( default: (props: ReactMarkdownOptions) => (
<ReactMarkdown <ReactMarkdown
{...props} {...props}
remarkPlugins={[remarkGfm, ...(props.remarkPlugins ?? [])]}
rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeSchema]]} rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeSchema]]}
/> />
), ),
+1
View File
@@ -36,6 +36,7 @@
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"failed": "Failed", "failed": "Failed",
"risky": "غير مؤكد",
"completed": "Completed", "completed": "Completed",
"disallowedIsp": "Disallowed ISP", "disallowedIsp": "Disallowed ISP",
"originalsOnly": "Originals Only", "originalsOnly": "Originals Only",
+1
View File
@@ -36,6 +36,7 @@
"yes": "Unterstützt", "yes": "Unterstützt",
"no": "Nicht unterstützt", "no": "Nicht unterstützt",
"failed": "Test fehlgeschlagen", "failed": "Test fehlgeschlagen",
"risky": "Ungewiss",
"completed": "Prüfung abgeschlossen", "completed": "Prüfung abgeschlossen",
"disallowedIsp": "Nicht zugelassener Internetdienstanbieter", "disallowedIsp": "Nicht zugelassener Internetdienstanbieter",
"originalsOnly": "Nur Original", "originalsOnly": "Nur Original",
+1
View File
@@ -36,6 +36,7 @@
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"failed": "Failed", "failed": "Failed",
"risky": "Risky",
"completed": "Completed", "completed": "Completed",
"disallowedIsp": "Disallowed ISP", "disallowedIsp": "Disallowed ISP",
"originalsOnly": "Originals Only", "originalsOnly": "Originals Only",
+1
View File
@@ -36,6 +36,7 @@
"yes": "Soportado", "yes": "Soportado",
"no": "No soportado", "no": "No soportado",
"failed": "Prueba fallida", "failed": "Prueba fallida",
"risky": "Dudoso",
"completed": "Detección completada", "completed": "Detección completada",
"disallowedIsp": "Proveedor de servicios de Internet no permitido", "disallowedIsp": "Proveedor de servicios de Internet no permitido",
"originalsOnly": "Solo originales", "originalsOnly": "Solo originales",
+1
View File
@@ -36,6 +36,7 @@
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"failed": "Failed", "failed": "Failed",
"risky": "نامشخص",
"completed": "Completed", "completed": "Completed",
"disallowedIsp": "Disallowed ISP", "disallowedIsp": "Disallowed ISP",
"originalsOnly": "Originals Only", "originalsOnly": "Originals Only",
+1
View File
@@ -36,6 +36,7 @@
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"failed": "Failed", "failed": "Failed",
"risky": "Tidak pasti",
"completed": "Completed", "completed": "Completed",
"disallowedIsp": "Disallowed ISP", "disallowedIsp": "Disallowed ISP",
"originalsOnly": "Originals Only", "originalsOnly": "Originals Only",
+1
View File
@@ -36,6 +36,7 @@
"yes": "サポートする", "yes": "サポートする",
"no": "サポートしない", "no": "サポートしない",
"failed": "テストに失敗しました。", "failed": "テストに失敗しました。",
"risky": "要確認",
"completed": "検査完了", "completed": "検査完了",
"disallowedIsp": "許可されていないインターネットサービスプロバイダー", "disallowedIsp": "許可されていないインターネットサービスプロバイダー",
"originalsOnly": "オリジナルのみ", "originalsOnly": "オリジナルのみ",
+1
View File
@@ -36,6 +36,7 @@
"yes": "예", "yes": "예",
"no": "아니오", "no": "아니오",
"failed": "실패", "failed": "실패",
"risky": "확인 필요",
"completed": "완료", "completed": "완료",
"disallowedIsp": "허용되지 않는 ISP", "disallowedIsp": "허용되지 않는 ISP",
"originalsOnly": "오리지널만", "originalsOnly": "오리지널만",
+1
View File
@@ -36,6 +36,7 @@
"yes": "Sim", "yes": "Sim",
"no": "Não", "no": "Não",
"failed": "Falhou", "failed": "Falhou",
"risky": "Incerto",
"completed": "Concluído", "completed": "Concluído",
"disallowedIsp": "ISP não permitido", "disallowedIsp": "ISP não permitido",
"originalsOnly": "Somente originais", "originalsOnly": "Somente originais",
+1
View File
@@ -36,6 +36,7 @@
"yes": "Да", "yes": "Да",
"no": "Нет", "no": "Нет",
"failed": "Ошибка", "failed": "Ошибка",
"risky": "Неясно",
"completed": "Завершено", "completed": "Завершено",
"disallowedIsp": "Провайдер заблокирован", "disallowedIsp": "Провайдер заблокирован",
"originalsOnly": "Только Originals", "originalsOnly": "Только Originals",
+1
View File
@@ -36,6 +36,7 @@
"yes": "Evet", "yes": "Evet",
"no": "Hayır", "no": "Hayır",
"failed": "Başarısız", "failed": "Başarısız",
"risky": "Belirsiz",
"completed": "Tamamlandı", "completed": "Tamamlandı",
"disallowedIsp": "İzin Verilmeyen ISP", "disallowedIsp": "İzin Verilmeyen ISP",
"originalsOnly": "Yalnızca Orijinaller", "originalsOnly": "Yalnızca Orijinaller",
+1
View File
@@ -36,6 +36,7 @@
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"failed": "Failed", "failed": "Failed",
"risky": "Билгесез",
"completed": "Completed", "completed": "Completed",
"disallowedIsp": "Disallowed ISP", "disallowedIsp": "Disallowed ISP",
"originalsOnly": "Originals Only", "originalsOnly": "Originals Only",
+1
View File
@@ -36,6 +36,7 @@
"yes": "支持", "yes": "支持",
"no": "不支持", "no": "不支持",
"failed": "测试失败", "failed": "测试失败",
"risky": "存疑",
"completed": "检测完成", "completed": "检测完成",
"disallowedIsp": "不允许的 ISP", "disallowedIsp": "不允许的 ISP",
"originalsOnly": "仅限原创", "originalsOnly": "仅限原创",
+1
View File
@@ -36,6 +36,7 @@
"yes": "支援", "yes": "支援",
"no": "不支援", "no": "不支援",
"failed": "測試失敗", "failed": "測試失敗",
"risky": "存疑",
"completed": "檢測完成", "completed": "檢測完成",
"disallowedIsp": "不允許的網際網路服務供應商", "disallowedIsp": "不允許的網際網路服務供應商",
"originalsOnly": "僅限原創", "originalsOnly": "僅限原創",
+1 -1
View File
@@ -55,7 +55,7 @@ const wrapCssInjectionWithScope = (css?: string) => {
return null return null
} }
const scopeRoot = CSS_INJECTION_SCOPE_ROOT const scopeRoot = CSS_INJECTION_SCOPE_ROOT
const scopeLimit = CSS_INJECTION_SCOPE_LIMIT const scopeLimit = `${CSS_INJECTION_SCOPE_LIMIT} > *`
const scopedBlock = `@scope (${scopeRoot}) to (${scopeLimit}) { const scopedBlock = `@scope (${scopeRoot}) to (${scopeLimit}) {
${css} ${css}
}` }`
+4 -2
View File
@@ -40,6 +40,7 @@ const STATUS_LABEL_KEYS: Record<string, string> = {
Yes: 'tests.statuses.test.yes', Yes: 'tests.statuses.test.yes',
No: 'tests.statuses.test.no', No: 'tests.statuses.test.no',
Failed: 'tests.statuses.test.failed', Failed: 'tests.statuses.test.failed',
Risky: 'tests.statuses.test.risky',
Completed: 'tests.statuses.test.completed', Completed: 'tests.statuses.test.completed',
'Disallowed ISP': 'tests.statuses.test.disallowedIsp', 'Disallowed ISP': 'tests.statuses.test.disallowedIsp',
'Originals Only': 'tests.statuses.test.originalsOnly', 'Originals Only': 'tests.statuses.test.originalsOnly',
@@ -196,7 +197,7 @@ const UnlockPage = () => {
if (status === 'Pending') return 'default' if (status === 'Pending') return 'default'
if (status === 'Yes') return 'success' if (status === 'Yes') return 'success'
if (status === 'No') return 'error' if (status === 'No') return 'error'
if (status === 'Soon') return 'warning' if (status === 'Soon' || status === 'Risky') return 'warning'
if (status.includes('Failed')) return 'error' if (status.includes('Failed')) return 'error'
if (status === 'Completed') return 'info' if (status === 'Completed') return 'info'
if ( if (
@@ -222,7 +223,8 @@ const UnlockPage = () => {
const getStatusBorderColor = (status: string) => { const getStatusBorderColor = (status: string) => {
if (status === 'Yes') return theme.palette.success.main if (status === 'Yes') return theme.palette.success.main
if (status === 'No') return theme.palette.error.main if (status === 'No') return theme.palette.error.main
if (status === 'Soon') return theme.palette.warning.main if (status === 'Soon' || status === 'Risky')
return theme.palette.warning.main
if (status.includes('Failed')) return theme.palette.error.main if (status.includes('Failed')) return theme.palette.error.main
if (status === 'Completed') return theme.palette.info.main if (status === 'Completed') return theme.palette.info.main
return theme.palette.divider return theme.palette.divider
+5 -1
View File
@@ -43,7 +43,11 @@ export const getWebdavStatus = (signature: string): WebdavStatus => {
} }
export const setWebdavStatus = (signature: string, status: WebdavStatus) => { export const setWebdavStatus = (signature: string, status: WebdavStatus) => {
if (!signature || !canUseStorage()) return if (!canUseStorage()) return
if (!signature) {
localStorage.removeItem(WEBDAV_STATUS_KEY)
return
}
const payload: WebdavStatusCache = { const payload: WebdavStatusCache = {
signature, signature,
+3 -1
View File
@@ -14,7 +14,9 @@ export default defineConfig({
legacy({ legacy({
modernTargets: ['edge>=109', 'safari>=14'], modernTargets: ['edge>=109', 'safari>=14'],
renderLegacyChunks: false, renderLegacyChunks: false,
modernPolyfills: ['es.object.has-own', 'web.structured-clone'], // Auto-detected from the emitted bundle, dependencies included,
// so post-Safari-14 APIs never depend on extending a manual list.
modernPolyfills: true,
additionalModernPolyfills: [ additionalModernPolyfills: [
path.resolve('./src/polyfills/matchMedia.js'), path.resolve('./src/polyfills/matchMedia.js'),
path.resolve('./src/polyfills/WeakRef.js'), path.resolve('./src/polyfills/WeakRef.js'),