Compare commits

...
13 Commits
Author SHA1 Message Date
Tunglies 13676d3335 Release 2.5.4 2026-09-20 19:49:13 +08:00
oomeow 2a7972fa5b fix(core): drain sidecar events before readiness probe to avoid start… (#7966)
* fix(core): drain sidecar events before readiness probe to avoid startup stall

* refactor(core): simplify core readiness generation handling

* docs: update Changelog.md
2026-09-20 09:43:31 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 19334368cd chore(deps): update dependency pnpm to v12.5.1 (#7976)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-20 07:43:37 +00:00
wonfen 73ea0fbd80 fix(dns): keep profile values the override leaves blank or off
Refs #7292 #7933
2026-09-20 15:08:33 +08:00
9f97d938f5 chore(i18n): complete Turkish (tr) translations (#7969)
Translate the 174 frontend strings and 8 tray/notification strings that were
still English copies in the Turkish locale. Values only; no keys changed.

Co-authored-by: qwist1233-cpu <qwist1233-cpu@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 09:21:06 +08:00
wonfen 04a2a09368 fix(linux): restore SELinux service startup with v2.7.2
Upgrade clash_verge_service_ipc and bundled service downloads to v2.7.2,
which labels runtime-installed executables before publication and repairs
existing core labels instead of timing out waiting for service IPC.

Register persistent bin_t rules for production and development service
binaries and cores in the RPM post-install script. Support older semanage
versions, apply rules to existing directories, and warn when optional
SELinux tools are unavailable.

Refs: #7965
2026-09-19 23:58:29 +08:00
❤是纱雾酱哟~ 94491b4ba4 chore(gh-aw): Bump gh-aw to version 0.89.15 (#7973)
Assisted-by: vscode-copilot-chat:deepseek-v4-pro codegraph

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
2026-09-19 12:08:12 +00:00
❤是纱雾酱哟~ 78516dc1dc chore(renovate): Prevent auto-updates of gh-aw (#7972)
- Stop automatically updating dependencies of `gh-aw` related stuff
- Mend Renovate have no ability to distinguish stable and pre-release
- Manual update via `gh aw upgrade` is preffered

Assisted-by: vscode-copilot-chat:deepseek-flash codegraph

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
2026-09-19 12:07:43 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 8f6fd3bbf9 chore(deps): update github/gh-aw-actions action to v0.89.17 (#7971)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-19 08:41:11 +00:00
Tunglies c599f5547e fix(proxy): validate user-entered URLs via isValidUrl
Delay test inputs now reject non-http(s) URLs with a warning toast and
fall back to the default URL instead of erroring every node in the
core. Subscription import and profile card host parsing reuse the same
validator, fixing bare-domain URLs shown as local files and
credential-bearing URLs leaking into the card.
2026-09-18 13:18:30 +08:00
wonfen f6fdb34745 perf(home): render proxy dropdown options on demand 2026-09-18 00:41:54 +08:00
wonfen 65ed8d3cf2 fix(home): stabilize proxy group and node dropdowns 2026-09-18 00:26:40 +08:00
Tunglies 2baa629ac4 chore: remove unimportant notice in changelog 2026-09-17 23:32:10 +08:00
52 changed files with 983 additions and 707 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
"features": {
"ghcr.io/devcontainers/features/node:2": {
"version": "24.21.0",
"pnpmVersion": "12.3.3"
"pnpmVersion": "12.5.1"
},
"ghcr.io/devcontainers/features/rust:1": {
"version": "1.98.1",
+22 -7
View File
@@ -5,10 +5,10 @@
"version": "v9.0.0",
"sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
},
"github/gh-aw-actions/setup@v0.89.1": {
"github/gh-aw-actions/setup@v0.89.15": {
"repo": "github/gh-aw-actions/setup",
"version": "v0.89.1",
"sha": "4537e5924c9abb366dcbece06750e498e0218fae"
"version": "v0.89.15",
"sha": "045beb2d14bda8d0c1f2e83b41527f63770d2855"
}
},
"containers": {
@@ -22,6 +22,11 @@
"digest": "sha256:f7df036c86575527b61f3f7df91c4412349a12b2a74988d929eafa2999230c98",
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.28.14@sha256:f7df036c86575527b61f3f7df91c4412349a12b2a74988d929eafa2999230c98"
},
"ghcr.io/github/gh-aw-firewall/agent:0.28.16": {
"image": "ghcr.io/github/gh-aw-firewall/agent:0.28.16",
"digest": "sha256:57a3e27388a6d7d32719088581e52567727fa0bf2f0d477bf565b0c4baa12a3f",
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.28.16@sha256:57a3e27388a6d7d32719088581e52567727fa0bf2f0d477bf565b0c4baa12a3f"
},
"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.13": {
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.13",
"digest": "sha256:61f12dcff38eb008b645d87a5541763452f409d9040eb243718764944167b1a2",
@@ -32,6 +37,11 @@
"digest": "sha256:6f95e2234dd9bd6333a8ff28ccea7ecf0204acd4a09108723844dbd2bf6268c5",
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.14@sha256:6f95e2234dd9bd6333a8ff28ccea7ecf0204acd4a09108723844dbd2bf6268c5"
},
"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.16": {
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.16",
"digest": "sha256:cd400948638ffe1b87ec319abf73fa29b6ac9881b015da58bba971c4cc13a400",
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.16@sha256:cd400948638ffe1b87ec319abf73fa29b6ac9881b015da58bba971c4cc13a400"
},
"ghcr.io/github/gh-aw-firewall/squid:0.28.13": {
"image": "ghcr.io/github/gh-aw-firewall/squid:0.28.13",
"digest": "sha256:58a8dc5be3fbafeef7da0532c38bd9047f86bf1b3c4cfd7d9b7d8d28e80a2b56",
@@ -42,10 +52,15 @@
"digest": "sha256:2ce8df3abf3e9b76e9c0cf5863da41f1ab3f89b20ad14b988806ab89e7bf2cd5",
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.28.14@sha256:2ce8df3abf3e9b76e9c0cf5863da41f1ab3f89b20ad14b988806ab89e7bf2cd5"
},
"ghcr.io/github/gh-aw-mcpg:v0.4.20": {
"image": "ghcr.io/github/gh-aw-mcpg:v0.4.20",
"digest": "sha256:980ea7aa4fb07e444f0e2c6e3af5aff8b45e4e415d617667b8ba064a1768b684",
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.20@sha256:980ea7aa4fb07e444f0e2c6e3af5aff8b45e4e415d617667b8ba064a1768b684"
"ghcr.io/github/gh-aw-firewall/squid:0.28.16": {
"image": "ghcr.io/github/gh-aw-firewall/squid:0.28.16",
"digest": "sha256:452197f2e241b2cda8eb0b5674960aa8ca544a64dc242e24f8263c3df6452919",
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.28.16@sha256:452197f2e241b2cda8eb0b5674960aa8ca544a64dc242e24f8263c3df6452919"
},
"ghcr.io/github/gh-aw-mcpg:v0.4.21": {
"image": "ghcr.io/github/gh-aw-mcpg:v0.4.21",
"digest": "sha256:f26f665840660581510746ed84f5fc706f4ca8c48833757dfb4d72d86587201e",
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.21@sha256:f26f665840660581510746ed84f5fc706f4ca8c48833757dfb4d72d86587201e"
},
"ghcr.io/github/gh-aw-node": {
"image": "ghcr.io/github/gh-aw-node",
+1 -1
View File
@@ -33,4 +33,4 @@ jobs:
- name: Install gh-aw extension
uses: github/gh-aw-actions/setup-cli@045beb2d14bda8d0c1f2e83b41527f63770d2855 # v0.89.15
with:
version: v0.89.1
version: v0.89.15
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
nodejs 24.21.0
pnpm 12.4.1
pnpm 12.5.1
rust 1.98.1
Generated
+3 -3
View File
@@ -1079,7 +1079,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "clash-verge"
version = "2.5.4-rc"
version = "2.5.4"
dependencies = [
"aes-gcm",
"anyhow",
@@ -1223,8 +1223,8 @@ dependencies = [
[[package]]
name = "clash_verge_service_ipc"
version = "2.7.1"
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc.git?tag=v2.7.1#807480e9afc7b9deb8ff3040b54be34326986dfd"
version = "2.7.2"
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc.git?tag=v2.7.2#cad270fe753553cfa2fd26fa5c338eabd210a2d5"
dependencies = [
"anyhow",
"kode-bridge",
+5 -3
View File
@@ -1,11 +1,9 @@
## v2.5.4
> [!IMPORTANT]
> 服务模式下,配置目录中的规则集、代理集合文件只是缓存副本,手动修改不会影响正在运行的内核。需要手动维护的文件请使用 `type: file`。
<details>
<summary><strong> 🐞 修复问题 </strong></summary>
- 修复首页代理组和节点下拉菜单:再次点击无法收起或误选、小窗口显示不全、切换模式后意外展开
- 修复系统代理切换失败后,开关状态显示错误的问题
- 修复代理守卫失败后反复重试且没有提示的问题
- 修复未开启系统代理时,启动仍提示代理错误的问题
@@ -32,6 +30,7 @@
- 修复 Windows 系统代理设置偶发失败的问题
- 修复 Windows 关机被应用阻止的问题
- 修复 Windows 服务模式下 TUN 使用 system/mixed 堆栈时无法接管流量的问题,防火墙放行规则改为自动添加
- 修复 Windows 配置中 GEO 规则较多时,首次启动内核失败的问题
**🍎 macOS**
@@ -42,6 +41,7 @@
**🐧 Linux**
- 修复 Linux 安装或卸载服务时授权失败的问题
- 修复 Fedora 等启用 SELinux 的系统更新后服务无法启动的问题
**🍎/🐧 macOS/Linux**
@@ -79,7 +79,9 @@
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 优化首页节点菜单关闭时的刷新性能
- 优化 TUN 配置优先级:界面设置优先,其他选项仍可通过订阅、覆写和脚本调整
- 优化 DNS 覆写:仅覆盖已填写的字段和已开启的开关,留空或关闭的项保留订阅设置
- 优化系统代理失败后的恢复流程,保持系统设置和界面状态一致
- 优化错误提示,支持复制完整原因,托盘和快捷键操作失败时也会提醒
- 优化代理列表刷新、排序和页面切换速度
+8 -8
View File
@@ -8,8 +8,8 @@ notifications:
body: '{mode} moduna geçildi.'
systemProxyToggled:
title: Sistem Vekil'i
'on': System proxy has been enabled.
'off': System proxy has been disabled.
'on': Sistem vekili etkinleştirildi.
'off': Sistem vekili devre dışı bırakıldı.
systemProxyFailed:
title: Sistem Vekili
body: Sistem vekil sunucusu ayarlanamadı. Nedenini görmek için Clash Verge'i açın.
@@ -18,8 +18,8 @@ notifications:
body: Art arda birkaç başarısızlıktan sonra durdu. Nedenini görmek için Clash Verge'i açın.
tunModeToggled:
title: TUN Modu
'on': TUN mode has been enabled.
'off': TUN mode has been disabled.
'on': TUN modu etkinleştirildi.
'off': TUN modu devre dışı bırakıldı.
lightweightModeEntered:
title: Hafif Mod
body: Hafif moda geçildi.
@@ -33,10 +33,10 @@ notifications:
title: Uygulama Gizlendi
body: Clash Verge arka planda çalışıyor.
updateReady:
title: Clash Verge Update
body: A new version (v{version}) has been downloaded and is ready to install.
installNow: Install Now
later: Later
title: Clash Verge Güncellemesi
body: Yeni bir sürüm (v{version}) indirildi ve kurulmaya hazır.
installNow: Şimdi Kur
later: Daha Sonra
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.
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.5.4-rc",
"version": "2.5.4",
"license": "GPL-3.0-only",
"scripts": {
"prepare": "husky || true",
@@ -144,5 +144,5 @@
]
},
"type": "module",
"packageManager": "pnpm@12.4.1"
"packageManager": "pnpm@12.5.1"
}
+61 -61
View File
@@ -7,153 +7,153 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.4.1
version: 12.4.1
specifier: 12.5.1
version: 12.5.1
packages:
'@pnpm/exe.android-arm64@12.4.1':
resolution: {integrity: sha512-/HwsqXMSmlOfgtV9+O0ratzjV6Vd/8n1hh4rGHpCGmDURvt52MwZxdbhyxP4K03ZfvgSDvotFPr8O+RzE5Eu8A==}
'@pnpm/exe.android-arm64@12.5.1':
resolution: {integrity: sha512-6LGBmQuDzUmgpHSuy+haG0jrmkwFNa4EuShDZqK3cIBv4Wfb75CzjJmowkzr+iJg40vnBKMQ62t4Ko6mwsId7A==}
cpu: [arm64]
os: [android]
'@pnpm/exe.android-x64@12.4.1':
resolution: {integrity: sha512-+l74Qb4c2YjOzNKHXJLg+1wr8xHM1ckkUhnU5KRUK9TJqiiczt6yeTZqqzHIlJ8i6pAoj5V0OJevDRwnQKLgrQ==}
'@pnpm/exe.android-x64@12.5.1':
resolution: {integrity: sha512-091oCltP3Rx89yxL1Wh5rXkUXEyFzyxIzSHKRFDyDFlQkb8Ox8FaDcm+M8PNkMyqnLA+qeGOLzY7Rjj9izl1Ww==}
cpu: [x64]
os: [android]
'@pnpm/exe.darwin-arm64@12.4.1':
resolution: {integrity: sha512-6rkZkT3iGfaxknUdGHraqSWFvTa6N0ajAHluv9Ax0GRWs0sIcGNiFhDopv6xSZCsJZmG483aNS/b6UEDy3blfw==}
'@pnpm/exe.darwin-arm64@12.5.1':
resolution: {integrity: sha512-r1vEMQPVjRexZMyL2HonxJoo3+nuB/+G+tg+f2PK6faRdXX1TDwevKXOxJAfqlPInTy6W/pm912+s+m0NcR4BQ==}
cpu: [arm64]
os: [darwin]
'@pnpm/exe.darwin-x64@12.4.1':
resolution: {integrity: sha512-Vb1CHlR88HghC1qUxjxjs82zQSXnXacPD+btG2CmG8Q/hBU7Q0/b2YWJKSQqZXicunV5khFtfTlAwJddV9RkYA==}
'@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.freebsd-x64@12.4.1':
resolution: {integrity: sha512-iT3iHz3Nl0Sxxj7UPOtZ/aQ81AFsQhjoeWMAlPkSRO04gsgDGAXv3UYxOFaesMWsfNxaGn0A+CITbuCHFF66FA==}
'@pnpm/exe.freebsd-x64@12.5.1':
resolution: {integrity: sha512-sFUMHiYFs6q5syFwgaVppzhQSmqnXoJIz/YwTXIOXTDE76ndIpTRpDGAnBCBKjsaEBnqjJA/2l6hpJcNDNoMiA==}
cpu: [x64]
os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.4.1':
resolution: {integrity: sha512-aBooZfNXM5f+OGUgCAMFWpE/kAhsWfvmqIyMtHy6zl3aNxyInWrcY/Saln/UElzo7lZWMC0Yktroxd/2J26lNQ==}
'@pnpm/exe.linux-arm64-musl@12.5.1':
resolution: {integrity: sha512-zupuxYZBuBEGjUz805/4/Asq01UNziFADANt8yp+LdUjU9bbMa3xY78Bx1PsGDqBy7aiad4sdUMK8ZxH4n4gmQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.4.1':
resolution: {integrity: sha512-TlOdacTTP09BgcMvwWBFRsu8VAjfwqwnslBj+XSq1JFM3ck4f3k+1O/747EuctxZxs3/o785b6Q3s7Pd92Ptsg==}
'@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-ppc64@12.4.1':
resolution: {integrity: sha512-r/ab/MlIBo75oizUP5ITiziCCrnXz4SJwfErLQ+603AshB+Yq7xTMCoMtzTSCAMu6aaymIKkAFtZvd2J75Wq0w==}
'@pnpm/exe.linux-ppc64@12.5.1':
resolution: {integrity: sha512-Gg3maDzaE/ZFMV9HIx/BwMN0Y6QbEs13URfxmiYHtKjjLAs5wEQZRMUP87YNLYO7cNaEfbM8GRbS9vPosyD9Gw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-riscv64@12.4.1':
resolution: {integrity: sha512-C/D1QWdKMiB8+wv/spl1rGITFUuqC+aI/fb6h8NB5sqxsOaGXeYc/g891oCuzggHn6SODk9p+I09hI76x/cMNw==}
'@pnpm/exe.linux-riscv64@12.5.1':
resolution: {integrity: sha512-6fgWxXYMBMsWQ8DAiRevscX4QZQjwh+FsyCj8ZOk8pMXvqeq84MqBLCmS6KckvnbdqJ+HlSVQ7fs/kd0ldwfhA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-s390x@12.4.1':
resolution: {integrity: sha512-nxz5zD4yXt94uzbStDk0QTPKW+aE92hH1b5tFXK9ctB1HE9Xcq1vjTiA2LsZMFC6p4gdu5OwQeFqYNAVGa6QlA==}
'@pnpm/exe.linux-s390x@12.5.1':
resolution: {integrity: sha512-nzMZruRvxDhEfYUhzu+7EmM39+82Dgagc/6hPFPqGqzXNGcIG0anuxbUxfeNtI64SHt91KuuYUmCB/inlM+Pag==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.4.1':
resolution: {integrity: sha512-5AwgFdGhVUg2kIweYGfxzSLEHiIG77PQhZAkXC3TwofQHsu1Wr+TrV5/rNX2PopFnHRzuE581zoB8F6Wle32yg==}
'@pnpm/exe.linux-x64-musl@12.5.1':
resolution: {integrity: sha512-kHCbegdTNHpzmPtUD4S5fqkVkqgdQR47spF9p2VPP3dIIkZl6Ytk8w0vhlAsAGgge8FmucfwX+vvPKgm0NtOCQ==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.4.1':
resolution: {integrity: sha512-FJOZuuuQMhp0oLzBtcKkLXknBI92hfkmSnlKc47vfin4HrmfID5khY2lGekL9tCzk1cpR+HShEw/meFl+nHtzQ==}
'@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.4.1':
resolution: {integrity: sha512-OO7eKBL9S+xk5hRy+JUUZSNJknGuGe3GEUffsrlC2LbqKF02g+VX1anGIzSbJDvkkdnpJhSlxF5AUz2JzJu2Jw==}
'@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.4.1':
resolution: {integrity: sha512-x7gJHZgHo6hp354xCYA2NvoFzYJkHwovt2kVsUBK5EmXULLcP51JGMq09CX+5FRFJUZFKoXjLu3mZWmfP7o6PQ==}
'@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64]
os: [win32]
pnpm@12.4.1:
resolution: {integrity: sha512-LoHjmdc/6DkNqyXgaqeIq3pZCCSNL1o3D4K0gRR6ano2e/gEj5pv22Rg8hpm8FQt7bi5TKLIcjWWdBkgsWVtTA==}
pnpm@12.5.1:
resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.android-arm64@12.4.1':
'@pnpm/exe.android-arm64@12.5.1':
optional: true
'@pnpm/exe.android-x64@12.4.1':
'@pnpm/exe.android-x64@12.5.1':
optional: true
'@pnpm/exe.darwin-arm64@12.4.1':
'@pnpm/exe.darwin-arm64@12.5.1':
optional: true
'@pnpm/exe.darwin-x64@12.4.1':
'@pnpm/exe.darwin-x64@12.5.1':
optional: true
'@pnpm/exe.freebsd-x64@12.4.1':
'@pnpm/exe.freebsd-x64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64-musl@12.4.1':
'@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true
'@pnpm/exe.linux-arm64@12.4.1':
'@pnpm/exe.linux-arm64@12.5.1':
optional: true
'@pnpm/exe.linux-ppc64@12.4.1':
'@pnpm/exe.linux-ppc64@12.5.1':
optional: true
'@pnpm/exe.linux-riscv64@12.4.1':
'@pnpm/exe.linux-riscv64@12.5.1':
optional: true
'@pnpm/exe.linux-s390x@12.4.1':
'@pnpm/exe.linux-s390x@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.4.1':
'@pnpm/exe.linux-x64-musl@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.4.1':
'@pnpm/exe.linux-x64@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.4.1':
'@pnpm/exe.win32-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.4.1':
'@pnpm/exe.win32-x64@12.5.1':
optional: true
pnpm@12.4.1:
pnpm@12.5.1:
optionalDependencies:
'@pnpm/exe.android-arm64': 12.4.1
'@pnpm/exe.android-x64': 12.4.1
'@pnpm/exe.darwin-arm64': 12.4.1
'@pnpm/exe.darwin-x64': 12.4.1
'@pnpm/exe.freebsd-x64': 12.4.1
'@pnpm/exe.linux-arm64': 12.4.1
'@pnpm/exe.linux-arm64-musl': 12.4.1
'@pnpm/exe.linux-ppc64': 12.4.1
'@pnpm/exe.linux-riscv64': 12.4.1
'@pnpm/exe.linux-s390x': 12.4.1
'@pnpm/exe.linux-x64': 12.4.1
'@pnpm/exe.linux-x64-musl': 12.4.1
'@pnpm/exe.win32-arm64': 12.4.1
'@pnpm/exe.win32-x64': 12.4.1
'@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.5.1
'@pnpm/exe.linux-ppc64': 12.5.1
'@pnpm/exe.linux-riscv64': 12.5.1
'@pnpm/exe.linux-s390x': 12.5.1
'@pnpm/exe.linux-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.5.1
'@pnpm/exe.win32-arm64': 12.5.1
'@pnpm/exe.win32-x64': 12.5.1
---
lockfileVersion: '9.0'
+6
View File
@@ -56,6 +56,12 @@
"matchFileNames": [".github/workflows/*.lock.yml"],
"enabled": false
},
{
"description": "Ignore gh-aw dependency updates in workflow files; upgrade agentic workflows with gh aw upgrade instead",
"matchFileNames": [".github/workflows/**"],
"matchPackageNames": ["/gh-aw/"],
"enabled": false
},
{
"description": "Skip TypeScript 7.0 series",
"matchManagers": ["npm"],
+50 -15
View File
@@ -1,5 +1,5 @@
/**
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json.
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, Cargo.lock, and src-tauri/tauri.conf.json.
*
* Usage:
* pnpm release-version <version>
@@ -108,17 +108,20 @@ async function updatePackageVersion(newVersion) {
'[INFO]: Current package.json version is: ',
packageJson.version,
)
packageJson.version = newVersion.startsWith('v')
const versionWithoutV = newVersion.startsWith('v')
? newVersion.slice(1)
: newVersion
await fs.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
'utf8',
)
console.log(
`[INFO]: package.json version updated to: ${packageJson.version}`,
const updatedData = data.replace(
/^(\s*"version"\s*:\s*)"[^"]+"/m,
`$1"${versionWithoutV}"`,
)
if (updatedData === data) {
throw new Error('version field was not found in package.json')
}
await fs.writeFile(packageJsonPath, updatedData, 'utf8')
console.log(`[INFO]: package.json version updated to: ${versionWithoutV}`)
} catch (error) {
console.error('Error updating package.json version:', error)
throw error
@@ -153,6 +156,34 @@ async function updateCargoVersion(newVersion) {
}
}
async function updateCargoLockVersion(newVersion) {
const _dirname = process.cwd()
const cargoLockPath = path.join(_dirname, 'Cargo.lock')
const versionWithoutV = newVersion.startsWith('v')
? newVersion.slice(1)
: newVersion
const packageVersionPattern =
/(\[\[package\]\]\r?\nname = "clash-verge"\r?\nversion = )"[^"]+"/
try {
const data = await fs.readFile(cargoLockPath, 'utf8')
const updatedData = data.replace(
packageVersionPattern,
`$1"${versionWithoutV}"`,
)
if (updatedData === data) {
throw new Error('clash-verge package entry was not found in Cargo.lock')
}
await fs.writeFile(cargoLockPath, updatedData, 'utf8')
console.log(`[INFO]: Cargo.lock version updated to: ${versionWithoutV}`)
} catch (error) {
console.error('Error updating Cargo.lock version:', error)
throw error
}
}
async function updateTauriConfigVersion(newVersion) {
const _dirname = process.cwd()
const tauriConfigPath = path.join(_dirname, 'src-tauri', 'tauri.conf.json')
@@ -168,13 +199,16 @@ async function updateTauriConfigVersion(newVersion) {
tauriConfig.version,
)
tauriConfig.version = versionWithoutV
await fs.writeFile(
tauriConfigPath,
JSON.stringify(tauriConfig, null, 2),
'utf8',
const updatedData = data.replace(
/^(\s*"version"\s*:\s*)"[^"]+"/m,
`$1"${versionWithoutV}"`,
)
if (updatedData === data) {
throw new Error('version field was not found in tauri.conf.json')
}
await fs.writeFile(tauriConfigPath, updatedData, 'utf8')
console.log(
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
)
@@ -239,6 +273,7 @@ async function main(versionArg) {
console.log(`[INFO]: Updating versions to: ${newVersion}`)
await updatePackageVersion(newVersion)
await updateCargoVersion(newVersion)
await updateCargoLockVersion(newVersion)
await updateTauriConfigVersion(newVersion)
console.log('[SUCCESS]: All version updates completed successfully!')
} catch (error) {
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "2.5.4-rc"
version = "2.5.4"
description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -99,7 +99,7 @@ tracing = { workspace = true }
tauri-plugin-devtools = { version = "2.2.1", optional = true }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", branch = "main" }
async-trait = "0.1.92"
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.1", version = "2.7.1", features = [
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.2", version = "2.7.2", features = [
"client",
] }
arc-swap = "1.9.2"
@@ -0,0 +1,38 @@
#!/bin/bash
set -e
chmod +x /usr/bin/clash-verge-service-install
chmod +x /usr/bin/clash-verge-service-uninstall
chmod +x /usr/bin/clash-verge-service
# The enforcement file exists in both Enforcing and Permissive mode.
if [ ! -e /sys/fs/selinux/enforce ]; then
exit 0
fi
if [ ! -x /usr/sbin/semanage ] || [ ! -x /usr/sbin/restorecon ]; then
echo "Warning: persistent SELinux rules were not installed; install policycoreutils and policycoreutils-python-utils, then reinstall this package. The service installer repairs executable labels, but a system relabel can reset them." >&2
exit 0
fi
# Keep these local rules on removal: they may predate this package or serve a standalone development service.
for service in clash-verge-service clash-verge-service-dev; do
for directory in bin cores; do
target="/var/lib/$service/$directory"
if [ -L "/var/lib/$service" ] || [ -L "$target" ]; then
echo "Refusing to relabel a symlinked service directory: $target" >&2
exit 1
fi
spec="$target(/.*)?"
# Older semanage versions reject -a when an exact rule already exists.
if ! add_error="$(/usr/sbin/semanage fcontext -a -t bin_t "$spec" 2>&1)"; then
if ! /usr/sbin/semanage fcontext -m -t bin_t "$spec"; then
printf '%s\n' "$add_error" >&2
exit 1
fi
fi
if [ -d "$target" ]; then
/usr/sbin/restorecon -R "$target"
fi
done
done
+2 -1
View File
@@ -210,7 +210,8 @@ pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
return Ok(ValidationOutcome::invalid_from_message("DNS config file not found"));
}
CoreConfigValidator::validate_config_file_outcome(dns_path_str, None)
// A fragment, not a runnable config; the merged result is validated on apply.
CoreConfigValidator::validate_config_file_outcome(dns_path_str, Some(true))
.await
.stringify_err()
}
+29 -26
View File
@@ -208,33 +208,10 @@ impl CoreManager {
let pid = child.pid();
tracing::Span::current().record("pid", pid);
let readiness = poll_sidecar_readiness(SIDECAR_READINESS_ATTEMPTS, SIDECAR_READINESS_INTERVAL, || async {
tokio::time::timeout(SIDECAR_READINESS_PROBE_TIMEOUT, async {
handle::Handle::mihomo().get_version().await
})
.await
.context("Mihomo readiness probe timed out")??;
Ok(())
})
.await;
if let Err(readiness_error) = readiness {
proxy_control::stop_guard().await;
self.core_stopped();
return match child.kill() {
Ok(()) => Err(readiness_error),
Err(kill_error) => Err(anyhow::anyhow!(
"{readiness_error:#}; failed to terminate unready sidecar PID {pid}: {kill_error:#}"
)),
};
}
#[cfg(target_os = "windows")]
self.set_job_handle(Some(job));
self.set_running_child_sidecar(child);
// The sidecar has to be drained from the moment it starts. tauri-plugin-shell buffers a
// single event, so an unread channel stalls its reader threads and the core blocks on a
// full stdout pipe before it ever creates the API socket.
let core_readiness_generation = self.mark_core_ready();
self.core_started(RunningMode::Sidecar);
self.restore_selected_nodes().await;
AsyncHandler::spawn(move || async move {
while let Some(event) = rx.recv().await {
match event {
@@ -264,6 +241,32 @@ impl CoreManager {
}
});
let readiness = poll_sidecar_readiness(SIDECAR_READINESS_ATTEMPTS, SIDECAR_READINESS_INTERVAL, || async {
tokio::time::timeout(SIDECAR_READINESS_PROBE_TIMEOUT, async {
handle::Handle::mihomo().get_version().await
})
.await
.context("Mihomo readiness probe timed out")??;
Ok(())
})
.await;
if let Err(readiness_error) = readiness {
proxy_control::stop_guard().await;
self.core_stopped();
return match child.kill() {
Ok(()) => Err(readiness_error),
Err(kill_error) => Err(anyhow::anyhow!(
"{readiness_error:#}; failed to terminate unready sidecar PID {pid}: {kill_error:#}"
)),
};
}
#[cfg(target_os = "windows")]
self.set_job_handle(Some(job));
self.set_running_child_sidecar(child);
self.core_started(RunningMode::Sidecar);
self.restore_selected_nodes().await;
Ok(())
}
+72 -50
View File
@@ -284,8 +284,7 @@ fn process_seq_items(
config
}
/// App 权威的顶层控制面键:核心连接、监听端口、UI/托盘开关。
/// 平台键随 cfg 门控;`dns.ipv6` 单独处理。
/// App 权威的顶层控制面键;`dns.ipv6` 单独处理。
const CONTROL_PLANE_KEYS: &[&str] = &[
"external-controller",
#[cfg(unix)]
@@ -308,25 +307,20 @@ const CONTROL_PLANE_KEYS: &[&str] = &[
"unified-delay",
];
/// The fields the app owns, held across the stages that apply the user's manual overrides.
///
/// Capturing and restoring are one value rather than two calls because the order is a
/// correctness requirement, not a style: capture must happen after the app has finished
/// deriving these fields and before any override runs, and restore must happen after every
/// override. As four separate calls that contract lived only in comments.
/// App-owned fields: captured after the app derives them, enforced after every override.
struct AuthoritativeFields {
control_plane: Mapping,
tun: Mapping,
/// Only tracked when the DNS page owns it; otherwise overrides may set `dns.ipv6` freely.
/// Only when the DNS page switched it on.
dns_ipv6: Option<Value>,
}
impl AuthoritativeFields {
fn capture(config: &Mapping, gui_tun_keys: &[Value], enable_dns_settings: bool) -> Self {
fn capture(config: &Mapping, gui_tun_keys: &[Value], owns_dns_ipv6: bool) -> Self {
Self {
control_plane: snapshot_control_plane(config),
tun: snapshot_tun(config, gui_tun_keys),
dns_ipv6: enable_dns_settings.then(|| snapshot_dns_ipv6(config)).flatten(),
dns_ipv6: owns_dns_ipv6.then(|| snapshot_dns_ipv6(config)).flatten(),
}
}
@@ -401,7 +395,7 @@ fn enforce_tun(mut config: Mapping, snapshot: Mapping) -> Mapping {
config
}
/// DNS 页权威的嵌套开关;只在 `enable_dns_settings` 时快照。
/// 只在 DNS 页开启 IPv6 时快照。
fn snapshot_dns_ipv6(config: &Mapping) -> Option<Value> {
config.get("dns")?.get("ipv6").cloned()
}
@@ -481,11 +475,7 @@ async fn process_profile_items(
(config, exists_keys, result_map)
}
/// Merge the Application Merge Config over the profile's own configuration.
///
/// Every switch it consults is a parameter: it used to read `enable_external_controller`
/// out of the global config from inside its loop, which made a stage that looks pure depend
/// on process state and left it untestable.
/// Merges the app's clash config over the profile; switches are parameters to keep it pure.
fn merge_default_config(
mut config: Mapping,
clash_config: Mapping,
@@ -683,9 +673,7 @@ fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
config
}
/// 当 DNS 处于 fake-ip 模式且启用 IPv6 时,补充缺失的 `fake-ip-range6`,
/// 否则 AAAA 查询无法获得 fake-ip,导致 IPv6 解析失败(见 issue #7373)。
/// 兼容旧版本生成的、缺少该字段的 dns_config.yaml。
/// fake-ip + IPv6 缺少 `fake-ip-range6` 时补默认值,否则 AAAA 无法解析(#7373)。
fn ensure_fake_ip_range6(dns: &mut Mapping) {
use serde_yaml_ng::Value;
@@ -708,38 +696,77 @@ fn ensure_fake_ip_range6(dns: &mut Mapping) {
}
}
async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> Mapping {
/// On, non-blank text, or non-empty list/map; anything else keeps the profile's value.
fn is_set(value: &Value) -> bool {
match value {
Value::Null => false,
Value::Bool(on) => *on,
Value::String(text) => !text.trim().is_empty(),
Value::Sequence(items) => !items.is_empty(),
Value::Mapping(map) => !map.is_empty(),
Value::Number(_) | Value::Tagged(_) => true,
}
}
/// Overlay set fields onto base; maps merge per key, lists are leaves.
fn merge_set_fields(mut base: Mapping, overlay: Mapping) -> Mapping {
for (key, value) in overlay {
let value = match (base.get_mut(&key), value) {
(Some(Value::Mapping(existing)), Value::Mapping(patch)) => {
*existing = merge_set_fields(std::mem::take(existing), patch);
continue;
}
(_, Value::Mapping(patch)) => Value::Mapping(merge_set_fields(Mapping::new(), patch)),
(_, value) => value,
};
if is_set(&value) {
base.insert(key, value);
}
}
base
}
fn take_mapping(config: &mut Mapping, key: &str) -> Mapping {
config
.get_mut(key)
.and_then(Value::as_mapping_mut)
.map(std::mem::take)
.unwrap_or_default()
}
/// Returns the merged config and whether the page set `dns.ipv6`.
async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> (Mapping, bool) {
let mut owns_ipv6 = false;
if enable_dns_settings && let Ok(app_dir) = dirs::app_home_dir() {
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
if dns_path.exists()
&& let Ok(dns_yaml) = fs::read_to_string(&dns_path).await
&& let Ok(dns_config) = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
&& let Ok(mut dns_config) = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml)
{
if let Some(hosts_value) = dns_config.get("hosts")
&& hosts_value.is_mapping()
{
config.insert("hosts".into(), hosts_value.clone());
if let Some(Value::Mapping(hosts)) = dns_config.remove("hosts") {
let hosts = merge_set_fields(take_mapping(&mut config, "hosts"), hosts);
config.insert("hosts".into(), hosts.into());
logging!(debug, Type::Core, "apply hosts configuration");
}
if let Some(dns_value) = dns_config.get("dns") {
if let Some(dns_mapping) = dns_value.as_mapping() {
let mut dns_mapping = dns_mapping.clone();
ensure_fake_ip_range6(&mut dns_mapping);
config.insert("dns".into(), dns_mapping.into());
logging!(debug, Type::Core, "apply dns_config.yaml (dns section)");
}
} else {
let mut dns_config = dns_config;
ensure_fake_ip_range6(&mut dns_config);
config.insert("dns".into(), dns_config.into());
// Legacy layout: no `dns` root.
let dns_override = match dns_config.remove("dns") {
Some(Value::Mapping(dns)) => Some(dns),
Some(_) => None,
None => Some(dns_config),
};
if let Some(dns_override) = dns_override {
owns_ipv6 = dns_override.get("ipv6").is_some_and(is_set);
let mut dns = merge_set_fields(take_mapping(&mut config, "dns"), dns_override);
ensure_fake_ip_range6(&mut dns);
config.insert("dns".into(), dns.into());
logging!(debug, Type::Core, "apply dns_config.yaml");
}
}
}
config
(config, owns_ipv6)
}
/// Returns the enhanced profile, its original keys, script logs, and DNS override decision.
@@ -800,9 +827,9 @@ pub async fn enhance(
let config = apply_builtin_scripts(config, clash_core, enable_builtin);
let config = use_tun(config, enable_tun);
let config = apply_dns_settings(config, enable_dns_settings).await;
let (config, owns_dns_ipv6) = apply_dns_settings(config, enable_dns_settings).await;
let authoritative = AuthoritativeFields::capture(&config, &gui_tun_keys, enable_dns_settings);
let authoritative = AuthoritativeFields::capture(&config, &gui_tun_keys, owns_dns_ipv6);
let (config, exists_keys, result_map) = process_global_items(
config,
@@ -864,8 +891,7 @@ mod fake_ip_tests {
#[test]
fn a_hand_edited_empty_range_counts_as_missing() {
// The reason this is not just a `contains_key` check: YAML edited by hand often
// leaves the key present with nothing after the colon.
// Hand-edited YAML may leave the key present but empty.
for blank in ["", " "] {
let mut config = dns(&[
("ipv6", Value::from(true)),
@@ -1004,8 +1030,7 @@ mod use_tun_tests {
#[tokio::test]
async fn switching_tun_off_leaves_dns_untouched() {
// Turning TUN off must not rewrite DNS the user or profile chose; only the system
// resolver is restored, which is not part of the configuration.
// TUN off must not rewrite the DNS the profile chose.
let config = mapping(&[("dns", Value::Mapping(mapping(&[("enable", Value::from(false))])))]);
let result = use_tun(config, false);
@@ -1038,8 +1063,7 @@ mod use_sort_tests {
#[test]
fn the_bulky_list_fields_are_written_last() {
// These are the fields that make a config file unreadable from the top, so they are
// pushed below the settings a human actually scans for.
// Bulky list fields go last so the top of the file stays readable.
let mut config = Mapping::new();
config.insert(Value::from("rules"), Value::from("rules"));
config.insert(Value::from("mode"), Value::from("rule"));
@@ -1129,8 +1153,7 @@ mod merge_default_config_tests {
#[test]
fn a_disabled_listener_is_removed_rather_than_merged() {
// Removed, not just skipped: the profile may have asked for the port itself, and a
// listener the user switched off must not be opened by the profile's own value.
// Removed, not skipped: the profile's own value must not reopen a disabled listener.
let config = mapping(&[("socks-port", Value::from(7891)), ("port", Value::from(7890))]);
let clash_config = mapping(&[("socks-port", Value::from(1080)), ("port", Value::from(8080))]);
@@ -1152,8 +1175,7 @@ mod merge_default_config_tests {
#[test]
fn a_disabled_external_controller_is_blanked_rather_than_dropped() {
// Blanked, not removed: an absent key lets mihomo fall back to its own default and
// listen anyway, which is the opposite of what switching it off means.
// Blanked, not removed: an absent key makes mihomo listen on its default.
let clash_config = mapping(&[("external-controller", Value::from("127.0.0.1:9090"))]);
let merged = merge(Mapping::new(), clash_config, false);
+4 -17
View File
@@ -228,27 +228,21 @@ async fn migrate_legacy_macos_service_logs(log_dir: &Path) -> Result<()> {
Ok(())
}
/// Raise existing sub-floor update intervals, once per install.
///
/// One-off repair, not a standing rule — the UI warns about shorter intervals but still saves
/// them, so the marker is written even when nothing needed raising.
/// Once per install; the marker is written even when no interval needed raising.
pub async fn migrate_short_update_intervals() -> Result<()> {
let marker = dirs::update_interval_migrated_path()?;
if fs::try_exists(&marker).await? {
return Ok(());
}
// Same order as every other writer holding it (lock, then draft permit): save_file runs
// inside the closure, ahead of the optimistic check, so a racing restore can't be undone.
// Same lock order as other writers; saving inside the closure keeps a racing restore intact.
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
let min = constants::profile::MIN_UPDATE_INTERVAL;
let raised = Config::profiles()
.await
.with_data_modify(|mut profiles: IProfiles| async move {
// `IProfiles::new` normalises items to Some when it read the file, and returns a
// bare default() when it failed. A failed load also raises zero — without this the
// marker would burn the one shot on profiles nobody managed to load.
// A failed profiles load (bare default) must not burn the one-shot marker.
if profiles.items.is_none() {
anyhow::bail!("profiles.yaml was not loaded; refusing to record the migration as done");
}
@@ -370,14 +364,7 @@ pub(super) async fn init_dns_config() -> Result<()> {
"nameserver-policy".into(),
Value::Mapping(serde_yaml_ng::Mapping::new()),
),
(
"proxy-server-nameserver".into(),
Value::Sequence(vec![
Value::String("https://doh.pub/dns-query".into()),
Value::String("https://dns.alidns.com/dns-query".into()),
Value::String("tls://223.5.5.5".into()),
]),
),
("proxy-server-nameserver".into(), Value::Sequence(vec![])),
("direct-nameserver".into(), Value::Sequence(vec![])),
("direct-nameserver-follow-policy".into(), Value::Bool(false)),
(
+1 -1
View File
@@ -1,5 +1,5 @@
{
"version": "2.5.4-rc",
"version": "2.5.4",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"active": true,
+2 -1
View File
@@ -15,11 +15,12 @@
},
"rpm": {
"depends": ["openssl", "libayatana-appindicator-gtk3"],
"recommends": ["policycoreutils", "policycoreutils-python-utils"],
"desktopTemplate": "./packages/linux/clash-verge.desktop",
"provides": ["clash-verge"],
"conflicts": ["clash-verge"],
"obsoletes": ["clash-verge"],
"postInstallScript": "./packages/linux/post-install.sh",
"postInstallScript": "./packages/linux/post-install-rpm.sh",
"preRemoveScript": "./packages/linux/pre-remove.sh"
}
},
+320 -214
View File
@@ -1,6 +1,7 @@
/* eslint-disable @eslint-react/set-state-in-effect */
import {
AccessTimeRounded,
ArrowDropDown,
ChevronRight,
NetworkCheckRounded,
WifiOff as SignalError,
@@ -15,25 +16,22 @@ import {
import {
Box,
Button,
ButtonBase,
Chip,
ClickAwayListener,
FormControl,
IconButton,
InputLabel,
type MenuProps,
MenuItem,
MenuList,
Paper,
Popper,
Select,
type SelectChangeEvent,
type PopperProps,
Tooltip,
Typography,
alpha,
useTheme,
} from '@mui/material'
import { useLockFn } from 'ahooks'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
@@ -68,6 +66,40 @@ const STORAGE_KEY_SORT_TYPE = 'clash-verge-proxy-sort-type'
const AUTO_CHECK_DEFAULT_INTERVAL_MINUTES = 5
const AUTO_CHECK_INITIAL_DELAY_MS = 100
const PROXY_MENU_MAX_HEIGHT = 500
const PROXY_MENU_VIEWPORT_MARGIN = 8
const proxyMenuModifiers: PopperProps['modifiers'] = [
{
name: 'menuSize',
enabled: true,
phase: 'beforeRead',
fn: ({ state }) => {
const popper = state.elements.popper
const anchor = state.elements.reference.getBoundingClientRect()
const viewportHeight = popper.ownerDocument.documentElement.clientHeight
// Use the larger side before flip runs so sizing cannot alternate with placement.
const availableHeight =
Math.max(anchor.top, viewportHeight - anchor.bottom) -
PROXY_MENU_VIEWPORT_MARGIN
popper.style.width = `${anchor.width}px`
popper.style.setProperty(
'--proxy-menu-max-height',
`${Math.max(0, Math.min(PROXY_MENU_MAX_HEIGHT, availableHeight))}px`,
)
state.rects.popper.width = popper.offsetWidth
state.rects.popper.height = popper.offsetHeight
},
},
{
name: 'showMenu',
enabled: true,
phase: 'beforeWrite',
requires: ['computeStyles'],
fn: ({ state }) => {
state.styles.popper.visibility = 'visible'
},
},
]
interface ProxyOption {
memberIndex: number
@@ -163,34 +195,6 @@ const optionValue = (memberIndex: number, member: ResolvedProxyMember) =>
member.kind === 'node' ? member.node.recordId : member.ref.name
}`
const hasPointerCoordinates = (
value: unknown,
): value is { clientX: number; clientY: number } =>
typeof value === 'object' &&
value !== null &&
'clientX' in value &&
typeof value.clientX === 'number' &&
'clientY' in value &&
typeof value.clientY === 'number'
const eventHitsElement = (event: unknown, element: HTMLElement | null) => {
if (!element) return false
const nativeEvent =
typeof event === 'object' && event !== null && 'nativeEvent' in event
? event.nativeEvent
: event
if (!hasPointerCoordinates(nativeEvent)) return false
const rect = element.getBoundingClientRect()
return (
nativeEvent.clientX >= rect.left &&
nativeEvent.clientX <= rect.right &&
nativeEvent.clientY >= rect.top &&
nativeEvent.clientY <= rect.bottom
)
}
const sortProxyOptions = (
options: ProxyOption[],
sortType: ProxySortType,
@@ -216,58 +220,71 @@ const sortProxyOptions = (
.map(({ option }) => option)
}
interface PersistentProxySelectProps {
interface PersistentSelectProps {
label: string
groupName: string
fixed?: string
value: string
selectedName: string
selectedDelay: number
options: ProxyOption[]
displayValue: React.ReactNode
open: boolean
disabled: boolean
keepOpenRef: React.RefObject<HTMLElement | null>
onOpen: () => void
onClose: () => void
onChange: (value: string) => void
onOpenChange: (open: boolean) => void
renderOptions: () => React.ReactNode
}
const PersistentProxySelect = ({
const PersistentSelect = ({
label,
groupName,
fixed,
value,
selectedName,
selectedDelay,
options,
displayValue,
open,
disabled,
keepOpenRef,
onOpen,
onClose,
onChange,
}: PersistentProxySelectProps) => {
const anchorRef = useRef<HTMLDivElement>(null)
const listboxId = 'current-proxy-node-listbox'
const labelId = 'proxy-select-label'
const fixedProxyInUsed = selectedName === fixed
onOpenChange,
renderOptions,
}: PersistentSelectProps) => {
const id = useId()
const labelId = `${id}-label`
const valueId = `${id}-value`
const listboxId = `${id}-listbox`
const anchorRef = useRef<HTMLButtonElement>(null)
const listRef = useRef<HTMLUListElement>(null)
const popperOptions = useMemo<PopperProps['popperOptions']>(
() => ({
onFirstUpdate: () => {
const list = listRef.current
const paper = list?.parentElement
const item =
list?.querySelector<HTMLElement>(
'[aria-selected="true"]:not([aria-disabled="true"])',
) ??
list?.querySelector<HTMLElement>(
'[role="option"]:not([aria-disabled="true"])',
)
if (!item || !paper) return
// Focusing before positioning can scroll the page instead of just the menu.
item.focus({ preventScroll: true })
paper.scrollTop +=
item.getBoundingClientRect().top -
paper.getBoundingClientRect().top -
(paper.clientHeight - item.offsetHeight) / 2
},
}),
[],
)
useEffect(() => {
if (!open) return
const closeFromKeyboard = (event: KeyboardEvent) => {
if (event.key === 'Tab') {
onClose()
onOpenChange(false)
} else if (event.key === 'Escape') {
onClose()
anchorRef.current
?.querySelector<HTMLElement>('[role="combobox"]')
?.focus()
onOpenChange(false)
anchorRef.current?.focus({ preventScroll: true })
}
}
document.addEventListener('keydown', closeFromKeyboard)
return () => document.removeEventListener('keydown', closeFromKeyboard)
}, [onClose, open])
}, [onOpenChange, open])
return (
<ClickAwayListener
@@ -277,134 +294,119 @@ const PersistentProxySelect = ({
if (target instanceof Node && keepOpenRef.current?.contains(target)) {
return
}
onClose()
onOpenChange(false)
}}
>
<Box>
<FormControl ref={anchorRef} fullWidth variant="outlined" size="small">
<InputLabel id={labelId}>{label}</InputLabel>
<Select
labelId={labelId}
value={value}
open={false}
onOpen={onOpen}
label={label}
<Box
sx={{
position: 'relative',
borderRadius: 1,
'& fieldset': {
borderColor: (theme) =>
alpha(theme.palette.text.primary, disabled ? 0.12 : 0.23),
},
...(!disabled && {
'&:hover fieldset': { borderColor: 'text.primary' },
'&:focus-within fieldset': {
borderColor: 'primary.main',
borderWidth: 2,
},
'&:focus-within legend': { color: 'primary.main' },
}),
}}
>
<ButtonBase
ref={anchorRef}
type="button"
role="combobox"
aria-labelledby={`${labelId} ${valueId}`}
aria-haspopup="listbox"
aria-controls={open ? listboxId : undefined}
aria-expanded={open}
disabled={disabled}
renderValue={() => (
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Typography noWrap>{selectedName}</Typography>
<Chip
size="small"
label={delayManager.formatDelay(selectedDelay)}
color={convertDelayColor(selectedDelay)}
/>
{fixedProxyInUsed && (
<span
style={{
position: 'absolute',
fontSize: '12px',
top: '-3px',
right: '25px',
}}
>
📌
</span>
)}
</Box>
)}
SelectDisplayProps={{
'aria-controls': open ? listboxId : undefined,
'aria-expanded': open ? 'true' : 'false',
onClick={() => onOpenChange(!open)}
onKeyDown={(event) => {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault()
onOpenChange(true)
}
}}
sx={{
width: '100%',
height: 40,
px: 1.75,
borderRadius: 'inherit',
justifyContent: 'space-between',
textAlign: 'left',
color: disabled ? 'text.disabled' : 'text.primary',
}}
>
<MenuItem value={value}>{selectedName}</MenuItem>
</Select>
</FormControl>
<Box id={valueId} sx={{ minWidth: 0, flex: 1 }}>
{displayValue}
</Box>
<ArrowDropDown
sx={{
ml: 1,
color: disabled ? 'action.disabled' : 'action.active',
transform: open ? 'rotate(180deg)' : undefined,
}}
/>
</ButtonBase>
<Box
component="fieldset"
sx={{
position: 'absolute',
inset: '-5px 0 0',
m: 0,
px: 1,
minWidth: 0,
border: '1px solid',
borderRadius: 'inherit',
pointerEvents: 'none',
}}
>
<Box
component="legend"
id={labelId}
sx={{
px: 0.5,
typography: 'caption',
lineHeight: '11px',
color: disabled ? 'text.disabled' : 'text.secondary',
}}
>
{label}
</Box>
</Box>
</Box>
<Popper
open={open}
anchorEl={anchorRef.current}
placement="bottom-start"
modifiers={proxyMenuModifiers}
popperOptions={popperOptions}
sx={{
width: anchorRef.current?.clientWidth,
visibility: 'hidden',
zIndex: (theme) => theme.zIndex.modal,
}}
>
<Paper
elevation={8}
sx={{ maxHeight: PROXY_MENU_MAX_HEIGHT, overflow: 'auto' }}
sx={{
maxHeight: `var(--proxy-menu-max-height, ${PROXY_MENU_MAX_HEIGHT}px)`,
overflow: 'auto',
}}
>
<MenuList
ref={listRef}
id={listboxId}
role="listbox"
aria-labelledby={labelId}
autoFocusItem
variant="selectedMenu"
>
{options.map((option) => {
const selected = option.value === value
const isFixed = option.name === fixed
const delay = option.disabled
? -1
: delayManager.getDelayFix(option.member, groupName)
return (
<MenuItem
key={option.value}
role="option"
aria-selected={selected}
selected={selected}
disabled={option.disabled}
onClick={() => {
if (!option.disabled) onChange(option.value)
}}
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
pr: 1,
}}
>
{isFixed && (
<span
style={{
position: 'absolute',
fontSize: '12px',
top: '-5px',
right: '5px',
...(!fixedProxyInUsed && {
filter: 'grayscale(1)',
}),
}}
>
📌
</span>
)}
<Typography noWrap sx={{ flex: 1, mr: 1 }}>
{option.name}
</Typography>
{!option.disabled && (
<Chip
size="small"
label={delayManager.formatDelay(delay)}
color={convertDelayColor(delay)}
sx={{
minWidth: '60px',
height: '22px',
flexShrink: 0,
}}
/>
)}
</MenuItem>
)
})}
{open && renderOptions()}
</MenuList>
</Paper>
</Popper>
@@ -413,6 +415,124 @@ const PersistentProxySelect = ({
)
}
interface PersistentProxySelectProps
extends Omit<PersistentSelectProps, 'displayValue' | 'renderOptions'> {
value: string
selectedName: string
groupName: string
fixed?: string
selectedDelay: number
options: ProxyOption[]
onChange: (value: string) => void
}
const PersistentProxySelect = ({
groupName,
fixed,
selectedName,
selectedDelay,
options,
value,
onChange,
...props
}: PersistentProxySelectProps) => {
const fixedProxyInUsed = selectedName === fixed
return (
<PersistentSelect
{...props}
displayValue={
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Typography noWrap>{selectedName}</Typography>
<Chip
size="small"
label={delayManager.formatDelay(selectedDelay)}
color={convertDelayColor(selectedDelay)}
/>
{fixedProxyInUsed && (
<span
style={{
position: 'absolute',
fontSize: '12px',
top: '-3px',
right: '25px',
}}
>
📌
</span>
)}
</Box>
}
renderOptions={() =>
options.map((option) => {
const selected = option.value === value
const isFixed = option.name === fixed
const delay = option.disabled
? -1
: delayManager.getDelayFix(option.member, groupName)
return (
<MenuItem
key={option.value}
role="option"
aria-selected={selected}
selected={selected}
disabled={option.disabled}
onClick={() => {
if (!option.disabled) onChange(option.value)
}}
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
pr: 1,
}}
>
{isFixed && (
<span
style={{
position: 'absolute',
fontSize: '12px',
top: '-5px',
right: '5px',
...(!fixedProxyInUsed && {
filter: 'grayscale(1)',
}),
}}
>
📌
</span>
)}
<Typography noWrap sx={{ flex: 1, mr: 1 }}>
{option.name}
</Typography>
{!option.disabled && (
<Chip
size="small"
label={delayManager.formatDelay(delay)}
color={convertDelayColor(delay)}
sx={{
minWidth: '60px',
height: '22px',
flexShrink: 0,
}}
/>
)}
</MenuItem>
)
})
}
/>
)
}
export const CurrentProxyCard = () => {
const { t } = useTranslation()
const navigate = useNavigate()
@@ -497,6 +617,11 @@ export const CurrentProxyCard = () => {
const [selectedGroupName, setSelectedGroupName] = useState('')
const [openSelect, setOpenSelect] = useState<OpenSelect>(null)
useEffect(() => {
setOpenSelect(null)
}, [mode])
const delayButtonRef = useRef<HTMLButtonElement>(null)
const delays = useGroupDelays(selectedGroupName || null)
@@ -621,9 +746,8 @@ export const CurrentProxyCard = () => {
latestProxyMemberRef.current = currentOption?.member ?? null
const handleGroupChange = useCallback(
(event: SelectChangeEvent<string>) => {
(newGroupName: string) => {
if (isGlobalMode || isDirectMode) return
const newGroupName = event.target.value
setSelectedGroupName(newGroupName)
writeProfileScopedItem(STORAGE_KEY_GROUP, newGroupName)
},
@@ -785,24 +909,9 @@ export const CurrentProxyCard = () => {
refreshProxy()
})
const handleGroupMenuClose = useCallback<NonNullable<MenuProps['onClose']>>(
(event, reason) => {
if (
reason === 'backdropClick' &&
eventHitsElement(event, delayButtonRef.current)
) {
void handleCheckDelay()
return
}
setOpenSelect(null)
},
[handleCheckDelay],
)
const proxyOptions = useMemo(
() =>
isDirectMode
isDirectMode || openSelect !== 'proxy'
? []
: sortProxyOptions(
unsortedProxyOptions,
@@ -813,6 +922,7 @@ export const CurrentProxyCard = () => {
[
delays,
isDirectMode,
openSelect,
unsortedProxyOptions,
sortType,
defaultLatencyTimeout,
@@ -975,32 +1085,29 @@ export const CurrentProxyCard = () => {
/>
)}
</Box>
<FormControl
fullWidth
variant="outlined"
size="small"
sx={{ mb: 1.5 }}
>
<InputLabel id="proxy-group-select-label">
{t('home.components.currentProxy.labels.group')}
</InputLabel>
<Select
labelId="proxy-group-select-label"
value={selectedGroupName}
onChange={handleGroupChange}
open={openSelect === 'group'}
onOpen={() => setOpenSelect('group')}
<Box sx={{ mb: 1.5 }}>
<PersistentSelect
label={t('home.components.currentProxy.labels.group')}
displayValue={<Typography noWrap>{selectedGroupName}</Typography>}
open={openSelect === 'group'}
disabled={isGlobalMode || isDirectMode}
MenuProps={{ onClose: handleGroupMenuClose }}
>
{selectableGroups.map((group) => (
<MenuItem key={group.name} value={group.name}>
{group.name}
</MenuItem>
))}
</Select>
</FormControl>
keepOpenRef={delayButtonRef}
onOpenChange={(open) => setOpenSelect(open ? 'group' : null)}
renderOptions={() =>
selectableGroups.map((group) => (
<MenuItem
key={group.name}
role="option"
aria-selected={group.name === selectedGroupName}
selected={group.name === selectedGroupName}
onClick={() => handleGroupChange(group.name)}
>
<Typography noWrap>{group.name}</Typography>
</MenuItem>
))
}
/>
</Box>
<PersistentProxySelect
label={t('home.components.currentProxy.labels.proxy')}
@@ -1013,12 +1120,11 @@ export const CurrentProxyCard = () => {
}
selectedName={selectedProxyName}
selectedDelay={currentDelay}
options={isDirectMode ? [] : proxyOptions}
options={proxyOptions}
open={openSelect === 'proxy'}
disabled={isDirectMode}
keepOpenRef={delayButtonRef}
onOpen={() => setOpenSelect('proxy')}
onClose={() => setOpenSelect(null)}
onOpenChange={(open) => setOpenSelect(open ? 'proxy' : null)}
onChange={handleProxyChange}
/>
</Box>
+2 -1
View File
@@ -27,6 +27,7 @@ import { useNavigate } from 'react-router'
import { useAppRefreshers } from '@/providers/app-data-context'
import { updateProfile } from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
import { isValidUrl } from '@/utils/network'
import { openExternalUrl } from '@/utils/open-external-url'
import parseTraffic from '@/utils/parse-traffic'
@@ -41,7 +42,7 @@ const round = keyframes`
// 辅助函数解析URL和过期时间
const parseUrl = (url?: string) => {
if (!url) return '-'
if (url.startsWith('http')) return new URL(url).host
if (isValidUrl(url)) return new URL(url).host
return 'local'
}
+2 -3
View File
@@ -43,6 +43,7 @@ import { showNotice } from '@/services/notice-service'
import { useLoadingCache, useSetLoadingCache } from '@/services/states'
import type { TranslationKey } from '@/types/generated/i18n-keys'
import { debugLog } from '@/utils/debug'
import { isValidUrl } from '@/utils/network'
import { openExternalUrl } from '@/utils/open-external-url'
import parseTraffic from '@/utils/parse-traffic'
@@ -965,9 +966,7 @@ export const ProfileItem = memo(ProfileItemBase)
function parseUrl(url?: string) {
if (!url) return ''
const regex = /https?:\/\/(.+?)\//
const result = url.match(regex)
return result ? result[1] : 'local file'
return isValidUrl(url) ? new URL(url).host : 'local file'
}
function parseExpire(expire?: number) {
@@ -17,6 +17,8 @@ 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 { BaseSearchBox, type SearchState } from '../base'
@@ -174,6 +176,10 @@ export const ProxyGroupTools = memo(function ProxyGroupTools(props: Props) {
if (testUrl?.trim() && textState !== 'filter') {
onHeadState({ textState: 'url' })
}
if (testUrl?.trim() && !isValidUrl(testUrl)) {
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
return
}
onCheckDelay()
}}
>
+6
View File
@@ -18,7 +18,9 @@ import { useTranslation } from 'react-i18next'
import { BaseSearchBox } from '@/components/base'
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 type { ProxySortType } from './use-filter-sort'
import type { HeadState } from './use-head-state'
@@ -95,6 +97,10 @@ export const ProxyHead = ({
debugLog(`[ProxyHead] 使用自定义测试URL: ${testUrl}`)
onHeadState({ textState: 'url' })
}
if (testUrl?.trim() && !isValidUrl(testUrl)) {
showNotice.warning('proxies.feedback.warnings.invalidTestUrl')
return
}
onCheckDelay()
}}
>
+2 -7
View File
@@ -171,11 +171,7 @@ const DEFAULT_DNS_CONFIG = {
],
fallback: [],
'nameserver-policy': {},
'proxy-server-nameserver': [
'https://doh.pub/dns-query',
'https://dns.alidns.com/dns-query',
'tls://223.5.5.5',
],
'proxy-server-nameserver': [],
'direct-nameserver': [],
'direct-nameserver-follow-policy': false,
'fallback-filter': {
@@ -340,8 +336,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
listen: values.listen,
'enhanced-mode': values.enhancedMode,
'fake-ip-range': values.fakeIpRange,
'fake-ip-range6':
values.fakeIpRange6 || DEFAULT_DNS_CONFIG['fake-ip-range6'],
'fake-ip-range6': values.fakeIpRange6,
'fake-ip-filter-mode': values.fakeIpFilterMode,
'prefer-h3': values.preferH3,
'respect-rules': values.respectRules,
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} 업데이트 성공",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} успешно обновлён",
+4 -4
View File
@@ -17,18 +17,18 @@
"type": "Tip"
},
"order": {
"default": "Default",
"default": "Varsayılan",
"uploadSpeed": "Yükleme Hızı",
"downloadSpeed": "İndirme Hızı"
},
"actions": {
"active": "Active",
"closed": "Closed",
"active": "Etkin",
"closed": "Kapatılan",
"closeConnection": "Bağlantıyı Kapat"
},
"columnManager": {
"title": "Sütunlar",
"dragHandle": "Drag handle"
"dragHandle": "Sürükleme tutamacı"
}
}
}
+5 -5
View File
@@ -45,7 +45,7 @@
"fields": {
"coreVersion": "Çekirdek Sürümü",
"systemProxyAddress": "Sistem Vekil Adresi",
"mixedPort": "Mixed Port",
"mixedPort": "Karma Port",
"uptime": "Çalışma Süresi",
"rulesCount": "Kural Sayısı"
}
@@ -127,7 +127,7 @@
"download": "İndirme"
},
"patterns": {
"minutes": "{{time}} Minutes"
"minutes": "{{time}} Dakika"
},
"unknownTime": "Bilinmeyen zaman",
"chartStyles": {
@@ -146,9 +146,9 @@
"direct": "Doğrudan Mod"
},
"descriptions": {
"rule": "Automatically choose proxies according to the rule set.",
"global": "Forward all network requests through the selected proxy.",
"direct": "Bypass the proxy and connect to the internet directly."
"rule": "Vekilleri kural kümesine göre otomatik olarak seçer.",
"global": "Tüm ağ isteklerini seçili vekil üzerinden iletir.",
"direct": "Vekili atlar ve internete doğrudan bağlanır."
}
}
}
+14 -14
View File
@@ -12,23 +12,23 @@
"settings": "Ayarlar"
},
"menu": {
"reorderMode": "Menu reorder mode",
"restoreDefaultOrder": "Restore default order",
"unlock": "Unlock menu order",
"lock": "Lock menu order",
"collapseNavBar": "Collapse navigation bar",
"expandNavBar": "Expand navigation bar"
"reorderMode": "Menü sıralama modu",
"restoreDefaultOrder": "Varsayılan sırayı geri yükle",
"unlock": "Menü sırasının kilidini aç",
"lock": "Menü sırasını kilitle",
"collapseNavBar": "Gezinme çubuğunu daralt",
"expandNavBar": "Gezinme çubuğunu genişlet"
}
},
"serviceMigration": {
"title": "Service update required",
"message": "An older Clash Verge service installation was detected. Reinstall it to use the current secure IPC protocol, or continue with Sidecar for this session.",
"checkingMessage": "Checking the system service status and current running mode. Please wait…",
"unavailableMessage": "The required Clash Verge service is not available. Install or repair it, or continue with Sidecar for this session.",
"reinstall": "Reinstall service",
"repair": "Repair service",
"continueSidecar": "Continue with Sidecar",
"success": "The Clash Verge service was updated successfully",
"title": "Hizmet güncellemesi gerekli",
"message": "Eski bir Clash Verge hizmet kurulumu algılandı. Güncel güvenli IPC protokolünü kullanmak için hizmeti yeniden kurun ya da bu oturumda Sidecar ile devam edin.",
"checkingMessage": "Sistem hizmetinin durumu ve geçerli çalışma modu denetleniyor. Lütfen bekleyin…",
"unavailableMessage": "Gerekli Clash Verge hizmeti kullanılamıyor. Hizmeti kurun veya onarın ya da bu oturumda Sidecar ile devam edin.",
"reinstall": "Hizmeti yeniden kur",
"repair": "Hizmeti onar",
"continueSidecar": "Sidecar ile devam et",
"success": "Clash Verge hizmeti başarıyla güncellendi",
"errors": {
"actionFailed": "Hizmet işlemi başarısız oldu",
"stateRefreshFailed": "Hizmet durumu yenilenemedi",
+2 -2
View File
@@ -3,7 +3,7 @@
"title": "Günlükler"
},
"actions": {
"showDescending": "Newest first",
"showAscending": "Oldest first"
"showDescending": "Önce en yeni",
"showAscending": "Önce en eski"
}
}
+16 -16
View File
@@ -27,7 +27,7 @@
},
"feedback": {
"errors": {
"invalidUrl": "Invalid profile URL. Please enter a URL starting with http:// or https://",
"invalidUrl": "Geçersiz profil URL'si. Lütfen http:// veya https:// ile başlayan bir URL girin",
"onlyYaml": "Yalnızca YAML Dosyaları Desteklenir",
"createFailed": "Profil oluşturulamadı",
"deleteFailed": "Profil silinemedi",
@@ -42,16 +42,16 @@
"notifications": {
"importRetry": "İçe aktarma başarısız oldu, Clash vekil ile yeniden deneniyor...",
"importFail": "Clash vekil ile bile içe aktarma başarısız oldu",
"importNeedsRefresh": "Profile imported but may need manual refresh",
"importSuccess": "Profile imported successfully, please restart if not visible",
"importNeedsRefresh": "Profil içe aktarıldı ancak elle yenilemek gerekebilir",
"importSuccess": "Profil başarıyla içe aktarıldı, görünmüyorsa lütfen yeniden başlatın",
"profileSwitched": "Profil Değiştirildi",
"profileReactivated": "Profil Yeniden Etkinleştirildi",
"switchBusy": "Profil değiştirme işlemi zaten devam ediyor",
"batchDeleted": "Seçili profiller başarıyla silindi"
},
"notices": {
"forceRefreshCompleted": "Force refresh completed",
"emergencyRefreshFailed": "Emergency refresh failed: {{message}}"
"forceRefreshCompleted": "Zorunlu yenileme tamamlandı",
"emergencyRefreshFailed": "Acil yenileme başarısız oldu: {{message}}"
},
"tooltips": {
"forceRefreshStaleData": "Profil verileri kullanılamıyor; zorla yenilemek için tıklayın"
@@ -71,7 +71,7 @@
"menu": {
"home": "Ana Sayfa",
"select": "Seç",
"shareQrCode": "Share QR Code",
"shareQrCode": "QR Kodu Paylaş",
"editInfo": "Bilgileri Düzenle",
"editFile": "Dosyayı Düzenle",
"editRules": "Kuralları Düzenle",
@@ -81,22 +81,22 @@
"extendScript": "Betik Genişletme",
"openFile": "Dosyayı Aç",
"update": "Güncelle",
"updateViaProxy": "Update via proxy"
"updateViaProxy": "Vekil üzerinden güncelle"
},
"more": {
"global": {
"merge": "Global Extend Config",
"script": "Global Extend Script"
"merge": "Küresel Yapılandırma Genişletme",
"script": "Küresel Betik Genişletme"
},
"chips": {
"merge": "Merge",
"script": "Script"
"merge": "Birleştirme",
"script": "Betik"
}
},
"profileItem": {
"tooltips": {
"showLast": "Click to show last update time",
"showNext": "Click to show next update"
"showLast": "Son güncelleme zamanını göstermek için tıklayın",
"showNext": "Sonraki güncellemeyi göstermek için tıklayın"
},
"status": {
"lastUpdateFailed": "Son güncelleme başarısız oldu",
@@ -118,12 +118,12 @@
"description": "Açıklamalar",
"subscriptionUrl": "Abonelik URL'si",
"userAgent": "Kullanıcı Aracısı",
"httpTimeout": "HTTP Request Timeout",
"httpTimeout": "HTTP İsteği Zaman Aşımı",
"updateInterval": "Güncelleme Aralığı",
"useSystemProxy": "Sistem Vekil'ini Kullan",
"useClashProxy": "Clash Vekil'ini Kullan",
"acceptInvalidCerts": "Geçersiz Sertifikalara İzin Ver (Tehlikeli)",
"allowAutoUpdate": "Allow Auto Update"
"allowAutoUpdate": "Otomatik Güncellemeye İzin Ver"
},
"types": {
"remote": "Uzak",
@@ -206,7 +206,7 @@
"title": "Betik Konsolu"
},
"qrViewer": {
"title": "Subscription QR Code"
"title": "Abonelik QR Kodu"
}
}
}
+27 -24
View File
@@ -1,16 +1,16 @@
{
"page": {
"modes": {
"rule": "Rule",
"global": "Global",
"direct": "Direct"
"rule": "Kural",
"global": "Küresel",
"direct": "Doğrudan"
},
"actions": {
"toggleChain": "Zincir Proxy",
"connect": "Connect",
"disconnect": "Disconnect",
"connecting": "Connecting...",
"clearChainConfig": "Delete Chain Config"
"connect": "Bağlan",
"disconnect": "Bağlantıyı Kes",
"connecting": "Bağlanıyor...",
"clearChainConfig": "Zincir Yapılandırmasını Sil"
},
"provider": {
"title": "Vekil Sağlayıcısı",
@@ -20,8 +20,8 @@
}
},
"rules": {
"title": "Proxy Rules",
"select": "Select Rules"
"title": "Vekil Kuralları",
"select": "Kural Seç"
},
"labels": {
"proxyCount": "Vekil Sayısı",
@@ -68,15 +68,15 @@
}
},
"chain": {
"header": "Chain Proxy Config",
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"instruction": "Click nodes in order to add to proxy chain",
"minimumNodes": "Chain proxy requires at least 2 nodes",
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
"connectFailed": "Failed to connect to proxy chain",
"disconnectFailed": "Failed to disconnect from proxy chain",
"duplicateNode": "Proxy node already exists in chain",
"header": "Zincir Vekil Yapılandırması",
"warning": "Zincir vekil ağ hızını belirgin biçimde düşürür. Açık bir ihtiyacınız yoksa ya da bu yapılandırmayı bilmiyorsanız devre dışı bırakın.",
"empty": "Zincir vekil ağ hızını belirgin biçimde düşürür. Açık bir ihtiyacınız yoksa ya da bu yapılandırmayı bilmiyorsanız devre dışı bırakın.",
"instruction": "Vekil zincirine eklemek için düğümlere sırayla tıklayın",
"minimumNodes": "Zincir vekil en az 2 düğüm gerektirir",
"minimumNodesHint": "Zincir vekil en az 2 düğüm gerektirir. Lütfen bir düğüm daha ekleyin.",
"connectFailed": "Vekil zincirine bağlanılamadı",
"disconnectFailed": "Vekil zinciriyle bağlantı kesilemedi",
"duplicateNode": "Vekil düğümü zincirde zaten var",
"entryNode": "Giriş",
"exitNode": "Çıkış"
},
@@ -85,17 +85,20 @@
},
"title": {
"default": "Vekil Grupları",
"chainMode": "Proxy Chain Mode"
"chainMode": "Vekil Zinciri Modu"
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL'si http:// veya https:// ile başlamalıdır"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
"updateFailed": "Failed to update {{name}}: {{message}}",
"genericError": "Update failed: {{message}}",
"none": "No providers available to update",
"allUpdated": "All providers updated successfully"
"updateSuccess": "{{name}} başarıyla güncellendi",
"updateFailed": "{{name}} güncellenemedi: {{message}}",
"genericError": "Güncelleme başarısız oldu: {{message}}",
"none": "Güncellenecek sağlayıcı yok",
"allUpdated": "Tüm sağlayıcılar başarıyla güncellendi"
}
}
},
+5 -5
View File
@@ -13,11 +13,11 @@
"feedback": {
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
"updateFailed": "Failed to update {{name}}: {{message}}",
"genericError": "Update failed: {{message}}",
"none": "No providers available to update",
"allUpdated": "All providers updated successfully"
"updateSuccess": "{{name}} başarıyla güncellendi",
"updateFailed": "{{name}} güncellenemedi: {{message}}",
"genericError": "Güncelleme başarısız oldu: {{message}}",
"none": "Güncellenecek sağlayıcı yok",
"allUpdated": "Tüm sağlayıcılar başarıyla güncellendi"
}
}
},
+84 -84
View File
@@ -3,7 +3,7 @@
"actions": {
"manual": "Kılavuz",
"telegram": "Telegram Kanalı",
"github": "Github Repo"
"github": "GitHub Deposu"
},
"actionsGroupLabel": "Ayar bağlantıları",
"title": "Ayarlar"
@@ -24,8 +24,8 @@
},
"notifications": {
"tunMode": {
"autoDisabled": "TUN Mode automatically disabled due to service unavailable",
"autoDisableFailed": "Failed to disable TUN Mode automatically"
"autoDisabled": "Hizmet kullanılamadığı için TUN Modu otomatik olarak devre dışı bırakıldı",
"autoDisableFailed": "TUN Modu otomatik olarak devre dışı bırakılamadı"
}
}
},
@@ -33,11 +33,11 @@
"tooltips": {
"systemProxy": "İşletim sisteminin vekil ayarlarını değiştirmek için etkinleştirin. Etkinleştirme başarısız olursa, işletim sisteminin proxy ayarlarını manuel olarak değiştirin",
"tunMode": "Tun (Sanal Ağ Kartı) modu: Tüm sistem trafiğini yakalar, etkinleştirildiğinde sistem vekil'ini etkinleştirmeye gerek yoktur.",
"tunUnavailable": "TUN requires Service Mode or Admin Mode"
"tunUnavailable": "TUN, Hizmet Modu veya Yönetici Modu gerektirir"
},
"actions": {
"installService": "Hizmeti Kur",
"uninstallService": "Uninstall Service",
"uninstallService": "Hizmeti Kaldır",
"switchToServiceMode": "Hizmet moduna geç"
},
"fields": {
@@ -92,9 +92,9 @@
}
},
"appearance": {
"light": "Light",
"dark": "Dark",
"system": "System"
"light": "Açık",
"dark": "Koyu",
"system": "Sistem"
},
"clash": {
"title": "Clash Ayarı",
@@ -144,11 +144,11 @@
},
"options": {
"logLevel": {
"debug": "Debug",
"info": "Info",
"warning": "Warn",
"error": "Error",
"silent": "Silent"
"debug": "Hata Ayıklama",
"info": "Bilgi",
"warning": "Uyarı",
"error": "Hata",
"silent": "Sessiz"
}
}
}
@@ -187,11 +187,11 @@
"liteMode": "GUI'yi kapatın ve yalnızca çekirdeği çalışır durumda tutun"
},
"actions": {
"copyVersion": "Copy Version"
"copyVersion": "Sürümü Kopyala"
},
"notifications": {
"latestVersion": "Şu Anda En Son Sürümdesiniz",
"versionCopied": "Version copied to clipboard"
"versionCopied": "Sürüm panoya kopyalandı"
},
"fields": {
"backupSetting": "Yedekleme Ayarı",
@@ -222,27 +222,27 @@
"cssInjection": "CSS Enjeksiyonu"
},
"actions": {
"editCss": "Edit CSS"
"editCss": "CSS'i Düzenle"
},
"dialogs": {
"editCssTitle": "Edit CSS"
"editCssTitle": "CSS'i Düzenle"
}
},
"layout": {
"title": "Düzen Ayarı",
"fields": {
"preferSystemTitlebar": "Prefer System Titlebar",
"preferSystemTitlebar": "Sistem Başlık Çubuğunu Tercih Et",
"trafficGraph": "Trafik Grafiği",
"memoryUsage": "Çekirdek Kullanımı",
"proxyGroupIcon": "Vekil Grup Simgesi",
"toastPosition": "Toast konumu",
"hoverNavigator": "Hover Jump Navigator",
"hoverNavigatorDelay": "Hover Jump Navigator Delay",
"hoverNavigator": "Üzerine Gelince Atlama Gezgini",
"hoverNavigatorDelay": "Üzerine Gelince Atlama Gecikmesi",
"navIcon": "Gezinme Simgesi",
"collapseNavBar": "Gezinme çubuğunu daralt",
"trayIcon": "Tepsi Simgesi",
"proxyGroupsDisplayMode": "Proxy Groups Display Mode",
"showOutboundModesInline": "Show Outbound Modes Inline",
"proxyGroupsDisplayMode": "Vekil Grupları Görüntüleme Modu",
"showOutboundModesInline": "Çıkış Modlarını Satır İçinde Göster",
"commonTrayIcon": "Genel Tepsi Simgesi",
"systemProxyTrayIcon": "Sistem Vekil Tepsi Simgesi",
"tunTrayIcon": "Tun Tepsi Simgesi",
@@ -251,8 +251,8 @@
"pauseRenderTrafficStatsOnBlur": "Odak kaybolduğunda trafik istatistiklerinin işlenmesini duraklat"
},
"tooltips": {
"hoverNavigator": "Automatically scroll to the corresponding proxy group when hovering over alphabet letters",
"hoverNavigatorDelay": "Delay before auto scrolling when hovering, in milliseconds"
"hoverNavigator": "Alfabe harflerinin üzerine gelindiğinde ilgili vekil grubuna otomatik olarak kaydırır",
"hoverNavigatorDelay": "Üzerine gelindiğinde otomatik kaydırmadan önceki gecikme, milisaniye cinsinden"
},
"options": {
"icon": {
@@ -267,9 +267,9 @@
"bottomRight": "Sağ alt"
},
"proxyGroupsDisplayMode": {
"default": "Default",
"inline": "Inline",
"disable": "Disable"
"default": "Varsayılan",
"inline": "Satır İçi",
"disable": "Devre Dışı"
}
}
}
@@ -289,9 +289,9 @@
"random": "Rastgele Port"
},
"messages": {
"portInUse": "Port {{port}} is already in use",
"saved": "Port settings saved",
"saveFailed": "Failed to save port settings",
"portInUse": "{{port}} portu zaten kullanımda",
"saved": "Port ayarları kaydedildi",
"saveFailed": "Port ayarları kaydedilemedi",
"automaticFallback": "{{oldPort}} portu kullanımda olduğu için bu oturum {{newPort}} portunu kullanıyor. Ayarınız değişmedi ve sonraki başlatmada {{oldPort}} yeniden denenecek.",
"automaticFallbackFailed": "Karma proxy portu otomatik olarak değiştirilemedi: {{error}}",
"portTooLow": "Bağlantı noktası 1000 veya daha yüksek olmalıdır",
@@ -325,24 +325,24 @@
"backup": {
"title": "Yedekleme Ayarı",
"tabs": {
"local": "Local backup",
"webdav": "WebDAV backup"
"local": "Yerel yedek",
"webdav": "WebDAV yedeği"
},
"actions": {
"selectTarget": "Select backup target",
"selectTarget": "Yedekleme hedefini seçin",
"backup": "Yedekle",
"export": "Export",
"exportBackup": "Export Backup",
"importBackup": "Import Backup",
"export": "Dışa Aktar",
"exportBackup": "Yedeği Dışa Aktar",
"importBackup": "Yedeği İçe Aktar",
"deleteBackup": "Yedeği Sil",
"restore": "Geri Yükle",
"restoreBackup": "Yedeği Geri Yükle",
"viewHistory": "View history"
"viewHistory": "Geçmişi görüntüle"
},
"fields": {
"webdavUrl": "WebDAV Sunucu URL'si",
"username": "Kullanıcı Adı",
"info": "Backups are stored locally in the application data directory. Use the list below to restore or delete backups."
"info": "Yedekler yerel olarak uygulama veri dizininde saklanır. Yedekleri geri yüklemek veya silmek için aşağıdaki listeyi kullanın."
},
"messages": {
"webdavUrlRequired": "WebDAV URL'si boş olamaz",
@@ -353,60 +353,60 @@
"webdavConfigSaveFailed": "WebDAV yapılandırması kaydedilemedi: {{error}}",
"backupCreated": "Yedek başarıyla oluşturuldu",
"backupFailed": "Yedekleme başarısız oldu: {{error}}",
"localBackupCreated": "Local backup created successfully",
"localBackupFailed": "Local backup failed",
"localBackupCreated": "Yerel yedek başarıyla oluşturuldu",
"localBackupFailed": "Yerel yedekleme başarısız oldu",
"restoreSuccess": "Geri Yükleme Başarılı, Uygulama 1 saniye içinde yeniden başlatılacak",
"localBackupExported": "Local backup exported successfully",
"localBackupExportFailed": "Failed to export local backup",
"localBackupImported": "Local backup imported successfully",
"localBackupImportFailed": "Failed to import local backup: {{error}}",
"webdavRefreshSuccess": "WebDAV refresh succeeded",
"webdavRefreshFailed": "WebDAV refresh failed: {{error}}",
"localBackupExported": "Yerel yedek başarıyla dışa aktarıldı",
"localBackupExportFailed": "Yerel yedek dışa aktarılamadı",
"localBackupImported": "Yerel yedek başarıyla içe aktarıldı",
"localBackupImportFailed": "Yerel yedek içe aktarılamadı: {{error}}",
"webdavRefreshSuccess": "WebDAV yenilemesi başarılı",
"webdavRefreshFailed": "WebDAV yenilemesi başarısız oldu: {{error}}",
"confirmDelete": "Bu yedek dosyasını silmeyi onaylıyor musunuz?",
"confirmRestore": "Bu yedek dosyasını geri yüklemeyi onaylıyor musunuz?"
},
"auto": {
"title": "Automatic backup",
"scheduleLabel": "Enable scheduled backup",
"scheduleHelper": "Create local backups in the background at the configured interval.",
"intervalLabel": "Backup frequency",
"changeLabel": "Backup on critical changes",
"changeHelper": "Automatically backup when Global Extend Config/Script changes.",
"title": "Otomatik yedekleme",
"scheduleLabel": "Zamanlanmış yedeklemeyi etkinleştir",
"scheduleHelper": "Yapılandırılan aralıkta arka planda yerel yedekler oluşturur.",
"intervalLabel": "Yedekleme sıklığı",
"changeLabel": "Kritik değişikliklerde yedekle",
"changeHelper": "Küresel Yapılandırma/Betik Genişletme değiştiğinde otomatik olarak yedekler.",
"options": {
"hours": "Every {{n}} hours",
"days": "Every {{n}} days"
"hours": "{{n}} saatte bir",
"days": "{{n}} günde bir"
}
},
"manual": {
"title": "Manual backup",
"local": "Creates a snapshot on this device, stored under the app data directory.",
"webdav": "Upload a snapshot to your WebDAV server once credentials are set.",
"configureWebdav": "Configure WebDAV"
"title": "Elle yedekleme",
"local": "Bu cihazda, uygulama veri dizini altında saklanan bir anlık görüntü oluşturur.",
"webdav": "Kimlik bilgileri ayarlandıktan sonra WebDAV sunucunuza bir anlık görüntü yükler.",
"configureWebdav": "WebDAV'ı Yapılandır"
},
"history": {
"title": "Backup history",
"summary": "{{count}} backups • latest {{recent}}",
"empty": "No backups available",
"unknownPlatform": "unknown",
"unknownTime": "Unknown time"
"title": "Yedekleme geçmişi",
"summary": "{{count}} yedek • en son {{recent}}",
"empty": "Kullanılabilir yedek yok",
"unknownPlatform": "bilinmiyor",
"unknownTime": "Bilinmeyen zaman"
},
"webdav": {
"title": "WebDAV settings"
"title": "WebDAV ayarları"
},
"table": {
"filename": "Dosya Adı",
"backupTime": "Yedekleme Zamanı",
"actions": "İşlemler",
"noBackups": "Kullanılabilir yedek yok",
"rowsPerPage": "Rows per page"
"rowsPerPage": "Sayfa başına satır"
}
},
"misc": {
"title": "Çeşitli",
"fields": {
"appLogLevel": "Uygulama Günlük Seviyesi",
"appLogMaxSize": "App Log Max Size",
"appLogMaxCount": "App Log Max Count",
"appLogMaxSize": "Uygulama Günlüğü En Büyük Boyutu",
"appLogMaxCount": "Uygulama Günlüğü En Fazla Dosya Sayısı",
"autoCloseConnections": "Bağlantıları Otomatik Kapat",
"autoCheckUpdate": "Otomatik Güncelleme Kontrolü",
"enableBuiltinEnhanced": "Yerleşik Geliştirilmiş Modu Etkinleştir",
@@ -434,7 +434,7 @@
}
},
"update": {
"title": "New Version v{{version}}",
"title": "Yeni Sürüm v{{version}}",
"actions": {
"goToRelease": "Sürüm Sayfasına Git",
"update": "Güncelle"
@@ -486,18 +486,18 @@
"title": "Tun Modu",
"fields": {
"stack": "Tun Yığını",
"device": "Device Name",
"device": "Aygıt Adı",
"autoRoute": "Otomatik Yönlendirme",
"routeExcludeAddress": "Yönlendirme Hariç Adresler",
"strictRoute": "Katı Yönlendirme",
"autoDetectInterface": "Arayüzü Otomatik Algıla",
"dnsHijack": "DNS Ele Geçirme",
"mtu": "Maksimum İletim Birimi",
"autoRedirect": "Auto Redirect"
"autoRedirect": "Otomatik Yönlendirme"
},
"tooltips": {
"dnsHijack": "Please use , to separate multiple DNS servers",
"autoRedirect": "Automatically configures nftables/iptables TCP redirects"
"dnsHijack": "Birden fazla DNS sunucusunu ayırmak için lütfen , kullanın",
"autoRedirect": "nftables/iptables TCP yönlendirmelerini otomatik olarak yapılandırır"
},
"messages": {
"applied": "Ayarlar Uygulandı",
@@ -511,11 +511,11 @@
"warning": "Bu ayarlarla ilgili bilginiz yoksa, lütfen bunları değiştirmeyin ve DNS Üzerine Yazma'yı devre dışı tutun"
},
"protection": {
"title": "Enable DNS Overwrite?",
"message": "This subscription contains custom DNS settings. DNS Overwrite may replace them, preventing nodes from connecting or some websites from loading. Enable it anyway?",
"enableAnyway": "Enable anyway",
"keepDisabled": "Keep disabled",
"autoDisabled": "DNS Overwrite was disabled to preserve the custom DNS settings in the current subscription."
"title": "DNS Üzerine Yazma etkinleştirilsin mi?",
"message": "Bu abonelik özel DNS ayarları içeriyor. DNS Üzerine Yazma bunların yerine geçebilir; bu da düğümlerin bağlanmasını veya bazı web sitelerinin yüklenmesini engelleyebilir. Yine de etkinleştirilsin mi?",
"enableAnyway": "Yine de etkinleştir",
"keepDisabled": "Devre dışı bırak",
"autoDisabled": "Geçerli abonelikteki özel DNS ayarlarını korumak için DNS Üzerine Yazma devre dışı bırakıldı."
},
"sections": {
"general": "DNS Ayarları",
@@ -535,7 +535,7 @@
},
"preferH3": {
"label": "H3'ü Tercih Et",
"description": "DNS DOH uses HTTP/3"
"description": "DNS DOH, HTTP/3 kullanır"
},
"respectRules": {
"label": "Kurallara Uy",
@@ -601,11 +601,11 @@
},
"messages": {
"saved": "DNS ayarları kaydedildi",
"configError": "DNS configuration error:"
"configError": "DNS yapılandırma hatası:"
},
"errors": {
"invalid": "Invalid configuration",
"invalidYaml": "Invalid YAML format"
"invalid": "Geçersiz yapılandırma",
"invalidYaml": "Geçersiz YAML biçimi"
}
},
"webUI": {
@@ -694,12 +694,12 @@
},
"statuses": {
"clash": {
"stopping": "Stopping Core...",
"restarting": "Restarting Core..."
"stopping": "Çekirdek Durduruluyor...",
"restarting": "Çekirdek Yeniden Başlatılıyor..."
},
"clashService": {
"installing": "Hizmet Kuruluyor...",
"uninstalling": "Uninstalling Service..."
"uninstalling": "Hizmet Kaldırılıyor..."
}
}
}
+17 -17
View File
@@ -12,23 +12,23 @@
"restart": "Yeniden Başlat",
"resetToDefault": "Varsayılana Sıfırla",
"refresh": "Yenile",
"retry": "Retry",
"refreshPage": "Refresh Page",
"showDetails": "Show Details",
"hideDetails": "Hide Details",
"retry": "Yeniden Dene",
"refreshPage": "Sayfayı Yenile",
"showDetails": "Ayrıntıları Göster",
"hideDetails": "Ayrıntıları Gizle",
"listView": "Liste Görünümü",
"tableView": "Tablo Görünümü",
"pause": "Duraklat",
"resume": "Sürdür",
"closeAll": "Tümünü Kapat",
"clear": "Temizle",
"previous": "Previous",
"next": "Next",
"previous": "Önceki",
"next": "Sonraki",
"check": "Denetle"
},
"labels": {
"updateAt": "Güncelleme Zamanı",
"timeout": "Timeout",
"timeout": "Zaman Aşımı",
"icon": "Simge",
"name": "İsim",
"readOnly": "Salt Okunur",
@@ -37,14 +37,14 @@
"usedTotal": "Kullanılan / Toplam",
"from": "Kaynak",
"password": "Şifre",
"retryAttempts": "Retry attempts",
"retryAttempts": "Yeniden deneme sayısı",
"downloaded": "İndirilen",
"uploaded": "Yüklenen"
},
"statuses": {
"enabled": "Etkin",
"disabled": "Devre Dışı",
"saving": "Saving...",
"saving": "Kaydediliyor...",
"empty": "Boş",
"loading": "Yükleniyor..."
},
@@ -54,7 +54,7 @@
"minutes": "dakika",
"hours": "saat",
"kilobytes": "KB",
"files": "Files"
"files": "Dosya"
},
"placeholders": {
"resetInput": "Giriş alanını temizle",
@@ -64,7 +64,7 @@
"useRegex": "Düzenli İfade Kullan"
},
"validation": {
"invalidRegex": "Invalid regular expression"
"invalidRegex": "Geçersiz düzenli ifade"
},
"window": {
"maximize": "Büyüt",
@@ -84,8 +84,8 @@
"componentStack": "Bileşen yığını",
"trafficUnavailable": "Trafik verileri kullanılamıyor",
"operationFailed": "İşlem başarısız oldu",
"trafficStats": "Traffic Statistics Error",
"trafficStatsDescription": "The traffic statistics component encountered an error and has been disabled to prevent crashes."
"trafficStats": "Trafik İstatistikleri Hatası",
"trafficStatsDescription": "Trafik istatistikleri bileşeni bir hatayla karşılaştı ve çökmeleri önlemek için devre dışı bırakıldı."
},
"notices": {
"raw": "{{message}}",
@@ -95,12 +95,12 @@
"importSuccess": "Profil Başarıyla İçe Aktarıldı",
"importSubscriptionSuccess": "Abonelik içe aktarımı başarılı",
"importWithClashProxy": "Profil Clash vekil ile içe aktarıldı",
"updateAvailable": "Update Available",
"saved": "Saved successfully",
"updateAvailable": "Güncelleme Mevcut",
"saved": "Başarıyla kaydedildi",
"common": {
"copySuccess": "Kopyalama Başarılı",
"saveSuccess": "Configuration saved successfully",
"saveFailed": "Failed to save configuration",
"saveSuccess": "Yapılandırma başarıyla kaydedildi",
"saveFailed": "Yapılandırma kaydedilemedi",
"refreshFailed": "Yenileme başarısız"
}
},
+2 -2
View File
@@ -49,10 +49,10 @@
"actions": {
"testing": "Test Ediliyor..."
},
"empty": "No unlock test items",
"empty": "Kilit açma testi öğesi yok",
"messages": {
"detectionFailedWithName": "{{name}} için tespit başarısız oldu",
"detectionTimeout": "Detection timeout or failed"
"detectionTimeout": "Algılama zaman aşımına uğradı veya başarısız oldu"
},
"title": "Kilit Açma Testi"
}
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} 更新成功",
+3
View File
@@ -89,6 +89,9 @@
}
},
"feedback": {
"warnings": {
"invalidTestUrl": "Test URL must start with http:// or https://"
},
"notifications": {
"provider": {
"updateSuccess": "{{name}} updated successfully",
+2 -1
View File
@@ -59,6 +59,7 @@ import {
useThemeMode,
} from '@/services/states'
import { debugLog } from '@/utils/debug'
import { isValidUrl } from '@/utils/network'
// 与 src-tauri/src/main.rs 的 worker_limit 上限(8)保持一致,避免前后端更新风暴不对齐
const PROFILE_UPDATE_WORKER_LIMIT = 8
@@ -214,7 +215,7 @@ const ProfilePage = () => {
const onImport = async () => {
if (!url) return
if (!/^https?:\/\//i.test(url)) {
if (!isValidUrl(url)) {
showNotice.error('profiles.page.feedback.errors.invalidUrl')
return
}
+6
View File
@@ -12,6 +12,7 @@ import {
} from '@/types/proxy-view'
import { debugLog } from '@/utils/debug'
import { classifyDelay, DEFAULT_DELAY_TIMEOUT } from '@/utils/delay'
import { isValidUrl } from '@/utils/network'
export type DelaySnapshot = {
of: (member: ResolvedProxyMember) => number
@@ -152,6 +153,11 @@ class DelayManager {
}
setUrl(group: string, url: string) {
if (!isValidUrl(url)) {
debugLog(`[DelayManager] 拒绝无效测试URL,组: ${group}, URL: ${url}`)
this.urlMap.delete(group)
return
}
debugLog(`[DelayManager] 设置测试URL,组: ${group}, URL: ${url}`)
this.urlMap.set(group, url)
}
+1
View File
@@ -324,6 +324,7 @@ export const translationKeys = [
'proxies.page.messages.directMode',
'proxies.page.title.default',
'proxies.page.title.chainMode',
'proxies.feedback.warnings.invalidTestUrl',
'proxies.feedback.notifications.provider.updateSuccess',
'proxies.feedback.notifications.provider.updateFailed',
'proxies.feedback.notifications.provider.genericError',
+3
View File
@@ -500,6 +500,9 @@ export interface TranslationResources {
updateSuccess: string
}
}
warnings: {
invalidTestUrl: string
}
}
page: {
actions: {