mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-03 13:32:16 +08:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b7e876ab0 | ||
|
|
bcbb5e3a58 | ||
|
|
726b2b362f | ||
|
|
a038e01cd5 | ||
|
|
f149604eb6 | ||
|
|
ebd78ae9ff | ||
|
|
3027e2b87f | ||
|
|
a0e57c5962 | ||
|
|
de15283ef6 | ||
|
|
3f39c2ceca | ||
|
|
160bb4fe8e | ||
|
|
a5b22fa512 |
Generated
+25
-25
@@ -1081,7 +1081,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "clash-verge"
|
||||
version = "2.5.9"
|
||||
version = "2.5.10"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -1229,8 +1229,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clash_verge_service_ipc"
|
||||
version = "2.7.6"
|
||||
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc.git?tag=v2.7.6#b2a7427d6681c8d3780ab83a73f4dc21da2f22d6"
|
||||
version = "2.7.7"
|
||||
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc.git?tag=v2.7.7#3ed29700b093190b5e7addcaa892e77f172af544"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"kode-bridge",
|
||||
@@ -1334,7 +1334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2015,7 +2015,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2274,7 +2274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2790,7 +2790,7 @@ dependencies = [
|
||||
"gobject-sys 0.21.5",
|
||||
"libc",
|
||||
"system-deps 7.0.8",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3344,7 +3344,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.62.2",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4215,7 +4215,7 @@ dependencies = [
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.20",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4382,7 +4382,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4859,7 +4859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5524,7 +5524,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6028,7 +6028,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6041,7 +6041,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6100,7 +6100,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6703,9 +6703,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "2.0.0"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a155190f9c039a1e9b6e92efbfa50ae68daad98a0fd6d5521a5a09ef782383e8"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"serde",
|
||||
@@ -6720,7 +6720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7786,10 +7786,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.3",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8455,7 +8455,7 @@ dependencies = [
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.20",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8564,7 +8564,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9106,7 +9106,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9127,7 +9127,7 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
@@ -9702,7 +9702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ reqwest = { version = "0.13.5", features = [
|
||||
] }
|
||||
rust_iso3166 = "0.2.0"
|
||||
|
||||
smartstring = { version = "2.0.0" }
|
||||
smartstring = { version = "1.0.1" }
|
||||
|
||||
serde = { version = "1.0.229" }
|
||||
serde_json = { version = "1.0.154" }
|
||||
|
||||
+30
-20
@@ -1,19 +1,33 @@
|
||||
## v2.5.9
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.33**
|
||||
<!-- Release-notes skeleton. Entries accumulate in ./Changelog.md as PRs land;
|
||||
before publishing a release, distill them:
|
||||
- user-visible changes only — no dev tooling, CI fixes, or refactors whose
|
||||
effect is already covered by another entry
|
||||
- describe outcomes, not internals: no internal module names, IPC or
|
||||
protocol details, or implementation mechanics — write what the user
|
||||
observes, not how the code achieves it
|
||||
- one line per theme: merge entries that share a root cause
|
||||
- each entry fits a single readable line (about 60 CJK characters); if it
|
||||
overflows, it is packing multiple themes or detail that belongs in the
|
||||
linked issue/PR — split it or drop the detail
|
||||
- line grammar: open with a verb (修复/新增/优化), with the platform
|
||||
first (Windows/macOS/Linux) when the change is platform-specific;
|
||||
merge sub-symptoms as 主题:症状、症状; full-width punctuation,
|
||||
「」 for UI labels, backticks for config keys and commands
|
||||
- platform entries sit under their platform heading (bold, emoji
|
||||
prefixed, ordered Windows/macOS/Linux, combined for pairs like
|
||||
macOS/Linux) at the end of their section;
|
||||
scripts-workflow/group_platforms.sh regroups, --check verifies
|
||||
HTML comments are stripped by the update dialog's sanitizer and hidden on
|
||||
GitHub, so they never reach users. -->
|
||||
## v2.5.10
|
||||
|
||||
<details>
|
||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||
|
||||
- 修复规则扩展包含空列表项时,「编辑规则」界面报错的问题
|
||||
- 修复 重启应用后定时自动备份不执行的问题
|
||||
- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口
|
||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
|
||||
- 修复 首页 网站测速失败后卡片一直显示测试中
|
||||
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
|
||||
- 修复 代理 在全局模式下无法展开节点组
|
||||
- 修复 自定义 CSS 无法修改编辑器字体和字号的问题
|
||||
- 修复 WebDAV 设置清空后无法保存
|
||||
- 修复全局模式缺少 `GLOBAL` 策略组,无法单独选择全局出口
|
||||
- 修复服务模式下,SSH 节点的 `private-key` 使用私钥文件时内核无法启动的问题
|
||||
- 修复服务模式下,内核更新的 GEO 数据库不会同步回应用目录,导致使用新分类的规则无法通过校验的问题
|
||||
- 修复订阅的 DNS 配置变化后,「DNS 覆写」实际已关闭但开关仍显示开启且没有提示的问题
|
||||
|
||||
**🍎 macOS**
|
||||
|
||||
@@ -23,24 +37,20 @@
|
||||
|
||||
- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
- Windows:修复安装程序重新安装或覆盖时未提示退出运行中应用,导致卸载报错或覆盖失败的问题
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为
|
||||
- 新增「IP 信息」自动刷新开关,关闭后仍可手动刷新
|
||||
- 新增 Muse AI 解锁检测,针对 Muse AI 特有的 Cloudflare 人机验证增加「Risky」状态标记
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 优化「链式代理」:开启前需确认网速影响提示,空面板新增「关闭」按钮
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
- Windows 系统代理默认绕过 `*.local` 域名,访问局域网设备的 `.local` 主机名时不再经过代理
|
||||
|
||||
</details>
|
||||
|
||||
@@ -1,3 +1,51 @@
|
||||
## v2.5.9
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.33**
|
||||
|
||||
<details>
|
||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||
|
||||
- 修复规则扩展包含空列表项时,「编辑规则」界面报错的问题
|
||||
- 修复 重启应用后定时自动备份不执行的问题
|
||||
- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口
|
||||
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
|
||||
- 修复 首页 网站测速失败后卡片一直显示测试中
|
||||
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
|
||||
- 修复 代理 在全局模式下无法展开节点组
|
||||
- 修复 自定义 CSS 无法修改编辑器字体和字号的问题
|
||||
- 修复 WebDAV 设置清空后无法保存
|
||||
|
||||
**🍎 macOS**
|
||||
|
||||
- macOS:修复 Bigsur 上首页测速和流量图无法显示的问题
|
||||
|
||||
**🐧 Linux**
|
||||
|
||||
- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- 新增虚拟网卡设置的「DNS 模式」选项,支持配置 mihomo 系统 DNS 行为
|
||||
- 新增「IP 信息」自动刷新开关,关闭后仍可手动刷新
|
||||
- 新增 Muse AI 解锁检测,针对 Muse AI 特有的 Cloudflare 人机验证增加「Risky」状态标记
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 优化「链式代理」:开启前需确认网速影响提示,空面板新增「关闭」按钮
|
||||
|
||||
**🖥️ Windows**
|
||||
|
||||
- Windows 系统代理默认绕过 `*.local` 域名,访问局域网设备的 `.local` 主机名时不再经过代理
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## v2.5.8
|
||||
|
||||
<details>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.5.9",
|
||||
"version": "2.5.10",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"prepare": "husky || true",
|
||||
|
||||
@@ -41,7 +41,10 @@ fi
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
{
|
||||
cat "$CHANGELOG"
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
process.stdout.write(fs.readFileSync(process.argv[1], "utf8").replace(/<!--[\s\S]*?-->/g, "").trimStart());
|
||||
' "$CHANGELOG"
|
||||
echo
|
||||
echo ""
|
||||
if [ -f "$HISTORY" ]; then
|
||||
|
||||
@@ -134,6 +134,31 @@ function fetchBranches() {
|
||||
)
|
||||
}
|
||||
|
||||
function discardClosedCandidate(branch, base, prefix) {
|
||||
const candidates = list(
|
||||
`pulls?state=all&base=${base}&head=${repository.split('/')[0]}:${branch}&per_page=100`,
|
||||
)
|
||||
const latest = candidates[0]
|
||||
if (!latest || candidates.some((pr) => pr.state === 'open'))
|
||||
throw new Error(
|
||||
`Branch ${branch} is reserved by an active or unknown candidate.`,
|
||||
)
|
||||
const pr = get(`pulls/${latest.number}`)
|
||||
requireBotPr(pr, base, prefix)
|
||||
if (pr.state !== 'closed' || pr.merged)
|
||||
throw new Error(
|
||||
`Close the unmerged candidate before retrying: ${pr.html_url}`,
|
||||
)
|
||||
updateRefs(pr.base.repo.node_id, [
|
||||
{
|
||||
name: `refs/heads/${branch}`,
|
||||
beforeOid: pr.head.sha,
|
||||
afterOid: '0'.repeat(40),
|
||||
},
|
||||
])
|
||||
summary(`Released the unused branch from closed PR: ${pr.html_url}`)
|
||||
}
|
||||
|
||||
function prepare() {
|
||||
let refresh
|
||||
if (env.GITHUB_EVENT_NAME === 'pull_request_target') {
|
||||
@@ -211,8 +236,15 @@ function prepare() {
|
||||
: last
|
||||
? `${base}-rc.${BigInt(last.match(rcPattern)[4] ?? 0) + 1n}`
|
||||
: `${base}-rc`)
|
||||
if (occupied.includes(tag))
|
||||
throw new Error(`Tag ${tag} already exists or is reserved.`)
|
||||
if (tags.includes(tag)) throw new Error(`Tag ${tag} already exists.`)
|
||||
for (const [prefix, target] of [
|
||||
['release/', 'dev'],
|
||||
['publish/', 'main'],
|
||||
]) {
|
||||
const branch = `${prefix}${tag}`
|
||||
if (refs.includes(`refs/heads/${branch}`) && branch !== refresh?.head.ref)
|
||||
discardClosedCandidate(branch, target, prefix)
|
||||
}
|
||||
const sha = git('rev-parse', 'HEAD')
|
||||
const body = releaseBody(tag, sha, tags, false)
|
||||
summary(body)
|
||||
@@ -453,7 +485,7 @@ function publishPr() {
|
||||
validateVersionCommit(tag, sha)
|
||||
const branch = `publish/${tag}`
|
||||
const existing = list(
|
||||
`pulls?state=all&base=main&head=${repository.split('/')[0]}:${branch}&per_page=100`,
|
||||
`pulls?state=open&base=main&head=${repository.split('/')[0]}:${branch}&per_page=100`,
|
||||
)
|
||||
if (existing.length) {
|
||||
const publication = existing[0]
|
||||
@@ -567,28 +599,31 @@ function publish() {
|
||||
requireApproval(pr, sha)
|
||||
const main = git('rev-parse', 'origin/main')
|
||||
git('merge-base', '--is-ancestor', main, sha)
|
||||
const object = api('git/tags', {
|
||||
git(
|
||||
'-c',
|
||||
'user.name=github-actions[bot]',
|
||||
'-c',
|
||||
'user.email=41898282+github-actions[bot]@users.noreply.github.com',
|
||||
'tag',
|
||||
'--annotate',
|
||||
'--no-sign',
|
||||
'--message',
|
||||
marker,
|
||||
tag,
|
||||
message: marker,
|
||||
object: sha,
|
||||
type: 'commit',
|
||||
})
|
||||
// The tag records run ownership in the same transaction as the fast-forward.
|
||||
updateRefs(pr.base.repo.node_id, [
|
||||
{ name: 'refs/heads/main', beforeOid: main, afterOid: sha, force: false },
|
||||
{
|
||||
name: `refs/heads/${pr.head.ref}`,
|
||||
beforeOid: sha,
|
||||
afterOid: sha,
|
||||
force: false,
|
||||
},
|
||||
{
|
||||
name: `refs/tags/${tag}`,
|
||||
beforeOid: '0'.repeat(40),
|
||||
afterOid: object.sha,
|
||||
force: false,
|
||||
},
|
||||
])
|
||||
sha,
|
||||
)
|
||||
// GraphQL updateRefs rejects annotated tag objects; Git preserves atomicity.
|
||||
git(
|
||||
'push',
|
||||
'--atomic',
|
||||
`--force-with-lease=refs/heads/main:${main}`,
|
||||
`--force-with-lease=refs/heads/${pr.head.ref}:${sha}`,
|
||||
`--force-with-lease=refs/tags/${tag}:`,
|
||||
'origin',
|
||||
`${sha}:refs/heads/main`,
|
||||
`${sha}:refs/heads/${pr.head.ref}`,
|
||||
`refs/tags/${tag}:refs/tags/${tag}`,
|
||||
)
|
||||
summary(`Fast-forwarded main and tagged ${tag} at approved commit ${sha}.`)
|
||||
}
|
||||
releaseOutput(tag, sha)
|
||||
@@ -617,9 +652,14 @@ function validateTag() {
|
||||
function prepareDraft() {
|
||||
const tag = env.RELEASE_TAG
|
||||
verifyTarget()
|
||||
const release = list('releases?per_page=100').find(
|
||||
(release) => release.tag_name === tag,
|
||||
)
|
||||
const result = run('gh', [
|
||||
'api',
|
||||
'--paginate',
|
||||
endpoint('releases?per_page=100'),
|
||||
'--jq',
|
||||
`.[] | select(.tag_name == ${JSON.stringify(tag)}) | {id, draft}`,
|
||||
])
|
||||
const release = result ? JSON.parse(result) : null
|
||||
if (release && !release.draft)
|
||||
throw new Error('This release is already public.')
|
||||
const draft =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.5.9"
|
||||
version = "2.5.10"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -98,7 +98,7 @@ tracing = { workspace = true }
|
||||
tauri-plugin-devtools = { version = "2.2.2", optional = true }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
|
||||
async-trait = "0.1.92"
|
||||
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.6", version = "2.7.6", features = [
|
||||
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [
|
||||
"client",
|
||||
] }
|
||||
arc-swap = "1.9.2"
|
||||
|
||||
@@ -21,6 +21,7 @@ ManifestDPIAwareness PerMonitorV2
|
||||
!include "FileAssociation.nsh"
|
||||
!include "Win\COM.nsh"
|
||||
!include "Win\Propkey.nsh"
|
||||
!include "Win\RestartManager.nsh"
|
||||
!include "WinVer.nsh"
|
||||
!include "LogicLib.nsh"
|
||||
!include "StrFunc.nsh"
|
||||
@@ -332,6 +333,8 @@ Function PageLeaveReinstall
|
||||
${EndIf}
|
||||
|
||||
reinst_uninstall:
|
||||
!insertmacro CheckIfAppIsRunning "$INSTDIR\${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||
|
||||
HideWindow
|
||||
ClearErrors
|
||||
|
||||
@@ -368,7 +371,10 @@ Function PageLeaveReinstall
|
||||
MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)"
|
||||
Abort
|
||||
${EndIf}
|
||||
Return
|
||||
|
||||
reinst_done:
|
||||
!insertmacro CheckIfAppIsRunning "$INSTDIR\${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||
FunctionEnd
|
||||
|
||||
; 5. Choose install directory page
|
||||
@@ -901,8 +907,7 @@ Section Install
|
||||
|
||||
nsExec::Exec 'netsh int tcp res'
|
||||
|
||||
!include "Win\RestartManager.nsh"
|
||||
!insertmacro CheckIfAppIsRunning "${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||
!insertmacro CheckIfAppIsRunning "$INSTDIR\${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
|
||||
; Ensure startup folders exist
|
||||
@@ -1101,7 +1106,7 @@ Section Uninstall
|
||||
!insertmacro NSIS_HOOK_PREUNINSTALL
|
||||
!endif
|
||||
|
||||
!insertmacro CheckIfAppIsRunning "${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||
!insertmacro CheckIfAppIsRunning "$INSTDIR\${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||
!insertmacro CheckAllVergeProcesses
|
||||
!insertmacro RemoveVergeService
|
||||
|
||||
|
||||
@@ -205,7 +205,6 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
logging!(info, Type::Config, "Config regenerated successfully");
|
||||
}
|
||||
|
||||
logging_error!(Type::Config, Config::sync_dns_override().await);
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -191,10 +191,6 @@ pub async fn delete_profile(index: std::string::String) -> CmdResult {
|
||||
drop(config_update_guard);
|
||||
drop(profile_write_guard);
|
||||
|
||||
if should_update {
|
||||
logging_error!(Type::Config, Config::sync_dns_override().await);
|
||||
}
|
||||
|
||||
if let Err(e) = Tray::global().update_tooltip().await {
|
||||
logging!(warn, Type::Cmd, "异步更新托盘提示失败: {e:#}");
|
||||
}
|
||||
@@ -264,7 +260,6 @@ async fn handle_success(current_value: Option<&String>) -> CmdResult<ValidationO
|
||||
handle::Handle::notify_profile_changed(current);
|
||||
}
|
||||
|
||||
logging_error!(Type::Config, Config::sync_dns_override().await);
|
||||
Ok(ValidationOutcome::Valid)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
},
|
||||
utils::dirs,
|
||||
};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use smartstring::alias::String;
|
||||
use tokio::fs;
|
||||
|
||||
@@ -168,7 +168,6 @@ async fn handle_saved_profile_file(
|
||||
);
|
||||
match CoreManager::global().update_config_forced().await {
|
||||
Ok(outcome) if outcome.is_valid() => {
|
||||
logging_error!(Type::Config, Config::sync_dns_override().await);
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(ValidationOutcome::Valid)
|
||||
}
|
||||
|
||||
@@ -435,6 +435,10 @@ impl CoreManager {
|
||||
{
|
||||
anyhow::bail!("Sidecar did not become ready");
|
||||
}
|
||||
// A Sidecar that was already running never loaded this generation.
|
||||
if matches!(*mode, RunningMode::NotRunning) {
|
||||
Config::runtime().await.apply();
|
||||
}
|
||||
tracing::Span::current().record("readiness_generation", self.current_core_readiness_generation());
|
||||
self.apply_proxy_after_start().await
|
||||
}
|
||||
@@ -492,6 +496,7 @@ impl CoreManager {
|
||||
{
|
||||
anyhow::bail!("Sidecar did not become ready after service uninstall");
|
||||
}
|
||||
Config::runtime().await.apply();
|
||||
tracing::Span::current().record("readiness_generation", self.current_core_readiness_generation());
|
||||
self.apply_proxy_after_start().await
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ mod state;
|
||||
|
||||
use anyhow::Result;
|
||||
use arc_swap::{ArcSwap, ArcSwapOption};
|
||||
use clash_verge_logging::{LogRing, Type, logging};
|
||||
use clash_verge_logging::{LogRing, Type, logging, logging_error};
|
||||
use std::{
|
||||
fmt,
|
||||
sync::{
|
||||
@@ -274,6 +274,10 @@ impl CoreManager {
|
||||
|
||||
pub(crate) fn finish_config_update(&self) {
|
||||
self.config_update_in_progress.store(false, Ordering::Release);
|
||||
// Spawned because the caller may still hold the config write lock this waits for.
|
||||
crate::process::AsyncHandler::spawn(|| async {
|
||||
logging_error!(Type::Config, crate::config::Config::sync_dns_override().await);
|
||||
});
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "info", fields(retries = 0))]
|
||||
|
||||
@@ -4,7 +4,7 @@ use clash_verge_service_ipc::{RemoteProvider, RuntimeAsset, RuntimeBundle};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::path::{Component, Path, PathBuf, Prefix};
|
||||
|
||||
pub(crate) const GEO_ASSETS: &[&str] = &[
|
||||
"Country.mmdb",
|
||||
@@ -165,6 +165,7 @@ fn collect_runtime_parts(mut config: Value, config_root: &Path) -> Result<Runtim
|
||||
&mut assets,
|
||||
&mut remote_providers,
|
||||
)?;
|
||||
collect_ssh_private_keys(&mut config, &config_root, &mut destinations, &mut assets);
|
||||
for filename in GEO_ASSETS {
|
||||
let source = config_root.join(filename);
|
||||
if source.is_file() && destinations.insert((*filename).to_string()) {
|
||||
@@ -270,6 +271,75 @@ fn collect_provider_assets(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stages file-based ssh keys, which mihomo resolves against the core home directory.
|
||||
fn collect_ssh_private_keys(
|
||||
config: &mut Value,
|
||||
config_root: &Path,
|
||||
destinations: &mut HashSet<String>,
|
||||
assets: &mut Vec<RuntimeAsset>,
|
||||
) {
|
||||
let Some(config) = config.as_mapping_mut() else {
|
||||
return;
|
||||
};
|
||||
// mihomo parses `payload` for every provider type, eagerly as fallback proxies for file and http.
|
||||
let mut proxy_lists = Vec::new();
|
||||
for (key, value) in config.iter_mut() {
|
||||
match key.as_str() {
|
||||
Some("proxies") => proxy_lists.extend(value.as_sequence_mut()),
|
||||
Some("proxy-providers") => proxy_lists.extend(
|
||||
value
|
||||
.as_mapping_mut()
|
||||
.into_iter()
|
||||
.flat_map(Mapping::values_mut)
|
||||
.filter_map(|provider| provider.get_mut("payload").and_then(Value::as_sequence_mut)),
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
for proxy in proxy_lists.into_iter().flatten().filter_map(Value::as_mapping_mut) {
|
||||
if proxy.get("type").and_then(Value::as_str) != Some("ssh") {
|
||||
continue;
|
||||
}
|
||||
// mihomo reads any value containing "PRIVATE KEY" as inline key material.
|
||||
let Some(raw_path) = proxy
|
||||
.get("private-key")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|key| !key.is_empty() && !key.contains("PRIVATE KEY") && is_local_path(key))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(source) = local_provider_source(config_root, raw_path)
|
||||
.ok()
|
||||
.filter(|source| source.is_file())
|
||||
else {
|
||||
let name = proxy.get("name").and_then(Value::as_str).unwrap_or_default();
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"ssh proxy {name:?} private key is unavailable below the config root; leaving it to the core"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
// A root-level alias keeps the user's path away from service name rules and mihomo's inline-key test.
|
||||
let source = source.to_string_lossy().into_owned();
|
||||
let destination = allocate_provider_destination("ssh-private-key", &source, destinations);
|
||||
assets.push(RuntimeAsset {
|
||||
source,
|
||||
destination: destination.clone(),
|
||||
});
|
||||
proxy.insert(Value::String("private-key".to_owned()), Value::String(destination));
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolving a UNC or device path would authenticate to its server, so those stay with the core.
|
||||
fn is_local_path(raw_path: &str) -> bool {
|
||||
match Path::new(raw_path).components().next() {
|
||||
Some(Component::Prefix(prefix)) => matches!(prefix.kind(), Prefix::Disk(_) | Prefix::VerbatimDisk(_)),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn local_provider_source(config_root: &Path, raw_path: &str) -> Result<PathBuf> {
|
||||
let path = Path::new(raw_path);
|
||||
let source = if path.is_absolute() {
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
CORE_REJECTED_PREFIX, OwnerRecoveryReason, OwnerSample, OwnerStep, OwnerWatch, PendingAction, RUN_STATE,
|
||||
ReadyWaitError, RunState, RunStateEnv, RunStateStore, ServiceHealth,
|
||||
},
|
||||
runtime_bundle::{RemoteProviderRef, collect_runtime_bundle, remote_providers_of},
|
||||
runtime_bundle::{GEO_ASSETS, RemoteProviderRef, collect_runtime_bundle, remote_providers_of},
|
||||
tray::Tray,
|
||||
},
|
||||
process::AsyncHandler,
|
||||
@@ -101,6 +101,7 @@ struct ActiveServiceSession {
|
||||
proof: OwnerSessionProof,
|
||||
supports_runtime_staging: bool,
|
||||
supports_runtime_file_read: bool,
|
||||
supports_runtime_geo_read: bool,
|
||||
}
|
||||
|
||||
fn generate_service_session_token() -> Result<String> {
|
||||
@@ -132,6 +133,13 @@ pub(crate) fn active_service_supports_runtime_file_read() -> bool {
|
||||
.is_some_and(|session| session.supports_runtime_file_read)
|
||||
}
|
||||
|
||||
fn active_service_supports_runtime_geo_read() -> bool {
|
||||
ACTIVE_SERVICE_SESSION
|
||||
.lock()
|
||||
.as_ref()
|
||||
.is_some_and(|session| session.supports_runtime_geo_read)
|
||||
}
|
||||
|
||||
pub(crate) fn clear_active_service_session() {
|
||||
ACTIVE_SERVICE_SESSION.lock().take();
|
||||
}
|
||||
@@ -140,6 +148,7 @@ pub(crate) fn clear_active_service_session() {
|
||||
struct ServiceCapabilities {
|
||||
runtime_staging: bool,
|
||||
runtime_file_read: bool,
|
||||
runtime_geo_read: bool,
|
||||
}
|
||||
|
||||
impl ServiceCapabilities {
|
||||
@@ -147,6 +156,7 @@ impl ServiceCapabilities {
|
||||
Self {
|
||||
runtime_staging: info.supports_runtime_staging(),
|
||||
runtime_file_read: info.supports_runtime_file_read(),
|
||||
runtime_geo_read: info.supports_runtime_geo_read(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -956,6 +966,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()
|
||||
},
|
||||
supports_runtime_staging: capabilities.runtime_staging,
|
||||
supports_runtime_file_read: capabilities.runtime_file_read,
|
||||
supports_runtime_geo_read: capabilities.runtime_geo_read,
|
||||
});
|
||||
|
||||
// PAC follows the Running Mode; the caller opens it via `core_started(Service)`.
|
||||
@@ -1206,6 +1217,62 @@ async fn sync_runtime_providers_by_service() -> Result<ProviderSync> {
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
/// Pulls geo databases the service core rewrote, because validation reads the app dir.
|
||||
pub(crate) async fn sync_runtime_geo_by_service() {
|
||||
if !active_service_supports_runtime_geo_read() {
|
||||
return;
|
||||
}
|
||||
let (Ok(credentials), Ok(session), Ok(app_dir)) = (
|
||||
current_owner_credentials(),
|
||||
active_service_session(),
|
||||
dirs::app_home_dir(),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
for asset in GEO_ASSETS {
|
||||
if let Err(error) = sync_runtime_geo(&credentials, &session, asset, &app_dir.join(asset)).await {
|
||||
logging!(warn, Type::Service, "failed to sync geo database {asset}: {error:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn sync_runtime_geo(
|
||||
credentials: &OwnerCredentials,
|
||||
session: &OwnerSessionProof,
|
||||
asset: &str,
|
||||
target: &Path,
|
||||
) -> Result<()> {
|
||||
let Ok(local) = tokio::fs::metadata(target).await else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some((Some(_), _, Some(runtime_mtime_ns))) = read_chunk(credentials, session, asset, u64::MAX).await? else {
|
||||
return Ok(());
|
||||
};
|
||||
// A newer local file is a change the next staging still has to copy into the runtime.
|
||||
if UNIX_EPOCH + Duration::from_nanos(runtime_mtime_ns) <= local.modified()? {
|
||||
return Ok(());
|
||||
}
|
||||
let (temp, file) = create_sync_temp(target).await?;
|
||||
let result = async {
|
||||
if let Fetch::Complete {
|
||||
mtime_ns: Some(mtime_ns),
|
||||
..
|
||||
} = fetch_runtime_file(credentials, session, asset, file).await?
|
||||
{
|
||||
// Matching the runtime copy's time lets the service skip copying these bytes straight back.
|
||||
std::fs::File::options()
|
||||
.write(true)
|
||||
.open(&temp)?
|
||||
.set_modified(UNIX_EPOCH + Duration::from_nanos(mtime_ns))?;
|
||||
tokio::fs::rename(&temp, target).await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.await;
|
||||
remove_temp(&temp).await;
|
||||
result
|
||||
}
|
||||
|
||||
// The on-disk YAML may describe a rejected apply; use the committed runtime.
|
||||
fn applied_remote_providers(runtime: &Draft<IRuntime>, app_dir: &Path) -> Result<Vec<RemoteProviderRef>> {
|
||||
let applied = runtime.data_arc();
|
||||
|
||||
@@ -324,6 +324,7 @@ impl CoreConfigValidator {
|
||||
return Self::validate_file_syntax_outcome(config_path).await;
|
||||
}
|
||||
|
||||
crate::core::service::sync_runtime_geo_by_service().await;
|
||||
let is_script = match Self::is_script_file(config_path).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
@@ -425,6 +426,8 @@ impl CoreConfigValidator {
|
||||
}
|
||||
logging!(info, Type::Validate, "生成临时配置文件用于验证");
|
||||
|
||||
// The test run reads the app dir, which never sees updates made by a service core.
|
||||
crate::core::service::sync_runtime_geo_by_service().await;
|
||||
let fingerprint = validation_fingerprint(yaml).await;
|
||||
if *LAST_VALIDATED.lock() == Some(fingerprint) {
|
||||
logging!(info, Type::Validate, "配置与上次通过验证的字节一致,跳过内核检查");
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use chrono::Utc;
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use reqwest_dav::list_cmd::ListFile;
|
||||
use serde::Serialize;
|
||||
use smartstring::alias::String;
|
||||
@@ -88,7 +88,6 @@ async fn finalize_restored_verge_config(
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
logging_error!(Type::Config, Config::sync_dns_override().await);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
utils::help::{mask_err, mask_url},
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use smartstring::alias::String;
|
||||
|
||||
/// Toggle proxy profile
|
||||
@@ -217,10 +217,7 @@ pub async fn update_profile(uid: &String, option: Option<&PrfOption>, is_mannual
|
||||
if should_refresh {
|
||||
logging!(debug, Type::Config, "[订阅更新] 更新内核配置");
|
||||
match CoreManager::global().update_config_with_force(is_mannual_trigger).await {
|
||||
Ok(outcome) if outcome.is_valid() => {
|
||||
logging_error!(Type::Config, Config::sync_dns_override().await);
|
||||
announce(Refresh::Clash);
|
||||
}
|
||||
Ok(outcome) if outcome.is_valid() => announce(Refresh::Clash),
|
||||
Ok(outcome @ (ValidationOutcome::Skipped { .. } | ValidationOutcome::Busy)) if !is_mannual_trigger => {
|
||||
logging!(info, Type::Config, "[订阅更新] 本次配置刷新已跳过: {}", outcome);
|
||||
}
|
||||
@@ -247,9 +244,5 @@ pub async fn update_profile(uid: &String, option: Option<&PrfOption>, is_mannual
|
||||
|
||||
/// 增强配置
|
||||
pub async fn enhance_profiles() -> Result<ValidationOutcome> {
|
||||
let outcome = CoreManager::global().update_config_forced().await?;
|
||||
if outcome.is_valid() {
|
||||
logging_error!(Type::Config, Config::sync_dns_override().await);
|
||||
}
|
||||
Ok(outcome)
|
||||
CoreManager::global().update_config_forced().await
|
||||
}
|
||||
|
||||
@@ -316,6 +316,7 @@ pub async fn hide() {
|
||||
{
|
||||
let _ = window.hide();
|
||||
}
|
||||
handle::Handle::global().set_activation_policy_accessory();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -289,6 +289,9 @@ pub fn run() -> std::process::ExitCode {
|
||||
}
|
||||
|
||||
pub fn handle_window_close(api: &tauri::WindowEvent) {
|
||||
#[cfg(target_os = "macos")]
|
||||
handle::Handle::global().set_activation_policy_accessory();
|
||||
|
||||
if core::handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.5.9",
|
||||
"version": "2.5.10",
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
|
||||
@@ -206,17 +206,20 @@ export const useRenderList = (
|
||||
verge?.default_latency_timeout,
|
||||
])
|
||||
|
||||
const renderGroups = useMemo(() => {
|
||||
if (!proxyView) return []
|
||||
if (mode === 'rule') return proxyView.groups
|
||||
const global = proxyView.global ? [proxyView.global] : []
|
||||
return mode === 'global' ? [...global, ...proxyView.groups] : global
|
||||
}, [mode, proxyView])
|
||||
|
||||
// Every group this list draws, so a test settling in any of them re-sorts that group.
|
||||
const renderedGroupNames = useMemo(() => {
|
||||
if (!proxyView) return []
|
||||
if (isChainMode)
|
||||
return selectedGroup ? [selectedGroup] : [CHAIN_DELAY_GROUP]
|
||||
return mode === 'rule' || mode === 'global'
|
||||
? proxyView.groups.map(({ name }) => name)
|
||||
: proxyView.global
|
||||
? [proxyView.global.name]
|
||||
: []
|
||||
}, [isChainMode, mode, proxyView, selectedGroup])
|
||||
return renderGroups.map(({ name }) => name)
|
||||
}, [isChainMode, proxyView, renderGroups, selectedGroup])
|
||||
const groupDelays = useGroupsDelays(renderedGroupNames)
|
||||
|
||||
const groupCacheRef = useRef<Map<string, GroupCache>>(new Map())
|
||||
@@ -258,13 +261,7 @@ export const useRenderList = (
|
||||
}
|
||||
|
||||
const useRule = mode === 'rule' || mode === 'global'
|
||||
const renderGroups = useRule
|
||||
? proxyView.groups
|
||||
: proxyView.global === null
|
||||
? []
|
||||
: [proxyView.global]
|
||||
const cache = groupCacheRef.current
|
||||
let anyChanged = false
|
||||
|
||||
const retList = renderGroups.flatMap((group) => {
|
||||
const headState = headStates[group.name] || DEFAULT_STATE
|
||||
@@ -281,7 +278,6 @@ export const useRenderList = (
|
||||
return cached.items
|
||||
}
|
||||
|
||||
anyChanged = true
|
||||
const ret: IRenderItem[] = [
|
||||
{
|
||||
type: 0,
|
||||
@@ -350,7 +346,10 @@ export const useRenderList = (
|
||||
const filtered = !useRule
|
||||
? retList.slice(1)
|
||||
: retList.filter((item) => !item.group.hidden)
|
||||
if (!anyChanged && prevListRef.current.length === filtered.length) {
|
||||
if (
|
||||
prevListRef.current.length === filtered.length &&
|
||||
filtered.every((item, index) => item === prevListRef.current[index])
|
||||
) {
|
||||
return prevListRef.current
|
||||
}
|
||||
prevListRef.current = filtered
|
||||
@@ -364,6 +363,7 @@ export const useRenderList = (
|
||||
latencyTimeout,
|
||||
mode,
|
||||
proxyView,
|
||||
renderGroups,
|
||||
selectedGroup,
|
||||
])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user