mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-10 04:12:08 +08:00
fix(ipc): reconcile Specta bindings with dev
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:2": {
|
||||
"version": "24.21.0",
|
||||
"pnpmVersion": "12.10.0"
|
||||
"pnpmVersion": "12.11.2"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "1.99.0",
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ff92739239d50701d56d10eb77fff5083a556fe471496674aa066c4f73b6b6d2","body_hash":"e7b165b458a07108c2e6bfe6f4ed61f5659ce81512691dda091165d5966ee732","compiler_version":"v0.89.21","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.87"}}
|
||||
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"8296ef86aad04ad225d29c7fe51dd18dfa50f258c46e8bd24e443193cc6f80fc","body_hash":"e7b165b458a07108c2e6bfe6f4ed61f5659ce81512691dda091165d5966ee732","compiler_version":"v0.89.21","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.87"}}
|
||||
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"924af5fdc64061cfbf66fb584c8b07e2ac230c60","version":"v0.89.21"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.23","digest":"sha256:2c78aaba1c108e130e2d6d01e4f2cca334ea04c53e6f258913ac34173fe7e3b2","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.23@sha256:2c78aaba1c108e130e2d6d01e4f2cca334ea04c53e6f258913ac34173fe7e3b2"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.23","digest":"sha256:c15c3d1208df10c5b588a3657be53742aa982ae0909d1eb1812525268794ca64","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.23@sha256:c15c3d1208df10c5b588a3657be53742aa982ae0909d1eb1812525268794ca64"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.23","digest":"sha256:02ffc56dd40158223064ef03a78c2d9717473c93723b4e403d455ea6f0b09ae0","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.23@sha256:02ffc56dd40158223064ef03a78c2d9717473c93723b4e403d455ea6f0b09ae0"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.25","digest":"sha256:9be0a86220e807a0ecc89e53d7453468f7a53fbc6b3d1efd2299025ffe01d086","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.25@sha256:9be0a86220e807a0ecc89e53d7453468f7a53fbc6b3d1efd2299025ffe01d086"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.12.2","digest":"sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6","pinned_image":"ghcr.io/github/github-mcp-server:v1.12.2@sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6"}],"has_pull_request_target":true,"mcp_servers":[{"name":"github","tools":["get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","issue_read","list_issue_types","list_issues","list_pull_requests","pull_request_read","search_issues","search_pull_requests"]},{"name":"safeoutputs","tools":["add_comment","add_labels","missing_data","missing_tool","noop","remove_labels"]}]}
|
||||
# This file was automatically generated by gh-aw (v0.89.21). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
|
||||
#
|
||||
@@ -541,7 +541,7 @@ jobs:
|
||||
GH_AW_SKILL_DIR: ".github/skills"
|
||||
run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh"
|
||||
- name: Make Copilot quota exhaustion non-blocking
|
||||
run: "node <<'NODE'\nconst fs = require('fs');\nconst harnessPath = `${process.env.RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs`;\nconst harness = fs.readFileSync(harnessPath, 'utf8');\nconst quotaBranch = [\n ' if (isQuotaExceeded) {',\n ' log(`attempt ${attempt + 1}: Copilot quota exceeded — not retrying`);',\n ' return { action: \"stop\" };',\n ' }',\n].join('\\n');\nconst nonBlockingQuotaBranch = [\n ' if (isQuotaExceeded) {',\n ' log(`attempt ${attempt + 1}: Copilot quota exceeded — not retrying`);',\n ' if (/\\\\b429\\\\b|Too Many Requests/i.test(result.output)) {',\n ' log(`attempt ${attempt + 1}: Copilot quota unavailable — exiting 0`);',\n ' return { action: \"stop\", exitCode: 0 };',\n ' }',\n ' return { action: \"stop\" };',\n ' }',\n].join('\\n');\n\nif (harness.split(quotaBranch).length !== 2) {\n throw new Error('Unexpected Copilot quota branch; re-audit the gh-aw upgrade');\n}\nfs.writeFileSync(harnessPath, harness.replace(quotaBranch, nonBlockingQuotaBranch));\nNODE"
|
||||
run: "node <<'NODE'\nconst fs = require('fs');\nconst harnessPath = `${process.env.RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs`;\nconst harness = fs.readFileSync(harnessPath, 'utf8');\nconst quotaBranch = [\n ' if (isQuotaExceeded) {',\n ' log(`attempt ${attempt + 1}: Copilot quota exceeded — not retrying`);',\n ' return { action: \"stop\" };',\n ' }',\n].join('\\n');\nconst nonBlockingQuotaBranch = [\n ' let quotaErrorMsg = null;',\n ' if (isQuotaExceeded) {',\n ' log(`attempt ${attempt + 1}: Copilot quota exceeded — not retrying`);',\n ' quotaErrorMsg = { action: \"stop\" };',\n ' }',\n ' if (/\\\\b429\\\\b|Too Many Requests/i.test(result.output)) {',\n ' log(`attempt ${attempt + 1}: Copilot quota unavailable — exiting 0`);',\n ' quotaErrorMsg = { action: \"stop\", exitCode: 0 };',\n ' }',\n ' if (quotaErrorMsg) {',\n ' return quotaErrorMsg;',\n ' }',\n].join('\\n');\n\nif (harness.split(quotaBranch).length !== 2) {\n throw new Error('Unexpected Copilot quota branch; re-audit the gh-aw upgrade');\n}\nfs.writeFileSync(harnessPath, harness.replace(quotaBranch, nonBlockingQuotaBranch));\nNODE"
|
||||
|
||||
- name: Download container images
|
||||
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.23@sha256:2c78aaba1c108e130e2d6d01e4f2cca334ea04c53e6f258913ac34173fe7e3b2 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.23@sha256:c15c3d1208df10c5b588a3657be53742aa982ae0909d1eb1812525268794ca64 ghcr.io/github/gh-aw-firewall/squid:0.28.23@sha256:02ffc56dd40158223064ef03a78c2d9717473c93723b4e403d455ea6f0b09ae0 ghcr.io/github/gh-aw-mcpg:v0.4.25@sha256:9be0a86220e807a0ecc89e53d7453468f7a53fbc6b3d1efd2299025ffe01d086 ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.12.2@sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6
|
||||
|
||||
@@ -41,13 +41,17 @@ pre-agent-steps:
|
||||
' }',
|
||||
].join('\n');
|
||||
const nonBlockingQuotaBranch = [
|
||||
' let quotaErrorMsg = null;',
|
||||
' if (isQuotaExceeded) {',
|
||||
' log(`attempt ${attempt + 1}: Copilot quota exceeded — not retrying`);',
|
||||
' if (/\\b429\\b|Too Many Requests/i.test(result.output)) {',
|
||||
' log(`attempt ${attempt + 1}: Copilot quota unavailable — exiting 0`);',
|
||||
' return { action: "stop", exitCode: 0 };',
|
||||
' }',
|
||||
' return { action: "stop" };',
|
||||
' quotaErrorMsg = { action: "stop" };',
|
||||
' }',
|
||||
' if (/\\b429\\b|Too Many Requests/i.test(result.output)) {',
|
||||
' log(`attempt ${attempt + 1}: Copilot quota unavailable — exiting 0`);',
|
||||
' quotaErrorMsg = { action: "stop", exitCode: 0 };',
|
||||
' }',
|
||||
' if (quotaErrorMsg) {',
|
||||
' return quotaErrorMsg;',
|
||||
' }',
|
||||
].join('\n');
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
name: SignPath Test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
HUSKY: 0
|
||||
|
||||
jobs:
|
||||
sign-windows:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: '1.99.0'
|
||||
targets: x86_64-pc-windows-msvc
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '24.21.0'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Prepare build resources
|
||||
run: pnpm run prebuild x86_64-pc-windows-msvc
|
||||
|
||||
- name: Build test installer
|
||||
shell: pwsh
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
run: |
|
||||
$configPath = Join-Path $env:RUNNER_TEMP 'signpath-test.json'
|
||||
'{"bundle":{"createUpdaterArtifacts":false}}' |
|
||||
Set-Content -LiteralPath $configPath -Encoding utf8
|
||||
pnpm exec tauri build --target x86_64-pc-windows-msvc --bundles nsis --config $configPath
|
||||
|
||||
- name: Upload unsigned installer
|
||||
id: unsigned
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows-x64-unsigned
|
||||
path: target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Sign with SignPath test certificate
|
||||
uses: signpath/github-action-submit-signing-request@v3
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: '5b298f4d-010d-4bae-b665-70551a58839c'
|
||||
project-slug: 'clash-verge-rev'
|
||||
signing-policy-slug: 'test-signing'
|
||||
artifact-configuration-slug: 'initial'
|
||||
github-artifact-id: ${{ steps.unsigned.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: signed
|
||||
|
||||
- name: Inspect installer signature
|
||||
shell: pwsh
|
||||
run: |
|
||||
Get-ChildItem -Path signed/*-setup.exe | ForEach-Object {
|
||||
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName
|
||||
if (-not $signature.SignerCertificate) {
|
||||
throw "No Authenticode signer certificate found for $($_.Name)"
|
||||
}
|
||||
$signature | Format-List Path, Status, StatusMessage, SignerCertificate
|
||||
}
|
||||
|
||||
- name: Upload signed test installer
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows-x64-test-signed
|
||||
path: signed/*-setup.exe
|
||||
if-no-files-found: error
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
nodejs 24.21.0
|
||||
pnpm 12.10.0
|
||||
pnpm 12.11.2
|
||||
rust 1.99.0
|
||||
|
||||
Generated
+8
-13
@@ -1089,7 +1089,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "clash-verge"
|
||||
version = "2.5.8"
|
||||
version = "2.5.9"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -1175,7 +1175,7 @@ dependencies = [
|
||||
"windows-service",
|
||||
"windows-sys 0.61.2",
|
||||
"winreg 0.56.0",
|
||||
"zip 8.6.0",
|
||||
"zip 9.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4048,9 +4048,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rust2"
|
||||
version = "0.16.5"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b"
|
||||
checksum = "fde178a3caf126c440fa15628147772d8ee590a39477d711353fbe2e58a73a5b"
|
||||
dependencies = [
|
||||
"sha2 0.11.0",
|
||||
]
|
||||
@@ -4223,8 +4223,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996"
|
||||
source = "git+https://github.com/wonfen/muda.git?rev=547de49352791653fbdfa785f20f3c7ece8468e2#547de49352791653fbdfa785f20f3c7ece8468e2"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"dpi",
|
||||
@@ -10151,9 +10150,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "8.6.0"
|
||||
version = "9.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
|
||||
checksum = "e49ade69c4434c55dd651d36fe05f38e0578db948b773cbc01c48c4b5e560692"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bzip2",
|
||||
@@ -10166,6 +10165,7 @@ dependencies = [
|
||||
"indexmap 2.14.0",
|
||||
"lzma-rust2",
|
||||
"memchr",
|
||||
"nix",
|
||||
"pbkdf2",
|
||||
"ppmd-rust",
|
||||
"sha1 0.11.0",
|
||||
@@ -10283,8 +10283,3 @@ dependencies = [
|
||||
"syn 3.0.5",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
[[patch.unused]]
|
||||
name = "tray-icon"
|
||||
version = "0.24.1"
|
||||
source = "git+https://github.com/Tunglies/tray-icon.git?branch=fix%2Fmacos-27-click-reentrancy#5750c67d02d32f2af100e120ad665959d4268cef"
|
||||
|
||||
+2
-4
@@ -47,10 +47,8 @@ codegen-units = 1
|
||||
split-debuginfo = "unpacked"
|
||||
|
||||
[patch.crates-io]
|
||||
# macOS Golden Gate fix for tray-icon click reentrancy issue
|
||||
# Remove the following patch once the fix is merged upstream(tauri)
|
||||
# https://github.com/tauri-apps/tray-icon/pull/365
|
||||
tray-icon = { git = "https://github.com/Tunglies/tray-icon.git", branch = "fix/macos-27-click-reentrancy" }
|
||||
# Backport Windows menu command ID recycling on the muda 0.20 compatibility branch.
|
||||
muda = { git = "https://github.com/wonfen/muda.git", rev = "547de49352791653fbdfa785f20f3c7ece8468e2" }
|
||||
|
||||
[workspace.dependencies]
|
||||
clash-verge-draft = { path = "crates/clash-verge-draft" }
|
||||
|
||||
+3
-20
@@ -1,24 +1,9 @@
|
||||
## v2.5.8
|
||||
## v2.5.9
|
||||
|
||||
<details>
|
||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||
|
||||
- 修复端口设置混淆保存值与运行值、输入无效时缺少提示的问题
|
||||
- 修复从开始菜单再次打开应用后,窗口反复弹出并提示启动失败的问题
|
||||
- 修复多个代理集合包含同名节点时,代理组中的节点变灰并显示 ambiguous、无法选择的问题
|
||||
- 修复手动更新时重复下载后台已下载的安装包、启动时选「稍后」后每次启动都重新下载的问题
|
||||
- 修复下载更新时镜像缓慢或卡住不会切换下载地址、点击「取消」无法中止下载的问题
|
||||
- 修复关闭「自动检查更新」后,启动时仍弹出已下载更新的安装提示的问题
|
||||
- 修复从 Dock 退出、注销或关机时,应用可能长时间卡住无法退出的问题
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
- 修复 Windows 升级后因文件权限异常无法启动、需要手动清理配置的问题
|
||||
- 修复 Windows 服务启动类型被改为手动后,软件启动卡住约两分钟的问题,并提供一键修复
|
||||
|
||||
**🍎 macOS**
|
||||
|
||||
- 修复 macOS 关闭 TUN 或退出后,系统 DNS 仍停留在 `114.114.114.114` 导致无法解析域名的问题
|
||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置的问题
|
||||
|
||||
</details>
|
||||
|
||||
@@ -31,11 +16,9 @@
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 优化端口设置窗口的实现,精简重复代码
|
||||
- 优化侧边栏流量图表的 CPU 占用
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
- 优化 Windows 服务安全检查未通过时的提示:启动、安装或修复服务时说明原因与内核占用,并提供处理方法
|
||||
- Windows 系统代理默认绕过 `*.local` 域名,访问局域网设备的 `.local` 主机名时不再经过代理
|
||||
|
||||
</details>
|
||||
|
||||
@@ -11,13 +11,14 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
|
||||
|
||||
<p align="center">
|
||||
Languages:
|
||||
<a href="./README.md">简体中文</a> ·
|
||||
<a href="./docs/README_en.md">English</a> ·
|
||||
<a href="./docs/README_es.md">Español</a> ·
|
||||
<a href="./docs/README_ru.md">Русский</a> ·
|
||||
<a href="./docs/README_fa.md">فارسی</a> ·
|
||||
<a href="./docs/README_ja.md">日本語</a> ·
|
||||
<a href="./docs/README_ko.md">한국어</a> ·
|
||||
<a href="./docs/README_fa.md">فارسی</a>
|
||||
<a href="./docs/README_pt.md">Português</a> ·
|
||||
<a href="./docs/README_ru.md">Русский</a> ·
|
||||
<a href="./README.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
## Preview
|
||||
|
||||
@@ -37,6 +37,8 @@ notifications:
|
||||
service:
|
||||
adminInstallPrompt: يتطلب تثبيت خدمة Clash Verge صلاحيات المسؤول.
|
||||
adminUninstallPrompt: يتطلب إلغاء تثبيت خدمة Clash Verge صلاحيات المسؤول.
|
||||
core:
|
||||
alreadyRunning: 'تعذّر بدء النواة: توجد عملية نواة قيد التشغيل بالفعل. أغلق التطبيق المعني أو أنهِ العملية المتبقية، ثم أعد المحاولة. إذا استمرت المشكلة، فأعد تشغيل الكمبيوتر.'
|
||||
launchGuard:
|
||||
currentExeError: 'تعذر تحديد موقع التطبيق: {error}'
|
||||
homeDirError: 'تعذر تحديد المجلد الرئيسي للمستخدم الحالي. سيتم إنهاء التطبيق: {error}'
|
||||
@@ -68,6 +70,8 @@ tray:
|
||||
global: عام
|
||||
profiles: الملفات التعريفية
|
||||
proxies: وكلاء
|
||||
testLatency: اختبار التأخير
|
||||
testingLatency: جارٍ اختبار التأخير…
|
||||
systemProxy: وكيل النظام
|
||||
tunMode: وضع TUN
|
||||
closeAllConnections: إغلاق كل الاتصالات
|
||||
|
||||
@@ -37,6 +37,8 @@ notifications:
|
||||
service:
|
||||
adminInstallPrompt: Für die Installation des Clash-Verge-Dienstes sind Administratorrechte erforderlich.
|
||||
adminUninstallPrompt: Für die Deinstallation des Clash-Verge-Dienstes sind Administratorrechte erforderlich.
|
||||
core:
|
||||
alreadyRunning: 'Der Kern kann nicht gestartet werden: Ein Kernprozess läuft bereits. Beenden Sie die zugehörige App oder den verbliebenen Prozess und versuchen Sie es erneut. Wenn das Problem weiterhin besteht, starten Sie den Computer neu.'
|
||||
launchGuard:
|
||||
currentExeError: 'Der Speicherort der Anwendung konnte nicht ermittelt werden: {error}'
|
||||
homeDirError: 'Das Benutzerverzeichnis des aktuellen Benutzers konnte nicht ermittelt werden. Die Anwendung wird beendet: {error}'
|
||||
@@ -68,6 +70,8 @@ tray:
|
||||
global: Global
|
||||
profiles: Profile
|
||||
proxies: Proxy
|
||||
testLatency: Latenz testen
|
||||
testingLatency: Latenz wird getestet…
|
||||
systemProxy: Systemproxy
|
||||
tunMode: TUN-Modus
|
||||
closeAllConnections: Alle Verbindungen schließen
|
||||
|
||||
@@ -37,6 +37,8 @@ notifications:
|
||||
service:
|
||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
||||
core:
|
||||
alreadyRunning: 'Cannot start the core: a core process is already running. Close the related app or stop the leftover process, then retry. If the problem persists, restart your computer.'
|
||||
launchGuard:
|
||||
currentExeError: 'Unable to determine the application location: {error}'
|
||||
homeDirError: 'Unable to determine the current user''s home directory. The application will exit: {error}'
|
||||
@@ -68,6 +70,8 @@ tray:
|
||||
global: Global
|
||||
profiles: Profiles
|
||||
proxies: Proxies
|
||||
testLatency: Test Latency
|
||||
testingLatency: Testing latency…
|
||||
systemProxy: System Proxy
|
||||
tunMode: TUN Mode
|
||||
closeAllConnections: Close All Connections
|
||||
|
||||
@@ -37,6 +37,8 @@ notifications:
|
||||
service:
|
||||
adminInstallPrompt: Instalar el servicio de Clash Verge requiere privilegios de administrador.
|
||||
adminUninstallPrompt: Desinstalar el servicio de Clash Verge requiere privilegios de administrador.
|
||||
core:
|
||||
alreadyRunning: 'No se puede iniciar el núcleo: ya hay un proceso del núcleo en ejecución. Cierre la aplicación correspondiente o finalice el proceso restante y vuelva a intentarlo. Si el problema persiste, reinicie el equipo.'
|
||||
launchGuard:
|
||||
currentExeError: 'No se pudo determinar la ubicación de la aplicación: {error}'
|
||||
homeDirError: 'No se pudo determinar la carpeta de inicio del usuario actual. La aplicación se cerrará: {error}'
|
||||
@@ -68,6 +70,8 @@ tray:
|
||||
global: Global
|
||||
profiles: Perfiles
|
||||
proxies: Proxies
|
||||
testLatency: Probar latencia
|
||||
testingLatency: Probando latencia…
|
||||
systemProxy: Proxy del sistema
|
||||
tunMode: Modo TUN
|
||||
closeAllConnections: Cerrar todas las conexiones
|
||||
|
||||
@@ -37,6 +37,8 @@ notifications:
|
||||
service:
|
||||
adminInstallPrompt: نصب سرویس Clash Verge به دسترسی مدیر نیاز دارد.
|
||||
adminUninstallPrompt: حذف سرویس Clash Verge به دسترسی مدیر نیاز دارد.
|
||||
core:
|
||||
alreadyRunning: 'هسته شروع نمیشود: یک فرایند هسته از قبل در حال اجراست. برنامه مرتبط را ببندید یا فرایند باقیمانده را متوقف کنید و دوباره تلاش کنید. اگر مشکل ادامه داشت، رایانه را راهاندازی مجدد کنید.'
|
||||
launchGuard:
|
||||
currentExeError: 'مکان برنامه مشخص نشد: {error}'
|
||||
homeDirError: 'پوشهٔ خانگی کاربر فعلی مشخص نشد. برنامه بسته خواهد شد: {error}'
|
||||
@@ -68,6 +70,8 @@ tray:
|
||||
global: سراسری
|
||||
profiles: پروفایلها
|
||||
proxies: پروکسیها
|
||||
testLatency: آزمایش تأخیر
|
||||
testingLatency: در حال آزمایش تأخیر…
|
||||
systemProxy: پروکسی سیستم
|
||||
tunMode: حالت TUN
|
||||
closeAllConnections: بستن همه اتصالها
|
||||
|
||||
@@ -37,6 +37,8 @@ notifications:
|
||||
service:
|
||||
adminInstallPrompt: Menginstal layanan Clash Verge memerlukan hak administrator.
|
||||
adminUninstallPrompt: Menghapus instalasi layanan Clash Verge memerlukan hak administrator.
|
||||
core:
|
||||
alreadyRunning: 'Tidak dapat memulai inti: proses inti sudah berjalan. Tutup aplikasi terkait atau hentikan proses yang tersisa, lalu coba lagi. Jika masalah berlanjut, mulai ulang komputer.'
|
||||
launchGuard:
|
||||
currentExeError: 'Lokasi aplikasi tidak dapat ditentukan: {error}'
|
||||
homeDirError: 'Direktori beranda pengguna saat ini tidak dapat ditentukan. Aplikasi akan ditutup: {error}'
|
||||
@@ -68,6 +70,8 @@ tray:
|
||||
global: Global
|
||||
profiles: Profil
|
||||
proxies: Proksi
|
||||
testLatency: Uji Latensi
|
||||
testingLatency: Menguji latensi…
|
||||
systemProxy: Proksi Sistem
|
||||
tunMode: Mode TUN
|
||||
closeAllConnections: Tutup Semua Koneksi
|
||||
|
||||
@@ -37,6 +37,8 @@ notifications:
|
||||
service:
|
||||
adminInstallPrompt: Clash Verge サービスのインストールには管理者権限が必要です。
|
||||
adminUninstallPrompt: Clash Verge サービスのアンインストールには管理者権限が必要です。
|
||||
core:
|
||||
alreadyRunning: コアを起動できません:コアプロセスが実行中です。関連アプリまたは残存プロセスを終了して再試行してください。解決しない場合は、コンピューターを再起動してください。
|
||||
launchGuard:
|
||||
currentExeError: アプリケーションの場所を確認できませんでした:{error}
|
||||
homeDirError: 現在のユーザーのホームディレクトリを確認できないため、アプリケーションを終了します:{error}
|
||||
@@ -68,6 +70,8 @@ tray:
|
||||
global: グローバル
|
||||
profiles: プロファイル
|
||||
proxies: プロキシ
|
||||
testLatency: 遅延テスト
|
||||
testingLatency: 遅延をテスト中…
|
||||
systemProxy: システムプロキシ
|
||||
tunMode: TUN モード
|
||||
closeAllConnections: すべての接続を閉じる
|
||||
|
||||
@@ -37,6 +37,8 @@ notifications:
|
||||
service:
|
||||
adminInstallPrompt: Clash Verge 서비스 설치에는 관리자 권한이 필요합니다.
|
||||
adminUninstallPrompt: Clash Verge 서비스 제거에는 관리자 권한이 필요합니다.
|
||||
core:
|
||||
alreadyRunning: 코어를 시작할 수 없습니다. 이미 실행 중인 코어 프로세스가 있습니다. 관련 앱이나 남아 있는 프로세스를 종료한 뒤 다시 시도하세요. 문제가 계속되면 컴퓨터를 다시 시작하세요.
|
||||
launchGuard:
|
||||
currentExeError: '애플리케이션 위치를 확인할 수 없습니다: {error}'
|
||||
homeDirError: '현재 사용자의 홈 디렉터리를 확인할 수 없어 애플리케이션을 종료합니다: {error}'
|
||||
@@ -68,6 +70,8 @@ tray:
|
||||
global: 글로벌
|
||||
profiles: 프로필
|
||||
proxies: 프록시
|
||||
testLatency: 지연 테스트
|
||||
testingLatency: 지연 테스트 중…
|
||||
systemProxy: 시스템 프록시
|
||||
tunMode: TUN 모드
|
||||
closeAllConnections: 모든 연결 닫기
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
_version: 1
|
||||
notifications:
|
||||
dashboardToggled:
|
||||
title: Painel
|
||||
body: A visibilidade do painel foi atualizada.
|
||||
clashModeChanged:
|
||||
title: Troca de modo
|
||||
body: Alterado para {mode}.
|
||||
systemProxyToggled:
|
||||
title: Proxy do sistema
|
||||
'on': O proxy do sistema foi ativado.
|
||||
'off': O proxy do sistema foi desativado.
|
||||
systemProxyFailed:
|
||||
title: Proxy do sistema
|
||||
body: Não foi possível definir o proxy do sistema. Abra o Clash Verge para ver o motivo.
|
||||
systemProxyGuardStopped:
|
||||
title: Proteção do proxy do sistema
|
||||
body: A proteção parou após várias falhas de execução. Abra o Clash Verge para ver o motivo.
|
||||
tunModeToggled:
|
||||
title: Modo TUN
|
||||
'on': O modo TUN foi ativado.
|
||||
'off': O modo TUN foi desativado.
|
||||
lightweightModeEntered:
|
||||
title: Modo leve
|
||||
body: Modo leve ativado.
|
||||
profilesReactivated:
|
||||
title: Perfis
|
||||
body: Perfil reativado.
|
||||
appQuit:
|
||||
title: Prestes a sair
|
||||
body: O Clash Verge está prestes a sair.
|
||||
updateReady:
|
||||
title: Atualização do Clash Verge
|
||||
body: Uma nova versão (v{version}) foi baixada e está pronta para instalar.
|
||||
installNow: Instalar agora
|
||||
later: Depois
|
||||
service:
|
||||
adminInstallPrompt: A instalação do serviço do Clash Verge requer privilégios de administrador.
|
||||
adminUninstallPrompt: A desinstalação do serviço do Clash Verge requer privilégios de administrador.
|
||||
core:
|
||||
alreadyRunning: 'Não é possível iniciar o núcleo: já há um processo do núcleo em execução. Feche o aplicativo correspondente ou encerre o processo remanescente e tente novamente. Se o problema persistir, reinicie o computador.'
|
||||
launchGuard:
|
||||
currentExeError: 'Não foi possível determinar a localização do aplicativo: {error}'
|
||||
homeDirError: 'Não foi possível determinar o diretório pessoal do usuário atual. O aplicativo será encerrado: {error}'
|
||||
translocated: O macOS está executando este aplicativo a partir de um caminho temporário de App Translocation. No Finder, mova-o manualmente para /Applications ou ~/Applications e abra-o novamente.
|
||||
rejected: |-
|
||||
Este aplicativo deve ser iniciado a partir de um diretório de Aplicativos.
|
||||
|
||||
{reason}
|
||||
notInBundle: O executável não está dentro de um pacote de aplicativo.
|
||||
canonicalizeBundleError: 'Não foi possível resolver o caminho do pacote do aplicativo: {error}'
|
||||
bundleNameError: Não foi possível determinar o nome do pacote do aplicativo. O aplicativo será encerrado.
|
||||
replacePrompt: Já existe um aplicativo em {destination}. Substituí-lo e reabrir a partir de Aplicativos?
|
||||
movePrompt: Para iniciar o serviço em segundo plano com segurança, o aplicativo precisa ser movido para {destination}. Mover e reabrir agora?
|
||||
createApplicationsDirError: 'Não foi possível criar o diretório de Aplicativos: {error}'
|
||||
moveFailed: Falha ao mover o aplicativo. Mova-o manualmente no Finder e tente novamente.
|
||||
replaceFailed: 'Não foi possível substituir o aplicativo existente com segurança: {error}'
|
||||
relaunchFailed: O aplicativo foi copiado, mas não pôde ser reaberto automaticamente. Abra-o manualmente a partir de Aplicativos.
|
||||
cancel: Cancelar
|
||||
move: Mover
|
||||
ok: OK
|
||||
tray:
|
||||
dashboard: Painel
|
||||
ruleMode: Modo regra
|
||||
globalMode: Modo global
|
||||
directMode: Modo direto
|
||||
outboundModes: Modos de saída
|
||||
rule: Regra
|
||||
direct: Direto
|
||||
global: Global
|
||||
profiles: Perfis
|
||||
proxies: Proxies
|
||||
testLatency: Test Latency
|
||||
testingLatency: Testing latency…
|
||||
systemProxy: Proxy do sistema
|
||||
tunMode: Modo TUN
|
||||
closeAllConnections: Fechar todas as conexões
|
||||
lightweightMode: Modo leve
|
||||
copyEnv: Copiar variáveis de ambiente
|
||||
confDir: Diretório de configuração
|
||||
coreDir: Diretório do núcleo
|
||||
logsDir: Diretório de logs
|
||||
openDir: Abrir diretório
|
||||
appLog: Log do aplicativo
|
||||
coreLog: Log do núcleo
|
||||
restartClash: Reiniciar o núcleo do Clash
|
||||
restartApp: Reiniciar o aplicativo
|
||||
vergeVersion: Versão do Verge
|
||||
more: Mais
|
||||
exit: Sair
|
||||
tooltip:
|
||||
systemProxy: Proxy do sistema
|
||||
tun: TUN
|
||||
profile: Perfil
|
||||
@@ -37,6 +37,8 @@ notifications:
|
||||
service:
|
||||
adminInstallPrompt: Для установки службы Clash Verge требуются права администратора.
|
||||
adminUninstallPrompt: Для удаления службы Clash Verge требуются права администратора.
|
||||
core:
|
||||
alreadyRunning: 'Не удалось запустить ядро: процесс ядра уже работает. Закройте соответствующее приложение или завершите оставшийся процесс и повторите попытку. Если проблема сохраняется, перезагрузите компьютер.'
|
||||
launchGuard:
|
||||
currentExeError: 'Не удалось определить расположение приложения: {error}'
|
||||
homeDirError: 'Не удалось определить домашний каталог текущего пользователя. Приложение будет закрыто: {error}'
|
||||
@@ -68,6 +70,8 @@ tray:
|
||||
global: Глобальный
|
||||
profiles: Профили
|
||||
proxies: Прокси
|
||||
testLatency: Проверить задержку
|
||||
testingLatency: Проверка задержки…
|
||||
systemProxy: Системный прокси
|
||||
tunMode: Режим TUN
|
||||
closeAllConnections: Закрыть все соединения
|
||||
|
||||
@@ -37,6 +37,8 @@ notifications:
|
||||
service:
|
||||
adminInstallPrompt: Clash Verge hizmetini kurmak için yönetici ayrıcalıkları gerekir.
|
||||
adminUninstallPrompt: Clash Verge hizmetini kaldırmak için yönetici ayrıcalıkları gerekir.
|
||||
core:
|
||||
alreadyRunning: 'Çekirdek başlatılamıyor: bir çekirdek işlemi zaten çalışıyor. İlgili uygulamayı kapatın veya kalan işlemi sonlandırıp yeniden deneyin. Sorun devam ederse bilgisayarı yeniden başlatın.'
|
||||
launchGuard:
|
||||
currentExeError: 'Uygulamanın konumu belirlenemedi: {error}'
|
||||
homeDirError: 'Geçerli kullanıcının ana dizini belirlenemedi. Uygulama kapatılacak: {error}'
|
||||
@@ -68,6 +70,8 @@ tray:
|
||||
global: Küresel
|
||||
profiles: Profiller
|
||||
proxies: Vekil'ler
|
||||
testLatency: Gecikmeyi Test Et
|
||||
testingLatency: Gecikme test ediliyor…
|
||||
systemProxy: Sistem Vekil'i
|
||||
tunMode: TUN Modu
|
||||
closeAllConnections: Tüm Bağlantıları Kapat
|
||||
|
||||
@@ -37,6 +37,8 @@ notifications:
|
||||
service:
|
||||
adminInstallPrompt: Clash Verge хезмәтен урнаштыру өчен администратор хокуклары кирәк.
|
||||
adminUninstallPrompt: Clash Verge хезмәтен бетерү өчен администратор хокуклары кирәк.
|
||||
core:
|
||||
alreadyRunning: 'Төшне башлап булмый: төш процессы инде эшли. Тиешле кушымтаны ябыгыз яки калган процессны тәмамлагыз һәм яңадан сынап карагыз. Проблема хәл ителмәсә, санакны яңадан кабызыгыз.'
|
||||
launchGuard:
|
||||
currentExeError: 'Кушымтаның урнашу урынын билгеләп булмады: {error}'
|
||||
homeDirError: 'Хәзерге кулланучының өй каталогын билгеләп булмады. Кушымта ябылачак: {error}'
|
||||
@@ -68,6 +70,8 @@ tray:
|
||||
global: Глобаль
|
||||
profiles: Профильләр
|
||||
proxies: Проксилар
|
||||
testLatency: Кичектерүне тикшерү
|
||||
testingLatency: Кичектерү тикшерелә…
|
||||
systemProxy: Системалы прокси
|
||||
tunMode: TUN режимы
|
||||
closeAllConnections: Барлык тоташуларны ябу
|
||||
|
||||
@@ -37,6 +37,8 @@ notifications:
|
||||
service:
|
||||
adminInstallPrompt: 安装 Clash Verge 服务需要管理员权限
|
||||
adminUninstallPrompt: 卸载 Clash Verge 服务需要管理员权限
|
||||
core:
|
||||
alreadyRunning: 无法启动内核:已有内核进程正在运行。请退出相关应用或清理残留进程后重试。若仍无法解决,请重启电脑。
|
||||
launchGuard:
|
||||
currentExeError: 无法确定应用位置:{error}
|
||||
homeDirError: 无法确定当前用户主目录,应用将退出:{error}
|
||||
@@ -68,6 +70,8 @@ tray:
|
||||
global: 全局
|
||||
profiles: 订阅
|
||||
proxies: 代理
|
||||
testLatency: 测试延迟
|
||||
testingLatency: 正在测试延迟…
|
||||
systemProxy: 系统代理
|
||||
tunMode: TUN 模式
|
||||
closeAllConnections: 关闭所有连接
|
||||
|
||||
@@ -37,6 +37,8 @@ notifications:
|
||||
service:
|
||||
adminInstallPrompt: 安裝 Clash Verge 服務需要管理員權限
|
||||
adminUninstallPrompt: 解除安裝 Clash Verge 服務需要管理員權限
|
||||
core:
|
||||
alreadyRunning: 無法啟動核心:已有核心程序正在執行。請結束相關應用程式或清理殘留程序後重試。若仍無法解決,請重新啟動電腦。
|
||||
launchGuard:
|
||||
currentExeError: 無法判斷應用程式位置:{error}
|
||||
homeDirError: 無法判斷目前使用者的個人目錄,應用程式將結束:{error}
|
||||
@@ -68,6 +70,8 @@ tray:
|
||||
global: 全域
|
||||
profiles: 訂閱
|
||||
proxies: 代理
|
||||
testLatency: 測試延遲
|
||||
testingLatency: 正在測試延遲…
|
||||
systemProxy: 系統代理
|
||||
tunMode: 虛擬網路介面卡模式
|
||||
closeAllConnections: 關閉所有連線
|
||||
|
||||
@@ -1,3 +1,55 @@
|
||||
## v2.5.8
|
||||
|
||||
<details>
|
||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||
|
||||
- 修复系统托盘代理组行不显示延迟、超时项与未测过项无法区分的问题
|
||||
- 修复不同订阅源的同名节点测速和延迟显示指向错误节点的问题
|
||||
- 修复端口设置混淆保存值与运行值、输入无效时缺少提示的问题
|
||||
- 修复已有内核进程阻止启动时缺少多语言说明和处理建议的问题
|
||||
- 修复从开始菜单再次打开应用后,窗口反复弹出并提示启动失败的问题
|
||||
- 修复多个代理集合包含同名节点时,代理组中的节点变灰并显示 ambiguous、无法选择的问题
|
||||
- 修复手动更新时重复下载后台已下载的安装包、启动时选「稍后」后每次启动都重新下载的问题
|
||||
- 修复下载更新时镜像缓慢或卡住不会切换下载地址、点击「取消」无法中止下载的问题
|
||||
- 修复关闭「自动检查更新」后,启动时仍弹出已下载更新的安装提示的问题
|
||||
- 修复从 Dock 退出、注销或关机时,应用可能长时间卡住无法退出的问题
|
||||
- 修复宽窗口下「订阅」页每行显示过多卡片,导致配置名称被截断的问题
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
- 修复 Windows 升级后因文件权限异常无法启动、需要手动清理配置的问题
|
||||
- 修复 Windows 服务启动类型被改为手动后,软件启动卡住约两分钟的问题,并提供一键修复
|
||||
- 修复 Windows 长时间运行后,托盘菜单点击无响应的问题
|
||||
|
||||
**🍎 macOS**
|
||||
|
||||
- 修复 macOS 关闭 TUN 或退出后,系统 DNS 仍停留在 `114.114.114.114` 导致无法解析域名的问题
|
||||
|
||||
**🖥️/🍎 Windows/macOS**
|
||||
|
||||
- 修复 Windows/macOS 打开系统托盘菜单时,节点延迟与自动选择状态未刷新的问题
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- 新增系统托盘代理组中的「测试延迟」菜单项
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 优化端口设置窗口的实现,精简重复代码
|
||||
- 优化侧边栏流量图表的 CPU 占用
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
- 优化 Windows 服务安全检查未通过时的提示:启动、安装或修复服务时说明原因与内核占用,并提供处理方法
|
||||
|
||||
</details>
|
||||
|
||||
## v2.5.7
|
||||
|
||||
<details>
|
||||
|
||||
+4
-3
@@ -11,13 +11,14 @@ A Clash Meta GUI built with <a href="https://github.com/tauri-apps/tauri">Tauri<
|
||||
|
||||
<p align="center">
|
||||
Languages:
|
||||
<a href="../README.md">简体中文</a> ·
|
||||
<a href="./README_en.md">English</a> ·
|
||||
<a href="./README_es.md">Español</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="./README_fa.md">فارسی</a> ·
|
||||
<a href="./README_ja.md">日本語</a> ·
|
||||
<a href="./README_ko.md">한국어</a> ·
|
||||
<a href="./README_fa.md">فارسی</a>
|
||||
<a href="./README_pt.md">Português</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="../README.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
## Preview
|
||||
|
||||
+4
-3
@@ -11,13 +11,14 @@ Una interfaz gráfica para Clash Meta construida con <a href="https://github.com
|
||||
|
||||
<p align="center">
|
||||
Idiomas:
|
||||
<a href="../README.md">简体中文</a> ·
|
||||
<a href="./README_en.md">English</a> ·
|
||||
<a href="./README_es.md">Español</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="./README_fa.md">فارسی</a> ·
|
||||
<a href="./README_ja.md">日本語</a> ·
|
||||
<a href="./README_ko.md">한국어</a> ·
|
||||
<a href="./README_fa.md">فارسی</a>
|
||||
<a href="./README_pt.md">Português</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="../README.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
## Vista previa
|
||||
|
||||
+4
-3
@@ -11,13 +11,14 @@
|
||||
|
||||
<p align="center">
|
||||
زبانها:
|
||||
<a href="../README.md">简体中文</a> ·
|
||||
<a href="./README_en.md">English</a> ·
|
||||
<a href="./README_es.md">Español</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="./README_fa.md">فارسی</a> ·
|
||||
<a href="./README_ja.md">日本語</a> ·
|
||||
<a href="./README_ko.md">한국어</a> ·
|
||||
<a href="./README_fa.md">فارسی</a>
|
||||
<a href="./README_pt.md">Português</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="../README.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
## پیشنمایش
|
||||
|
||||
+4
-3
@@ -11,13 +11,14 @@
|
||||
|
||||
<p align="center">
|
||||
言語:
|
||||
<a href="../README.md">简体中文</a> ·
|
||||
<a href="./README_en.md">English</a> ·
|
||||
<a href="./README_es.md">Español</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="./README_fa.md">فارسی</a> ·
|
||||
<a href="./README_ja.md">日本語</a> ·
|
||||
<a href="./README_ko.md">한국어</a> ·
|
||||
<a href="./README_fa.md">فارسی</a>
|
||||
<a href="./README_pt.md">Português</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="../README.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
## プレビュー
|
||||
|
||||
+4
-3
@@ -11,13 +11,14 @@
|
||||
|
||||
<p align="center">
|
||||
언어:
|
||||
<a href="../README.md">简体中文</a> ·
|
||||
<a href="./README_en.md">English</a> ·
|
||||
<a href="./README_es.md">Español</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="./README_fa.md">فارسی</a> ·
|
||||
<a href="./README_ja.md">日本語</a> ·
|
||||
<a href="./README_ko.md">한국어</a> ·
|
||||
<a href="./README_fa.md">فارسی</a>
|
||||
<a href="./README_pt.md">Português</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="../README.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
## 미리보기
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<h1 align="center">
|
||||
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
|
||||
<br>
|
||||
Continuação do <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<h3 align="center">
|
||||
Uma interface gráfica para o Clash Meta construída com <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
|
||||
</h3>
|
||||
|
||||
<p align="center">
|
||||
Idiomas:
|
||||
<a href="./README_en.md">English</a> ·
|
||||
<a href="./README_es.md">Español</a> ·
|
||||
<a href="./README_fa.md">فارسی</a> ·
|
||||
<a href="./README_ja.md">日本語</a> ·
|
||||
<a href="./README_ko.md">한국어</a> ·
|
||||
<a href="./README_pt.md">Português</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="../README.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
## Pré-visualização
|
||||
|
||||
| Escuro | Claro |
|
||||
| -------------------------------------- | -------------------------------------- |
|
||||
|  |  |
|
||||
|
||||
## Instalação
|
||||
|
||||
Acesse a [página de lançamentos](https://github.com/clash-verge-rev/clash-verge-rev/releases) e baixe o instalador correspondente à sua plataforma.<br>
|
||||
Oferecemos pacotes para Windows (x64/x86), Linux (x64/arm64) e macOS 10.15+ (Intel/Apple).
|
||||
|
||||
#### Como escolher o canal de lançamento
|
||||
|
||||
| Canal | Descrição | Link |
|
||||
| :---------- | :--------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- |
|
||||
| Stable | Versões oficiais de alta confiabilidade, ideais para o uso diário. | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
|
||||
| Alpha (EOL) | Versões antigas usadas para validar o fluxo de publicação. | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
|
||||
| AutoBuild | Versões contínuas para testes e feedback. Espere mudanças típicas de beta. | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
|
||||
|
||||
#### Guias de instalação e perguntas frequentes
|
||||
|
||||
Consulte a [documentação do projeto](https://clash-verge-rev.github.io/) para ver os passos de instalação, a solução de problemas e as perguntas frequentes.
|
||||
|
||||
### Canal do Telegram
|
||||
|
||||
Entre em [@clash_verge_rev](https://t.me/clash_verge_re) para acompanhar as novidades.
|
||||
|
||||
---
|
||||
|
||||
## Promoções
|
||||
|
||||
#### [Doggygo VPN — Acelerador global focado em desempenho](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
- Serviço internacional de alto desempenho com teste gratuito, planos com desconto, desbloqueio de streaming e suporte de primeira linha ao protocolo Hysteria.
|
||||
- Cadastre-se pelo link exclusivo do Clash Verge e ganhe um teste de 3 dias com 1 GB de tráfego diário: [Cadastre-se](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||
- Cupom exclusivo de 20% de desconto para usuários do Clash Verge: `verge20` (limitado a 500 usos)
|
||||
- Plano promocional a partir de ¥15,8 por mês com 160 GB, mais 20% de desconto adicional no pagamento anual
|
||||
- Equipe sediada no exterior para um serviço confiável, com até 50% de comissão compartilhada
|
||||
- Clusters balanceados com rotas dedicadas de alta velocidade (compatíveis com clientes antigos), latência extremamente baixa, reprodução 4K sem travamentos
|
||||
- Primeiro provedor global com **protocolo QUIC**, agora com protocolos da família QUIC mais rápidos (ideal para o cliente Clash Verge)
|
||||
- Desbloqueia serviços de streaming e acesso ao ChatGPT
|
||||
- Site oficial: [https://狗狗加速.com](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||
|
||||
### 🤖 [GPTKefu — Plataforma de atendimento ao cliente com IA integrada ao Crisp](https://gptkefu.com)
|
||||
|
||||
- 🧠 Compreensão profunda de todo o contexto da conversa + reconhecimento de imagens, com respostas profissionais e precisas de forma automática, sem respostas robóticas.
|
||||
- ♾️ **Respostas ilimitadas**, sem preocupação com cotas — ao contrário de outros produtos de IA que cobram por mensagem.
|
||||
- 💬 Consultas de pré-venda, suporte pós-venda, resolução de problemas complexos — cobre todos os cenários com facilidade, com casos reais comprovados.
|
||||
- ⚡ Configuração em 3 minutos, sem curva de aprendizado — melhora na hora a eficiência e a satisfação do cliente.
|
||||
- 🎁 Teste gratuito de 14 dias do plano Premium — experimente antes de pagar: 👉 [Testar grátis](https://gptkefu.com)
|
||||
- 📢 Canal do Telegram do atendimento com IA: [@crisp_ai](https://t.me/crisp_ai)
|
||||
|
||||
---
|
||||
|
||||
## Funcionalidades
|
||||
|
||||
- Baseado em Rust de alto desempenho e no framework Tauri 2
|
||||
- Inclui o núcleo integrado [Clash.Meta (mihomo)](https://github.com/MetaCubeX/mihomo) e permite trocar para o canal `Alpha`
|
||||
- Interface limpa e elegante, com controles de cor do tema, ícones de grupos de proxy/bandeja e `CSS Injection`
|
||||
- Gerenciamento avançado de perfis (ferramentas Merge e Script) com sugestões de sintaxe para as configurações
|
||||
- Controle do proxy do sistema, modo guardião e suporte a `TUN` (adaptador de rede virtual)
|
||||
- Editores visuais para nós e regras
|
||||
- Backup e sincronização via WebDAV
|
||||
|
||||
### Perguntas frequentes
|
||||
|
||||
Acesse a [página de FAQ](https://clash-verge-rev.github.io/faq/windows.html) para ver instruções específicas por plataforma.
|
||||
|
||||
### Doações
|
||||
|
||||
[Apoie o desenvolvimento do Clash Verge Rev](https://github.com/sponsors/clash-verge-rev)
|
||||
|
||||
## Desenvolvimento
|
||||
|
||||
Consulte o [CONTRIBUTING.md](../CONTRIBUTING.md) para ver as diretrizes de contribuição.
|
||||
|
||||
Depois de instalar todos os pré-requisitos do **Tauri**, execute o ambiente de desenvolvimento com:
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
pnpm run prebuild
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Contribuições
|
||||
|
||||
Issues e pull requests são bem-vindos.
|
||||
|
||||
## Agradecimentos
|
||||
|
||||
O Clash Verge Rev é baseado nos projetos a seguir, ou se inspira neles:
|
||||
|
||||
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): Interface gráfica para o Clash baseada em Tauri. Compatível com Windows, macOS e Linux.
|
||||
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Crie aplicativos de desktop menores, mais rápidos e mais seguros com um frontend web.
|
||||
- [Dreamacro/clash](https://github.com/Dreamacro/clash): Túnel baseado em regras escrito em Go.
|
||||
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): Túnel baseado em regras escrito em Go.
|
||||
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): Interface do Clash para Windows e macOS.
|
||||
- [vitejs/vite](https://github.com/vitejs/vite): Ferramentas de frontend de nova geração com uma experiência ultrarrápida.
|
||||
|
||||
## Licença
|
||||
|
||||
Licença GPL-3.0. Consulte o [arquivo de licença](../LICENSE) para mais detalhes.
|
||||
+4
-3
@@ -11,13 +11,14 @@ Clash Meta GUI базируется на <a href="https://github.com/tauri-apps/
|
||||
|
||||
<p align="center">
|
||||
Языки:
|
||||
<a href="../README.md">简体中文</a> ·
|
||||
<a href="./README_en.md">English</a> ·
|
||||
<a href="./README_es.md">Español</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="./README_fa.md">فارسی</a> ·
|
||||
<a href="./README_ja.md">日本語</a> ·
|
||||
<a href="./README_ko.md">한국어</a> ·
|
||||
<a href="./README_fa.md">فارسی</a>
|
||||
<a href="./README_pt.md">Português</a> ·
|
||||
<a href="./README_ru.md">Русский</a> ·
|
||||
<a href="../README.md">简体中文</a>
|
||||
</p>
|
||||
## Предпросмотр
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.5.8",
|
||||
"version": "2.5.9",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"prepare": "husky || true",
|
||||
@@ -146,5 +146,5 @@
|
||||
]
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@12.10.0"
|
||||
"packageManager": "pnpm@12.11.2"
|
||||
}
|
||||
|
||||
Generated
+61
-61
@@ -7,153 +7,153 @@ importers:
|
||||
configDependencies: {}
|
||||
packageManagerDependencies:
|
||||
pnpm:
|
||||
specifier: 12.10.0
|
||||
version: 12.10.0
|
||||
specifier: 12.11.2
|
||||
version: 12.11.2
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm/exe.android-arm64@12.10.0':
|
||||
resolution: {integrity: sha512-QMMRzOI/7CU4xLbKY6O2BZn3VI4+a8LLyim18H7it8llBWuBPMqDqUURE8045k+7lI8/lI2/tElhyKptEviKDQ==}
|
||||
'@pnpm/exe.android-arm64@12.11.2':
|
||||
resolution: {integrity: sha512-owLw13OfPkEoY+hLBNeeG+dCFhywr7q2d/EHPz4mNL2RhKnv8gNwMARkjkiDQu234aItVwFNDeGnmQXm+ikkAg==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@pnpm/exe.android-x64@12.10.0':
|
||||
resolution: {integrity: sha512-41zwlROW+t523Nfqo4rWp8IPiSK5vnAWrnCey26Je45rX1SMNc4ABY7OFmPw41/t+Y0CE+4tjTj+0s9YGqPM5w==}
|
||||
'@pnpm/exe.android-x64@12.11.2':
|
||||
resolution: {integrity: sha512-GYPkVebl4iokOA/4MPSpGoUVfFlsth7uqOCe1XiaiM23LAdvUOMJBObqHJijr89ZqjwHm9pO6vO45GRwJ2U2qA==}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.10.0':
|
||||
resolution: {integrity: sha512-L+r7AD8Fjf8Vcc4ZRJl+Pjzc27CIeQ/FiRwPmOluFO2UcXubmGHdW+OuMUxrU0vkoXiGR61Ft0RtaPwTRY4W/g==}
|
||||
'@pnpm/exe.darwin-arm64@12.11.2':
|
||||
resolution: {integrity: sha512-n9M4On66y8FDxUAKgMO1FXt0qihyG4e+56p3/F0uo8Bx9KplW8BwKVnXigzCJ8aNM6sVVPkOE6M9XXbeVIxnZA==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.10.0':
|
||||
resolution: {integrity: sha512-/TCF1K6E+4PnbkLgqGS1Iqlf5bHp6FDzuXgu6++PMMAFkF7Yk5EF3BcfG1gYYjpkvlJBccvVMsRBnWFbSyqu1w==}
|
||||
'@pnpm/exe.darwin-x64@12.11.2':
|
||||
resolution: {integrity: sha512-oSW7n3sBT0c0cyk4kj10T2Y6Mpm+OFEiCAWozgX039tdoI6yqJF1uaFD3sw7ugLdu6VV3+uY0pa1TUZIPdxDWA==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.freebsd-x64@12.10.0':
|
||||
resolution: {integrity: sha512-k7/WFA91E2qhnR/2EOEv402U8bjP758y2pybbdiB5Hrd4KhgviVOJp5cFrXpKgd8HZJs/RZN7cdmMvU2Q2BVgg==}
|
||||
'@pnpm/exe.freebsd-x64@12.11.2':
|
||||
resolution: {integrity: sha512-qbzc3KoDpa/Lkf8vs3jnDe6dUUbso+6eLIyssYFMiliLpYNxn8dirk11YoNnOQcdHVB50v5RFls1Yid6ttT5pg==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.10.0':
|
||||
resolution: {integrity: sha512-ChD8bN0lRCRaL+CMOaMUgZQ4WHzEkDXmDHrZgXgdoo6yymQKKzr66Ic7Sl/D7Ky1cVUcFEzHjlq4fGDJwt6Lqg==}
|
||||
'@pnpm/exe.linux-arm64-musl@12.11.2':
|
||||
resolution: {integrity: sha512-uMEYDwKRktGwANubQ+kYCwmhNwg3Ik6qK89ahsbQ5cP30R/5FVJb/T99vr9XRUBC/A7mZI6eeT92oyVas9ACyg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.10.0':
|
||||
resolution: {integrity: sha512-egl5RUSQFh7kETBdmMhaTuaQKP1pjH6Cxq370zgpFFKJXF+GGkIzK+jLNazrjX9pKIWF3iqn7kUeyP6yKxzBoA==}
|
||||
'@pnpm/exe.linux-arm64@12.11.2':
|
||||
resolution: {integrity: sha512-6e8ZAQUwFXAWHHHv5FesWqf/kEK04wuitBv5f/gxzkC0DiBfaixr+f+6JQede8SF4Ji5LcMwVFJbzAtffpWxEg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-ppc64@12.10.0':
|
||||
resolution: {integrity: sha512-YJ6bEZ+stPpv71CGfGQQeyIpMx/zSMEcAXQzG+rjBtzVfyNsxYciRevhJ+PE9b2qKqN78Mh/1tOMmx9ly4VXGw==}
|
||||
'@pnpm/exe.linux-ppc64@12.11.2':
|
||||
resolution: {integrity: sha512-Ui8A4nepkVupx2VpuFfysEluMvWK6gTB5rgVts7Ss/+8dj0RD28x3F/seNeH/zSypgC6yKdV6qsKVQh/uffTuQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-riscv64@12.10.0':
|
||||
resolution: {integrity: sha512-njfd4euctC1zH1nVBbm6+r1c+0y/TtgqN523rk/KMSxqItpV5QAVa9Yz48Rt5DOIKmuLq7xXOhsQodBlbTk3pw==}
|
||||
'@pnpm/exe.linux-riscv64@12.11.2':
|
||||
resolution: {integrity: sha512-i1yLdQ5bu+wjQ+srtFoJAaY2gN/+oXHFdjul4llCeoDxHE0rTIG3R41NhHfCjyFPW4+CpbBT58933CL9sMOCnA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-s390x@12.10.0':
|
||||
resolution: {integrity: sha512-4JI2F8pYGMcsG6ny61OiBSvZeDozc64TbPozbTU2X5uCsaFCsCvcpr/WwyypwCv+/mNjcIhaKhlxpwJYsnteIg==}
|
||||
'@pnpm/exe.linux-s390x@12.11.2':
|
||||
resolution: {integrity: sha512-YS1/lhPjjTsN4QL50VaFZKRa3Uc/bDnhE8eKbJMFvuX06UuFS5YaiDg04APoZdfVSGqTfX27QAg7rfLMSWaqxA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.10.0':
|
||||
resolution: {integrity: sha512-xayd2iG8ugdvjlGeOvsfm+iSe0bFu9RTX3/8HWOLAQmHdbLKi4hPbxP0S4c9tj8pNej/IxVBQLJksRXFlhNjPw==}
|
||||
'@pnpm/exe.linux-x64-musl@12.11.2':
|
||||
resolution: {integrity: sha512-7eIxQR05fvJxCujUSKp5KTtehJ7QqrgqP/HD1+BcB/SaV2Be4XI1PEHiOgA1zxsiMIeMvTF6E4G4dYHrFjeXRg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-x64@12.10.0':
|
||||
resolution: {integrity: sha512-PFkzWEghFLKuocxMHtn57mlJ59f5aBbqjX6FCUtiunEHqxTxOibSduQPs3Llm6VWbAXBjD3VrX29Z+bjU1Dpyw==}
|
||||
'@pnpm/exe.linux-x64@12.11.2':
|
||||
resolution: {integrity: sha512-FK7QKe24AYgppJwO3mpooYptduuvIGqKJPi5ac7MdnWkjUy46JBfYhCW0grnkWKC8YOf0x+vGviuQqJEj3SpQQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.10.0':
|
||||
resolution: {integrity: sha512-0HtTS+TLONWd/dNSczyOkK0UeZ36cbdhC/2ljZ61DPhvc4jTb7ur0yU47On0in7Im3yXwzTY4vtF8ykrDwc+NQ==}
|
||||
'@pnpm/exe.win32-arm64@12.11.2':
|
||||
resolution: {integrity: sha512-PCTjVDdJb6VvraZxrEeFv6EqEA0hhIsAySx28e95J2Tqpi88hLpgOXgNqV0vpxq9A+b8GLQfyk/D+1UlJPv/YQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@pnpm/exe.win32-x64@12.10.0':
|
||||
resolution: {integrity: sha512-IcW3rehTGzJ7YnmHEWnbnJakjbBmnH0r29qiajK9+6b9psMF9IjocFr0dyitRVSfXxyW/FI5PIK3mOswGoyFTg==}
|
||||
'@pnpm/exe.win32-x64@12.11.2':
|
||||
resolution: {integrity: sha512-8sgX86OvRjJ3yZs/TvGR2L31wH9ScPZ+f+geKI3ikuB0RRiK3i4LlTRO8OXN6pdRqj8epMde4312MCjj7JsJdA==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
pnpm@12.10.0:
|
||||
resolution: {integrity: sha512-Eip7bd+dQCaLwNsal4CJm5Xk+out/Ke9RQYVrYz3wmWfX1acIO18U4hrx7CNDVClT0rVBwaEf1aC7GxQSEFlOQ==}
|
||||
pnpm@12.11.2:
|
||||
resolution: {integrity: sha512-JaUWUqGWeFr18ovabf6NxYLr76SsHRgCIc4TkCMcX7dmN4IupLbkYzznSs3LCnf3Ja8RutOBd/3t2u6BipVcuQ==}
|
||||
engines: {node: '>=18.*'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm/exe.android-arm64@12.10.0':
|
||||
'@pnpm/exe.android-arm64@12.11.2':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.android-x64@12.10.0':
|
||||
'@pnpm/exe.android-x64@12.11.2':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.10.0':
|
||||
'@pnpm/exe.darwin-arm64@12.11.2':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.10.0':
|
||||
'@pnpm/exe.darwin-x64@12.11.2':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.freebsd-x64@12.10.0':
|
||||
'@pnpm/exe.freebsd-x64@12.11.2':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.10.0':
|
||||
'@pnpm/exe.linux-arm64-musl@12.11.2':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.10.0':
|
||||
'@pnpm/exe.linux-arm64@12.11.2':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-ppc64@12.10.0':
|
||||
'@pnpm/exe.linux-ppc64@12.11.2':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-riscv64@12.10.0':
|
||||
'@pnpm/exe.linux-riscv64@12.11.2':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-s390x@12.10.0':
|
||||
'@pnpm/exe.linux-s390x@12.11.2':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.10.0':
|
||||
'@pnpm/exe.linux-x64-musl@12.11.2':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64@12.10.0':
|
||||
'@pnpm/exe.linux-x64@12.11.2':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.10.0':
|
||||
'@pnpm/exe.win32-arm64@12.11.2':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-x64@12.10.0':
|
||||
'@pnpm/exe.win32-x64@12.11.2':
|
||||
optional: true
|
||||
|
||||
pnpm@12.10.0:
|
||||
pnpm@12.11.2:
|
||||
optionalDependencies:
|
||||
'@pnpm/exe.android-arm64': 12.10.0
|
||||
'@pnpm/exe.android-x64': 12.10.0
|
||||
'@pnpm/exe.darwin-arm64': 12.10.0
|
||||
'@pnpm/exe.darwin-x64': 12.10.0
|
||||
'@pnpm/exe.freebsd-x64': 12.10.0
|
||||
'@pnpm/exe.linux-arm64': 12.10.0
|
||||
'@pnpm/exe.linux-arm64-musl': 12.10.0
|
||||
'@pnpm/exe.linux-ppc64': 12.10.0
|
||||
'@pnpm/exe.linux-riscv64': 12.10.0
|
||||
'@pnpm/exe.linux-s390x': 12.10.0
|
||||
'@pnpm/exe.linux-x64': 12.10.0
|
||||
'@pnpm/exe.linux-x64-musl': 12.10.0
|
||||
'@pnpm/exe.win32-arm64': 12.10.0
|
||||
'@pnpm/exe.win32-x64': 12.10.0
|
||||
'@pnpm/exe.android-arm64': 12.11.2
|
||||
'@pnpm/exe.android-x64': 12.11.2
|
||||
'@pnpm/exe.darwin-arm64': 12.11.2
|
||||
'@pnpm/exe.darwin-x64': 12.11.2
|
||||
'@pnpm/exe.freebsd-x64': 12.11.2
|
||||
'@pnpm/exe.linux-arm64': 12.11.2
|
||||
'@pnpm/exe.linux-arm64-musl': 12.11.2
|
||||
'@pnpm/exe.linux-ppc64': 12.11.2
|
||||
'@pnpm/exe.linux-riscv64': 12.11.2
|
||||
'@pnpm/exe.linux-s390x': 12.11.2
|
||||
'@pnpm/exe.linux-x64': 12.11.2
|
||||
'@pnpm/exe.linux-x64-musl': 12.11.2
|
||||
'@pnpm/exe.win32-arm64': 12.11.2
|
||||
'@pnpm/exe.win32-x64': 12.11.2
|
||||
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.5.8"
|
||||
version = "2.5.9"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -80,7 +80,7 @@ tauri-plugin-fs = "2.6.0"
|
||||
tauri-plugin-process = "2.4.0"
|
||||
tauri-plugin-deep-link = "2.6.1"
|
||||
tauri-plugin-window-state = "2.5.0"
|
||||
zip = "8.6.0"
|
||||
zip = "9.0.1"
|
||||
flate2 = "1.1.10"
|
||||
reqwest_dav = "0.3.3"
|
||||
aes-gcm = { version = "0.11.1" }
|
||||
|
||||
@@ -373,6 +373,8 @@ pub struct ProfilePatch {
|
||||
#[specta(type = Option<std::string::String>)]
|
||||
pub url: Option<String>,
|
||||
pub selected: Option<Vec<crate::config::PrfSelected>>,
|
||||
#[specta(type = Option<std::collections::BTreeMap<std::string::String, std::string::String>>)]
|
||||
pub latency_test_urls: Option<std::collections::BTreeMap<String, String>>,
|
||||
pub extra: Option<crate::config::PrfExtra>,
|
||||
pub updated: Option<usize>,
|
||||
pub option: Option<PrfOption>,
|
||||
@@ -387,6 +389,7 @@ impl From<ProfilePatch> for PrfItem {
|
||||
file: patch.file,
|
||||
url: patch.url,
|
||||
selected: patch.selected,
|
||||
latency_test_urls: patch.latency_test_urls,
|
||||
extra: patch.extra,
|
||||
updated: patch.updated,
|
||||
option: patch.option,
|
||||
|
||||
@@ -108,20 +108,32 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_proxy_view() -> CmdResult<ProxyViewV1> {
|
||||
proxy_view(None).await
|
||||
}
|
||||
|
||||
pub(crate) async fn proxy_view(provider_timeout: Option<std::time::Duration>) -> CmdResult<ProxyViewV1> {
|
||||
let runtime = Config::runtime().await;
|
||||
let latest_runtime = runtime.latest_arc();
|
||||
let runtime_group_order = runtime_group_order(latest_runtime.config.as_ref());
|
||||
let group_scopes = runtime_group_scopes(latest_runtime.config.as_ref());
|
||||
|
||||
let mihomo = Handle::mihomo();
|
||||
let (proxies, providers) = tokio::join!(mihomo.get_proxies(), mihomo.get_proxy_providers(),);
|
||||
let (proxies, providers) = tokio::join!(mihomo.get_proxies(), async {
|
||||
match provider_timeout {
|
||||
Some(timeout) => tokio::time::timeout(timeout, mihomo.get_proxy_providers())
|
||||
.await
|
||||
.ok()
|
||||
.and_then(Result::ok),
|
||||
None => mihomo.get_proxy_providers().await.ok(),
|
||||
}
|
||||
});
|
||||
let proxies = proxies.stringify_err()?;
|
||||
|
||||
Ok(ProxyViewBuilder::build(ProxyViewInput {
|
||||
runtime_group_order,
|
||||
group_scopes,
|
||||
proxies,
|
||||
providers: providers.ok(),
|
||||
providers,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,10 @@ pub struct PrfItem {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub selected: Option<Vec<PrfSelected>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[specta(type = Option<std::collections::BTreeMap<std::string::String, std::string::String>>)]
|
||||
pub latency_test_urls: Option<std::collections::BTreeMap<String, String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra: Option<PrfExtra>,
|
||||
|
||||
@@ -229,6 +233,7 @@ impl PrfItem {
|
||||
desc: Some(desc),
|
||||
file: Some(file),
|
||||
url: None,
|
||||
latency_test_urls: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: Some(PrfOption {
|
||||
@@ -408,6 +413,7 @@ impl PrfItem {
|
||||
desc: desc.cloned(),
|
||||
file: Some(file),
|
||||
url: Some(url.as_str().into()),
|
||||
latency_test_urls: None,
|
||||
selected: None,
|
||||
extra,
|
||||
option: Some(PrfOption {
|
||||
|
||||
@@ -250,6 +250,7 @@ impl IProfiles {
|
||||
patch!(each, item, file);
|
||||
patch!(each, item, url);
|
||||
patch!(each, item, selected);
|
||||
patch!(each, item, latency_test_urls);
|
||||
patch!(each, item, extra);
|
||||
patch!(each, item, updated);
|
||||
patch!(each, item, option);
|
||||
|
||||
@@ -400,7 +400,9 @@ impl CoreManager {
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
if matches!(*mode, RunningMode::NotRunning) {
|
||||
clash_verge_service_ipc::execution::check_sidecar_available().await?;
|
||||
clash_verge_service_ipc::execution::check_sidecar_available()
|
||||
.await
|
||||
.map_err(super::state::explain_core_occupancy)?;
|
||||
}
|
||||
if !matches!(status, ServiceStatus::SidecarAllowed) {
|
||||
SERVICE_MANAGER.allow_sidecar_for_session()?;
|
||||
@@ -640,7 +642,11 @@ impl CoreManager {
|
||||
{
|
||||
run_service_start_with_sidecar_fallback(
|
||||
|| self.start_core_by_service(),
|
||||
clash_verge_service_ipc::execution::check_sidecar_available,
|
||||
|| async {
|
||||
clash_verge_service_ipc::execution::check_sidecar_available()
|
||||
.await
|
||||
.map_err(super::state::explain_core_occupancy)
|
||||
},
|
||||
|error| self.start_sidecar_after_service_failure(error),
|
||||
|reason| {
|
||||
crate::core::runstate::RUN_STATE
|
||||
@@ -685,7 +691,10 @@ impl CoreManager {
|
||||
ServiceHealth::VersionMismatch => "registered service is unavailable or incompatible".to_owned(),
|
||||
_ => return Ok(()),
|
||||
};
|
||||
if let Err(error) = clash_verge_service_ipc::execution::check_sidecar_available().await {
|
||||
if let Err(error) = clash_verge_service_ipc::execution::check_sidecar_available()
|
||||
.await
|
||||
.map_err(super::state::explain_core_occupancy)
|
||||
{
|
||||
logging!(
|
||||
warn,
|
||||
Type::Core,
|
||||
|
||||
@@ -17,6 +17,15 @@ use std::path::Path;
|
||||
use tauri_plugin_mihomo::MihomoExt as _;
|
||||
use tauri_plugin_shell::ShellExt as _;
|
||||
|
||||
pub(super) fn explain_core_occupancy(error: anyhow::Error) -> anyhow::Error {
|
||||
let detail = error.to_string();
|
||||
if detail.starts_with("process verge-mihomo") && detail.ends_with("; refusing a second core") {
|
||||
error.context(clash_verge_i18n::t!("core.alreadyRunning").into_owned())
|
||||
} else {
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn sidecar_config_without_tun(yaml: &str) -> Result<std::string::String> {
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
@@ -154,7 +163,8 @@ impl CoreManager {
|
||||
self.wait_for_sidecar_exit().await?;
|
||||
let execution = clash_verge_service_ipc::execution::reserve_sidecar()
|
||||
.await
|
||||
.inspect_err(service::record_residual_service)?;
|
||||
.inspect_err(service::record_residual_service)
|
||||
.map_err(explain_core_occupancy)?;
|
||||
self.core_stopped();
|
||||
|
||||
let sidecar_ipc = dirs::sidecar_ipc_path()?;
|
||||
@@ -511,6 +521,20 @@ mod readiness_tests {
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn core_occupancy_explanation_preserves_the_original_error() {
|
||||
for detail in [
|
||||
"process verge-mihomo remains after IPC failure; refusing a second core",
|
||||
"process verge-mihomo-alpha.exe (PID 123) is still running; refusing a second core",
|
||||
] {
|
||||
let error = super::explain_core_occupancy(anyhow::anyhow!(detail));
|
||||
assert_eq!(error.to_string(), clash_verge_i18n::t!("core.alreadyRunning"));
|
||||
assert_eq!(error.root_cause().to_string(), detail);
|
||||
}
|
||||
let error = super::explain_core_occupancy(anyhow::anyhow!("could not inspect remaining cores"));
|
||||
assert_eq!(error.to_string(), "could not inspect remaining cores");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sidecar_readiness_poll_is_bounded_and_accepts_a_real_api_response() -> anyhow::Result<()> {
|
||||
let attempts = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri_plugin_mihomo::models::{
|
||||
DelayHistory, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
|
||||
DelayHistory, Extra, Proxies, Proxy, ProxyProvider, ProxyProviders, ProxyType, VehicleType,
|
||||
};
|
||||
|
||||
pub struct ProxyViewInput {
|
||||
@@ -77,6 +77,8 @@ pub struct ProxyGroupView {
|
||||
pub test_url: Option<String>,
|
||||
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
||||
pub history: Vec<DelayHistory>,
|
||||
#[specta(type = HashMap<String, crate::ipc::ProxyExtra>)]
|
||||
pub extra: HashMap<String, Extra>,
|
||||
#[serde(flatten)]
|
||||
pub capabilities: ProxyCapabilities,
|
||||
pub members: Vec<ProxyMemberRef>,
|
||||
@@ -93,6 +95,8 @@ pub struct ProxyNodeView {
|
||||
pub alive: bool,
|
||||
#[specta(type = Vec<crate::ipc::DelayHistory>)]
|
||||
pub history: Vec<DelayHistory>,
|
||||
#[specta(type = HashMap<String, crate::ipc::ProxyExtra>)]
|
||||
pub extra: HashMap<String, Extra>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -258,7 +262,7 @@ impl ProxyViewBuilder {
|
||||
} else {
|
||||
ProxyViewProviderState::Unavailable
|
||||
};
|
||||
let (mut core_groups, core_nodes) = partition_core(proxies);
|
||||
let (mut core_groups, core_nodes) = partition_core(proxies, providers.is_some());
|
||||
let (mut records, core_node_ids) = build_core_records(core_nodes);
|
||||
let (providers, provider_candidates) = build_provider_records(providers, &mut records);
|
||||
let resolver = MemberResolver {
|
||||
@@ -291,14 +295,14 @@ impl ProxyViewBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
fn partition_core(proxies: Proxies) -> (BTreeMap<String, Proxy>, BTreeMap<String, Proxy>) {
|
||||
fn partition_core(proxies: Proxies, providers_available: bool) -> (BTreeMap<String, Proxy>, BTreeMap<String, Proxy>) {
|
||||
let mut groups = BTreeMap::new();
|
||||
let mut nodes = BTreeMap::new();
|
||||
|
||||
for (name, proxy) in proxies.proxies {
|
||||
if proxy.all.is_some() {
|
||||
groups.insert(name, proxy);
|
||||
} else {
|
||||
} else if !providers_available || proxy.provider_name.is_empty() {
|
||||
nodes.insert(name, proxy);
|
||||
}
|
||||
}
|
||||
@@ -406,6 +410,7 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
|
||||
test_url,
|
||||
alive,
|
||||
history,
|
||||
extra,
|
||||
udp,
|
||||
xudp,
|
||||
tfo,
|
||||
@@ -439,6 +444,7 @@ fn build_group(name: String, proxy: Proxy, resolver: &MemberResolver<'_>) -> Pro
|
||||
icon,
|
||||
test_url,
|
||||
history,
|
||||
extra,
|
||||
capabilities: ProxyCapabilities {
|
||||
udp,
|
||||
xudp,
|
||||
@@ -458,6 +464,7 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
|
||||
test_url,
|
||||
alive,
|
||||
history,
|
||||
extra,
|
||||
udp,
|
||||
xudp,
|
||||
tfo,
|
||||
@@ -473,6 +480,7 @@ fn build_node(record_id: String, name: String, proxy: Proxy, source: ProxyNodeSo
|
||||
proxy_type,
|
||||
alive,
|
||||
history,
|
||||
extra,
|
||||
id,
|
||||
hidden,
|
||||
icon,
|
||||
@@ -526,3 +534,62 @@ fn build_standalone(core_node_ids: &BTreeMap<String, String>) -> Vec<String> {
|
||||
);
|
||||
standalone
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn provider_echo_in_proxies_does_not_override_group_membership() -> anyhow::Result<()> {
|
||||
let view = ProxyViewBuilder::build(ProxyViewInput {
|
||||
runtime_group_order: vec!["A".into(), "B".into()],
|
||||
group_scopes: ["A", "B"]
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
(
|
||||
name.into(),
|
||||
GroupScope {
|
||||
providers: vec![name.into()],
|
||||
exclude_types: vec![],
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
proxies: serde_json::from_value(json!({"proxies": {
|
||||
"A": {"all": ["node"], "type": "Selector"},
|
||||
"B": {"all": ["node"], "type": "Selector"},
|
||||
"node": {"name": "node", "provider-name": "A", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 32}]}
|
||||
}}))?,
|
||||
providers: Some(serde_json::from_value(json!({"providers": {
|
||||
"A": {"vehicleType": "Inline", "proxies": [{"name": "node", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 32}]}]},
|
||||
"B": {"vehicleType": "Inline", "proxies": [{"name": "node", "history": [{"time": "2026-10-08T00:00:00Z", "delay": 123}]}]}
|
||||
}}))?),
|
||||
});
|
||||
for (group, expected) in view.groups.iter().zip([32, 123]) {
|
||||
let ProxyMemberRef::Node { record_id, .. } = &group.members[0] else {
|
||||
anyhow::bail!("Expected node")
|
||||
};
|
||||
let record = &view.records[record_id];
|
||||
assert_eq!(record.history.last().map(|entry| entry.delay), Some(expected));
|
||||
assert!(
|
||||
matches!(&record.source, ProxyNodeSource::Provider { provider_name, .. } if provider_name == &group.name)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn unavailable_provider_keeps_core_echo_interactable() -> anyhow::Result<()> {
|
||||
let view = ProxyViewBuilder::build(ProxyViewInput {
|
||||
runtime_group_order: vec!["A".into()],
|
||||
group_scopes: BTreeMap::new(),
|
||||
proxies: serde_json::from_value(json!({"proxies": {
|
||||
"A": {"all": ["node"], "type": "Selector"},
|
||||
"node": {"name": "node", "provider-name": "provider"}
|
||||
}}))?,
|
||||
providers: None,
|
||||
});
|
||||
assert!(matches!(&view.groups[0].members[0], ProxyMemberRef::Node { .. }));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ impl Default for Sysopt {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>";
|
||||
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;*.local;<local>";
|
||||
#[cfg(target_os = "windows")]
|
||||
static BYPASS_SEPARATOR: &str = ";";
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
use super::Tray;
|
||||
use crate::{
|
||||
Type, cmd,
|
||||
config::Config,
|
||||
core::{
|
||||
handle,
|
||||
notify::{Refresh, announce},
|
||||
proxy_view::{ProxyMemberRef, ProxyNodeSource},
|
||||
},
|
||||
logging,
|
||||
};
|
||||
|
||||
const TRAY_DELAY_TEST_CONCURRENCY: usize = 10;
|
||||
const TRAY_DEFAULT_LATENCY_URL: &str = "http://cp.cloudflare.com/generate_204";
|
||||
|
||||
fn is_latency_test_url(raw: &str) -> bool {
|
||||
let raw = raw.trim();
|
||||
raw.split_once("://")
|
||||
.is_some_and(|(scheme, _)| scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https"))
|
||||
&& tauri::Url::parse(raw).is_ok_and(|url| url.has_host() && matches!(url.scheme(), "http" | "https"))
|
||||
}
|
||||
|
||||
pub(super) fn test_url(
|
||||
group: &crate::core::proxy_view::ProxyGroupView,
|
||||
verge: &crate::config::IVerge,
|
||||
profiles: &crate::config::IProfiles,
|
||||
) -> String {
|
||||
let custom_url = profiles
|
||||
.current
|
||||
.as_ref()
|
||||
.and_then(|uid| profiles.get_item(uid).ok())
|
||||
.and_then(|profile| profile.latency_test_urls.as_ref())
|
||||
.and_then(|urls| urls.get(group.name.as_str()))
|
||||
.map(|url| url.as_str())
|
||||
.filter(|url| !url.trim().is_empty());
|
||||
let group_url = group.test_url.as_deref().filter(|url| !url.trim().is_empty());
|
||||
let default_url = verge
|
||||
.default_latency_test
|
||||
.as_deref()
|
||||
.filter(|url| !url.trim().is_empty());
|
||||
custom_url
|
||||
.or(group_url)
|
||||
.or(default_url)
|
||||
.filter(|&url| is_latency_test_url(url))
|
||||
.unwrap_or(TRAY_DEFAULT_LATENCY_URL)
|
||||
.trim()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
static RUNNING: parking_lot::Mutex<std::collections::BTreeSet<String>> =
|
||||
parking_lot::Mutex::new(std::collections::BTreeSet::new());
|
||||
|
||||
pub(super) fn is_running(group_name: &str) -> bool {
|
||||
RUNNING.lock().contains(group_name)
|
||||
}
|
||||
|
||||
pub(super) async fn test_proxy_group_delay(group_name: &str) {
|
||||
if !RUNNING.lock().insert(group_name.to_owned()) {
|
||||
return;
|
||||
}
|
||||
Tray::global().refresh_latency_item(group_name);
|
||||
let _running = scopeguard::guard(group_name, |name| {
|
||||
RUNNING.lock().remove(name);
|
||||
Tray::global().refresh_latency_item(name);
|
||||
});
|
||||
use futures::{StreamExt as _, stream};
|
||||
|
||||
let view = match cmd::proxy::get_proxy_view().await {
|
||||
Ok(view) => view,
|
||||
Err(err) => {
|
||||
logging!(warn, Type::Tray, "Failed to read proxies for tray latency test: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
|
||||
let group = view
|
||||
.groups
|
||||
.iter()
|
||||
.chain(view.global.iter())
|
||||
.find(|group| group.name.as_str() == group_name);
|
||||
let Some(group) = group else {
|
||||
logging!(warn, Type::Tray, "Tray latency test group not found: {group_name}");
|
||||
return;
|
||||
};
|
||||
|
||||
let profiles = Config::profiles().await.latest_arc();
|
||||
let url = test_url(group, &verge, &profiles);
|
||||
let timeout = verge
|
||||
.default_latency_timeout
|
||||
.filter(|timeout| *timeout > 0)
|
||||
.unwrap_or(10000) as u32;
|
||||
|
||||
let mut targets: Vec<(std::string::String, Option<std::string::String>)> = Vec::with_capacity(group.members.len());
|
||||
for member in &group.members {
|
||||
match member {
|
||||
ProxyMemberRef::Node { name, record_id } => match view.records.get(record_id).map(|node| &node.source) {
|
||||
Some(ProxyNodeSource::Provider {
|
||||
provider_name,
|
||||
proxy_name,
|
||||
}) => targets.push((proxy_name.clone(), Some(provider_name.clone()))),
|
||||
_ => targets.push((name.clone(), None)),
|
||||
},
|
||||
ProxyMemberRef::Group { name } => targets.push((name.clone(), None)),
|
||||
ProxyMemberRef::Unresolved { .. } => {}
|
||||
}
|
||||
}
|
||||
let mihomo = handle::Handle::mihomo();
|
||||
|
||||
stream::iter(targets.into_iter().map(move |(member, provider)| {
|
||||
let url = url.clone();
|
||||
async move {
|
||||
let result = match provider {
|
||||
Some(provider_name) => {
|
||||
mihomo
|
||||
.healthcheck_node_in_provider(&provider_name, &member, &url, timeout)
|
||||
.await
|
||||
}
|
||||
None => mihomo.delay_proxy_by_name(&member, &url, timeout).await,
|
||||
};
|
||||
if let Err(err) = result {
|
||||
logging!(debug, Type::Tray, "Tray latency test failed: {member} - {err}");
|
||||
}
|
||||
}
|
||||
}))
|
||||
.buffer_unordered(TRAY_DELAY_TEST_CONCURRENCY)
|
||||
.for_each(|()| std::future::ready(()))
|
||||
.await;
|
||||
|
||||
if let Err(err) = cmd::proxy::sync_tray_proxy_selection().await {
|
||||
logging!(warn, Type::Tray, "Failed to refresh tray after latency test: {err}");
|
||||
}
|
||||
|
||||
announce(Refresh::Proxies);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn latency_urls_require_a_valid_http_host_and_port() {
|
||||
for invalid in [
|
||||
"http://?",
|
||||
"http://host:invalid",
|
||||
"http://bad host/",
|
||||
"ftp://localhost",
|
||||
"http:localhost",
|
||||
] {
|
||||
assert!(!is_latency_test_url(invalid), "{invalid}");
|
||||
}
|
||||
for valid in ["http://localhost/", " https://example.com/ ", "HTTP://localhost:8080/"] {
|
||||
assert!(is_latency_test_url(valid), "{valid}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ define_menu! {
|
||||
outbound_modes => OUTBOUND_MODES, "tray_outbound_modes", "tray.outboundModes",
|
||||
profiles => PROFILES, "tray_profiles", "tray.profiles",
|
||||
proxies => PROXIES, "tray_proxies", "tray.proxies",
|
||||
test_latency => TEST_LATENCY, "tray_test_latency", "tray.testLatency",
|
||||
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "tray.systemProxy",
|
||||
tun_mode => TUN_MODE, "tray_tun_mode", "tray.tunMode",
|
||||
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "tray.closeAllConnections",
|
||||
|
||||
+357
-136
@@ -1,5 +1,6 @@
|
||||
use crate::config::{IProfilePreview, IVerge};
|
||||
use crate::core::lightweight;
|
||||
use crate::core::proxy_view::{ProxyGroupView, ProxyMemberRef, ProxyNodeSource, ProxyViewProviderState, ProxyViewV1};
|
||||
use crate::core::tray::menu_def::TrayAction;
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::singleton;
|
||||
@@ -14,7 +15,6 @@ use crate::{
|
||||
use clash_verge_limiter::{Limiter, SystemClock, SystemLimiter};
|
||||
use clash_verge_logging::logging_error;
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
||||
use tauri_plugin_mihomo::models::Proxies;
|
||||
use tokio::fs;
|
||||
|
||||
use super::handle;
|
||||
@@ -28,6 +28,7 @@ use tauri::{
|
||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
};
|
||||
|
||||
mod latency;
|
||||
mod menu_def;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod speed_task;
|
||||
@@ -55,8 +56,25 @@ enum IconKind {
|
||||
Tun,
|
||||
}
|
||||
|
||||
struct LatencyMenuItem {
|
||||
item: MenuItem<Wry>,
|
||||
running: bool,
|
||||
proxies: Vec<ProxyLatencyItem>,
|
||||
provider_state: ProxyViewProviderState,
|
||||
}
|
||||
|
||||
struct ProxyLatencyItem {
|
||||
item: CheckMenuItem<Wry>,
|
||||
name: std::string::String,
|
||||
provider_name: Option<std::string::String>,
|
||||
text: std::string::String,
|
||||
}
|
||||
|
||||
pub struct Tray {
|
||||
limiter: SystemLimiter,
|
||||
proxy_hover_limiter: SystemLimiter,
|
||||
menu_update: tokio::sync::Mutex<()>,
|
||||
latency_items: parking_lot::Mutex<HashMap<std::string::String, LatencyMenuItem>>,
|
||||
#[cfg(target_os = "macos")]
|
||||
speed_controller: speed_task::TraySpeedController,
|
||||
}
|
||||
@@ -130,6 +148,9 @@ impl Default for Tray {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
limiter: Limiter::new(Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS), SystemClock),
|
||||
proxy_hover_limiter: Limiter::new(Duration::from_secs(1), SystemClock),
|
||||
menu_update: tokio::sync::Mutex::new(()),
|
||||
latency_items: parking_lot::Mutex::new(HashMap::new()),
|
||||
#[cfg(target_os = "macos")]
|
||||
speed_controller: speed_task::TraySpeedController::new(),
|
||||
}
|
||||
@@ -195,6 +216,7 @@ impl Tray {
|
||||
}
|
||||
|
||||
async fn update_menu_internal(&self, app_handle: &AppHandle, include_proxy_groups: bool) -> Result<()> {
|
||||
let _update = self.menu_update.lock().await;
|
||||
let Some(tray) = app_handle.tray_by_id(TRAY_ID) else {
|
||||
logging!(warn, Type::Tray, "Failed to update tray menu: tray not found");
|
||||
return Ok(());
|
||||
@@ -219,29 +241,133 @@ impl Tray {
|
||||
let profiles_preview = profiles_arc.profiles_preview().unwrap_or_default();
|
||||
let is_lightweight_mode = is_in_lightweight_mode();
|
||||
|
||||
logging_error!(
|
||||
Type::Tray,
|
||||
tray.set_menu(Some(
|
||||
create_tray_menu(
|
||||
app_handle,
|
||||
Some(mode.as_str()),
|
||||
*system_proxy,
|
||||
*tun_mode,
|
||||
tun_mode_available,
|
||||
profiles_preview,
|
||||
TrayMenuOptions {
|
||||
is_lightweight_mode,
|
||||
include_proxy_groups,
|
||||
},
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
);
|
||||
let (menu, latency_items) = create_tray_menu(
|
||||
app_handle,
|
||||
Some(mode.as_str()),
|
||||
*system_proxy,
|
||||
*tun_mode,
|
||||
tun_mode_available,
|
||||
profiles_preview,
|
||||
TrayMenuOptions {
|
||||
is_lightweight_mode,
|
||||
include_proxy_groups,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
tray.set_menu(Some(menu))?;
|
||||
let groups: Vec<_> = latency_items.keys().cloned().collect();
|
||||
*self.latency_items.lock() = latency_items;
|
||||
for group in groups {
|
||||
self.refresh_latency_item(&group);
|
||||
}
|
||||
|
||||
logging!(debug, Type::Tray, "托盘菜单更新成功");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn refresh_proxy_items(&'static self) {
|
||||
let Ok(update) = self.menu_update.try_lock() else {
|
||||
return;
|
||||
};
|
||||
if self.latency_items.lock().is_empty() || handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
let Ok(Ok(view)) = tokio::time::timeout(
|
||||
Duration::from_secs(1),
|
||||
cmd::proxy::proxy_view(Some(Duration::from_millis(750))),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if self
|
||||
.latency_items
|
||||
.lock()
|
||||
.values()
|
||||
.any(|entry| entry.provider_state != view.provider_state)
|
||||
{
|
||||
// Provider fallback changes member identities; rebuild through the existing queue.
|
||||
drop(update);
|
||||
logging_error!(Type::Tray, cmd::proxy::sync_tray_proxy_selection().await);
|
||||
return;
|
||||
}
|
||||
let verge = Config::verge().await.latest_arc();
|
||||
let profiles = Config::profiles().await.latest_arc();
|
||||
logging_error!(
|
||||
Type::Tray,
|
||||
handle::Handle::app_handle().run_on_main_thread(move || {
|
||||
// Keep rebuilding serialized until these handles have received their snapshot.
|
||||
let _update = update;
|
||||
let mut items = self.latency_items.lock();
|
||||
for (group_name, entry) in items.iter_mut() {
|
||||
let Some(group) = find_group(&view, group_name) else {
|
||||
continue;
|
||||
};
|
||||
let url = latency::test_url(group, &verge, &profiles);
|
||||
let mut members = HashMap::with_capacity(group.members.len());
|
||||
for member in &group.members {
|
||||
members
|
||||
.entry((member_name(member), member_provider(&view, member)))
|
||||
.or_insert(member);
|
||||
}
|
||||
for proxy in &mut entry.proxies {
|
||||
let Some(member) = members.get(&(proxy.name.as_str(), proxy.provider_name.as_deref())) else {
|
||||
continue;
|
||||
};
|
||||
let text = proxy_item_text(&view, member, &url, &verge);
|
||||
let checked = group.now.as_ref() == Some(&proxy.name);
|
||||
if text != proxy.text {
|
||||
match proxy.item.set_text(&text) {
|
||||
Ok(()) => proxy.text = text,
|
||||
Err(err) => logging!(warn, Type::Tray, "Failed to refresh proxy latency: {err}"),
|
||||
}
|
||||
}
|
||||
match proxy.item.is_checked() {
|
||||
Ok(current) if current != checked => {
|
||||
logging_error!(Type::Tray, proxy.item.set_checked(checked))
|
||||
}
|
||||
Err(err) => logging!(warn, Type::Tray, "Failed to read proxy selection: {err}"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fn refresh_latency_item(&self, group_name: &str) {
|
||||
let group_name = group_name.to_owned();
|
||||
logging_error!(
|
||||
Type::Tray,
|
||||
handle::Handle::app_handle().run_on_main_thread(move || {
|
||||
Self::global().refresh_latency_item_on_main_thread(&group_name);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fn refresh_latency_item_on_main_thread(&self, group_name: &str) {
|
||||
let running = latency::is_running(group_name);
|
||||
let item = {
|
||||
let mut items = self.latency_items.lock();
|
||||
items.get_mut(group_name).and_then(|entry| {
|
||||
if entry.running == running {
|
||||
return None;
|
||||
}
|
||||
entry.running = running;
|
||||
Some(entry.item.clone())
|
||||
})
|
||||
};
|
||||
if let Some(item) = item {
|
||||
let label = if running {
|
||||
clash_verge_i18n::t!("tray.testingLatency")
|
||||
} else {
|
||||
clash_verge_i18n::t!("tray.testLatency")
|
||||
};
|
||||
logging_error!(Type::Tray, item.set_text(label));
|
||||
logging_error!(Type::Tray, item.set_enabled(!running));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_icon(&self, verge: &IVerge) -> Result<()> {
|
||||
if handle::Handle::global().is_exiting() {
|
||||
logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新");
|
||||
@@ -457,96 +583,175 @@ fn create_profile_menu_item(
|
||||
.collect()
|
||||
}
|
||||
|
||||
const UNTESTED_DELAY_TEXT: &str = "-ms";
|
||||
const TIMEOUT_DELAY_TEXT: &str = "T/O";
|
||||
const TRAY_GROUP_DEPTH_LIMIT: usize = 8;
|
||||
|
||||
const fn member_name(member: &ProxyMemberRef) -> &str {
|
||||
match member {
|
||||
ProxyMemberRef::Node { name, .. }
|
||||
| ProxyMemberRef::Group { name }
|
||||
| ProxyMemberRef::Unresolved { name, .. } => name.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_group<'a>(view: &'a ProxyViewV1, name: &str) -> Option<&'a ProxyGroupView> {
|
||||
view.groups
|
||||
.iter()
|
||||
.chain(view.global.iter())
|
||||
.find(|group| group.name.as_str() == name)
|
||||
}
|
||||
|
||||
/// The delay recorded for `url`, falling back to the node's latest measurement under any URL, so a
|
||||
/// node the core tested on its own still shows a value.
|
||||
fn node_delay(view: &ProxyViewV1, record_id: &str, url: &str) -> Option<u16> {
|
||||
view.records
|
||||
.get(record_id)
|
||||
.map(|node| node.extra.get(url).map_or(&node.history, |extra| &extra.history))
|
||||
.and_then(|history| history.last())
|
||||
.map(|history| history.delay)
|
||||
}
|
||||
|
||||
/// A group row shows the delay of the node the group currently selects. The core never writes a
|
||||
/// history for the group itself, so `now` is followed until the chain reaches a node.
|
||||
fn selected_member_delay(view: &ProxyViewV1, group_name: &str, url: &str) -> Option<u16> {
|
||||
let mut current = group_name;
|
||||
for _ in 0..TRAY_GROUP_DEPTH_LIMIT {
|
||||
let Some(group) = find_group(view, current) else { break };
|
||||
let Some(now) = group.now.as_deref().filter(|now| !now.is_empty() && *now != current) else {
|
||||
break;
|
||||
};
|
||||
let Some(member) = group.members.iter().find(|member| member_name(member) == now) else {
|
||||
break;
|
||||
};
|
||||
match member {
|
||||
ProxyMemberRef::Node { record_id, .. } => return node_delay(view, record_id, url),
|
||||
ProxyMemberRef::Group { name } => current = name.as_str(),
|
||||
ProxyMemberRef::Unresolved { .. } => break,
|
||||
}
|
||||
}
|
||||
find_group(view, group_name)
|
||||
.map(|group| group.extra.get(url).map_or(&group.history, |extra| &extra.history))
|
||||
.and_then(|history| history.last())
|
||||
.map(|history| history.delay)
|
||||
}
|
||||
|
||||
fn member_provider<'a>(view: &'a ProxyViewV1, member: &ProxyMemberRef) -> Option<&'a str> {
|
||||
let ProxyMemberRef::Node { record_id, .. } = member else {
|
||||
return None;
|
||||
};
|
||||
match &view.records.get(record_id)?.source {
|
||||
ProxyNodeSource::Provider { provider_name, .. } => Some(provider_name),
|
||||
ProxyNodeSource::Core { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy_item_text(view: &ProxyViewV1, member: &ProxyMemberRef, url: &str, verge: &IVerge) -> std::string::String {
|
||||
let timeout = verge
|
||||
.default_latency_timeout
|
||||
.filter(|timeout| *timeout > 0)
|
||||
.unwrap_or(10000);
|
||||
let delay = match member {
|
||||
ProxyMemberRef::Node { record_id, .. } => node_delay(view, record_id, url),
|
||||
ProxyMemberRef::Group { name } => selected_member_delay(view, name, url),
|
||||
ProxyMemberRef::Unresolved { .. } => None,
|
||||
};
|
||||
let delay_text = delay.map_or_else(
|
||||
|| UNTESTED_DELAY_TEXT.to_owned(),
|
||||
|delay| {
|
||||
if delay == 0 || u32::from(delay) >= timeout as u32 {
|
||||
TIMEOUT_DELAY_TEXT.to_owned()
|
||||
} else {
|
||||
format!("{delay}ms")
|
||||
}
|
||||
},
|
||||
);
|
||||
format!("{} | {delay_text}", member_name(member))
|
||||
}
|
||||
|
||||
fn create_subcreate_proxy_menu_item(
|
||||
app_handle: &AppHandle,
|
||||
proxy_mode: &str,
|
||||
proxy_group_order_map: Option<HashMap<String, usize>>,
|
||||
proxy_nodes_data: Option<Proxies>,
|
||||
view: Option<ProxyViewV1>,
|
||||
latency_items: &mut HashMap<std::string::String, LatencyMenuItem>,
|
||||
texts: &MenuTexts,
|
||||
verge: &IVerge,
|
||||
profiles: &crate::config::IProfiles,
|
||||
) -> Vec<Submenu<Wry>> {
|
||||
let proxy_submenus: Vec<Submenu<Wry>> = {
|
||||
let mut submenus: Vec<(String, usize, Submenu<Wry>)> = Vec::new();
|
||||
|
||||
// TODO: 应用启动时,内核还未启动完全,无法获取代理节点信息
|
||||
if let Some(proxy_nodes_data) = proxy_nodes_data {
|
||||
for (group_name, group_data) in proxy_nodes_data.proxies.iter() {
|
||||
let should_show = match proxy_mode {
|
||||
"global" => group_name == "GLOBAL",
|
||||
_ => group_name != "GLOBAL",
|
||||
} && !group_data.hidden.unwrap_or_default();
|
||||
|
||||
if !should_show {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(all_proxies) = group_data.all.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let now_proxy = group_data.now.as_deref().unwrap_or_default();
|
||||
|
||||
let group_items: Vec<CheckMenuItem<Wry>> = all_proxies
|
||||
.iter()
|
||||
.filter_map(|proxy_str| {
|
||||
let is_selected = *proxy_str == now_proxy;
|
||||
let item_id = format!("proxy_{}_{}", group_name, proxy_str);
|
||||
|
||||
let delay_text = proxy_nodes_data
|
||||
.proxies
|
||||
.get(proxy_str)
|
||||
.and_then(|h| h.history.last())
|
||||
.map(|h| match h.delay {
|
||||
0 => "-ms".into(),
|
||||
delay if delay >= 10000 => "-ms".into(),
|
||||
_ => format!("{}ms", h.delay),
|
||||
})
|
||||
.unwrap_or_else(|| "-ms".into());
|
||||
|
||||
let display_text = format!("{} | {}", proxy_str, delay_text);
|
||||
|
||||
CheckMenuItem::with_id(app_handle, item_id, display_text, true, is_selected, None::<&str>)
|
||||
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {}", e))
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
|
||||
if group_items.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let group_display_name = group_name.to_string();
|
||||
|
||||
let group_items_refs: Vec<&dyn IsMenuItem<Wry>> =
|
||||
group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>).collect();
|
||||
|
||||
if let Ok(submenu) = Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
format!("proxy_group_{}", group_name),
|
||||
group_display_name,
|
||||
true,
|
||||
&group_items_refs,
|
||||
) {
|
||||
let insertion_index = submenus.len();
|
||||
submenus.push((group_name.into(), insertion_index, submenu));
|
||||
} else {
|
||||
logging!(warn, Type::Tray, "Failed to create proxy group submenu: {}", group_name);
|
||||
}
|
||||
let Some(view) = view else { return Vec::new() };
|
||||
view.groups
|
||||
.iter()
|
||||
.chain(view.global.iter())
|
||||
.filter_map(|group| {
|
||||
if group.hidden.unwrap_or_default() || (proxy_mode == "global") != (group.name == "GLOBAL") {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(order_map) = proxy_group_order_map.as_ref() {
|
||||
submenus.sort_by(|(name_a, original_index_a, _), (name_b, original_index_b, _)| {
|
||||
match (order_map.get(name_a), order_map.get(name_b)) {
|
||||
(Some(index_a), Some(index_b)) => index_a.cmp(index_b),
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||
(None, None) => original_index_a.cmp(original_index_b),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submenus.into_iter().map(|(_, _, submenu)| submenu).collect()
|
||||
};
|
||||
proxy_submenus
|
||||
let url = latency::test_url(group, verge, profiles);
|
||||
let proxies: Vec<ProxyLatencyItem> = group
|
||||
.members
|
||||
.iter()
|
||||
.filter_map(|member| {
|
||||
let name = member_name(member);
|
||||
let text = proxy_item_text(&view, member, &url, verge);
|
||||
let checked = group.now.as_deref() == Some(name);
|
||||
CheckMenuItem::with_id(
|
||||
app_handle,
|
||||
format!("proxy_{}_{}", group.name, name),
|
||||
&text,
|
||||
true,
|
||||
checked,
|
||||
None::<&str>,
|
||||
)
|
||||
.map(|item| ProxyLatencyItem {
|
||||
item,
|
||||
name: name.to_owned(),
|
||||
provider_name: member_provider(&view, member).map(str::to_owned),
|
||||
text,
|
||||
})
|
||||
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy menu item: {e}"))
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
let group_items: Vec<_> = proxies.iter().map(|proxy| proxy.item.clone()).collect();
|
||||
if group_items.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let running = latency::is_running(&group.name);
|
||||
let test_item = MenuItem::with_id(
|
||||
app_handle,
|
||||
format!("{}_{}", MenuIds::TEST_LATENCY, group.name),
|
||||
if running {
|
||||
clash_verge_i18n::t!("tray.testingLatency")
|
||||
} else {
|
||||
texts.test_latency.clone()
|
||||
},
|
||||
!running,
|
||||
None::<&str>,
|
||||
)
|
||||
.ok()?;
|
||||
latency_items.insert(
|
||||
group.name.clone(),
|
||||
LatencyMenuItem {
|
||||
item: test_item.clone(),
|
||||
running,
|
||||
proxies,
|
||||
provider_state: view.provider_state,
|
||||
},
|
||||
);
|
||||
let separator = PredefinedMenuItem::separator(app_handle).ok()?;
|
||||
let mut items: Vec<&dyn IsMenuItem<Wry>> = vec![&test_item, &separator];
|
||||
items.extend(group_items.iter().map(|item| item as &dyn IsMenuItem<Wry>));
|
||||
Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
format!("proxy_group_{}", group.name),
|
||||
&group.name,
|
||||
true,
|
||||
&items,
|
||||
)
|
||||
.map_err(|e| logging!(warn, Type::Tray, "Failed to create proxy group submenu: {e}"))
|
||||
.ok()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn create_proxy_menu_item(
|
||||
@@ -593,54 +798,38 @@ async fn create_tray_menu(
|
||||
tun_mode_available: bool,
|
||||
profiles_preview: Vec<IProfilePreview<'_>>,
|
||||
options: TrayMenuOptions,
|
||||
) -> Result<tauri::menu::Menu<Wry>> {
|
||||
) -> Result<(tauri::menu::Menu<Wry>, HashMap<std::string::String, LatencyMenuItem>)> {
|
||||
let current_proxy_mode = mode.unwrap_or("");
|
||||
|
||||
let mut verge_settings = Config::verge().await.latest_arc();
|
||||
let fetch_proxy_groups =
|
||||
options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable");
|
||||
|
||||
// TODO: should update tray menu again when it was timeout error
|
||||
let (proxy_nodes_data, runtime_proxy_groups_order) = if fetch_proxy_groups {
|
||||
let proxy_nodes_data =
|
||||
tokio::time::timeout(Duration::from_millis(1000), handle::Handle::mihomo().get_proxies())
|
||||
.await
|
||||
.map_or(None, |res| res.ok());
|
||||
|
||||
let runtime = Config::runtime().await.latest_arc();
|
||||
let runtime_proxy_groups_order = runtime.config.as_ref().map(|config| {
|
||||
config
|
||||
.get("proxy-groups")
|
||||
.and_then(|groups| groups.as_sequence())
|
||||
.map(|groups| {
|
||||
groups
|
||||
.iter()
|
||||
.filter_map(|group| group.get("name"))
|
||||
.filter_map(|name| name.as_str())
|
||||
.enumerate()
|
||||
.map(|(index, name)| (name.into(), index))
|
||||
.collect::<HashMap<String, usize>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
(proxy_nodes_data, runtime_proxy_groups_order)
|
||||
// Proxy data must remain optional so a stopped Core cannot roll back unrelated settings.
|
||||
let proxy_view = if fetch_proxy_groups {
|
||||
tokio::time::timeout(
|
||||
Duration::from_millis(1000),
|
||||
cmd::proxy::proxy_view(Some(Duration::from_millis(750))),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(Result::ok)
|
||||
} else {
|
||||
(None, None)
|
||||
None
|
||||
};
|
||||
|
||||
if fetch_proxy_groups {
|
||||
verge_settings = Config::verge().await.latest_arc();
|
||||
}
|
||||
|
||||
let profiles_config = Config::profiles().await.latest_arc();
|
||||
|
||||
let tray_proxy_groups_display_mode = verge_settings
|
||||
.tray_proxy_groups_display_mode
|
||||
.as_deref()
|
||||
.unwrap_or("default");
|
||||
let include_proxy_groups = options.include_proxy_groups && tray_proxy_groups_display_mode != "disable";
|
||||
|
||||
let proxy_group_order_map = runtime_proxy_groups_order;
|
||||
|
||||
let show_outbound_modes_inline = verge_settings.tray_inline_outbound_modes.unwrap_or(false);
|
||||
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
@@ -720,9 +909,17 @@ async fn create_tray_menu(
|
||||
&profile_menu_items_refs,
|
||||
)?;
|
||||
|
||||
let mut latency_items = HashMap::new();
|
||||
let (proxies_menu, inline_proxy_items) = if include_proxy_groups {
|
||||
let proxy_sub_menus =
|
||||
create_subcreate_proxy_menu_item(app_handle, current_proxy_mode, proxy_group_order_map, proxy_nodes_data);
|
||||
let proxy_sub_menus = create_subcreate_proxy_menu_item(
|
||||
app_handle,
|
||||
current_proxy_mode,
|
||||
proxy_view,
|
||||
&mut latency_items,
|
||||
&texts,
|
||||
&verge_settings,
|
||||
&profiles_config,
|
||||
);
|
||||
|
||||
match tray_proxy_groups_display_mode {
|
||||
"default" => create_proxy_menu_item(app_handle, false, proxy_sub_menus, &texts.proxies)?,
|
||||
@@ -866,10 +1063,25 @@ async fn create_tray_menu(
|
||||
]);
|
||||
|
||||
let menu = tauri::menu::MenuBuilder::new(app_handle).items(&menu_items).build()?;
|
||||
Ok(menu)
|
||||
Ok((menu, latency_items))
|
||||
}
|
||||
|
||||
fn on_tray_icon_event(_tray_icon: &TrayIcon, tray_event: TrayIconEvent) {
|
||||
let refresh_proxies = match tray_event {
|
||||
TrayIconEvent::Enter { .. } => Tray::global().proxy_hover_limiter.check(),
|
||||
TrayIconEvent::Click {
|
||||
button: MouseButton::Left | MouseButton::Right,
|
||||
button_state: MouseButtonState::Down,
|
||||
..
|
||||
} => true,
|
||||
_ => false,
|
||||
};
|
||||
if refresh_proxies {
|
||||
AsyncHandler::spawn(|| async {
|
||||
Tray::global().refresh_proxy_items().await;
|
||||
});
|
||||
}
|
||||
|
||||
if matches!(
|
||||
tray_event,
|
||||
TrayIconEvent::Move { .. } | TrayIconEvent::Leave { .. } | TrayIconEvent::Enter { .. }
|
||||
@@ -991,6 +1203,15 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
|
||||
};
|
||||
feat::toggle_proxy_profile(profile_index.into()).await;
|
||||
}
|
||||
id if id.starts_with(MenuIds::TEST_LATENCY) => {
|
||||
let group_name = id
|
||||
.strip_prefix(MenuIds::TEST_LATENCY)
|
||||
.and_then(|rest| rest.strip_prefix('_'));
|
||||
if let Some(group_name) = group_name {
|
||||
logging!(info, Type::Tray, "Tray menu: test group latency {}", group_name);
|
||||
latency::test_proxy_group_delay(group_name).await;
|
||||
}
|
||||
}
|
||||
id if id.starts_with("proxy_") => {
|
||||
let rest = match id.strip_prefix("proxy_") {
|
||||
Some(r) => r,
|
||||
|
||||
@@ -172,6 +172,12 @@ pub struct DelayHistory {
|
||||
pub delay: u16,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct ProxyExtra {
|
||||
pub alive: bool,
|
||||
pub history: Vec<DelayHistory>,
|
||||
}
|
||||
|
||||
fn platform_types() -> specta::Types {
|
||||
use specta::datatype::{DataType, Fields};
|
||||
let mut types = specta::Types::default().register::<crate::config::IVerge>();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.5.8",
|
||||
"version": "2.5.9",
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
|
||||
@@ -47,7 +47,10 @@ import {
|
||||
useCoreDataStatus,
|
||||
useProxiesData,
|
||||
} from '@/providers/app-data-context'
|
||||
import { syncTrayProxySelection } from '@/services/cmds'
|
||||
import delayManager from '@/services/delay'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { profileTestUrls } from '@/services/proxy-test-url'
|
||||
import {
|
||||
findCurrentGroupMember,
|
||||
getRecord,
|
||||
@@ -675,6 +678,20 @@ export const CurrentProxyCard = () => {
|
||||
[proxyView],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedGroup) return
|
||||
const customUrl = currentProfile
|
||||
? profileTestUrls(currentProfile)[selectedGroup.name]
|
||||
: undefined
|
||||
delayManager.setUrl(
|
||||
selectedGroup.name,
|
||||
customUrl?.trim() ||
|
||||
selectedGroup.testUrl?.trim() ||
|
||||
verge?.default_latency_test?.trim() ||
|
||||
'http://cp.cloudflare.com/generate_204',
|
||||
)
|
||||
}, [selectedGroup, currentProfile, verge?.default_latency_test])
|
||||
|
||||
const unsortedProxyOptions = useMemo(
|
||||
() => optionsForGroup(selectedGroup),
|
||||
[optionsForGroup, selectedGroup],
|
||||
@@ -907,6 +924,12 @@ export const CurrentProxyCard = () => {
|
||||
}
|
||||
|
||||
refreshProxy()
|
||||
mutate(() => syncTrayProxySelection(), {
|
||||
id: 'sync-tray-selection',
|
||||
errorNotice: false,
|
||||
}).catch((error) => {
|
||||
console.error('[CurrentProxyCard] Failed to sync tray latency:', error)
|
||||
})
|
||||
})
|
||||
|
||||
const proxyOptions = useMemo(
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { createInstance } from 'i18next'
|
||||
import { createElement } from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { I18nextProvider } from 'react-i18next'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
|
||||
import settings from '@/locales/en/settings.json'
|
||||
import {
|
||||
getSnapshotNotices,
|
||||
hideNotice,
|
||||
showNotice,
|
||||
syncSidecarFailure,
|
||||
} from '@/services/notice-service'
|
||||
|
||||
import { NoticeManager } from './notice-manager'
|
||||
|
||||
vi.mock('react', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('react')>()),
|
||||
useSyncExternalStore: (_subscribe: unknown, snapshot: () => unknown) =>
|
||||
snapshot(),
|
||||
}))
|
||||
vi.mock('@/utils/open-external-url', () => ({ openExternalUrl: vi.fn() }))
|
||||
|
||||
afterEach(() => {
|
||||
getSnapshotNotices().forEach(({ id }) => hideNotice(id))
|
||||
})
|
||||
|
||||
it.each([
|
||||
[
|
||||
'startup',
|
||||
'process verge-mihomo remains after IPC failure; refusing a second core',
|
||||
],
|
||||
[
|
||||
'restart',
|
||||
'process verge-mihomo-alpha remains after IPC failure; refusing a second core',
|
||||
],
|
||||
[
|
||||
'sidecar',
|
||||
'Service core rejected: permission denied; Sidecar fallback failed: process verge-mihomo.exe (PID 123) is still running; refusing a second core',
|
||||
],
|
||||
])(
|
||||
'explains an occupied core during %s and preserves diagnostics',
|
||||
async (operation, detail) => {
|
||||
if (operation === 'startup') {
|
||||
showNotice.error('settings.feedback.errors.clash.startFailed', detail)
|
||||
} else if (operation === 'restart') {
|
||||
showNotice.error({ code: 'CORE_RESTART_FAILED', detail })
|
||||
} else {
|
||||
syncSidecarFailure({ revision: 1, detail })
|
||||
}
|
||||
const i18n = createInstance()
|
||||
await i18n.init({
|
||||
lng: 'en',
|
||||
resources: { en: { translation: { settings } } },
|
||||
})
|
||||
const html = renderToStaticMarkup(
|
||||
createElement(I18nextProvider, { i18n }, createElement(NoticeManager)),
|
||||
)
|
||||
expect(html).toContain('a core process is already running')
|
||||
expect(html).toContain('Close the related app or stop the leftover process')
|
||||
expect(html).toContain(detail)
|
||||
if (operation === 'sidecar') {
|
||||
expect(html).toContain('service-core-permissions')
|
||||
expect(html).toContain('PID 123')
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -70,8 +70,8 @@ const resolveNoticeMessage = (
|
||||
|
||||
const detail = i18n.params?.message
|
||||
const existingCore =
|
||||
notice.code === 'SERVICE_SIDECAR_FAILED' && typeof detail === 'string'
|
||||
? /process verge-mihomo(?:-alpha)?(?:\.exe)? \(PID (\d+)\) is still running; refusing a second core\s*$/.exec(
|
||||
typeof detail === 'string'
|
||||
? /process verge-mihomo(?:-alpha|-al)?(?:\.exe)? (?:\(PID (\d+)\) is still running|remains after IPC failure); refusing a second core\s*$/.exec(
|
||||
detail,
|
||||
)
|
||||
: null
|
||||
@@ -101,9 +101,8 @@ const resolveNoticeMessage = (
|
||||
</Link>
|
||||
</Box>
|
||||
)}
|
||||
{t('settings.feedback.errors.clashService.coreAlreadyRunning', {
|
||||
pid: existingCore[1],
|
||||
})}
|
||||
{t('settings.feedback.errors.clashService.coreAlreadyRunning')}
|
||||
{existingCore[1] && <Box sx={{ mt: 1 }}>PID {existingCore[1]}</Box>}
|
||||
<Box component="details" sx={{ mt: 1 }}>
|
||||
<Box component="summary" sx={{ cursor: 'pointer' }}>
|
||||
{t('shared.feedback.errors.details')}
|
||||
|
||||
@@ -684,6 +684,9 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
ruleType.name !== 'SUB-RULE' && (
|
||||
<TextField
|
||||
autoComplete="new-password"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
size="small"
|
||||
sx={{ minWidth: '240px' }}
|
||||
slotProps={{
|
||||
|
||||
@@ -9,7 +9,7 @@ import VisibilityOffRounded from '@mui/icons-material/VisibilityOffRounded'
|
||||
import VisibilityRounded from '@mui/icons-material/VisibilityRounded'
|
||||
import WifiTetheringOffRounded from '@mui/icons-material/WifiTetheringOffRounded'
|
||||
import WifiTetheringRounded from '@mui/icons-material/WifiTetheringRounded'
|
||||
import { Box, IconButton, type SxProps, TextField } from '@mui/material'
|
||||
import { Box, IconButton, type SxProps } from '@mui/material'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import { memo, useEffect, useRef } from 'react'
|
||||
import { flushSync } from 'react-dom'
|
||||
@@ -18,10 +18,11 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import delayManager from '@/services/delay'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
import { isValidLatencyTestUrl } from '@/utils/network'
|
||||
|
||||
import { BaseSearchBox, type SearchState } from '../base'
|
||||
|
||||
import { ProxyTestUrlInput } from './proxy-test-url-input'
|
||||
import type { ProxySortType } from './use-filter-sort'
|
||||
import type { HeadState } from './use-head-state'
|
||||
|
||||
@@ -69,7 +70,10 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl)
|
||||
delayManager.setUrl(
|
||||
groupName,
|
||||
testUrl?.trim() || url?.trim() || defaultLatencyUrl,
|
||||
)
|
||||
}, [groupName, testUrl, defaultLatencyUrl, url])
|
||||
|
||||
// 过滤输入是高频操作,且每次都会触发整组代理的重新过滤/排序与虚拟列表重渲染,
|
||||
@@ -112,7 +116,8 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
||||
)}
|
||||
|
||||
{textState === 'url' && (
|
||||
<TextField
|
||||
<ProxyTestUrlInput
|
||||
name={groupName}
|
||||
inputRef={inputRef}
|
||||
autoComplete="new-password"
|
||||
hiddenLabel
|
||||
@@ -125,7 +130,7 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onChange={(e) => onHeadState({ testUrl: e.target.value })}
|
||||
onCommit={(testUrl) => onHeadState({ testUrl })}
|
||||
sx={{ flex: '1 1 auto', input: { py: 0.65, px: 1 } }}
|
||||
/>
|
||||
)}
|
||||
@@ -176,7 +181,7 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
|
||||
if (testUrl?.trim() && textState !== 'filter') {
|
||||
onHeadState({ textState: 'url' })
|
||||
}
|
||||
if (testUrl?.trim() && !isValidUrl(testUrl)) {
|
||||
if (testUrl?.trim() && !isValidLatencyTestUrl(testUrl)) {
|
||||
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ import { useProfiles } from '@/hooks/use-profiles'
|
||||
import { useProxySelection } from '@/hooks/use-proxy-selection'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { useProxiesData, useSystemData } from '@/providers/app-data-context'
|
||||
import { syncTrayProxySelection } from '@/services/cmds'
|
||||
import delayManager from '@/services/delay'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import {
|
||||
isInteractableMember,
|
||||
resolveMember,
|
||||
@@ -133,6 +135,12 @@ function useProxyRenderState(
|
||||
console.error(`[ProxyGroups] 延迟测试出错,组: ${groupName}`, error)
|
||||
} finally {
|
||||
onProxies()
|
||||
mutate(() => syncTrayProxySelection(), {
|
||||
id: 'sync-tray-selection',
|
||||
errorNotice: false,
|
||||
}).catch((error) => {
|
||||
console.error('[ProxyGroups] Failed to sync tray latency:', error)
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SortByAlphaRounded,
|
||||
SortRounded,
|
||||
} from '@mui/icons-material'
|
||||
import { Box, IconButton, TextField, type SxProps } from '@mui/material'
|
||||
import { Box, IconButton, type SxProps } from '@mui/material'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -20,8 +20,9 @@ import { useVerge } from '@/hooks/use-verge'
|
||||
import delayManager from '@/services/delay'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
import { isValidLatencyTestUrl } from '@/utils/network'
|
||||
|
||||
import { ProxyTestUrlInput } from './proxy-test-url-input'
|
||||
import type { ProxySortType } from './use-filter-sort'
|
||||
import type { HeadState } from './use-head-state'
|
||||
|
||||
@@ -72,7 +73,10 @@ export const ProxyHead = ({
|
||||
'http://cp.cloudflare.com/generate_204'
|
||||
|
||||
useEffect(() => {
|
||||
delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl)
|
||||
delayManager.setUrl(
|
||||
groupName,
|
||||
testUrl?.trim() || url?.trim() || defaultLatencyUrl,
|
||||
)
|
||||
}, [groupName, testUrl, defaultLatencyUrl, url])
|
||||
|
||||
return (
|
||||
@@ -97,7 +101,7 @@ export const ProxyHead = ({
|
||||
debugLog(`[ProxyHead] 使用自定义测试URL: ${testUrl}`)
|
||||
onHeadState({ textState: 'url' })
|
||||
}
|
||||
if (testUrl?.trim() && !isValidUrl(testUrl)) {
|
||||
if (testUrl?.trim() && !isValidLatencyTestUrl(testUrl)) {
|
||||
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
|
||||
return
|
||||
}
|
||||
@@ -188,7 +192,8 @@ export const ProxyHead = ({
|
||||
)}
|
||||
|
||||
{textState === 'url' && (
|
||||
<TextField
|
||||
<ProxyTestUrlInput
|
||||
name={groupName}
|
||||
autoComplete="new-password"
|
||||
autoFocus={autoFocus}
|
||||
hiddenLabel
|
||||
@@ -197,7 +202,7 @@ export const ProxyHead = ({
|
||||
size="small"
|
||||
variant="outlined"
|
||||
placeholder={t('proxies.page.placeholders.delayCheckUrl')}
|
||||
onChange={(e) => onHeadState({ testUrl: e.target.value })}
|
||||
onCommit={(testUrl) => onHeadState({ testUrl })}
|
||||
sx={{ ml: 0.5, flex: '1 1 auto', input: { py: 0.65, px: 1 } }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { TextField, type TextFieldProps } from '@mui/material'
|
||||
import { useEffect, useEffectEvent, useRef, useState } from 'react'
|
||||
|
||||
import { useProfiles } from '@/hooks/use-profiles'
|
||||
|
||||
type Props = Omit<TextFieldProps, 'value' | 'onChange'> & {
|
||||
value: string
|
||||
onCommit: (url: string) => void
|
||||
}
|
||||
|
||||
export const ProxyTestUrlInput = ({ value, name, ...props }: Props) => {
|
||||
const { profiles } = useProfiles()
|
||||
return (
|
||||
<DraftTestUrlInput
|
||||
key={JSON.stringify([profiles?.current, name])}
|
||||
name={name}
|
||||
value={value}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DraftTestUrlInput = ({ value, onCommit, ...props }: Props) => {
|
||||
const [draft, setDraft] = useState<string | null>(null)
|
||||
const pendingRef = useRef<string | null>(null)
|
||||
const commitOnUnmount = useEffectEvent(() => {
|
||||
if (pendingRef.current !== null && pendingRef.current !== value)
|
||||
onCommit(pendingRef.current)
|
||||
})
|
||||
useEffect(() => () => commitOnUnmount(), [])
|
||||
|
||||
return (
|
||||
<TextField
|
||||
{...props}
|
||||
value={draft ?? value}
|
||||
onChange={(event) => {
|
||||
pendingRef.current = event.target.value
|
||||
setDraft(event.target.value)
|
||||
}}
|
||||
onBlur={() => {
|
||||
const next = pendingRef.current
|
||||
pendingRef.current = null
|
||||
setDraft(null)
|
||||
if (next !== null && next !== value) onCommit(next)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
const input = event.target as HTMLInputElement
|
||||
input.blur()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -13,11 +13,18 @@ const node = (memberIndex: number, delay: number, name = `${memberIndex}`) =>
|
||||
kind: 'node',
|
||||
ref: { kind: 'node', name, recordId: `${memberIndex}` },
|
||||
node: {
|
||||
recordId: `${memberIndex}`,
|
||||
history: [{ delay }],
|
||||
extra: {
|
||||
'http://cp.cloudflare.com/generate_204': {
|
||||
alive: true,
|
||||
history: [{ time: new Date(0).toISOString(), delay }],
|
||||
},
|
||||
},
|
||||
source: { kind: 'provider', providerName: `${memberIndex}` },
|
||||
},
|
||||
},
|
||||
}) as ResolvedMemberOccurrence
|
||||
}) as unknown as ResolvedMemberOccurrence
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
@@ -36,7 +43,9 @@ test('matches the previous comparator for cached, fallback and sentinel delays',
|
||||
for (const cached of [false, true]) {
|
||||
const group = `sort-${cached}`
|
||||
if (cached) {
|
||||
values.forEach((delay, i) => delayManager.setDelay(`${i}`, group, delay))
|
||||
values.forEach((delay, i) =>
|
||||
delayManager.setDelay(list[i].member, group, delay),
|
||||
)
|
||||
}
|
||||
for (const timeout of [10000, 20, 0, NaN]) {
|
||||
const expected = list
|
||||
@@ -66,7 +75,7 @@ test('reads each occurrence once and observes cache updates and expiry on the ne
|
||||
node(3, 25),
|
||||
]
|
||||
const cachedOrder = [list[0], list[2], list[1], list[3]]
|
||||
delayManager.setDelay('0', 'expiry', 1)
|
||||
delayManager.setDelay(list[0].member, 'expiry', 1)
|
||||
const get = vi.spyOn(delayManager, 'getDelayFix')
|
||||
expect(filterSort(list, 'expiry', '', 1)).toEqual(cachedOrder)
|
||||
expect(get).toHaveBeenCalledTimes(list.length)
|
||||
@@ -80,7 +89,7 @@ test('reads each occurrence once and observes cache updates and expiry on the ne
|
||||
list[3],
|
||||
list[0],
|
||||
])
|
||||
delayManager.setDelay('0', 'expiry', 2)
|
||||
delayManager.setDelay(list[0].member, 'expiry', 2)
|
||||
expect(filterSort(list, 'expiry', '', 1)).toEqual(cachedOrder)
|
||||
get.mockClear()
|
||||
filterSort([], 'expiry', '', 1)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useCallback, useEffect, useReducer } from 'react'
|
||||
|
||||
import { useProfiles } from '@/hooks/use-profiles'
|
||||
import {
|
||||
profileTestUrls,
|
||||
commitProfileTestUrl,
|
||||
} from '@/services/proxy-test-url'
|
||||
|
||||
import { ProxySortType } from './use-filter-sort'
|
||||
|
||||
@@ -32,22 +36,45 @@ export const DEFAULT_STATE: HeadState = {
|
||||
}
|
||||
|
||||
type HeadStateAction =
|
||||
| { type: 'reset' }
|
||||
| { type: 'replace'; payload: Record<string, HeadState> }
|
||||
| { type: 'reset'; profile: string }
|
||||
| { type: 'replace'; profile: string; payload: Record<string, HeadState> }
|
||||
| { type: 'update'; groupName: string; patch: Partial<HeadState> }
|
||||
| { type: 'urls'; urls: Record<string, string> }
|
||||
|
||||
interface LoadedHeadState {
|
||||
profile: string | null
|
||||
heads: Record<string, HeadState>
|
||||
}
|
||||
|
||||
function headStateReducer(
|
||||
state: Record<string, HeadState>,
|
||||
state: LoadedHeadState,
|
||||
action: HeadStateAction,
|
||||
): Record<string, HeadState> {
|
||||
): LoadedHeadState {
|
||||
switch (action.type) {
|
||||
case 'reset':
|
||||
return {}
|
||||
return { profile: action.profile, heads: {} }
|
||||
case 'replace':
|
||||
return action.payload
|
||||
return { profile: action.profile, heads: action.payload }
|
||||
case 'urls': {
|
||||
let heads = state.heads
|
||||
for (const [name, testUrl] of Object.entries(action.urls)) {
|
||||
if (heads[name]?.testUrl === testUrl) continue
|
||||
heads = {
|
||||
...heads,
|
||||
[name]: { ...DEFAULT_STATE, ...heads[name], testUrl },
|
||||
}
|
||||
}
|
||||
return heads === state.heads ? state : { ...state, heads }
|
||||
}
|
||||
case 'update': {
|
||||
const prev = state[action.groupName] || DEFAULT_STATE
|
||||
return { ...state, [action.groupName]: { ...prev, ...action.patch } }
|
||||
const prev = state.heads[action.groupName] || DEFAULT_STATE
|
||||
return {
|
||||
...state,
|
||||
heads: {
|
||||
...state.heads,
|
||||
[action.groupName]: { ...prev, ...action.patch },
|
||||
},
|
||||
}
|
||||
}
|
||||
default:
|
||||
return state
|
||||
@@ -55,30 +82,42 @@ function headStateReducer(
|
||||
}
|
||||
|
||||
export function useHeadStateNew() {
|
||||
const { profiles } = useProfiles()
|
||||
const { profiles, current: profile } = useProfiles()
|
||||
const current = profiles?.current || ''
|
||||
|
||||
const [state, dispatch] = useReducer(headStateReducer, {})
|
||||
const [{ profile: loadedProfile, heads: state }, dispatch] = useReducer(
|
||||
headStateReducer,
|
||||
{ profile: null, heads: {} },
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const urls = profile ? profileTestUrls(profile) : {}
|
||||
if (loadedProfile === current) {
|
||||
dispatch({ type: 'urls', urls })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(
|
||||
localStorage.getItem(HEAD_STATE_KEY)!,
|
||||
localStorage.getItem(HEAD_STATE_KEY) || '{}',
|
||||
) as HeadStateStorage
|
||||
|
||||
const value = data[current] || {}
|
||||
const value = data?.[current] || {}
|
||||
for (const [name, testUrl] of Object.entries(urls)) {
|
||||
value[name] = { ...DEFAULT_STATE, ...value[name], testUrl }
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
dispatch({ type: 'replace', payload: value })
|
||||
dispatch({ type: 'replace', profile: current, payload: value })
|
||||
} else {
|
||||
dispatch({ type: 'reset' })
|
||||
dispatch({ type: 'reset', profile: current })
|
||||
}
|
||||
} catch {
|
||||
dispatch({ type: 'reset' })
|
||||
dispatch({ type: 'reset', profile: current })
|
||||
}
|
||||
}, [current])
|
||||
}, [current, loadedProfile, profile])
|
||||
|
||||
useEffect(() => {
|
||||
if (loadedProfile !== current) return
|
||||
const timer = setTimeout(() => {
|
||||
try {
|
||||
const item = localStorage.getItem(HEAD_STATE_KEY)
|
||||
@@ -94,13 +133,16 @@ export function useHeadStateNew() {
|
||||
})
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [state, current])
|
||||
}, [state, current, loadedProfile])
|
||||
|
||||
const setHeadState = useCallback(
|
||||
(groupName: string, obj: Partial<HeadState>) => {
|
||||
dispatch({ type: 'update', groupName, patch: obj })
|
||||
if (current && obj.testUrl !== undefined) {
|
||||
void commitProfileTestUrl(current, groupName, obj.testUrl)
|
||||
}
|
||||
},
|
||||
[],
|
||||
[current],
|
||||
)
|
||||
|
||||
return [state, setHeadState] as const
|
||||
|
||||
@@ -117,6 +117,7 @@ const virtualGroup = (members: ProxyGroupView['members']): ProxyGroupView => ({
|
||||
mptcp: false,
|
||||
smux: false,
|
||||
history: [],
|
||||
extra: {},
|
||||
members,
|
||||
})
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
const defaultBypass = () => {
|
||||
if (isWindows) {
|
||||
return 'localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>'
|
||||
return 'localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;*.local;<local>'
|
||||
}
|
||||
if (systemName === 'linux') {
|
||||
return 'localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1'
|
||||
|
||||
@@ -45,6 +45,7 @@ const languageOptions = supportedLanguages.map((code) => {
|
||||
es: 'Español',
|
||||
jp: '日本語',
|
||||
zhtw: '繁體中文',
|
||||
pt: 'Português',
|
||||
}
|
||||
const label = labels[code] || code
|
||||
return { code, label }
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
import type { ProfilePatch, ProfileSelection } from '@/services/bindings'
|
||||
import { getProfiles, patchProfile, patchProfilesConfig } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { migrateProfileTestUrls } from '@/services/proxy-test-url'
|
||||
import { fetchCacheData, setCacheData, useQuery } from '@/services/query-client'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
|
||||
@@ -35,6 +36,10 @@ export const useProfiles = () => {
|
||||
refetchInterval: false,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (profiles) migrateProfileTestUrls(profiles)
|
||||
}, [profiles])
|
||||
|
||||
const refetchRef = useRef(refetch)
|
||||
refetchRef.current = refetch
|
||||
const mutateProfiles = useCallback(async () => {
|
||||
|
||||
@@ -34,7 +34,6 @@ export function useProxyDelayState(
|
||||
groupName: string,
|
||||
): UseProxyDelayState {
|
||||
const name = member.ref.name
|
||||
const details = memberDetails(member)
|
||||
const unresolved = member.kind === 'unresolved'
|
||||
const isPreset = unresolved || PRESET_PROXY_NAMES.includes(name)
|
||||
const [delayState, setDelayState] = useReducer(identity, INITIAL_DELAY)
|
||||
@@ -43,18 +42,18 @@ export function useProxyDelayState(
|
||||
|
||||
useEffect(() => {
|
||||
if (isPreset) return
|
||||
delayManager.setListener(name, groupName, setDelayState)
|
||||
delayManager.setListener(member, groupName, setDelayState)
|
||||
return () => {
|
||||
delayManager.removeListener(name, groupName)
|
||||
delayManager.removeListener(member, groupName)
|
||||
}
|
||||
}, [name, groupName, isPreset])
|
||||
}, [member, groupName, isPreset])
|
||||
|
||||
const updateDelay = useCallback(() => {
|
||||
if (unresolved) {
|
||||
setDelayState(INITIAL_DELAY)
|
||||
return
|
||||
}
|
||||
const cachedUpdate = delayManager.getDelayUpdate(name, groupName)
|
||||
const cachedUpdate = delayManager.getDelayUpdate(member, groupName)
|
||||
if (cachedUpdate) {
|
||||
setDelayState({ ...cachedUpdate })
|
||||
return
|
||||
@@ -67,7 +66,9 @@ export function useProxyDelayState(
|
||||
}
|
||||
|
||||
let updatedAt = 0
|
||||
const history = details?.history
|
||||
const history =
|
||||
delayManager.getHistory(member, groupName) ??
|
||||
memberDetails(member)?.history
|
||||
if (history && history.length > 0) {
|
||||
const lastRecord = history[history.length - 1]
|
||||
const parsed = Date.parse(lastRecord.time)
|
||||
@@ -77,16 +78,18 @@ export function useProxyDelayState(
|
||||
}
|
||||
|
||||
setDelayState({ delay: fallbackDelay, updatedAt })
|
||||
}, [details?.history, groupName, member, name, unresolved])
|
||||
}, [groupName, member, unresolved])
|
||||
|
||||
useEffect(() => {
|
||||
updateDelay()
|
||||
}, [updateDelay])
|
||||
return delayManager.addGroupListener(groupName, updateDelay)
|
||||
}, [groupName, updateDelay])
|
||||
|
||||
const onDelay = useLockFn(async () => {
|
||||
if (!isInteractableMember(member)) return
|
||||
setDelayState({ delay: -2, updatedAt: Date.now() })
|
||||
setDelayState(await delayManager.checkDelay(member, groupName, timeout))
|
||||
await delayManager.checkDelay(member, groupName, timeout)
|
||||
updateDelay()
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -667,7 +667,7 @@
|
||||
"reinstallFailed": "تعذّر إعادة تثبيت الخدمة",
|
||||
"repairFailed": "تعذّر إصلاح الخدمة",
|
||||
"sidecarFailed": "تعذّر المتابعة باستخدام Sidecar",
|
||||
"coreAlreadyRunning": "تعذّر بدء وضع Sidecar: لا تزال نواة أخرى قيد التشغيل (PID {{pid}}). أغلق نسخ Clash Verge الأخرى أو التطبيق الذي يستخدم هذه النواة، ثم أعد المحاولة."
|
||||
"coreAlreadyRunning": "تعذّر بدء النواة: توجد عملية نواة قيد التشغيل بالفعل. أغلق التطبيق المعني أو أنهِ العملية المتبقية، ثم أعد المحاولة. إذا استمرت المشكلة، فأعد تشغيل الكمبيوتر."
|
||||
},
|
||||
"sysproxy": {
|
||||
"privilegeRequired": "تعذّر ضبط وكيل النظام: رفض macOS التغيير لعدم كفاية الصلاحيات. ثبّت خدمة Clash Verge؛ وإن كنت مسؤولًا فيمكنك أيضًا إيقاف اشتراط كلمة سر المسؤول للإعدادات على مستوى النظام من الخيارات المتقدمة في إعدادات النظام ← الخصوصية والأمان.",
|
||||
|
||||
@@ -667,7 +667,7 @@
|
||||
"reinstallFailed": "Dienst konnte nicht neu installiert werden",
|
||||
"repairFailed": "Dienst konnte nicht repariert werden",
|
||||
"sidecarFailed": "Fortsetzung mit Sidecar fehlgeschlagen",
|
||||
"coreAlreadyRunning": "Der Sidecar-Modus konnte nicht gestartet werden: Ein anderer Kern läuft noch (PID {{pid}}). Beenden Sie andere Clash-Verge-Instanzen oder die App, die diesen Kern verwendet, und versuchen Sie es erneut."
|
||||
"coreAlreadyRunning": "Der Kern kann nicht gestartet werden: Ein Kernprozess läuft bereits. Beenden Sie die zugehörige App oder den verbliebenen Prozess und versuchen Sie es erneut. Wenn das Problem weiterhin besteht, starten Sie den Computer neu."
|
||||
},
|
||||
"sysproxy": {
|
||||
"privilegeRequired": "Systemproxy kann nicht gesetzt werden: macOS hat die Änderung mangels Berechtigung abgelehnt. Installieren Sie den Clash-Verge-Dienst; als Administrator können Sie in den erweiterten Optionen unter Systemeinstellungen → Datenschutz & Sicherheit auch die Passwortpflicht für systemweite Einstellungen deaktivieren.",
|
||||
|
||||
@@ -667,7 +667,7 @@
|
||||
"reinstallFailed": "Failed to reinstall the service",
|
||||
"repairFailed": "Failed to repair the service",
|
||||
"sidecarFailed": "Failed to continue with Sidecar",
|
||||
"coreAlreadyRunning": "Could not start normal mode (Sidecar): another core is still running (PID {{pid}}). Exit other Clash Verge instances or the app using this core, then retry."
|
||||
"coreAlreadyRunning": "Cannot start the core: a core process is already running. Close the related app or stop the leftover process, then retry. If the problem persists, restart your computer."
|
||||
},
|
||||
"sysproxy": {
|
||||
"privilegeRequired": "Cannot set the system proxy: macOS refused the change for lack of privileges. Install the Clash Verge service; an administrator can also turn off the administrator-password requirement for system-wide settings, in the advanced options of System Settings → Privacy & Security.",
|
||||
|
||||
@@ -667,7 +667,7 @@
|
||||
"reinstallFailed": "No se pudo reinstalar el servicio",
|
||||
"repairFailed": "No se pudo reparar el servicio",
|
||||
"sidecarFailed": "No se pudo continuar con Sidecar",
|
||||
"coreAlreadyRunning": "No se pudo iniciar el modo Sidecar: hay otro núcleo en ejecución (PID {{pid}}). Cierre las otras instancias de Clash Verge o la aplicación que usa ese núcleo y vuelva a intentarlo."
|
||||
"coreAlreadyRunning": "No se puede iniciar el núcleo: ya hay un proceso del núcleo en ejecución. Cierre la aplicación correspondiente o finalice el proceso restante y vuelva a intentarlo. Si el problema persiste, reinicie el equipo."
|
||||
},
|
||||
"sysproxy": {
|
||||
"privilegeRequired": "No se puede establecer el proxy del sistema: macOS rechazó el cambio por falta de permisos. Instale el servicio de Clash Verge; si es administrador, también puede desactivar la exigencia de contraseña de administrador para los ajustes de todo el sistema en las opciones avanzadas de Ajustes del Sistema → Privacidad y seguridad.",
|
||||
|
||||
@@ -667,7 +667,7 @@
|
||||
"reinstallFailed": "نصب مجدد سرویس ناموفق بود",
|
||||
"repairFailed": "تعمیر سرویس ناموفق بود",
|
||||
"sidecarFailed": "ادامه با Sidecar ناموفق بود",
|
||||
"coreAlreadyRunning": "حالت Sidecar شروع نشد: هسته دیگری هنوز در حال اجراست (PID {{pid}}). نمونههای دیگر Clash Verge یا برنامهای که از این هسته استفاده میکند را ببندید و دوباره تلاش کنید."
|
||||
"coreAlreadyRunning": "هسته شروع نمیشود: یک فرایند هسته از قبل در حال اجراست. برنامه مرتبط را ببندید یا فرایند باقیمانده را متوقف کنید و دوباره تلاش کنید. اگر مشکل ادامه داشت، رایانه را راهاندازی مجدد کنید."
|
||||
},
|
||||
"sysproxy": {
|
||||
"privilegeRequired": "تنظیم پراکسی سیستم ممکن نیست: macOS به دلیل نداشتن دسترسی، این تغییر را رد کرد. سرویس Clash Verge را نصب کنید؛ اگر مدیر هستید میتوانید در گزینههای پیشرفتهٔ تنظیمات سیستم ← حریم خصوصی و امنیت، الزام رمز مدیر برای تنظیمات سراسری سیستم را نیز خاموش کنید.",
|
||||
|
||||
@@ -667,7 +667,7 @@
|
||||
"reinstallFailed": "Gagal memasang ulang layanan",
|
||||
"repairFailed": "Gagal memperbaiki layanan",
|
||||
"sidecarFailed": "Gagal melanjutkan dengan Sidecar",
|
||||
"coreAlreadyRunning": "Tidak dapat memulai mode Sidecar: inti lain masih berjalan (PID {{pid}}). Tutup instans Clash Verge lain atau aplikasi yang menggunakan inti tersebut, lalu coba lagi."
|
||||
"coreAlreadyRunning": "Tidak dapat memulai inti: proses inti sudah berjalan. Tutup aplikasi terkait atau hentikan proses yang tersisa, lalu coba lagi. Jika masalah berlanjut, mulai ulang komputer."
|
||||
},
|
||||
"sysproxy": {
|
||||
"privilegeRequired": "Tidak dapat mengatur proxy sistem: macOS menolak perubahan ini karena hak akses tidak mencukupi. Pasang layanan Clash Verge; jika Anda administrator, Anda juga dapat mematikan keharusan kata sandi administrator untuk pengaturan seluruh sistem di opsi lanjutan Pengaturan Sistem → Privasi & Keamanan.",
|
||||
|
||||
@@ -667,7 +667,7 @@
|
||||
"reinstallFailed": "サービスの再インストールに失敗しました",
|
||||
"repairFailed": "サービスの修復に失敗しました",
|
||||
"sidecarFailed": "Sidecar での続行に失敗しました",
|
||||
"coreAlreadyRunning": "通常モード(Sidecar)を開始できません:別のコアが実行中です(PID {{pid}})。他の Clash Verge またはこのコアを使用しているアプリを終了してから、再試行してください。"
|
||||
"coreAlreadyRunning": "コアを起動できません:コアプロセスが実行中です。関連アプリまたは残存プロセスを終了して再試行してください。解決しない場合は、コンピューターを再起動してください。"
|
||||
},
|
||||
"sysproxy": {
|
||||
"privilegeRequired": "システムプロキシを設定できません: 権限が不足しているため macOS が変更を拒否しました。Clash Verge サービスをインストールしてください。管理者であれば、システム設定 → プライバシーとセキュリティの詳細オプションで、システム全体の設定に管理者パスワードを要求する設定をオフにすることもできます。",
|
||||
|
||||
@@ -667,7 +667,7 @@
|
||||
"reinstallFailed": "서비스를 다시 설치하지 못했습니다",
|
||||
"repairFailed": "서비스를 복구하지 못했습니다",
|
||||
"sidecarFailed": "Sidecar로 계속하지 못했습니다",
|
||||
"coreAlreadyRunning": "일반 모드(Sidecar)를 시작할 수 없습니다. 다른 코어가 실행 중입니다(PID {{pid}}). 다른 Clash Verge 인스턴스 또는 해당 코어를 사용하는 앱을 종료한 후 다시 시도하세요."
|
||||
"coreAlreadyRunning": "코어를 시작할 수 없습니다. 이미 실행 중인 코어 프로세스가 있습니다. 관련 앱이나 남아 있는 프로세스를 종료한 뒤 다시 시도하세요. 문제가 계속되면 컴퓨터를 다시 시작하세요."
|
||||
},
|
||||
"sysproxy": {
|
||||
"privilegeRequired": "시스템 프록시를 설정할 수 없습니다: 권한이 부족하여 macOS가 변경을 거부했습니다. Clash Verge 서비스를 설치하십시오. 관리자라면 시스템 설정 → 개인 정보 보호 및 보안의 고급 옵션에서, 시스템 전체 설정에 관리자 암호를 요구하는 설정을 끌 수도 있습니다.",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"page": {
|
||||
"title": "Conexões"
|
||||
},
|
||||
"components": {
|
||||
"fields": {
|
||||
"host": "Host",
|
||||
"dlSpeed": "Vel. de download",
|
||||
"ulSpeed": "Vel. de upload",
|
||||
"chains": "Cadeias",
|
||||
"rule": "Regra",
|
||||
"process": "Processo",
|
||||
"time": "Hora",
|
||||
"source": "Origem",
|
||||
"destination": "Destino",
|
||||
"destinationPort": "Porta de destino",
|
||||
"type": "Tipo"
|
||||
},
|
||||
"order": {
|
||||
"default": "Padrão",
|
||||
"uploadSpeed": "Velocidade de upload",
|
||||
"downloadSpeed": "Velocidade de download"
|
||||
},
|
||||
"actions": {
|
||||
"active": "Ativas",
|
||||
"closed": "Fechadas",
|
||||
"closeConnection": "Fechar conexão"
|
||||
},
|
||||
"columnManager": {
|
||||
"title": "Colunas",
|
||||
"dragHandle": "Alça de arrastar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
{
|
||||
"page": {
|
||||
"tooltips": {
|
||||
"lightweightMode": "Modo leve",
|
||||
"manual": "Manual",
|
||||
"settings": "Configurações do início"
|
||||
},
|
||||
"cards": {
|
||||
"trafficStats": "Estatísticas de tráfego",
|
||||
"networkSettings": "Configurações de rede",
|
||||
"proxyMode": "Modo de proxy"
|
||||
},
|
||||
"settings": {
|
||||
"cards": {
|
||||
"profile": "Cartão de perfil",
|
||||
"currentProxy": "Cartão do proxy atual",
|
||||
"network": "Cartão de configurações de rede",
|
||||
"proxyMode": "Cartão do modo de proxy",
|
||||
"traffic": "Cartão de estatísticas de tráfego",
|
||||
"tests": "Cartão de testes de sites",
|
||||
"ip": "Cartão de informações de IP",
|
||||
"clashInfo": "Cartões de informações do Clash",
|
||||
"systemInfo": "Cartões de informações do sistema"
|
||||
},
|
||||
"title": "Configurações do início"
|
||||
},
|
||||
"title": "Início"
|
||||
},
|
||||
"components": {
|
||||
"proxyTun": {
|
||||
"status": {
|
||||
"systemProxyEnabled": "Proxy do sistema ativado",
|
||||
"systemProxyDisabled": "Proxy do sistema desativado",
|
||||
"tunModeServiceRequired": "Modo TUN requer o serviço",
|
||||
"tunModeEnabled": "Modo TUN ativado",
|
||||
"tunModeDisabled": "Modo TUN desativado"
|
||||
},
|
||||
"tooltips": {
|
||||
"systemProxy": "Informações do proxy do sistema",
|
||||
"tunMode": "Informações de interceptação do modo TUN"
|
||||
}
|
||||
},
|
||||
"clashInfo": {
|
||||
"title": "Informações do Clash",
|
||||
"fields": {
|
||||
"coreVersion": "Versão do núcleo",
|
||||
"systemProxyAddress": "Endereço do proxy do sistema",
|
||||
"mixedPort": "Porta mista",
|
||||
"uptime": "Tempo ativo",
|
||||
"rulesCount": "Quantidade de regras"
|
||||
}
|
||||
},
|
||||
"systemInfo": {
|
||||
"title": "Informações do sistema",
|
||||
"fields": {
|
||||
"osInfo": "Informações do SO",
|
||||
"autoLaunch": "Início automático",
|
||||
"runningMode": "Modo de execução",
|
||||
"lastCheckUpdate": "Última verificação de atualização",
|
||||
"vergeVersion": "Versão do Verge"
|
||||
},
|
||||
"actions": {
|
||||
"settings": "Configurações"
|
||||
},
|
||||
"badges": {
|
||||
"notRunning": "Núcleo não está em execução",
|
||||
"adminMode": "Modo administrador",
|
||||
"serviceMode": "Modo serviço",
|
||||
"sidecarMode": "Modo usuário",
|
||||
"adminServiceMode": "Modo admin + serviço"
|
||||
}
|
||||
},
|
||||
"ipInfo": {
|
||||
"title": "Informações de IP",
|
||||
"labels": {
|
||||
"ip": "IP",
|
||||
"asn": "ASN",
|
||||
"isp": "ISP",
|
||||
"org": "ORG",
|
||||
"location": "Localização",
|
||||
"timezone": "Fuso horário",
|
||||
"autoRefresh": "Atualização automática",
|
||||
"unknown": "Desconhecido"
|
||||
},
|
||||
"errors": {
|
||||
"load": "Falha ao obter informações de IP",
|
||||
"loadWithDetails": "Falha ao obter informações de IP: {{message}}",
|
||||
"noServices": "Nenhum serviço de detecção de IP disponível"
|
||||
}
|
||||
},
|
||||
"currentProxy": {
|
||||
"title": "Nó atual",
|
||||
"actions": {
|
||||
"refreshDelay": "Teste de latência"
|
||||
},
|
||||
"labels": {
|
||||
"globalMode": "Modo global",
|
||||
"directMode": "Modo direto",
|
||||
"group": "Grupo",
|
||||
"proxy": "Proxy",
|
||||
"noActiveNode": "Nenhum nó de proxy ativo"
|
||||
},
|
||||
"status": {
|
||||
"testing": "Testando",
|
||||
"untested": "Não testado",
|
||||
"error": "Erro",
|
||||
"timeout": "Tempo esgotado",
|
||||
"latencyHigh": "Latência alta",
|
||||
"latencyMedium": "Latência moderada",
|
||||
"latencyGood": "Latência boa",
|
||||
"latencyExcellent": "Latência excelente",
|
||||
"uninitialized": "Não inicializado",
|
||||
"noProxyNode": "Nenhum nó de proxy"
|
||||
}
|
||||
},
|
||||
"tests": {
|
||||
"title": "Testes de sites"
|
||||
},
|
||||
"traffic": {
|
||||
"metrics": {
|
||||
"uploadSpeed": "Velocidade de upload",
|
||||
"downloadSpeed": "Velocidade de download",
|
||||
"activeConnections": "Conexões ativas",
|
||||
"memoryUsage": "Uso do núcleo"
|
||||
},
|
||||
"legends": {
|
||||
"upload": "Envio",
|
||||
"download": "Recebimento"
|
||||
},
|
||||
"patterns": {
|
||||
"minutes": "{{time}} minutos"
|
||||
},
|
||||
"unknownTime": "Horário desconhecido",
|
||||
"chartStyles": {
|
||||
"smooth": "Suave",
|
||||
"linear": "Linear"
|
||||
},
|
||||
"diagnostics": "Pontos: {{points}} | Agrupados: {{compressed}} | FPS: {{fps}}"
|
||||
},
|
||||
"clashMode": {
|
||||
"errors": {
|
||||
"communication": "Erro de comunicação com o núcleo, reinicie o computador e tente novamente"
|
||||
},
|
||||
"labels": {
|
||||
"rule": "Regra",
|
||||
"global": "Global",
|
||||
"direct": "Direto"
|
||||
},
|
||||
"descriptions": {
|
||||
"rule": "Escolhe proxies automaticamente de acordo com o conjunto de regras.",
|
||||
"global": "Encaminha todas as requisições de rede pelo proxy selecionado.",
|
||||
"direct": "Ignora o proxy e conecta diretamente à internet."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"components": {
|
||||
"navigation": {
|
||||
"tabs": {
|
||||
"home": "Início",
|
||||
"proxies": "Proxies",
|
||||
"profiles": "Perfis",
|
||||
"connections": "Conexões",
|
||||
"rules": "Regras",
|
||||
"logs": "Logs",
|
||||
"unlock": "Teste",
|
||||
"settings": "Ajustes"
|
||||
},
|
||||
"menu": {
|
||||
"reorderMode": "Modo de reordenação do menu",
|
||||
"restoreDefaultOrder": "Restaurar ordem padrão",
|
||||
"unlock": "Desbloquear ordem do menu",
|
||||
"lock": "Bloquear ordem do menu",
|
||||
"collapseNavBar": "Recolher barra de navegação",
|
||||
"expandNavBar": "Expandir barra de navegação"
|
||||
}
|
||||
},
|
||||
"serviceMigration": {
|
||||
"locationRefusedMessage": "O serviço não pode iniciar o núcleo porque o caminho de instalação ou as permissões do diretório falharam em uma verificação de segurança. Reinstale o serviço para restaurar seus arquivos e permissões e tente novamente. O Windows pode solicitar aprovação de administrador.",
|
||||
"notAutoStartedMessage": "O tipo de inicialização do serviço Clash Verge foi alterado para Manual ou Desativado (geralmente por ferramentas de otimização do sistema), então ele não inicia mais com o Windows; esta sessão está usando Sidecar. Reinstale o serviço para restaurar a inicialização automática. O Windows pode solicitar aprovação de administrador.",
|
||||
"title": "Atualização do serviço necessária",
|
||||
"message": "A versão do serviço Clash Verge não corresponde à versão exigida por este app. Escolha Reinstalar serviço para desinstalar o serviço existente e instalar a versão exigida, ou continue com o Sidecar nesta sessão.",
|
||||
"checkingMessage": "Verificando o status do serviço do sistema e o modo de execução atual. Aguarde…",
|
||||
"unavailableMessage": "O serviço Clash Verge necessário não está disponível. Instale ou repare-o, ou continue com o Sidecar nesta sessão.",
|
||||
"reinstall": "Reinstalar serviço",
|
||||
"repair": "Reparar serviço",
|
||||
"continueSidecar": "Continuar com Sidecar",
|
||||
"success": "O serviço Clash Verge foi atualizado com sucesso",
|
||||
"errors": {
|
||||
"actionFailed": "A operação do serviço falhou",
|
||||
"stateRefreshFailed": "Falha ao atualizar o estado do serviço",
|
||||
"restartFailed": "Falha ao reiniciar o núcleo",
|
||||
"revalidationFailed": "Falha ao atualizar o estado de execução",
|
||||
"sidecarFailed": "Falha ao continuar com o Sidecar"
|
||||
}
|
||||
},
|
||||
"sysproxyPrivilege": {
|
||||
"title": "Não foi possível definir o proxy do sistema",
|
||||
"message": "O macOS recusou a alteração por falta de privilégios. Instalar o serviço Clash Verge permite que ele faça alterações de proxy como administrador, e este aviso não voltará.",
|
||||
"serviceReadyMessage": "O serviço Clash Verge está pronto, mas o núcleo ainda está em execução como sidecar, por isso a definição do proxy do sistema foi recusada. Mudar para o modo serviço resolve — sem precisar reinstalar.",
|
||||
"alternative": "Como alternativa, desative “exigir senha de administrador para acessar as configurações do sistema” em Ajustes do Sistema → Privacidade e Segurança → Avançado.",
|
||||
"later": "Depois",
|
||||
"installing": "Instalando o serviço do sistema. Pode ser solicitada a senha de administrador.",
|
||||
"restarting": "Serviço instalado. Reiniciando o núcleo por meio dele…",
|
||||
"applying": "Aplicando a alteração do proxy do sistema…",
|
||||
"tunTitle": "O modo TUN precisa do serviço do sistema",
|
||||
"tunMessage": "O modo TUN precisa do serviço do sistema Clash Verge para assumir o tráfego de rede. Depois de instalado, o modo TUN será ativado para você."
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"errors": {
|
||||
"restartCoreStopFailed": "Não foi possível parar o núcleo com segurança, então a reinicialização foi cancelada",
|
||||
"quitCoreStopFailed": "Não foi possível parar o núcleo com segurança, então a saída foi cancelada"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"page": {
|
||||
"title": "Logs"
|
||||
},
|
||||
"actions": {
|
||||
"showDescending": "Mais recentes primeiro",
|
||||
"showAscending": "Mais antigos primeiro"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"page": {
|
||||
"actions": {
|
||||
"updateAll": "Atualizar todos os perfis",
|
||||
"viewRuntimeConfig": "Ver configuração em execução",
|
||||
"reactivate": "Reativar perfis",
|
||||
"import": "Importar"
|
||||
},
|
||||
"batch": {
|
||||
"actions": {
|
||||
"delete": "Excluir perfis selecionados",
|
||||
"selectAll": "Selecionar tudo",
|
||||
"deselectAll": "Desmarcar tudo",
|
||||
"done": "Concluído"
|
||||
},
|
||||
"summary": {
|
||||
"selected": "Selecionados",
|
||||
"items": "itens"
|
||||
},
|
||||
"title": "Operações em lote"
|
||||
},
|
||||
"importForm": {
|
||||
"placeholder": "URL do perfil",
|
||||
"actions": {
|
||||
"paste": "Colar"
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"errors": {
|
||||
"invalidUrl": "URL de perfil inválida. Insira uma URL começando com http:// ou https://",
|
||||
"onlyYaml": "Somente arquivos YAML são suportados",
|
||||
"createFailed": "Falha ao criar o perfil",
|
||||
"deleteFailed": "Falha ao excluir o perfil",
|
||||
"enhanceFailed": "Falha ao reativar os perfis",
|
||||
"importFailed": "Falha ao importar o perfil",
|
||||
"openFailed": "Falha ao abrir o perfil",
|
||||
"readFailed": "Falha ao ler o perfil",
|
||||
"reorderFailed": "Falha ao reordenar os perfis",
|
||||
"switchFailed": "Falha ao trocar de perfil",
|
||||
"updateFailed": "Falha ao atualizar o perfil"
|
||||
},
|
||||
"notifications": {
|
||||
"importRetry": "Falha na importação, tentando novamente com o proxy Clash...",
|
||||
"importFail": "Falha na importação mesmo com o proxy Clash",
|
||||
"importNeedsRefresh": "Perfil importado, mas pode precisar de atualização manual",
|
||||
"importSuccess": "Perfil importado com sucesso, reinicie se não aparecer",
|
||||
"profileSwitched": "Perfil trocado",
|
||||
"profileReactivated": "Perfil reativado",
|
||||
"switchBusy": "A troca de perfil já está em andamento",
|
||||
"batchDeleted": "Perfis selecionados excluídos com sucesso"
|
||||
},
|
||||
"notices": {
|
||||
"forceRefreshCompleted": "Atualização forçada concluída",
|
||||
"discardedKeys": "As extensões gravaram {{keys}}, que são gerenciados pelas Configurações, então esses valores foram descartados",
|
||||
"emergencyRefreshFailed": "Falha na atualização de emergência: {{message}}"
|
||||
},
|
||||
"tooltips": {
|
||||
"forceRefreshStaleData": "Os dados do perfil estão indisponíveis; clique para forçar a atualização"
|
||||
}
|
||||
},
|
||||
"title": "Perfis"
|
||||
},
|
||||
"components": {
|
||||
"card": {
|
||||
"labels": {
|
||||
"clickToImport": "Clique para importar a assinatura"
|
||||
}
|
||||
},
|
||||
"fileInput": {
|
||||
"chooseFile": "Escolher arquivo"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Início",
|
||||
"select": "Selecionar",
|
||||
"shareQrCode": "Compartilhar QR Code",
|
||||
"editInfo": "Editar informações",
|
||||
"editFile": "Editar arquivo",
|
||||
"editRules": "Editar regras",
|
||||
"editProxies": "Editar proxies",
|
||||
"editGroups": "Editar grupos de proxy",
|
||||
"extendConfig": "Configuração de extensão",
|
||||
"extendScript": "Script de extensão",
|
||||
"openFile": "Abrir arquivo",
|
||||
"update": "Atualizar",
|
||||
"updateViaProxy": "Atualizar via proxy"
|
||||
},
|
||||
"more": {
|
||||
"global": {
|
||||
"merge": "Extensão global",
|
||||
"script": "Script de extensão global"
|
||||
},
|
||||
"chips": {
|
||||
"merge": "Mesclar",
|
||||
"script": "Script"
|
||||
}
|
||||
},
|
||||
"profileItem": {
|
||||
"tooltips": {
|
||||
"showLast": "Clique para mostrar a última atualização",
|
||||
"showNext": "Clique para mostrar a próxima atualização"
|
||||
},
|
||||
"status": {
|
||||
"lastUpdateFailed": "Falha na última atualização",
|
||||
"nextUp": "Próxima",
|
||||
"noSchedule": "Sem agendamento",
|
||||
"unknown": "Desconhecido",
|
||||
"autoUpdateDisabled": "Atualização automática desativada"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"profileForm": {
|
||||
"title": {
|
||||
"create": "Criar perfil",
|
||||
"edit": "Editar perfil"
|
||||
},
|
||||
"fields": {
|
||||
"type": "Tipo",
|
||||
"description": "Descrição",
|
||||
"subscriptionUrl": "URL da assinatura",
|
||||
"userAgent": "User Agent",
|
||||
"httpTimeout": "Tempo limite HTTP",
|
||||
"updateInterval": "Intervalo de atualização",
|
||||
"useSystemProxy": "Usar proxy do sistema",
|
||||
"useClashProxy": "Usar proxy Clash",
|
||||
"acceptInvalidCerts": "Permitir certificados inválidos (perigoso)",
|
||||
"allowAutoUpdate": "Permitir atualização automática"
|
||||
},
|
||||
"types": {
|
||||
"remote": "Remoto",
|
||||
"local": "Local"
|
||||
},
|
||||
"errors": {
|
||||
"typeRequired": "Selecione um tipo de perfil",
|
||||
"urlRequired": "Insira a URL da assinatura",
|
||||
"uidMissing": "O identificador do perfil está ausente",
|
||||
"saveFailed": "Falha ao salvar o perfil"
|
||||
},
|
||||
"warnings": {
|
||||
"frequentUpdate": "Um intervalo curto recarrega o núcleo com essa frequência e derruba as conexões existentes. Recomendados: {{minutes}} minutos (24 horas)."
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"creationRetry": "Falha ao criar o perfil, tentando novamente com o proxy Clash...",
|
||||
"creationSuccess": "Perfil criado com sucesso com o proxy Clash"
|
||||
}
|
||||
}
|
||||
},
|
||||
"proxiesEditor": {
|
||||
"title": "Editar proxies",
|
||||
"placeholders": {
|
||||
"multiUri": "Use quebras de linha para várias URIs (Base64 suportado)"
|
||||
},
|
||||
"actions": {
|
||||
"prepend": "Adicionar proxy no início",
|
||||
"append": "Adicionar proxy no fim"
|
||||
}
|
||||
},
|
||||
"groupsEditor": {
|
||||
"title": "Editar grupos de proxy",
|
||||
"errors": {
|
||||
"nameRequired": "Nome do grupo obrigatório",
|
||||
"nameExists": "O nome do grupo já existe"
|
||||
},
|
||||
"fields": {
|
||||
"type": "Tipo de grupo",
|
||||
"name": "Nome do grupo",
|
||||
"icon": "Ícone do grupo de proxy",
|
||||
"proxies": "Usar proxies",
|
||||
"provider": "Usar provedor",
|
||||
"healthCheckUrl": "URL de verificação de saúde",
|
||||
"expectedStatus": "Status esperado",
|
||||
"interval": "Intervalo",
|
||||
"maxFailedTimes": "Máximo de falhas",
|
||||
"interfaceName": "Nome da interface",
|
||||
"routingMark": "Marca de roteamento",
|
||||
"filter": "Filtro",
|
||||
"excludeFilter": "Filtro de exclusão",
|
||||
"excludeType": "Tipo de exclusão",
|
||||
"includeAll": "Incluir todos os proxies e provedores",
|
||||
"includeAllProxies": "Incluir todos os proxies",
|
||||
"includeAllProviders": "Incluir todos os provedores"
|
||||
},
|
||||
"toggles": {
|
||||
"lazy": "Lazy",
|
||||
"disableUdp": "Desativar UDP",
|
||||
"hidden": "Oculto"
|
||||
},
|
||||
"actions": {
|
||||
"prepend": "Adicionar grupo no início",
|
||||
"append": "Adicionar grupo no fim"
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
"actions": {
|
||||
"format": "Formatar documento"
|
||||
},
|
||||
"messages": {
|
||||
"readOnly": "Não é possível editar em um editor somente leitura"
|
||||
},
|
||||
"enhance": {
|
||||
"globalMerge": "Aplica-se a todos os perfis, após as configurações do app. O script global, a configuração do perfil e o script do perfil ainda podem alterar o resultado.",
|
||||
"globalScript": "Aplica-se a todos os perfis, usando o resultado da mesclagem da configuração global. A configuração do perfil e o script do perfil ainda podem alterar o resultado.",
|
||||
"profileMerge": "Aplica-se somente a este perfil, após a configuração global e o script global. O script do perfil ainda pode alterar o resultado.",
|
||||
"profileScript": "Aplica-se somente a este perfil, usando o resultado da mesclagem da configuração do perfil. Esta é a última etapa de extensão antes de as configurações do app serem reaplicadas.",
|
||||
"order": "Ordem: Perfil original → Configurações do app → <globalMerge>Configuração de extensão global</globalMerge> → <globalScript>Script de extensão global</globalScript> → <profileMerge>Configuração de extensão do perfil</profileMerge> → <profileScript>Script de extensão do perfil</profileScript> → Configurações do app reaplicadas",
|
||||
"settingsPriority": "Nota: os campos gerenciados pelas configurações do app sempre usam essas configurações na configuração final. Alterar esses campos nas extensões não tem efeito."
|
||||
}
|
||||
},
|
||||
"confirmDelete": {
|
||||
"title": "Confirmar exclusão",
|
||||
"message": "Esta operação não é reversível"
|
||||
},
|
||||
"logViewer": {
|
||||
"title": "Console de scripts"
|
||||
},
|
||||
"qrViewer": {
|
||||
"title": "QR Code da assinatura"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"page": {
|
||||
"modes": {
|
||||
"rule": "Regra",
|
||||
"global": "Global",
|
||||
"direct": "Direto"
|
||||
},
|
||||
"actions": {
|
||||
"toggleChain": "Proxy em cadeia",
|
||||
"connect": "Conectar",
|
||||
"disconnect": "Desconectar",
|
||||
"connecting": "Conectando...",
|
||||
"clearChainConfig": "Excluir configuração da cadeia"
|
||||
},
|
||||
"provider": {
|
||||
"title": "Provedor de proxy",
|
||||
"actions": {
|
||||
"updateAll": "Atualizar tudo",
|
||||
"update": "Atualizar"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"title": "Regras de proxy",
|
||||
"select": "Selecionar regras"
|
||||
},
|
||||
"labels": {
|
||||
"proxyCount": "Quantidade de proxies",
|
||||
"delayCheckReset": "Teste de latência para cancelar a fixação",
|
||||
"nodeCount": "{{count}} nós"
|
||||
},
|
||||
"tooltips": {
|
||||
"locate": "localizar",
|
||||
"delayCheck": "Teste de latência",
|
||||
"sortDefault": "Ordenar por padrão",
|
||||
"sortDelay": "Ordenar por latência",
|
||||
"sortName": "Ordenar por nome",
|
||||
"delayCheckUrl": "URL do teste de latência",
|
||||
"showBasic": "Proxy básico",
|
||||
"showDetail": "Proxy detalhado",
|
||||
"filter": "Filtro"
|
||||
},
|
||||
"placeholders": {
|
||||
"delayCheckUrl": "URL do teste de latência"
|
||||
},
|
||||
"empty": {
|
||||
"noAvailableGroups": "Nenhum grupo de proxy disponível",
|
||||
"noProxies": "Nenhum proxy",
|
||||
"noSubscriptions": {
|
||||
"title": "Nenhuma assinatura importada",
|
||||
"description": "Não há nada para exibir na página Proxies. Vá para Perfis para importar e ativar uma assinatura."
|
||||
},
|
||||
"inactiveSubscription": {
|
||||
"title": "Nenhuma assinatura ativa",
|
||||
"description": "Foram encontradas assinaturas, mas nenhuma está ativa. Vá para Perfis e selecione uma para ativar."
|
||||
},
|
||||
"coreUnavailable": {
|
||||
"title": "Núcleo temporariamente indisponível",
|
||||
"description": "Não foi possível obter as informações de proxy do núcleo. Tente reiniciá-lo. Se o problema persistir, abra a pasta de logs e verifique os logs do núcleo."
|
||||
},
|
||||
"noProxyInfo": {
|
||||
"title": "Nenhuma informação de proxy disponível",
|
||||
"description": "Não há grupos de proxy nem nós para exibir. Verifique se a assinatura ativa contém grupos e nós de proxy válidos, ou reinicie o núcleo e revise os logs."
|
||||
},
|
||||
"actions": {
|
||||
"openProfiles": "Ir para Perfis",
|
||||
"restartCore": "Reiniciar núcleo",
|
||||
"openLogs": "Abrir pasta de logs"
|
||||
}
|
||||
},
|
||||
"chain": {
|
||||
"header": "Configuração do proxy em cadeia",
|
||||
"warning": "O proxy em cadeia reduz significativamente a velocidade da rede. Se você não tem uma necessidade clara ou não entende esta configuração, mantenha-o desativado.",
|
||||
"empty": "O proxy em cadeia reduz significativamente a velocidade da rede. Se você não tem uma necessidade clara ou não entende esta configuração, mantenha-o desativado.",
|
||||
"instruction": "Clique nos nós em ordem para adicioná-los à cadeia de proxy",
|
||||
"minimumNodes": "O proxy em cadeia requer pelo menos 2 nós",
|
||||
"minimumNodesHint": "O proxy em cadeia requer pelo menos 2 nós. Adicione mais um nó.",
|
||||
"connectFailed": "Falha ao conectar à cadeia de proxy",
|
||||
"disconnectFailed": "Falha ao desconectar da cadeia de proxy",
|
||||
"duplicateNode": "O nó de proxy já existe na cadeia",
|
||||
"entryNode": "Entrada",
|
||||
"exitNode": "Saída"
|
||||
},
|
||||
"messages": {
|
||||
"directMode": "Modo direto"
|
||||
},
|
||||
"title": {
|
||||
"default": "Grupos de proxy",
|
||||
"chainMode": "Modo de proxy em cadeia"
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"warnings": {
|
||||
"invalidTestUrl": "A URL de teste deve começar com http:// ou https://"
|
||||
},
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} atualizado com sucesso",
|
||||
"updateFailed": "Falha ao atualizar {{name}}: {{message}}",
|
||||
"genericError": "Falha na atualização: {{message}}",
|
||||
"none": "Nenhum provedor disponível para atualizar",
|
||||
"allUpdated": "Todos os provedores atualizados com sucesso"
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"enums": {
|
||||
"strategies": {
|
||||
"select": "Selecionar proxy manualmente",
|
||||
"url-test": "Selecionar proxy com base na latência do teste de URL",
|
||||
"fallback": "Alternar para outro proxy em caso de erro",
|
||||
"load-balance": "Distribuir proxies com balanceamento de carga",
|
||||
"relay": "Passar pela cadeia de proxy definida"
|
||||
},
|
||||
"policies": {
|
||||
"DIRECT": "Os dados saem diretamente (DIRECT)",
|
||||
"REJECT": "Intercepta as requisições (REJECT)",
|
||||
"REJECT-DROP": "Descarta as requisições (REJECT-DROP)",
|
||||
"PASS": "Ignora esta regra quando houver correspondência (PASS)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"page": {
|
||||
"provider": {
|
||||
"trigger": "Provedor de regras",
|
||||
"dialogTitle": "Provedores de regras",
|
||||
"actions": {
|
||||
"updateAll": "Atualizar tudo",
|
||||
"update": "Atualizar"
|
||||
}
|
||||
},
|
||||
"title": "Regras"
|
||||
},
|
||||
"feedback": {
|
||||
"notifications": {
|
||||
"provider": {
|
||||
"updateSuccess": "{{name}} atualizado com sucesso",
|
||||
"updateFailed": "Falha ao atualizar {{name}}: {{message}}",
|
||||
"genericError": "Falha na atualização: {{message}}",
|
||||
"none": "Nenhum provedor disponível para atualizar",
|
||||
"allUpdated": "Todos os provedores foram atualizados com sucesso"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"editor": {
|
||||
"form": {
|
||||
"labels": {
|
||||
"type": "Tipo de regra",
|
||||
"content": "Conteúdo da regra",
|
||||
"proxyPolicy": "Política de proxy"
|
||||
},
|
||||
"toggles": {
|
||||
"noResolve": "Sem resolução"
|
||||
},
|
||||
"actions": {
|
||||
"prependRule": "Adicionar regra no início",
|
||||
"appendRule": "Adicionar regra no fim"
|
||||
},
|
||||
"validation": {
|
||||
"conditionRequired": "Condição da regra obrigatória",
|
||||
"invalidRule": "Regra inválida"
|
||||
}
|
||||
},
|
||||
"ruleTypes": {
|
||||
"DOMAIN": "Corresponder ao domínio completo (DOMAIN)",
|
||||
"DOMAIN-SUFFIX": "Corresponder ao sufixo de domínio (DOMAIN-SUFFIX)",
|
||||
"DOMAIN-KEYWORD": "Corresponder à palavra-chave do domínio (DOMAIN-KEYWORD)",
|
||||
"DOMAIN-REGEX": "Corresponder ao domínio por regex (DOMAIN-REGEX)",
|
||||
"GEOSITE": "Corresponder a domínios do GeoSite (GEOSITE)",
|
||||
"GEOIP": "Corresponder ao código de país do IP (GEOIP)",
|
||||
"SRC-GEOIP": "Corresponder ao código de país do IP de origem (SRC-GEOIP)",
|
||||
"IP-ASN": "Corresponder ao ASN do IP (IP-ASN)",
|
||||
"SRC-IP-ASN": "Corresponder ao ASN do IP de origem (SRC-IP-ASN)",
|
||||
"IP-CIDR": "Corresponder à faixa de endereços IP (IP-CIDR)",
|
||||
"IP-CIDR6": "Corresponder à faixa de endereços IPv6 (IP-CIDR6)",
|
||||
"SRC-IP-CIDR": "Corresponder à faixa de endereços IP de origem (SRC-IP-CIDR)",
|
||||
"IP-SUFFIX": "Corresponder à faixa de sufixo de IP (IP-SUFFIX)",
|
||||
"SRC-IP-SUFFIX": "Corresponder à faixa de sufixo de IP de origem (SRC-IP-SUFFIX)",
|
||||
"SRC-PORT": "Corresponder à faixa de portas de origem (SRC-PORT)",
|
||||
"DST-PORT": "Corresponder à faixa de portas de destino (DST-PORT)",
|
||||
"IN-PORT": "Corresponder à porta de entrada (IN-PORT)",
|
||||
"DSCP": "Tag DSCP (apenas entrada TPROXY UDP) (DSCP)",
|
||||
"PROCESS-NAME": "Corresponder ao nome do processo (PROCESS-NAME)",
|
||||
"PROCESS-PATH": "Corresponder ao caminho completo do processo (PROCESS-PATH)",
|
||||
"PROCESS-NAME-REGEX": "Corresponder ao nome do processo por regex (PROCESS-NAME-REGEX)",
|
||||
"PROCESS-PATH-REGEX": "Corresponder ao caminho completo do processo por regex (PROCESS-PATH-REGEX)",
|
||||
"NETWORK": "Corresponder ao protocolo de rede (TCP/UDP) (NETWORK)",
|
||||
"UID": "Corresponder ao ID de usuário Linux (UID)",
|
||||
"IN-TYPE": "Corresponder ao tipo de entrada (IN-TYPE)",
|
||||
"IN-USER": "Corresponder ao usuário de entrada (IN-USER)",
|
||||
"IN-NAME": "Corresponder ao nome da entrada (IN-NAME)",
|
||||
"SUB-RULE": "Sub-regra (SUB-RULE)",
|
||||
"RULE-SET": "Corresponder ao conjunto de regras (RULE-SET)",
|
||||
"AND": "E lógico (AND)",
|
||||
"OR": "OU lógico (OR)",
|
||||
"NOT": "NÃO lógico (NOT)",
|
||||
"MATCH": "Corresponder a todas as requisições (MATCH)"
|
||||
},
|
||||
"title": "Editar regras"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,716 @@
|
||||
{
|
||||
"page": {
|
||||
"actions": {
|
||||
"manual": "Manual",
|
||||
"telegram": "Canal do Telegram",
|
||||
"github": "Repositório do Github"
|
||||
},
|
||||
"actionsGroupLabel": "Links das configurações",
|
||||
"title": "Configurações"
|
||||
},
|
||||
"sections": {
|
||||
"system": {
|
||||
"title": "Configurações do Sistema",
|
||||
"toggles": {
|
||||
"tunMode": "Modo Tun",
|
||||
"systemProxy": "Proxy do Sistema"
|
||||
},
|
||||
"tooltips": {
|
||||
"silentStart": "Inicia o programa em segundo plano sem exibir o painel"
|
||||
},
|
||||
"fields": {
|
||||
"autoLaunch": "Iniciar com o Sistema",
|
||||
"silentStart": "Início Silencioso"
|
||||
},
|
||||
"notifications": {
|
||||
"tunMode": {
|
||||
"autoDisabled": "Modo TUN desativado automaticamente porque o serviço está indisponível",
|
||||
"autoDisableFailed": "Falha ao desativar o Modo TUN automaticamente"
|
||||
}
|
||||
}
|
||||
},
|
||||
"proxyControl": {
|
||||
"tooltips": {
|
||||
"systemProxy": "Ative para modificar as configurações de proxy do sistema operacional. Se a ativação falhar, modifique as configurações de proxy do sistema operacional manualmente",
|
||||
"tunMode": "Modo Tun (placa de rede virtual): captura todo o tráfego do sistema. Quando ativado, não é necessário ativar o proxy do sistema.",
|
||||
"tunUnavailable": "O TUN requer o Modo Serviço ou o Modo Administrador"
|
||||
},
|
||||
"actions": {
|
||||
"installService": "Instalar Serviço",
|
||||
"uninstallService": "Desinstalar Serviço",
|
||||
"switchToServiceMode": "Mudar para o modo serviço"
|
||||
},
|
||||
"fields": {
|
||||
"systemProxy": "Proxy do Sistema",
|
||||
"tunMode": "Modo Tun"
|
||||
},
|
||||
"messages": {
|
||||
"installedCheckProxy": "O serviço está instalado e o núcleo está em execução por meio dele. Verifique o interruptor do proxy do sistema e refaça a alteração se ela não tiver sido aplicada.",
|
||||
"installedCoreNotOnService": "O serviço está instalado, mas o núcleo não está em execução por meio dele. Verifique o núcleo e tente novamente.",
|
||||
"installedProxyRestored": "O serviço está instalado, o núcleo está em execução por meio dele e a alteração do proxy do sistema foi aplicada."
|
||||
}
|
||||
},
|
||||
"externalController": {
|
||||
"title": "Controlador Externo",
|
||||
"fields": {
|
||||
"enable": "Ativar Controlador Externo",
|
||||
"address": "Controlador Externo",
|
||||
"secret": "Segredo do Núcleo"
|
||||
},
|
||||
"placeholders": {
|
||||
"address": "Obrigatório",
|
||||
"secret": "Recomendado"
|
||||
},
|
||||
"tooltips": {
|
||||
"copy": "Copiar para a área de transferência"
|
||||
},
|
||||
"messages": {
|
||||
"addressRequired": "O endereço do controlador não pode estar vazio",
|
||||
"secretRequired": "O segredo não pode estar vazio",
|
||||
"copyFailed": "Falha ao copiar",
|
||||
"controllerCopied": "Endereço do controlador copiado para a área de transferência",
|
||||
"secretCopied": "Segredo copiado para a área de transferência"
|
||||
}
|
||||
},
|
||||
"externalCors": {
|
||||
"title": "Configuração de CORS Externo",
|
||||
"fields": {
|
||||
"allowPrivateNetwork": "Permitir acesso à rede privada",
|
||||
"allowedOrigins": "Origens Permitidas"
|
||||
},
|
||||
"placeholders": {
|
||||
"origin": "Insira uma URL válida"
|
||||
},
|
||||
"actions": {
|
||||
"add": "Adicionar"
|
||||
},
|
||||
"messages": {
|
||||
"alwaysIncluded": "Origens sempre incluídas: {{urls}}"
|
||||
},
|
||||
"tooltips": {
|
||||
"open": "Configurações de CORS Externo"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
"light": "Claro",
|
||||
"dark": "Escuro",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"clash": {
|
||||
"title": "Configurações do Clash",
|
||||
"form": {
|
||||
"fields": {
|
||||
"allowLan": "Permitir LAN",
|
||||
"dnsOverwrite": "Sobrescrever DNS",
|
||||
"ipv6": "IPv6",
|
||||
"unifiedDelay": "Atraso Unificado",
|
||||
"logLevel": "Nível de Log",
|
||||
"portConfig": "Configuração de Portas",
|
||||
"external": "Externo",
|
||||
"webUI": "Web UI",
|
||||
"clashCore": "Núcleo do Clash",
|
||||
"openUwpTool": "Abrir ferramenta UWP",
|
||||
"updateGeoData": "Atualizar GeoData",
|
||||
"tunnels": {
|
||||
"title": "Gerenciamento de Túneis",
|
||||
"localAddr": "Endereço de Escuta Local",
|
||||
"localPort": "Porta de Escuta Local",
|
||||
"targetAddr": "Endereço de Destino",
|
||||
"targetPort": "Porta de Destino",
|
||||
"proxyGroup": "Grupo de Proxy",
|
||||
"proxyNode": "Nó de Proxy",
|
||||
"protocols": "Protocolo",
|
||||
"existing": "Túneis Existentes",
|
||||
"default": "Seguir a Configuração Atual",
|
||||
"optional": "Opcional",
|
||||
"messages": {
|
||||
"incomplete": "Preencha todos os campos obrigatórios do túnel",
|
||||
"invalidLocalAddr": "Endereço de escuta local inválido",
|
||||
"invalidLocalPort": "Porta de escuta local inválida",
|
||||
"invalidTargetAddr": "Endereço de destino inválido",
|
||||
"invalidTargetPort": "Porta de destino inválida"
|
||||
},
|
||||
"actions": {
|
||||
"add": "Adicionar",
|
||||
"addNew": "Adicionar Novo Túnel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tooltips": {
|
||||
"networkInterface": "Interface de Rede",
|
||||
"unifiedDelay": "Com o atraso unificado ativado, dois testes de atraso são realizados para eliminar as diferenças de atraso entre diferentes tipos de nós causadas por handshakes de conexão, etc.",
|
||||
"logLevel": "Este parâmetro é válido apenas para os arquivos de log do núcleo na pasta Service do diretório de logs",
|
||||
"openUwpTool": "Desde o Windows 8, os aplicativos UWP (como a Microsoft Store) são impedidos de acessar diretamente os serviços de rede do host local, e esta ferramenta pode ser usada para contornar essa restrição"
|
||||
},
|
||||
"options": {
|
||||
"logLevel": {
|
||||
"debug": "Debug",
|
||||
"info": "Info",
|
||||
"warning": "Aviso",
|
||||
"error": "Erro",
|
||||
"silent": "Silencioso"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"verge": {
|
||||
"basic": {
|
||||
"title": "Configurações Básicas do Verge",
|
||||
"actions": {
|
||||
"browse": "Procurar"
|
||||
},
|
||||
"trayOptions": {
|
||||
"showMainWindow": "Abrir Janela",
|
||||
"showTrayMenu": "Abrir Menu",
|
||||
"disable": "Desativar"
|
||||
},
|
||||
"fields": {
|
||||
"language": "Idioma",
|
||||
"themeMode": "Modo do Tema",
|
||||
"trayClickEvent": "Ação de Clique na Bandeja",
|
||||
"copyEnvType": "Tipo de Env ao Copiar",
|
||||
"startPage": "Página Inicial",
|
||||
"startupScript": "Script de Inicialização",
|
||||
"themeSetting": "Configurações do Tema",
|
||||
"layoutSetting": "Configurações de Layout",
|
||||
"misc": "Diversos",
|
||||
"hotkeySetting": "Configurações de Atalhos"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Configurações Avançadas do Verge",
|
||||
"tooltips": {
|
||||
"backupInfo": "Suporta backup local ou via WebDAV dos arquivos de configuração",
|
||||
"openConfDir": "Se o software funcionar de forma anormal, FAÇA BACKUP e exclua todos os arquivos desta pasta e reinicie o software",
|
||||
"liteMode": "Fecha a interface gráfica e mantém apenas o núcleo em execução"
|
||||
},
|
||||
"actions": {
|
||||
"copyVersion": "Copiar Versão"
|
||||
},
|
||||
"notifications": {
|
||||
"latestVersion": "Você já está na versão mais recente",
|
||||
"versionCopied": "Versão copiada para a área de transferência"
|
||||
},
|
||||
"fields": {
|
||||
"backupSetting": "Configurações de Backup",
|
||||
"runtimeConfig": "Configuração em Execução",
|
||||
"openConfDir": "Abrir Pasta de Configuração",
|
||||
"openCoreDir": "Abrir Pasta do Núcleo",
|
||||
"openLogsDir": "Abrir Pasta de Logs",
|
||||
"checkUpdates": "Verificar Atualizações",
|
||||
"openDevTools": "Ferramentas de Desenvolvedor",
|
||||
"liteModeSettings": "Configurações do Modo Leve",
|
||||
"exit": "Sair",
|
||||
"exportDiagnostics": "Exportar Informações de Diagnóstico",
|
||||
"vergeVersion": "Versão do Verge"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"title": "Configurações do Tema",
|
||||
"fields": {
|
||||
"primaryColor": "Cor Primária",
|
||||
"secondaryColor": "Cor Secundária",
|
||||
"primaryText": "Texto Primário",
|
||||
"secondaryText": "Texto Secundário",
|
||||
"infoColor": "Cor de Informação",
|
||||
"warningColor": "Cor de Aviso",
|
||||
"errorColor": "Cor de Erro",
|
||||
"successColor": "Cor de Sucesso",
|
||||
"fontFamily": "Família da Fonte",
|
||||
"cssInjection": "Injeção de CSS"
|
||||
},
|
||||
"actions": {
|
||||
"editCss": "Editar CSS"
|
||||
},
|
||||
"dialogs": {
|
||||
"editCssTitle": "Editar CSS"
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"title": "Configurações de Layout",
|
||||
"fields": {
|
||||
"preferSystemTitlebar": "Preferir Barra de Título do Sistema",
|
||||
"trafficGraph": "Gráfico de Tráfego",
|
||||
"memoryUsage": "Uso do Núcleo",
|
||||
"proxyGroupIcon": "Ícone do Grupo de Proxy",
|
||||
"toastPosition": "Posição das Notificações",
|
||||
"hoverNavigator": "Navegador de Salto ao Passar o Mouse",
|
||||
"hoverNavigatorDelay": "Atraso do Navegador de Salto ao Passar o Mouse",
|
||||
"navIcon": "Ícone de Navegação",
|
||||
"collapseNavBar": "Recolher Barra de Navegação",
|
||||
"trayIcon": "Ícone da Bandeja",
|
||||
"proxyGroupsDisplayMode": "Modo de Exibição dos Grupos de Proxy",
|
||||
"showOutboundModesInline": "Mostrar Modos de Saída em Linha",
|
||||
"commonTrayIcon": "Ícone Comum da Bandeja",
|
||||
"systemProxyTrayIcon": "Ícone da Bandeja do Proxy do Sistema",
|
||||
"tunTrayIcon": "Ícone da Bandeja do Tun",
|
||||
"enableTrayIcon": "Ativar Ícone da Bandeja",
|
||||
"enableTraySpeed": "Ativar Velocidade na Bandeja",
|
||||
"pauseRenderTrafficStatsOnBlur": "Pausar a renderização das estatísticas de tráfego quando sem foco"
|
||||
},
|
||||
"tooltips": {
|
||||
"hoverNavigator": "Rola automaticamente até o grupo de proxy correspondente ao passar o mouse sobre as letras do alfabeto",
|
||||
"hoverNavigatorDelay": "Atraso antes da rolagem automática ao passar o mouse, em milissegundos"
|
||||
},
|
||||
"options": {
|
||||
"icon": {
|
||||
"monochrome": "Monocromático",
|
||||
"colorful": "Colorido",
|
||||
"disable": "Desativar"
|
||||
},
|
||||
"toastPosition": {
|
||||
"topLeft": "Superior Esquerdo",
|
||||
"topRight": "Superior Direito",
|
||||
"bottomLeft": "Inferior Esquerdo",
|
||||
"bottomRight": "Inferior Direito"
|
||||
},
|
||||
"proxyGroupsDisplayMode": {
|
||||
"default": "Padrão",
|
||||
"inline": "Em Linha",
|
||||
"disable": "Desativar"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"clashPort": {
|
||||
"title": "Configuração de Portas",
|
||||
"fields": {
|
||||
"mixed": "Porta Mista",
|
||||
"socks": "Porta Socks",
|
||||
"http": "Porta Http(s)",
|
||||
"redir": "Porta Redir",
|
||||
"tproxy": "Porta Tproxy"
|
||||
},
|
||||
"actions": {
|
||||
"random": "Porta Aleatória"
|
||||
},
|
||||
"messages": {
|
||||
"portInUse": "A porta {{port}} já está em uso",
|
||||
"runningPort": "Usando atualmente a porta {{port}}",
|
||||
"duplicatePort": "Os listeners ativados devem usar portas diferentes",
|
||||
"invalidPort": "Insira uma porta entre 1 e 65535",
|
||||
"saved": "Configurações de porta salvas",
|
||||
"saveFailed": "Falha ao salvar as configurações de porta",
|
||||
"automaticFallback": "A porta {{oldPort}} estava em uso, então esta sessão é executada em {{newPort}}. Sua configuração não foi alterada e {{oldPort}} será tentada novamente na próxima inicialização.",
|
||||
"automaticFallbackFailed": "Falha no fallback automático da porta mista do proxy: {{error}}",
|
||||
"portTooLow": "A porta deve ser 1000 ou maior",
|
||||
"portTooHigh": "A porta deve ser 65535 ou menor"
|
||||
}
|
||||
},
|
||||
"clashCore": {
|
||||
"variants": {
|
||||
"release": "Versão Release",
|
||||
"alpha": "Versão Alpha"
|
||||
}
|
||||
},
|
||||
"liteMode": {
|
||||
"title": "Configurações do Modo Leve",
|
||||
"actions": {
|
||||
"enterNow": "Entrar no Modo Leve Agora"
|
||||
},
|
||||
"toggles": {
|
||||
"autoEnter": "Entrar Automaticamente no Modo Leve"
|
||||
},
|
||||
"tooltips": {
|
||||
"autoEnter": "Ative para acionar automaticamente o Modo Leve depois que a janela ficar fechada por um período"
|
||||
},
|
||||
"fields": {
|
||||
"delay": "Atraso para Entrar Automaticamente no Modo Leve"
|
||||
},
|
||||
"messages": {
|
||||
"autoEnterHint": "Ao fechar a janela, o Modo Leve será ativado automaticamente após {{n}} minutos"
|
||||
}
|
||||
},
|
||||
"backup": {
|
||||
"title": "Configurações de Backup",
|
||||
"tabs": {
|
||||
"local": "Backup local",
|
||||
"webdav": "Backup WebDAV"
|
||||
},
|
||||
"actions": {
|
||||
"selectTarget": "Selecionar destino do backup",
|
||||
"backup": "Backup",
|
||||
"export": "Exportar",
|
||||
"exportBackup": "Exportar Backup",
|
||||
"importBackup": "Importar Backup",
|
||||
"deleteBackup": "Excluir Backup",
|
||||
"restore": "Restaurar",
|
||||
"restoreBackup": "Restaurar Backup",
|
||||
"viewHistory": "Ver histórico"
|
||||
},
|
||||
"fields": {
|
||||
"webdavUrl": "URL do Servidor WebDAV",
|
||||
"username": "Usuário",
|
||||
"info": "Os backups são armazenados localmente no diretório de dados do aplicativo. Use a lista abaixo para restaurar ou excluir backups."
|
||||
},
|
||||
"messages": {
|
||||
"webdavUrlRequired": "A URL do WebDAV não pode estar vazia",
|
||||
"invalidWebdavUrl": "Formato de URL do WebDAV inválido",
|
||||
"usernameRequired": "O usuário não pode estar vazio",
|
||||
"passwordRequired": "A senha não pode estar vazia",
|
||||
"webdavConfigSaved": "Configuração do WebDAV salva com sucesso",
|
||||
"webdavConfigSaveFailed": "Falha ao salvar a configuração do WebDAV: {{error}}",
|
||||
"backupCreated": "Backup criado com sucesso",
|
||||
"backupFailed": "Falha no backup: {{error}}",
|
||||
"localBackupCreated": "Backup local criado com sucesso",
|
||||
"localBackupFailed": "Falha no backup local",
|
||||
"restoreSuccess": "Restauração concluída, o aplicativo será reiniciado em 1s",
|
||||
"localBackupExported": "Backup local exportado com sucesso",
|
||||
"localBackupExportFailed": "Falha ao exportar o backup local",
|
||||
"localBackupImported": "Backup local importado com sucesso",
|
||||
"localBackupImportFailed": "Falha ao importar o backup local: {{error}}",
|
||||
"webdavRefreshSuccess": "Atualização do WebDAV concluída com sucesso",
|
||||
"webdavRefreshFailed": "Falha na atualização do WebDAV: {{error}}",
|
||||
"confirmDelete": "Confirmar a exclusão deste arquivo de backup?",
|
||||
"confirmRestore": "Confirmar a restauração deste arquivo de backup?"
|
||||
},
|
||||
"auto": {
|
||||
"title": "Backup automático",
|
||||
"scheduleLabel": "Ativar backup agendado",
|
||||
"scheduleHelper": "Cria backups locais em segundo plano no intervalo configurado.",
|
||||
"intervalLabel": "Frequência do backup",
|
||||
"changeLabel": "Backup em alterações críticas",
|
||||
"changeHelper": "Faz backup automaticamente quando a Configuração/Script Global Estendido é alterado.",
|
||||
"options": {
|
||||
"hours": "A cada {{n}} horas",
|
||||
"days": "A cada {{n}} dias"
|
||||
}
|
||||
},
|
||||
"manual": {
|
||||
"title": "Backup manual",
|
||||
"local": "Cria um snapshot neste dispositivo, armazenado no diretório de dados do aplicativo.",
|
||||
"webdav": "Envia um snapshot para o seu servidor WebDAV assim que as credenciais forem definidas.",
|
||||
"configureWebdav": "Configurar WebDAV"
|
||||
},
|
||||
"history": {
|
||||
"title": "Histórico de backups",
|
||||
"summary": "{{count}} backups • mais recente {{recent}}",
|
||||
"empty": "Nenhum backup disponível",
|
||||
"unknownPlatform": "desconhecida",
|
||||
"unknownTime": "Hora desconhecida"
|
||||
},
|
||||
"webdav": {
|
||||
"title": "Configurações do WebDAV"
|
||||
},
|
||||
"table": {
|
||||
"filename": "Nome do Arquivo",
|
||||
"backupTime": "Hora do Backup",
|
||||
"actions": "Ações",
|
||||
"noBackups": "Nenhum backup disponível",
|
||||
"rowsPerPage": "Linhas por página"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"title": "Diversos",
|
||||
"fields": {
|
||||
"appLogLevel": "Nível de Log do Aplicativo",
|
||||
"appLogMaxSize": "Tamanho Máximo do Log do Aplicativo",
|
||||
"appLogMaxCount": "Quantidade Máxima de Logs do Aplicativo",
|
||||
"autoCloseConnections": "Fechar Conexões Automaticamente",
|
||||
"autoCheckUpdate": "Verificar Atualizações Automaticamente",
|
||||
"enableBuiltinEnhanced": "Ativar Aprimoramento Integrado",
|
||||
"proxyLayoutColumns": "Colunas do Layout de Proxy",
|
||||
"autoLogClean": "Limpeza Automática de Logs",
|
||||
"autoDelayDetection": "Detecção Automática de Atraso",
|
||||
"autoDelayDetectionInterval": "Intervalo da Detecção Automática de Atraso",
|
||||
"defaultLatencyTest": "Teste de Latência Padrão",
|
||||
"defaultLatencyTimeout": "Tempo Limite de Latência Padrão"
|
||||
},
|
||||
"tooltips": {
|
||||
"autoCloseConnections": "Encerra as conexões estabelecidas quando a seleção do grupo de proxy ou o modo de proxy muda",
|
||||
"enableBuiltinEnhanced": "Tratamento de compatibilidade para o arquivo de configuração",
|
||||
"autoDelayDetection": "Testa periodicamente a latência do nó atual em segundo plano",
|
||||
"defaultLatencyTest": "Usado apenas para testes de requisição do cliente HTTP e não afeta o arquivo de configuração"
|
||||
},
|
||||
"options": {
|
||||
"proxyLayoutColumns": {
|
||||
"auto": "Colunas Automáticas"
|
||||
},
|
||||
"autoLogClean": {
|
||||
"never": "Nunca Limpar",
|
||||
"retainDays": "Manter {{n}} Dias"
|
||||
}
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"title": "Nova Versão v{{version}}",
|
||||
"actions": {
|
||||
"goToRelease": "Ir para a Página da Release",
|
||||
"update": "Atualizar"
|
||||
},
|
||||
"messages": {
|
||||
"breakChangeError": "Esta versão é uma atualização importante e não oferece suporte a atualizações dentro do aplicativo. Desinstale-a e baixe e instale a nova versão manualmente",
|
||||
"available": "Uma nova versão está disponível"
|
||||
},
|
||||
"alerts": {
|
||||
"note": "Nota",
|
||||
"tip": "Dica",
|
||||
"important": "Importante",
|
||||
"warning": "Aviso",
|
||||
"caution": "Cuidado"
|
||||
}
|
||||
},
|
||||
"sysproxy": {
|
||||
"title": "Configurações do Proxy do Sistema",
|
||||
"fieldsets": {
|
||||
"currentStatus": "Proxy do Sistema Atual"
|
||||
},
|
||||
"fields": {
|
||||
"enableStatus": "Status de Ativação:",
|
||||
"serverAddr": "Endereço do Servidor: ",
|
||||
"pacUrl": "URL do PAC: ",
|
||||
"proxyHost": "Host do Proxy",
|
||||
"usePacMode": "Usar Modo PAC",
|
||||
"proxyGuard": "Proteção do Proxy",
|
||||
"guardDuration": "Duração da Proteção",
|
||||
"alwaysUseDefaultBypass": "Sempre usar o Bypass Padrão",
|
||||
"enableBypassCheck": "Validar Formato do Bypass do Proxy",
|
||||
"proxyBypass": "Configurações de Bypass do Proxy: ",
|
||||
"bypass": "Bypass: ",
|
||||
"pacScriptContent": "Conteúdo do Script PAC"
|
||||
},
|
||||
"tooltips": {
|
||||
"proxyGuard": "Ative para impedir que outros softwares modifiquem as configurações de proxy do sistema operacional"
|
||||
},
|
||||
"messages": {
|
||||
"durationTooShort": "A duração do daemon do proxy não pode ser menor que 1 segundo",
|
||||
"invalidBypass": "Formato de Bypass inválido",
|
||||
"invalidProxyHost": "Formato de host do proxy inválido"
|
||||
},
|
||||
"actions": {
|
||||
"editPac": "Editar PAC"
|
||||
}
|
||||
},
|
||||
"tun": {
|
||||
"title": "Modo Tun",
|
||||
"fields": {
|
||||
"stack": "Stack do Tun",
|
||||
"device": "Nome do Dispositivo",
|
||||
"autoRoute": "Rota Automática",
|
||||
"routeExcludeAddress": "Endereços Excluídos da Rota",
|
||||
"strictRoute": "Rota Estrita",
|
||||
"autoDetectInterface": "Detectar Interface Automaticamente",
|
||||
"dnsHijack": "Sequestro de DNS",
|
||||
"mtu": "Unidade Máxima de Transmissão",
|
||||
"autoRedirect": "Redirecionamento Automático"
|
||||
},
|
||||
"tooltips": {
|
||||
"dnsHijack": "Use , para separar vários servidores DNS",
|
||||
"autoRedirect": "Configura automaticamente os redirecionamentos TCP do nftables/iptables"
|
||||
},
|
||||
"messages": {
|
||||
"applied": "Configurações Aplicadas",
|
||||
"invalidRouteExcludeAddress": "Insira um bloco CIDR válido",
|
||||
"routeExcludeAddressHint": "Somente CIDR IPv4/IPv6 é suportado, como 192.168.0.0/16 ou fd00::/8"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
"dialog": {
|
||||
"title": "Sobrescrever DNS",
|
||||
"profileScope": "A opção de sobrescrever DNS é salva separadamente para cada perfil e se aplica somente a esse perfil.",
|
||||
"warning": "Se você não está familiarizado com estas configurações, não as modifique e mantenha a Sobrescrita de DNS desativada"
|
||||
},
|
||||
"protection": {
|
||||
"title": "Ativar Sobrescrita de DNS?",
|
||||
"message": "Esta assinatura contém configurações de DNS personalizadas. A Sobrescrita de DNS pode substituí-las, impedindo que os nós se conectem ou que alguns sites sejam carregados. Ativar mesmo assim?",
|
||||
"enableAnyway": "Ativar mesmo assim",
|
||||
"keepDisabled": "Manter desativada",
|
||||
"autoDisabled": "A Sobrescrita de DNS foi desativada para preservar as configurações de DNS personalizadas da assinatura atual."
|
||||
},
|
||||
"sections": {
|
||||
"general": "Configurações de DNS",
|
||||
"fallbackFilter": "Configurações do Filtro de Fallback",
|
||||
"hosts": "Configurações de Hosts"
|
||||
},
|
||||
"fields": {
|
||||
"enable": "Ativar DNS",
|
||||
"listen": "Escuta do DNS",
|
||||
"enhancedMode": "Modo Aprimorado",
|
||||
"fakeIpRange": "Intervalo de Fake IP",
|
||||
"fakeIpRange6": "Intervalo de Fake IP (IPv6)",
|
||||
"fakeIpFilterMode": "Modo do Filtro de Fake IP",
|
||||
"ipv6": {
|
||||
"label": "IPv6",
|
||||
"description": "Ativar resolução DNS IPv6"
|
||||
},
|
||||
"preferH3": {
|
||||
"label": "Preferir H3",
|
||||
"description": "O DNS DOH usa HTTP/3"
|
||||
},
|
||||
"respectRules": {
|
||||
"label": "Respeitar Regras",
|
||||
"description": "As conexões DNS seguem as regras de roteamento"
|
||||
},
|
||||
"useHosts": {
|
||||
"label": "Usar Hosts",
|
||||
"description": "Ative para resolver hosts por meio do arquivo hosts"
|
||||
},
|
||||
"useSystemHosts": {
|
||||
"label": "Usar Hosts do Sistema",
|
||||
"description": "Ative para resolver hosts por meio do arquivo hosts do sistema"
|
||||
},
|
||||
"directPolicy": {
|
||||
"label": "Nameserver Direto Segue a Política",
|
||||
"description": "Define se deve seguir a política de nameserver"
|
||||
},
|
||||
"defaultNameserver": {
|
||||
"label": "Nameserver Padrão",
|
||||
"description": "Servidores DNS padrão usados para resolver servidores DNS"
|
||||
},
|
||||
"nameserver": {
|
||||
"label": "Nameserver",
|
||||
"description": "Lista de servidores DNS, separados por vírgula"
|
||||
},
|
||||
"fallback": {
|
||||
"label": "Fallback",
|
||||
"description": "Lista de servidores DNS de fallback, separados por vírgula"
|
||||
},
|
||||
"proxy": {
|
||||
"label": "Nameserver do Servidor Proxy",
|
||||
"description": "Servidores DNS para resolução de domínios dos nós de proxy"
|
||||
},
|
||||
"directNameserver": {
|
||||
"label": "Nameserver Direto",
|
||||
"description": "Servidores DNS para resolução de domínios de saída direta, suporta a palavra-chave 'system', separados por vírgula"
|
||||
},
|
||||
"fakeIpFilter": {
|
||||
"label": "Filtro de Fake IP",
|
||||
"description": "Domínios que ignoram a resolução de fake IP, separados por vírgula"
|
||||
},
|
||||
"nameserverPolicy": {
|
||||
"label": "Política de Nameserver",
|
||||
"description": "Servidor DNS específico por domínio, vários servidores separados por ponto e vírgula, formato: domínio=servidor1;servidor2"
|
||||
},
|
||||
"geoipFiltering": {
|
||||
"label": "Filtragem GeoIP",
|
||||
"description": "Ativar filtragem GeoIP para o fallback"
|
||||
},
|
||||
"geoipCode": "Código GeoIP",
|
||||
"fallbackIpCidr": {
|
||||
"label": "CIDR de IP do Fallback",
|
||||
"description": "CIDRs de IP que não usam servidores de fallback, separados por vírgula"
|
||||
},
|
||||
"fallbackDomain": {
|
||||
"label": "Domínio do Fallback",
|
||||
"description": "Domínios que usam servidores de fallback, separados por vírgula"
|
||||
},
|
||||
"hosts": {
|
||||
"label": "Hosts",
|
||||
"description": "Mapeamento personalizado de domínio para IP ou domínio"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"saved": "Configurações de DNS salvas",
|
||||
"configError": "Erro na configuração de DNS:"
|
||||
},
|
||||
"errors": {
|
||||
"invalid": "Configuração inválida",
|
||||
"invalidYaml": "Formato YAML inválido"
|
||||
}
|
||||
},
|
||||
"webUI": {
|
||||
"actions": {
|
||||
"openUrl": "Abrir URL"
|
||||
},
|
||||
"title": "Web UI",
|
||||
"messages": {
|
||||
"supportedPlaceholders": "Suporta %host, %port, %secret",
|
||||
"placeholderInstruction": "Substitua host, port, secret por %host, %port, %secret"
|
||||
},
|
||||
"errors": {
|
||||
"clashInfoUnavailable": "As informações de conexão do núcleo não estão disponíveis",
|
||||
"invalidServer": "O endereço do servidor do núcleo \"{{server}}\" é inválido",
|
||||
"openFailed": "Falha ao abrir a Web UI"
|
||||
}
|
||||
},
|
||||
"hotkey": {
|
||||
"toggles": {
|
||||
"enableGlobal": "Ativar Atalho Global"
|
||||
},
|
||||
"title": "Configurações de Atalhos",
|
||||
"functions": {
|
||||
"rule": "Modo Regra",
|
||||
"global": "Modo Global",
|
||||
"openOrCloseDashboard": "Abrir/Fechar Painel",
|
||||
"toggleSystemProxy": "Ativar/Desativar Proxy do Sistema",
|
||||
"toggleTunMode": "Ativar/Desativar Modo Tun",
|
||||
"entryLightweightMode": "Entrar no Modo Leve",
|
||||
"direct": "Modo Direto",
|
||||
"reactivateProfiles": "Reativar Perfis"
|
||||
}
|
||||
},
|
||||
"networkInterface": {
|
||||
"title": "Interface de Rede",
|
||||
"fields": {
|
||||
"ipAddress": "Endereço IP",
|
||||
"macAddress": "Endereço MAC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"errors": {
|
||||
"clash": {
|
||||
"configUpdateFailed": "Falha ao atualizar a configuração do núcleo",
|
||||
"modeUpdateFailed": "Falha ao alterar o modo de proxy",
|
||||
"restartFailed": "Falha ao reiniciar o núcleo",
|
||||
"startFailed": "Falha ao iniciar o núcleo",
|
||||
"serviceCoreStopped": "O núcleo do serviço parou inesperadamente",
|
||||
"stopFailed": "Falha ao parar o núcleo"
|
||||
},
|
||||
"clashService": {
|
||||
"installFailed": "Falha ao instalar o serviço",
|
||||
"uninstallFailed": "Falha ao desinstalar o serviço",
|
||||
"reinstallFailed": "Falha ao reinstalar o serviço",
|
||||
"repairFailed": "Falha ao reparar o serviço",
|
||||
"sidecarFailed": "Falha ao continuar com o Sidecar",
|
||||
"coreAlreadyRunning": "Não é possível iniciar o núcleo: já há um processo do núcleo em execução. Feche o aplicativo correspondente ou encerre o processo remanescente e tente novamente. Se o problema persistir, reinicie o computador."
|
||||
},
|
||||
"sysproxy": {
|
||||
"privilegeRequired": "Não é possível definir o proxy do sistema: o macOS recusou a alteração por falta de privilégios. Instale o serviço do Clash Verge; um administrador também pode desativar a exigência de senha de administrador para configurações do sistema, nas opções avançadas em Ajustes do Sistema → Privacidade e Segurança.",
|
||||
"directFallback": "Falha ao definir o proxy do sistema; o tráfego foi revertido para conexão direta.",
|
||||
"guardStopped": "A proteção do proxy do sistema parou após falhar várias vezes ao executar. O proxy do sistema não está mais sendo mantido.",
|
||||
"sidecarWhileServiceReady": "Não foi possível definir o proxy do sistema porque o núcleo ainda está em execução como Sidecar, embora o serviço do Clash Verge esteja pronto. Reinicie o núcleo para mudar para o modo serviço.",
|
||||
"coreNotReady": "Não foi possível ativar o proxy do sistema porque o núcleo não está em execução. Inicie o núcleo (ou instale/repare o serviço do Clash Verge) e tente novamente.",
|
||||
"systemCallFailed": "Não foi possível gravar o proxy do sistema porque uma chamada de sistema do Windows falhou. Geralmente é temporário, então tente novamente."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"clash": {
|
||||
"restartSuccess": "Núcleo do Clash Reiniciado",
|
||||
"versionUpdated": "Versão do Núcleo Atualizada",
|
||||
"alreadyLatestVersion": "Já Está Usando a Versão Mais Recente do Núcleo",
|
||||
"changeSuccess": "Núcleo alterado com sucesso",
|
||||
"changeFailed": "Falha ao alterar o núcleo",
|
||||
"geoDataUpdated": "GeoData Atualizado"
|
||||
},
|
||||
"clashService": {
|
||||
"appDataNotOwned": "O serviço se recusou a iniciar o núcleo porque, no disco, a pasta de dados do aplicativo não pertence à sua conta (comum quando a pasta pessoal está em um disco externo). Reparar o serviço não resolverá isso. Execute este comando em um terminal e reinicie o aplicativo:",
|
||||
"installSuccess": "Serviço Instalado com Sucesso",
|
||||
"permissionFallback": "{{reason}} Mudou para o modo normal (Sidecar); o TUN requer privilégios de administrador. Siga o guia para corrigir e reinstalar o serviço. Se preferir não corrigir ou não souber como, execute o aplicativo como administrador sem instalar o serviço.",
|
||||
"permissionWritableReason": "Contas comuns podem modificar os arquivos do núcleo ou uma pasta superior, então o serviço se recusou a executar o núcleo com privilégios elevados.",
|
||||
"permissionRejectedReason": "O serviço não conseguiu confirmar que os arquivos e pastas do núcleo são seguros para execução com privilégios elevados.",
|
||||
"permissionRepairGuide": "Ver instruções de reparo",
|
||||
"sidecarFallback": "O serviço não conseguiu iniciar o núcleo. Mudou para o modo Sidecar; o serviço continua indisponível.",
|
||||
"uninstallSuccess": "Serviço Desinstalado com Sucesso"
|
||||
},
|
||||
"updater": {
|
||||
"withClashProxySuccess": "Atualização com o proxy do Clash concluída com sucesso",
|
||||
"withClashProxyFailed": "A atualização falhou mesmo com o proxy do Clash"
|
||||
}
|
||||
}
|
||||
},
|
||||
"statuses": {
|
||||
"clash": {
|
||||
"stopping": "Parando o Núcleo...",
|
||||
"restarting": "Reiniciando o Núcleo..."
|
||||
},
|
||||
"clashService": {
|
||||
"installing": "Instalando o Serviço...",
|
||||
"uninstalling": "Desinstalando o Serviço..."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"actions": {
|
||||
"cancel": "Cancelar",
|
||||
"close": "Fechar",
|
||||
"confirm": "Confirmar",
|
||||
"save": "Salvar",
|
||||
"delete": "Excluir",
|
||||
"edit": "Editar",
|
||||
"new": "Novo",
|
||||
"enable": "Ativar",
|
||||
"upgrade": "Atualizar versão",
|
||||
"restart": "Reiniciar",
|
||||
"resetToDefault": "Restaurar padrão",
|
||||
"refresh": "Atualizar",
|
||||
"retry": "Tentar novamente",
|
||||
"refreshPage": "Recarregar página",
|
||||
"showDetails": "Mostrar detalhes",
|
||||
"hideDetails": "Ocultar detalhes",
|
||||
"listView": "Visualização em lista",
|
||||
"tableView": "Visualização em tabela",
|
||||
"pause": "Pausar",
|
||||
"resume": "Retomar",
|
||||
"closeAll": "Fechar tudo",
|
||||
"clear": "Limpar",
|
||||
"previous": "Anterior",
|
||||
"next": "Próximo",
|
||||
"check": "Verificar"
|
||||
},
|
||||
"labels": {
|
||||
"updateAt": "Atualizado em",
|
||||
"timeout": "Tempo limite",
|
||||
"icon": "Ícone",
|
||||
"name": "Nome",
|
||||
"readOnly": "Somente leitura",
|
||||
"expireTime": "Expira em",
|
||||
"updateTime": "Hora da atualização",
|
||||
"usedTotal": "Usado / Total",
|
||||
"from": "De",
|
||||
"password": "Senha",
|
||||
"retryAttempts": "Tentativas",
|
||||
"downloaded": "Baixado",
|
||||
"uploaded": "Enviado"
|
||||
},
|
||||
"statuses": {
|
||||
"enabled": "Ativado",
|
||||
"disabled": "Desativado",
|
||||
"saving": "Salvando...",
|
||||
"empty": "Vazio",
|
||||
"loading": "Carregando..."
|
||||
},
|
||||
"units": {
|
||||
"milliseconds": "ms",
|
||||
"seconds": "segundos",
|
||||
"minutes": "min",
|
||||
"hours": "h",
|
||||
"kilobytes": "KB",
|
||||
"files": "Arquivos"
|
||||
},
|
||||
"placeholders": {
|
||||
"resetInput": "Limpar entrada",
|
||||
"filter": "Condições do filtro",
|
||||
"matchCase": "Diferenciar maiúsculas",
|
||||
"matchWholeWord": "Palavra inteira",
|
||||
"useRegex": "Usar expressão regular"
|
||||
},
|
||||
"validation": {
|
||||
"invalidRegex": "Expressão regular inválida"
|
||||
},
|
||||
"window": {
|
||||
"maximize": "Maximizar",
|
||||
"minimize": "Minimizar"
|
||||
},
|
||||
"editorModes": {
|
||||
"visualization": "Visualização",
|
||||
"advanced": "Avançado"
|
||||
},
|
||||
"feedback": {
|
||||
"errors": {
|
||||
"unexpected": "Algo deu errado",
|
||||
"label": "Erro",
|
||||
"unknown": "Erro desconhecido",
|
||||
"details": "Detalhes do erro",
|
||||
"stack": "Pilha de erros",
|
||||
"componentStack": "Pilha de componentes",
|
||||
"trafficUnavailable": "Dados de tráfego indisponíveis",
|
||||
"operationFailed": "A operação falhou",
|
||||
"trafficStats": "Erro nas estatísticas de tráfego",
|
||||
"trafficStatsDescription": "O componente de estatísticas de tráfego encontrou um erro e foi desativado para evitar falhas."
|
||||
},
|
||||
"notices": {
|
||||
"raw": "{{message}}",
|
||||
"prefixedRaw": "{{prefix}} {{message}}"
|
||||
},
|
||||
"notifications": {
|
||||
"importSuccess": "Perfil importado com sucesso",
|
||||
"importSubscriptionSuccess": "Assinatura importada com sucesso",
|
||||
"importWithClashProxy": "Perfil importado com o proxy do Clash",
|
||||
"updateAvailable": "Atualização disponível",
|
||||
"saved": "Salvo com sucesso",
|
||||
"common": {
|
||||
"copySuccess": "Copiado com sucesso",
|
||||
"saveSuccess": "Configuração salva com sucesso",
|
||||
"saveFailed": "Falha ao salvar a configuração",
|
||||
"refreshFailed": "Falha ao atualizar; exibindo o último valor carregado"
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"config": {
|
||||
"failed": "A validação da configuração da assinatura falhou. Verifique o arquivo de configuração da assinatura; as modificações foram revertidas.",
|
||||
"bootFailed": "A validação da configuração da assinatura na inicialização falhou. Iniciado com a configuração padrão; verifique o arquivo de configuração da assinatura.",
|
||||
"processTerminated": "O processo de validação foi encerrado. Verifique se algum software de segurança está impedindo a execução do núcleo ou se a CPU e o ambiente de execução do seu computador são compatíveis com o executável do núcleo."
|
||||
},
|
||||
"script": {
|
||||
"syntaxError": "Erro de sintaxe no script, alterações revertidas",
|
||||
"missingMain": "Erro no script, alterações revertidas",
|
||||
"fileNotFound": "Arquivo ausente, alterações revertidas",
|
||||
"fileError": "Erro no arquivo de script, alterações revertidas"
|
||||
},
|
||||
"yaml": {
|
||||
"syntaxError": "Erro de sintaxe YAML, alterações revertidas",
|
||||
"readError": "Erro de leitura YAML, alterações revertidas",
|
||||
"mappingError": "Erro de mapeamento YAML, alterações revertidas"
|
||||
},
|
||||
"merge": {
|
||||
"syntaxError": "Erro de sintaxe no arquivo de mesclagem, alterações revertidas",
|
||||
"mappingError": "Erro de mapeamento no arquivo de mesclagem, alterações revertidas"
|
||||
}
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"logLevels": {
|
||||
"all": "ALL",
|
||||
"debug": "DEBUG",
|
||||
"info": "INFO",
|
||||
"warn": "WARN",
|
||||
"error": "ERROR"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"page": {
|
||||
"actions": {
|
||||
"testAll": "Testar tudo"
|
||||
},
|
||||
"title": "Testar"
|
||||
},
|
||||
"components": {
|
||||
"item": {
|
||||
"actions": {
|
||||
"test": "Testar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"test": {
|
||||
"title": {
|
||||
"create": "Criar teste",
|
||||
"edit": "Editar teste"
|
||||
},
|
||||
"fields": {
|
||||
"url": "URL de teste"
|
||||
},
|
||||
"errors": {
|
||||
"nameRequired": "Informe um nome",
|
||||
"urlRequired": "Informe uma URL de teste",
|
||||
"invalidSvg": "O ícone não é um SVG válido",
|
||||
"uidMissing": "O identificador do item de teste está ausente",
|
||||
"saveFailed": "Falha ao salvar o item de teste"
|
||||
}
|
||||
}
|
||||
},
|
||||
"statuses": {
|
||||
"test": {
|
||||
"pending": "Pendente",
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
"failed": "Falhou",
|
||||
"completed": "Concluído",
|
||||
"disallowedIsp": "ISP não permitido",
|
||||
"originalsOnly": "Somente originais",
|
||||
"noDisney": "Não (IP banido pelo Disney+)",
|
||||
"unsupportedRegion": "País/região não suportado",
|
||||
"failedNetwork": "Falhou (conexão de rede)"
|
||||
}
|
||||
},
|
||||
"unlock": {
|
||||
"page": {
|
||||
"actions": {
|
||||
"testing": "Testando..."
|
||||
},
|
||||
"empty": "Nenhum item de teste de desbloqueio",
|
||||
"messages": {
|
||||
"detectionFailedWithName": "Falha na detecção de {{name}}",
|
||||
"detectionTimeout": "Tempo limite da detecção esgotado ou falhou"
|
||||
},
|
||||
"title": "Teste de desbloqueio"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -667,7 +667,7 @@
|
||||
"reinstallFailed": "Не удалось переустановить службу",
|
||||
"repairFailed": "Не удалось восстановить службу",
|
||||
"sidecarFailed": "Не удалось продолжить в режиме Sidecar",
|
||||
"coreAlreadyRunning": "Не удалось запустить режим Sidecar: другое ядро ещё работает (PID {{pid}}). Закройте другие экземпляры Clash Verge или приложение, использующее это ядро, и повторите попытку."
|
||||
"coreAlreadyRunning": "Не удалось запустить ядро: процесс ядра уже работает. Закройте соответствующее приложение или завершите оставшийся процесс и повторите попытку. Если проблема сохраняется, перезагрузите компьютер."
|
||||
},
|
||||
"sysproxy": {
|
||||
"privilegeRequired": "Не удалось настроить системный прокси: macOS отклонил изменение из-за нехватки прав. Установите службу Clash Verge; администратор также может отключить требование пароля администратора для общесистемных настроек в дополнительных параметрах раздела «Системные настройки → Конфиденциальность и безопасность».",
|
||||
|
||||
@@ -667,7 +667,7 @@
|
||||
"reinstallFailed": "Hizmet yeniden kurulamadı",
|
||||
"repairFailed": "Hizmet onarılamadı",
|
||||
"sidecarFailed": "Sidecar ile devam edilemedi",
|
||||
"coreAlreadyRunning": "Sidecar modu başlatılamadı: başka bir çekirdek hâlâ çalışıyor (PID {{pid}}). Diğer Clash Verge örneklerini veya bu çekirdeği kullanan uygulamayı kapatıp yeniden deneyin."
|
||||
"coreAlreadyRunning": "Çekirdek başlatılamıyor: bir çekirdek işlemi zaten çalışıyor. İlgili uygulamayı kapatın veya kalan işlemi sonlandırıp yeniden deneyin. Sorun devam ederse bilgisayarı yeniden başlatın."
|
||||
},
|
||||
"sysproxy": {
|
||||
"privilegeRequired": "Sistem vekil sunucusu ayarlanamıyor: macOS yetki yetersizliği nedeniyle değişikliği reddetti. Clash Verge hizmetini kurun; yöneticiyseniz Sistem Ayarları → Gizlilik ve Güvenlik bölümündeki gelişmiş seçeneklerden, sistem genelindeki ayarlar için yönetici parolası zorunluluğunu da kapatabilirsiniz.",
|
||||
|
||||
@@ -667,7 +667,7 @@
|
||||
"reinstallFailed": "Хезмәтне яңадан урнаштырып булмады",
|
||||
"repairFailed": "Хезмәтне төзәтеп булмады",
|
||||
"sidecarFailed": "Sidecar белән дәвам итеп булмады",
|
||||
"coreAlreadyRunning": "Sidecar режимын башлап булмады: башка төш әле эшли (PID {{pid}}). Башка Clash Verge нөсхәләрен яки бу төшне кулланучы кушымтаны ябыгыз һәм яңадан сынап карагыз."
|
||||
"coreAlreadyRunning": "Төшне башлап булмый: төш процессы инде эшли. Тиешле кушымтаны ябыгыз яки калган процессны тәмамлагыз һәм яңадан сынап карагыз. Проблема хәл ителмәсә, санакны яңадан кабызыгыз."
|
||||
},
|
||||
"sysproxy": {
|
||||
"privilegeRequired": "Система проксиен көйләп булмый: хокуклар җитмәгәнгә күрә macOS бу үзгәрешне кире какты. Clash Verge хезмәтен урнаштырыгыз; әгәр сез администратор булсагыз, Система көйләнмәләре → Шәхсилек вә иминлек бүлегенең өстәмә параметрларында, система дәрәҗәсендәге көйләнмәләр өчен администратор паролен таләп итүне сүндерә аласыз.",
|
||||
|
||||
@@ -667,7 +667,7 @@
|
||||
"reinstallFailed": "重新安装服务失败",
|
||||
"repairFailed": "修复服务失败",
|
||||
"sidecarFailed": "继续使用 Sidecar 失败",
|
||||
"coreAlreadyRunning": "无法启动普通模式(Sidecar):已有内核进程正在运行(PID {{pid}})。请先退出其他 Clash Verge 实例或使用该内核的程序,然后重试。"
|
||||
"coreAlreadyRunning": "无法启动内核:已有内核进程正在运行。请退出相关应用或清理残留进程后重试。若仍无法解决,请重启电脑。"
|
||||
},
|
||||
"sysproxy": {
|
||||
"privilegeRequired": "无法设置系统代理:macOS 以权限不足为由拒绝了这次修改。请安装 Clash Verge 服务;管理员也可以在系统设置 → 隐私与安全性的高级选项中,关闭访问系统范围设置需要管理员密码的要求。",
|
||||
|
||||
@@ -667,7 +667,7 @@
|
||||
"reinstallFailed": "重新安裝服務失敗",
|
||||
"repairFailed": "修復服務失敗",
|
||||
"sidecarFailed": "繼續使用 Sidecar 失敗",
|
||||
"coreAlreadyRunning": "無法啟動一般模式(Sidecar):已有核心程序正在執行(PID {{pid}})。請先結束其他 Clash Verge 執行個體或使用該核心的程式,然後重試。"
|
||||
"coreAlreadyRunning": "無法啟動核心:已有核心程序正在執行。請結束相關應用程式或清理殘留程序後重試。若仍無法解決,請重新啟動電腦。"
|
||||
},
|
||||
"sysproxy": {
|
||||
"privilegeRequired": "無法設定系統代理:macOS 以權限不足為由拒絕了這次修改。請安裝 Clash Verge 服務;管理者也可以在系統設定 → 隱私權與安全性的進階選項中,關閉存取系統層級設定需要管理者密碼的要求。",
|
||||
|
||||
+15
-7
@@ -212,10 +212,12 @@ const ProfilePage = () => {
|
||||
}
|
||||
setLoading(true)
|
||||
|
||||
const prevCurrent = profiles.current
|
||||
|
||||
const handleImportSuccess = async (noticeKey: string) => {
|
||||
showNotice.success(noticeKey)
|
||||
setUrl('')
|
||||
await performRobustRefresh()
|
||||
await performRobustRefresh(prevCurrent)
|
||||
}
|
||||
try {
|
||||
await mutate(() => importProfile(url), {
|
||||
@@ -261,7 +263,7 @@ const ProfilePage = () => {
|
||||
}
|
||||
|
||||
// `useProfiles` already retries three times; add only one business-level retry.
|
||||
const performRobustRefresh = async () => {
|
||||
const performRobustRefresh = async (prevCurrent?: string | null) => {
|
||||
let retryCount = 0
|
||||
const maxRetries = 1
|
||||
const baseDelay = 200
|
||||
@@ -270,13 +272,15 @@ const ProfilePage = () => {
|
||||
try {
|
||||
debugLog(`[导入刷新] 第${retryCount + 1}次尝试刷新配置数据`)
|
||||
|
||||
await mutateProfiles()
|
||||
const fresh = await fetchProfilesIntoCache()
|
||||
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, baseDelay * (retryCount + 1)),
|
||||
)
|
||||
|
||||
await onEnhance(false)
|
||||
if (fresh?.current !== prevCurrent) {
|
||||
await onEnhance(false)
|
||||
}
|
||||
return
|
||||
} catch (error) {
|
||||
console.error(`[导入刷新] 第${retryCount + 1}次刷新失败:`, error)
|
||||
@@ -289,8 +293,10 @@ const ProfilePage = () => {
|
||||
|
||||
console.warn(`[导入刷新] 常规刷新失败,尝试清除缓存重新获取`)
|
||||
try {
|
||||
await fetchProfilesIntoCache()
|
||||
await onEnhance(false)
|
||||
const fresh = await fetchProfilesIntoCache()
|
||||
if (fresh?.current !== prevCurrent) {
|
||||
await onEnhance(false)
|
||||
}
|
||||
showNotice.error(
|
||||
'profiles.page.feedback.notifications.importNeedsRefresh',
|
||||
3000,
|
||||
@@ -888,7 +894,9 @@ const ProfilePage = () => {
|
||||
mb: 1.5,
|
||||
display: 'grid',
|
||||
overflow: 'hidden',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))',
|
||||
// Up to 4 columns until a column would pass 480px, then more; 24px is the 3 gaps from `gap: 1`.
|
||||
gridTemplateColumns:
|
||||
'repeat(auto-fill, minmax(clamp(260px, calc((100% - 24px) / 4), 480px), 1fr))',
|
||||
gap: 1,
|
||||
px: 0.5,
|
||||
}}
|
||||
|
||||
@@ -659,6 +659,7 @@ export type PrfItem_Deserialize = {
|
||||
desc?: string | null
|
||||
url?: string | null
|
||||
selected?: PrfSelected[] | null
|
||||
latency_test_urls?: { [key in string]: string } | null
|
||||
extra?: PrfExtra | null
|
||||
updated?: number | null
|
||||
option?: PrfOption_Deserialize | null
|
||||
@@ -674,6 +675,7 @@ export type PrfItem_Serialize = {
|
||||
desc?: string | null
|
||||
url?: string | null
|
||||
selected?: PrfSelected[] | null
|
||||
latency_test_urls?: { [key in string]: string } | null
|
||||
extra?: PrfExtra | null
|
||||
updated: number | null
|
||||
option?: PrfOption_Serialize | null
|
||||
@@ -768,6 +770,7 @@ export type ProfilePatch_Deserialize = {
|
||||
file?: string | null
|
||||
url?: string | null
|
||||
selected?: PrfSelected[] | null
|
||||
latency_test_urls?: { [key in string]: string } | null
|
||||
extra?: PrfExtra | null
|
||||
updated?: number | null
|
||||
option?: PrfOption_Deserialize | null
|
||||
@@ -780,6 +783,7 @@ export type ProfilePatch_Serialize = {
|
||||
file: string | null
|
||||
url: string | null
|
||||
selected: PrfSelected[] | null
|
||||
latency_test_urls: { [key in string]: string } | null
|
||||
extra: PrfExtra | null
|
||||
updated: number | null
|
||||
option: PrfOption_Serialize | null
|
||||
@@ -801,6 +805,11 @@ export type ProxyCapabilities = {
|
||||
smux: boolean
|
||||
}
|
||||
|
||||
export type ProxyExtra = {
|
||||
alive: boolean
|
||||
history: DelayHistory[]
|
||||
}
|
||||
|
||||
export type ProxyGroupView =
|
||||
| ProxyGroupView_Serialize
|
||||
| ProxyGroupView_Deserialize
|
||||
@@ -815,6 +824,7 @@ export type ProxyGroupView_Deserialize = {
|
||||
icon: string | null
|
||||
testUrl: string | null
|
||||
history: DelayHistory[]
|
||||
extra: { [key in string]: ProxyExtra }
|
||||
members: ProxyMemberRef[]
|
||||
} & ProxyCapabilities
|
||||
|
||||
@@ -828,6 +838,7 @@ export type ProxyGroupView_Serialize = {
|
||||
icon?: string | null
|
||||
testUrl?: string | null
|
||||
history: DelayHistory[]
|
||||
extra: { [key in string]: ProxyExtra }
|
||||
members: ProxyMemberRef[]
|
||||
} & ProxyCapabilities
|
||||
|
||||
@@ -853,6 +864,7 @@ export type ProxyNodeView_Deserialize = {
|
||||
type: string
|
||||
alive: boolean
|
||||
history: DelayHistory[]
|
||||
extra: { [key in string]: ProxyExtra }
|
||||
id: string | null
|
||||
hidden: boolean | null
|
||||
icon: string | null
|
||||
@@ -866,6 +878,7 @@ export type ProxyNodeView_Serialize = {
|
||||
type: string
|
||||
alive: boolean
|
||||
history: DelayHistory[]
|
||||
extra: { [key in string]: ProxyExtra }
|
||||
id?: string | null
|
||||
hidden?: boolean | null
|
||||
icon?: string | null
|
||||
|
||||
+13
-2
@@ -91,8 +91,18 @@ export async function deleteProfile(index: string) {
|
||||
}
|
||||
|
||||
export async function patchProfile(index: string, profile: ProfilePatch) {
|
||||
const { type, name, desc, file, url, selected, extra, updated, option } =
|
||||
profile
|
||||
const {
|
||||
type,
|
||||
name,
|
||||
desc,
|
||||
file,
|
||||
url,
|
||||
selected,
|
||||
latency_test_urls,
|
||||
extra,
|
||||
updated,
|
||||
option,
|
||||
} = profile
|
||||
return commands.patchProfile(index, {
|
||||
type,
|
||||
name,
|
||||
@@ -100,6 +110,7 @@ export async function patchProfile(index: string, profile: ProfilePatch) {
|
||||
file,
|
||||
url,
|
||||
selected,
|
||||
latency_test_urls,
|
||||
extra,
|
||||
updated,
|
||||
option,
|
||||
|
||||
@@ -59,3 +59,140 @@ describe('group delay completion', () => {
|
||||
stop()
|
||||
})
|
||||
})
|
||||
|
||||
test('uses newer core history after a tray test without hiding an active page test', () => {
|
||||
const member = node('tray-refresh')
|
||||
if (member.kind !== 'node') throw new Error('Expected node')
|
||||
const cached = delayManager.setDelay(member, 'tray-group', 50)
|
||||
member.node.extra = {
|
||||
[delayManager.getUrl('tray-group')]: {
|
||||
alive: true,
|
||||
history: [
|
||||
{ time: new Date(cached.updatedAt + 1000).toISOString(), delay: 300 },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
expect(delayManager.getDelayFix(member, 'tray-group')).toBe(300)
|
||||
expect(delayManager.getDelayUpdate(member, 'tray-group')?.delay).toBe(300)
|
||||
|
||||
delayManager.setDelay(member, 'tray-group', -2)
|
||||
expect(delayManager.getDelayFix(member, 'tray-group')).toBe(-2)
|
||||
})
|
||||
|
||||
test('latency URL validation rejects malformed hosts and ports and trims valid URLs', () => {
|
||||
for (const url of [
|
||||
'http://?',
|
||||
'http://host:invalid',
|
||||
'http://bad host/',
|
||||
'ftp://localhost',
|
||||
'http:localhost',
|
||||
]) {
|
||||
delayManager.setUrl('url-validation', url)
|
||||
expect(delayManager.getUrl('url-validation')).toBe(
|
||||
'http://cp.cloudflare.com/generate_204',
|
||||
)
|
||||
}
|
||||
delayManager.setUrl('url-validation', ' HTTP://localhost:8080/ ')
|
||||
expect(delayManager.getUrl('url-validation')).toBe('HTTP://localhost:8080/')
|
||||
})
|
||||
|
||||
test('another URL cannot replace a group measurement, but a newer matching URL can', () => {
|
||||
const member = node('url-isolation')
|
||||
if (member.kind !== 'node') throw new Error('Expected node')
|
||||
delayManager.setUrl('url-a', 'http://localhost/a')
|
||||
const cached = delayManager.setDelay(member, 'url-a', 50)
|
||||
const newer = new Date(cached.updatedAt + 1000).toISOString()
|
||||
Object.assign(member.node, {
|
||||
history: [{ time: newer, delay: 126 }],
|
||||
extra: {
|
||||
'http://localhost/a': {
|
||||
alive: true,
|
||||
history: [
|
||||
{ time: new Date(cached.updatedAt - 1000).toISOString(), delay: 49 },
|
||||
],
|
||||
},
|
||||
'http://localhost/b': {
|
||||
alive: true,
|
||||
history: [{ time: newer, delay: 126 }],
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(delayManager.getDelayFix(member, 'url-a')).toBe(50)
|
||||
Object.assign(member.node, {
|
||||
extra: {
|
||||
'http://localhost/a': {
|
||||
alive: true,
|
||||
history: [{ time: newer, delay: 75 }],
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(delayManager.getDelayFix(member, 'url-a')).toBe(75)
|
||||
})
|
||||
|
||||
test('same-named provider members keep separate measurements and listeners', async () => {
|
||||
const first = node('same-provider-name')
|
||||
const second = node('same-provider-name')
|
||||
if (first.kind !== 'node' || second.kind !== 'node')
|
||||
throw new Error('Expected nodes')
|
||||
first.node.recordId = 'provider-a:N'
|
||||
first.node.source = { kind: 'provider', providerName: 'a', proxyName: 'N' }
|
||||
second.node.recordId = 'provider-b:N'
|
||||
second.node.source = { kind: 'provider', providerName: 'b', proxyName: 'N' }
|
||||
const { healthcheckNodeInProvider } = await import('tauri-plugin-mihomo-api')
|
||||
vi.mocked(healthcheckNodeInProvider)
|
||||
.mockResolvedValueOnce({ delay: 40 })
|
||||
.mockResolvedValueOnce({ delay: 125 })
|
||||
const firstListener = vi.fn()
|
||||
const secondListener = vi.fn()
|
||||
delayManager.setListener(first, 'provider-collision', firstListener)
|
||||
delayManager.setListener(second, 'provider-collision', secondListener)
|
||||
await delayManager.checkListDelay([first, second], 'provider-collision', 5000)
|
||||
await flush()
|
||||
expect(firstListener).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ delay: 40 }),
|
||||
)
|
||||
expect(secondListener).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ delay: 125 }),
|
||||
)
|
||||
delayManager.removeListener(first, 'provider-collision')
|
||||
delayManager.removeListener(second, 'provider-collision')
|
||||
expect(delayManager.getDelayFix(first, 'provider-collision')).toBe(40)
|
||||
expect(delayManager.getDelayFix(second, 'provider-collision')).toBe(125)
|
||||
})
|
||||
|
||||
test.each(['core', 'provider'] as const)(
|
||||
'cached %s delays follow source identity after record IDs move',
|
||||
(kind) => {
|
||||
const original = node(`stable-${kind}`)
|
||||
const replacement = node(`replacement-${kind}`)
|
||||
if (original.kind !== 'node' || replacement.kind !== 'node')
|
||||
throw new Error('Expected nodes')
|
||||
const source = (proxyName: string) =>
|
||||
kind === 'core'
|
||||
? { kind, proxyName }
|
||||
: { kind, providerName: 'subscription', proxyName }
|
||||
original.node.source = source(original.ref.name)
|
||||
replacement.node.source = source(replacement.ref.name)
|
||||
original.node.recordId = 'position:0'
|
||||
replacement.node.recordId = 'position:0'
|
||||
delayManager.setDelay(original, `reorder-${kind}`, 40)
|
||||
expect(delayManager.getDelayFix(replacement, `reorder-${kind}`)).toBe(-1)
|
||||
original.node.recordId = 'position:1'
|
||||
expect(delayManager.getDelayFix(original, `reorder-${kind}`)).toBe(40)
|
||||
},
|
||||
)
|
||||
|
||||
test('unmeasured selectors show automatic history without replacing a manual URL result', () => {
|
||||
const member = node('selector-member')
|
||||
if (member.kind !== 'node') throw new Error('Expected node')
|
||||
member.node.history = [
|
||||
{ time: new Date(Date.now() + 1000).toISOString(), delay: 126 },
|
||||
]
|
||||
member.node.extra = {
|
||||
'http://localhost/automatic': { alive: true, history: member.node.history },
|
||||
}
|
||||
expect(delayManager.getDelayFix(member, 'selector')).toBe(126)
|
||||
delayManager.setDelay(member, 'selector', 50)
|
||||
expect(delayManager.getDelayFix(member, 'selector')).toBe(50)
|
||||
})
|
||||
|
||||
+58
-38
@@ -12,13 +12,23 @@ import {
|
||||
} from '@/types/proxy-view'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
import { classifyDelay, DEFAULT_DELAY_TIMEOUT } from '@/utils/delay'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
import { isValidLatencyTestUrl } from '@/utils/network'
|
||||
|
||||
export type DelaySnapshot = {
|
||||
of: (member: ResolvedProxyMember) => number
|
||||
}
|
||||
|
||||
const hashKey = (name: string, group: string) => `${group ?? ''}::${name}`
|
||||
const hashKey = (member: ResolvedProxyMember, group: string) => {
|
||||
if (member.kind !== 'node')
|
||||
return JSON.stringify([group, member.kind, member.ref.name])
|
||||
const source = member.node.source
|
||||
return JSON.stringify([
|
||||
group,
|
||||
source.kind,
|
||||
source.kind === 'provider' ? source.providerName : null,
|
||||
source.proxyName,
|
||||
])
|
||||
}
|
||||
|
||||
export interface DelayUpdate {
|
||||
delay: number
|
||||
@@ -29,7 +39,7 @@ export interface DelayUpdate {
|
||||
const CACHE_TTL = 30 * 60 * 1000
|
||||
|
||||
class DelayManager {
|
||||
private cache = new Map<string, DelayUpdate>()
|
||||
private cache = new Map<string, DelayUpdate & { url: string }>()
|
||||
private urlMap = new Map<string, string>()
|
||||
|
||||
private listenerMap = new Map<string, (update: DelayUpdate) => void>()
|
||||
@@ -153,13 +163,13 @@ class DelayManager {
|
||||
}
|
||||
|
||||
setUrl(group: string, url: string) {
|
||||
if (!isValidUrl(url)) {
|
||||
debugLog(`[DelayManager] 拒绝无效测试URL,组: ${group}, URL: ${url}`)
|
||||
const previous = this.getUrl(group)
|
||||
if (isValidLatencyTestUrl(url)) {
|
||||
this.urlMap.set(group, url.trim())
|
||||
} else {
|
||||
this.urlMap.delete(group)
|
||||
return
|
||||
}
|
||||
debugLog(`[DelayManager] 设置测试URL,组: ${group}, URL: ${url}`)
|
||||
this.urlMap.set(group, url)
|
||||
if (previous !== this.getUrl(group)) this.queueGroupNotification(group)
|
||||
}
|
||||
|
||||
getUrl(group: string) {
|
||||
@@ -171,16 +181,16 @@ class DelayManager {
|
||||
}
|
||||
|
||||
setListener(
|
||||
name: string,
|
||||
member: ResolvedProxyMember,
|
||||
group: string,
|
||||
listener: (update: DelayUpdate) => void,
|
||||
) {
|
||||
const key = hashKey(name, group)
|
||||
const key = hashKey(member, group)
|
||||
this.listenerMap.set(key, listener)
|
||||
}
|
||||
|
||||
removeListener(name: string, group: string) {
|
||||
const key = hashKey(name, group)
|
||||
removeListener(member: ResolvedProxyMember, group: string) {
|
||||
const key = hashKey(member, group)
|
||||
this.listenerMap.delete(key)
|
||||
}
|
||||
|
||||
@@ -199,14 +209,14 @@ class DelayManager {
|
||||
}
|
||||
|
||||
setDelay(
|
||||
name: string,
|
||||
member: ResolvedProxyMember,
|
||||
group: string,
|
||||
delay: number,
|
||||
meta?: { elapsed?: number },
|
||||
meta?: { elapsed?: number; url?: string },
|
||||
): DelayUpdate {
|
||||
const key = hashKey(name, group)
|
||||
const key = hashKey(member, group)
|
||||
debugLog(
|
||||
`[DelayManager] 设置延迟,代理: ${name}, 组: ${group}, 延迟: ${delay}`,
|
||||
`[DelayManager] 设置延迟,代理: ${member.ref.name}, 组: ${group}, 延迟: ${delay}`,
|
||||
)
|
||||
const update: DelayUpdate = {
|
||||
delay,
|
||||
@@ -214,7 +224,9 @@ class DelayManager {
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
|
||||
this.cache.set(key, update)
|
||||
const url = meta?.url ?? this.getUrl(group)
|
||||
if (url !== this.getUrl(group)) return update
|
||||
this.cache.set(key, { ...update, url })
|
||||
|
||||
const queue = this.pendingItemUpdates.get(key)
|
||||
if (queue) {
|
||||
@@ -227,9 +239,16 @@ class DelayManager {
|
||||
return update
|
||||
}
|
||||
|
||||
getDelayUpdate(name: string, group: string) {
|
||||
const key = hashKey(name, group)
|
||||
const entry = this.cache.get(key)
|
||||
getDelayUpdate(member: ResolvedProxyMember, group: string) {
|
||||
const history = this.getHistory(member, group)
|
||||
const key = hashKey(member, group)
|
||||
const cached = this.cache.get(key)
|
||||
const entry = cached?.url === this.getUrl(group) ? cached : undefined
|
||||
const latest = history?.at(-1)
|
||||
const updatedAt = latest ? Date.parse(latest.time) : 0
|
||||
if (latest && entry?.delay !== -2 && updatedAt > (entry?.updatedAt ?? 0)) {
|
||||
return { delay: latest.delay || 1e6, updatedAt }
|
||||
}
|
||||
if (!entry) return undefined
|
||||
|
||||
if (Date.now() - entry.updatedAt > CACHE_TTL) {
|
||||
@@ -240,23 +259,25 @@ class DelayManager {
|
||||
return { ...entry }
|
||||
}
|
||||
|
||||
getDelay(name: string, group: string) {
|
||||
const update = this.getDelayUpdate(name, group)
|
||||
getDelay(member: ResolvedProxyMember, group: string) {
|
||||
const update = this.getDelayUpdate(member, group)
|
||||
return update ? update.delay : -1
|
||||
}
|
||||
|
||||
getHistory(member: ResolvedProxyMember, group: string) {
|
||||
return memberDetails(member)?.extra?.[this.getUrl(group)]?.history
|
||||
}
|
||||
|
||||
getDelayFix(member: ResolvedProxyMember, group: string) {
|
||||
if (member.kind === 'unresolved') return -1
|
||||
const details = memberDetails(member)
|
||||
const name = member.ref.name
|
||||
const update = this.getDelayUpdate(name, group)
|
||||
const history =
|
||||
this.getHistory(member, group) ?? memberDetails(member)?.history
|
||||
const update = this.getDelayUpdate(member, group)
|
||||
if (update && (update.delay >= 0 || update.delay === -2)) {
|
||||
return update.delay
|
||||
}
|
||||
|
||||
if (details?.history && details.history.length > 0) {
|
||||
return details.history[details.history.length - 1].delay || 1e6
|
||||
}
|
||||
if (history?.length) return history[history.length - 1].delay || 1e6
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -298,12 +319,12 @@ class DelayManager {
|
||||
`[DelayManager] 开始测试延迟,代理: ${name}, 组: ${group}, 超时: ${timeout}ms`,
|
||||
)
|
||||
|
||||
this.setDelay(name, group, -2)
|
||||
const url = this.getUrl(group)
|
||||
this.setDelay(member, group, -2, { url })
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
const url = this.getUrl(group)
|
||||
debugLog(`[DelayManager] 调用API测试延迟,代理: ${name}, URL: ${url}`)
|
||||
|
||||
const timeoutPromise = new Promise<ProxyDelay>((resolve) => {
|
||||
@@ -324,14 +345,14 @@ class DelayManager {
|
||||
const elapsed = elapsedTime
|
||||
debugLog(`[DelayManager] 延迟测试完成,代理: ${name}, 结果: ${delay}ms`)
|
||||
|
||||
return this.setDelay(name, group, delay, { elapsed })
|
||||
return this.setDelay(member, group, delay, { elapsed, url })
|
||||
} catch (error) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
console.error(`[DelayManager] 延迟测试出错,代理: ${name}`, error)
|
||||
const delay = 1e6 // error
|
||||
const elapsed = Date.now() - startTime
|
||||
|
||||
return this.setDelay(name, group, delay, { elapsed })
|
||||
return this.setDelay(member, group, delay, { elapsed, url })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,10 +365,9 @@ class DelayManager {
|
||||
debugLog(
|
||||
`[DelayManager] 批量测试延迟开始,组: ${group}, 数量: ${proxies.length}, 并发数: ${concurrency}`,
|
||||
)
|
||||
const names = proxies.map((member) => member.ref.name)
|
||||
this.activeBatches.set(group, (this.activeBatches.get(group) ?? 0) + 1)
|
||||
names.forEach((name) => {
|
||||
this.setDelay(name, group, -2)
|
||||
proxies.forEach((member) => {
|
||||
this.setDelay(member, group, -2)
|
||||
})
|
||||
|
||||
let index = 0
|
||||
@@ -359,7 +379,7 @@ class DelayManager {
|
||||
const currName = currMember.ref.name
|
||||
|
||||
try {
|
||||
this.setDelay(currName, group, -2)
|
||||
this.setDelay(currMember, group, -2)
|
||||
|
||||
// Stagger requests so a batch does not hit the core at once.
|
||||
if (index > 1) {
|
||||
@@ -375,13 +395,13 @@ class DelayManager {
|
||||
`[DelayManager] 批量测试单个代理出错,代理: ${currName}`,
|
||||
error,
|
||||
)
|
||||
this.setDelay(currName, group, 1e6)
|
||||
this.setDelay(currMember, group, 1e6)
|
||||
}
|
||||
|
||||
return help()
|
||||
}
|
||||
|
||||
const actualConcurrency = Math.min(concurrency, names.length, 10)
|
||||
const actualConcurrency = Math.min(concurrency, proxies.length, 10)
|
||||
debugLog(`[DelayManager] 实际并发数: ${actualConcurrency}`)
|
||||
|
||||
const promiseList: Promise<void>[] = []
|
||||
|
||||
@@ -15,6 +15,7 @@ export const supportedLanguages = [
|
||||
'es',
|
||||
'jp',
|
||||
'zhtw',
|
||||
'pt',
|
||||
]
|
||||
|
||||
export const FALLBACK_LANGUAGE = 'zh'
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { afterEach, expect, test, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/services/cmds', () => ({
|
||||
getProfiles: vi.fn(),
|
||||
patchProfile: vi.fn().mockResolvedValue(undefined),
|
||||
syncTrayProxySelection: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
vi.mock('@/services/query-client', () => ({ revalidateQueries: vi.fn() }))
|
||||
vi.mock('@/services/notice-service', () => ({ showNotice: { error: vi.fn() } }))
|
||||
|
||||
import {
|
||||
getProfiles,
|
||||
patchProfile,
|
||||
syncTrayProxySelection,
|
||||
} from '@/services/cmds'
|
||||
import { revalidateQueries } from '@/services/query-client'
|
||||
|
||||
import {
|
||||
profileTestUrls,
|
||||
saveProfileTestUrls,
|
||||
commitProfileTestUrl,
|
||||
} from './proxy-test-url'
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
test('committed edits retain their profile and merge with fresh saved URLs', async () => {
|
||||
vi.mocked(getProfiles).mockResolvedValue({
|
||||
current: 'new-profile',
|
||||
items: [
|
||||
{
|
||||
uid: 'old-profile',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { untouched: 'https://example.com/' },
|
||||
},
|
||||
],
|
||||
})
|
||||
await commitProfileTestUrl('old-profile', 'A', ' http://localhost/new ')
|
||||
expect(patchProfile).toHaveBeenCalledExactlyOnceWith('old-profile', {
|
||||
latency_test_urls: {
|
||||
untouched: 'https://example.com/',
|
||||
A: 'http://localhost/new',
|
||||
},
|
||||
})
|
||||
expect(revalidateQueries).toHaveBeenCalledWith([['getProfiles']])
|
||||
expect(syncTrayProxySelection).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
test('migration cannot overwrite a previously saved or cleared override', async () => {
|
||||
vi.mocked(getProfiles).mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
uid: 'p',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { A: '' },
|
||||
},
|
||||
],
|
||||
})
|
||||
await saveProfileTestUrls('p', { A: 'http://localhost/stale' }, true)
|
||||
expect(patchProfile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('pending edits survive a reload and older save acknowledgements', async () => {
|
||||
vi.useFakeTimers()
|
||||
const profile = {
|
||||
uid: 'pending-profile',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { A: 'http://localhost/old' },
|
||||
}
|
||||
vi.mocked(getProfiles).mockResolvedValue({ items: [profile] })
|
||||
let finish!: () => void
|
||||
vi.mocked(patchProfile).mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<null>((resolve) => {
|
||||
finish = () => resolve(null)
|
||||
}),
|
||||
)
|
||||
const first = commitProfileTestUrl(profile.uid, 'A', 'http://localhost/new')
|
||||
expect(profileTestUrls(profile).A).toBe('http://localhost/new')
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
expect(profileTestUrls(profile).A).toBe('http://localhost/new')
|
||||
const second = commitProfileTestUrl(
|
||||
profile.uid,
|
||||
'A',
|
||||
'http://localhost/newest',
|
||||
)
|
||||
finish()
|
||||
await first
|
||||
expect(profileTestUrls(profile).A).toBe('http://localhost/newest')
|
||||
await second
|
||||
expect(patchProfile).toHaveBeenLastCalledWith(profile.uid, {
|
||||
latency_test_urls: { A: 'http://localhost/newest' },
|
||||
})
|
||||
})
|
||||
|
||||
test('a failed save preserves the committed input across profile refreshes', async () => {
|
||||
const profile = {
|
||||
uid: 'failed-profile',
|
||||
type: null,
|
||||
name: null,
|
||||
file: null,
|
||||
updated: null,
|
||||
latency_test_urls: { A: 'http://localhost/old' },
|
||||
}
|
||||
vi.mocked(getProfiles).mockResolvedValue({ items: [profile] })
|
||||
vi.mocked(patchProfile).mockRejectedValueOnce(new Error('save failed'))
|
||||
const log = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
await commitProfileTestUrl(profile.uid, 'A', 'http://localhost/new')
|
||||
expect(profileTestUrls(profile).A).toBe('http://localhost/new')
|
||||
await commitProfileTestUrl(profile.uid, 'A', 'http://localhost/new')
|
||||
expect(profileTestUrls(profile).A).toBe('http://localhost/old')
|
||||
log.mockRestore()
|
||||
})
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
getProfiles,
|
||||
patchProfile,
|
||||
syncTrayProxySelection,
|
||||
} from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
|
||||
function readLegacyTestUrls(uid: string): Record<string, string> {
|
||||
try {
|
||||
const states = JSON.parse(localStorage.getItem('proxy-head-state') || '{}')[
|
||||
uid
|
||||
]
|
||||
return Object.fromEntries(
|
||||
Object.entries(states || {}).flatMap(([name, state]) => {
|
||||
const url = (state as { testUrl?: string }).testUrl
|
||||
return typeof url === 'string' && url.trim() ? [[name, url.trim()]] : []
|
||||
}),
|
||||
)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveProfileTestUrls(
|
||||
uid: string,
|
||||
urls: Record<string, string>,
|
||||
migrateOnly = false,
|
||||
) {
|
||||
return mutate(
|
||||
async () => {
|
||||
const profiles = await getProfiles()
|
||||
const profile = profiles.items?.find((item) => item.uid === uid)
|
||||
if (!profile || (migrateOnly && profile.latency_test_urls !== undefined))
|
||||
return
|
||||
await patchProfile(uid, {
|
||||
latency_test_urls: {
|
||||
...(profile.latency_test_urls ?? readLegacyTestUrls(uid)),
|
||||
...urls,
|
||||
},
|
||||
})
|
||||
await syncTrayProxySelection().catch((error) => {
|
||||
console.error(
|
||||
'Failed to refresh tray after saving latency test URL:',
|
||||
error,
|
||||
)
|
||||
})
|
||||
},
|
||||
{ id: `patch-profile:${uid}`, revalidate: [['getProfiles']] },
|
||||
)
|
||||
}
|
||||
|
||||
const migrating = new Set<string>()
|
||||
|
||||
export function migrateProfileTestUrls(profiles: IProfilesConfig) {
|
||||
for (const profile of profiles.items || []) {
|
||||
const uid = profile.uid
|
||||
if (!uid) continue
|
||||
if (profile.latency_test_urls !== undefined || migrating.has(uid)) continue
|
||||
const urls = readLegacyTestUrls(uid)
|
||||
if (!Object.keys(urls).length) continue
|
||||
migrating.add(uid)
|
||||
void saveProfileTestUrls(uid, urls, true)
|
||||
.catch((error) => {
|
||||
console.error('Failed to migrate latency test URLs:', error)
|
||||
})
|
||||
.finally(() => migrating.delete(uid))
|
||||
}
|
||||
}
|
||||
|
||||
const pending = new Map<string, Record<string, { url: string }>>()
|
||||
|
||||
export async function commitProfileTestUrl(
|
||||
uid: string,
|
||||
group: string,
|
||||
url: string,
|
||||
) {
|
||||
const edit = { url: url.trim() }
|
||||
const urls = { ...pending.get(uid), [group]: edit }
|
||||
pending.set(uid, urls)
|
||||
try {
|
||||
await saveProfileTestUrls(uid, { [group]: edit.url })
|
||||
const current = pending.get(uid)
|
||||
if (current?.[group] === edit) {
|
||||
delete current[group]
|
||||
if (!Object.keys(current).length) pending.delete(uid)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save latency test URL:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function profileTestUrls(profile: IProfileItem): Record<string, string> {
|
||||
return {
|
||||
...profile.latency_test_urls,
|
||||
...Object.fromEntries(
|
||||
Object.entries(
|
||||
(profile.uid ? pending.get(profile.uid) : undefined) ?? {},
|
||||
).map(([group, edit]) => [group, edit.url]),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,19 @@ export const isValidUrl = (value: string) =>
|
||||
require_tld: false,
|
||||
})
|
||||
|
||||
export const isValidLatencyTestUrl = (value: string) => {
|
||||
const raw = value.trim()
|
||||
if (!/^https?:\/\//i.test(raw)) return false
|
||||
try {
|
||||
const url = new URL(raw)
|
||||
return (
|
||||
(url.protocol === 'http:' || url.protocol === 'https:') && !!url.hostname
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const isValidPort = (value: string) => isPort(value.trim())
|
||||
|
||||
export const normalizeHost = (value: string): string | null => {
|
||||
|
||||
Reference in New Issue
Block a user