fix: keep OneDrive on direct routing (#7)
CI / lite (push) Successful in 9s

## 变更

- 将 `OneDrive.exe` 设为全局 `DIRECT`,优先于供应商 Microsoft 规则
- 为 Clash Verge 全局脚本补充国内可达的阿里 DNS / 腾讯 DNS DoH fallback
- 同步更新路由器规则投影与跨平台生成说明

## 验证

- `python -m unittest discover -s tests -v`:7 项通过
- mihomo DNS API 成功解析 OneDrive 目标
- 实际日志确认 `OneDrive.exe -> skydrive.wns.windows.com:443` 命中 `ProcessName(OneDrive.exe)` 并使用 `DIRECT`

Reviewed-on: http://192.168.50.2:3000/ageorge156/vpn-rules/pulls/7
This commit was merged in pull request #7.
This commit is contained in:
2026-08-30 16:55:17 +08:00
parent de26a0edc0
commit 854b8e1cdd
6 changed files with 132 additions and 10 deletions
+7 -3
View File
@@ -20,22 +20,26 @@ vpn-rules/
## 为什么需要它
- 香港代理节点有流量配额(如 BygCloud 100GiB 用满),国内站点/内网流量被隧道进代理会白白烧配额。
- 必须直连的对象:FRP 控制面(mTLS)、B 站全系、DeepSeek/MiniMax 等国内 LLM API、
- 必须直连的对象:Windows OneDrive 客户端、FRP 控制面(mTLS)、B 站全系、DeepSeek/MiniMax 等国内 LLM API、
内网/私有 IP 段、国区 iCloud、UGREEN 绿联 NAS 云。
- **踩坑**:Clash Verge 全局 `Merge.yaml` 的 `prepend-rules` 会被内核忽略(不生效),
全局直连规则的唯一正确机制是 `profiles/Script.js`(`main(config)` 把规则 prepend 进 `config.rules`)。
## 消费端接入
### Clash Verge(macOS)
### Clash Verge(Windows / macOS)
```bash
# 生成并写入 Clash Verge 数据目录的 Script.js
# 按当前平台生成并写入 Clash Verge 数据目录的全局 Script.js
python3 scripts/gen_clash_verge_script.py
# 然后在 Clash Verge 界面点「重启内核」生效
```
对当前及以后新增/切换的所有订阅生效;订阅更新不会覆盖它。
脚本还会把国内可达的阿里 DNS 和腾讯 DNS DoH 端点补入 `dns.fallback`,避免境外域名
命中 `DIRECT` 后因供应商的境外 DNS fallback 不可达而解析失败。
Windows 默认写入 `%APPDATA%\io.github.clash-verge-rev.clash-verge-rev\profiles\Script.js`;
macOS 默认写入 `~/Library/Application Support/io.github.clash-verge-rev.clash-verge-rev/profiles/Script.js`。
### 路由器 mihomo(Magic Catling2 / OpenWrt)
+3
View File
@@ -20,6 +20,9 @@
# 路由器端则等 rule-provider 的 interval 到期或手动 reload。
payload:
# Windows OneDrive 客户端始终直连,避免被供应商 Microsoft 规则送入代理
- PROCESS-NAME,OneDrive.exe
# FRP 控制面(mTLS)保持直连
- DOMAIN,frp.puyuanfh.cn
- IP-CIDR,8.133.177.3/32,no-resolve
+1
View File
@@ -1,3 +1,4 @@
PROCESS-NAME,OneDrive.exe
DOMAIN,frp.puyuanfh.cn
IP-CIDR,8.133.177.3/32,no-resolve
DOMAIN-SUFFIX,bilibili.com
+1
View File
@@ -11,6 +11,7 @@
# 内网源:http://192.168.50.2:3000/ageorge156/vpn-rules/raw/branch/main/direct.yaml
rules:
- PROCESS-NAME,OneDrive.exe,DIRECT
- DOMAIN,frp.puyuanfh.cn,DIRECT
- IP-CIDR,8.133.177.3/32,DIRECT,no-resolve
- DOMAIN-SUFFIX,bilibili.com,DIRECT
+49 -6
View File
@@ -4,14 +4,39 @@
用法:
python3 scripts/gen_clash_verge_script.py [--out 目标路径]
默认输出到 Clash Verge 数据目录 profiles/Script.js(全局直连规则的唯一正确机制,
见 clash-verge-config 记忆)。生成后需在 Clash Verge 界面点「重启内核」生效。
默认按 Windows/macOS 平台输出到 Clash Verge 数据目录 profiles/Script.js(全局直连
规则的唯一正确机制)。生成后需在 Clash Verge 界面点「重启内核」生效。
"""
import argparse
import os
from pathlib import Path
import sys
REPO_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_OUT = Path.home() / "Library/Application Support/io.github.clash-verge-rev.clash-verge-rev/profiles/Script.js"
APP_ID = "io.github.clash-verge-rev.clash-verge-rev"
def default_output_path(
platform_name: str = sys.platform,
*,
home: Path | None = None,
appdata: Path | None = None,
) -> Path:
"""返回当前平台的 Clash Verge 全局 Script.js 路径。"""
home = home or Path.home()
if platform_name == "win32":
if appdata is None:
roaming = os.environ.get("APPDATA")
if not roaming:
raise SystemExit("[error] Windows 环境缺少 APPDATA,无法定位 Clash Verge 数据目录")
appdata = Path(roaming)
return appdata / APP_ID / "profiles" / "Script.js"
if platform_name == "darwin":
return home / "Library/Application Support" / APP_ID / "profiles" / "Script.js"
return home / ".local/share" / APP_ID / "profiles" / "Script.js"
DEFAULT_OUT = default_output_path()
def load_payload(path: Path) -> list[str]:
@@ -22,7 +47,7 @@ def load_payload(path: Path) -> list[str]:
if line.startswith("- "):
payload.append(line[2:].strip())
if not payload:
raise SystemExit(f"❌ direct.yaml 里没有 payload 条目: {path}")
raise SystemExit(f"[error] direct.yaml 里没有 payload 条目: {path}")
return payload
@@ -37,19 +62,37 @@ def render_script(payload: list[str]) -> str:
rules = [f" {to_rule(e)!r}," for e in payload]
lines = "\n".join(rules)
return f"""// 由 vpn-rules/direct.yaml 生成 — 勿手改,改真源后重跑 gen_clash_verge_script.py。
// 全局直连规则:FRP 控制面、B站、国内 LLM、内网段、国区 iCloud、UGREEN 一律 DIRECT。
// 全局直连规则:OneDrive、FRP 控制面、B站、国内 LLM、内网段、国区 iCloud、UGREEN 一律 DIRECT。
const prependRules = [
{lines}
];
// 国内可达的 DoH 同时作为 fallback,确保 OneDrive 等境外域名在 DIRECT 出站前可解析。
const directDnsFallbacks = [
'https://dns.alidns.com/dns-query',
'https://doh.pub/dns-query',
];
function main(config, profileName) {{
const existingRules = Array.isArray(config.rules) ? config.rules : [];
const existingRuleSet = new Set(existingRules);
const dns = config.dns && typeof config.dns === 'object' && !Array.isArray(config.dns)
? config.dns
: {{}};
const existingFallbacks = Array.isArray(dns.fallback)
? dns.fallback
: (typeof dns.fallback === 'string' ? [dns.fallback] : []);
const existingFallbackSet = new Set(existingFallbacks);
config.rules = [
...prependRules.filter((rule) => !existingRuleSet.has(rule)),
...existingRules,
];
dns.fallback = [
...directDnsFallbacks.filter((server) => !existingFallbackSet.has(server)),
...existingFallbacks,
];
config.dns = dns;
return config;
}}
@@ -66,7 +109,7 @@ def main() -> None:
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(script, encoding="utf-8")
print(f"✅ 已生成 {out}({len(payload)} 条规则)")
print(f"[ok] 已生成 {out}({len(payload)} 条规则)")
print(" 请在 Clash Verge 界面点「重启内核」生效。")
+71 -1
View File
@@ -1,7 +1,10 @@
from __future__ import annotations
import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path, PurePosixPath
@@ -10,10 +13,60 @@ ROOT = Path(__file__).resolve().parents[1]
SPEC_PATH = ROOT / "MODULE_SPEC.yaml"
sys.path.insert(0, str(ROOT / "scripts"))
from gen_clash_verge_script import load_payload, to_rule # noqa: E402
from gen_clash_verge_script import ( # noqa: E402
default_output_path,
load_payload,
render_script,
to_rule,
)
class ModuleSpecTests(unittest.TestCase):
def test_clash_verge_default_output_path_is_platform_aware(self) -> None:
home = Path("C:/Users/example")
appdata = Path("C:/Users/example/AppData/Roaming")
self.assertEqual(
default_output_path("win32", home=home, appdata=appdata),
appdata
/ "io.github.clash-verge-rev.clash-verge-rev"
/ "profiles"
/ "Script.js",
)
self.assertEqual(
default_output_path("darwin", home=home),
home
/ "Library/Application Support"
/ "io.github.clash-verge-rev.clash-verge-rev"
/ "profiles"
/ "Script.js",
)
def test_clash_verge_generator_supports_windows_gbk_console(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
output = Path(temp_dir) / "Script.js"
env = os.environ.copy()
env["PYTHONIOENCODING"] = "gbk"
result = subprocess.run(
[
sys.executable,
str(ROOT / "scripts/gen_clash_verge_script.py"),
"--out",
str(output),
],
cwd=ROOT,
env=env,
capture_output=True,
check=False,
)
self.assertEqual(
result.returncode,
0,
result.stderr.decode("gbk", errors="replace"),
)
self.assertTrue(output.is_file())
def test_module_spec_declares_rule_authority_and_safe_context(self) -> None:
spec = json.loads(SPEC_PATH.read_text(encoding="utf-8"))
@@ -141,6 +194,23 @@ class ModuleSpecTests(unittest.TestCase):
)
self.assertTrue(all(",DIRECT" not in entry for entry in payload))
def test_onedrive_process_is_forced_to_use_direct_routing(self) -> None:
payload = load_payload(ROOT / "direct.yaml")
self.assertIn("PROCESS-NAME,OneDrive.exe", payload)
self.assertEqual(
to_rule("PROCESS-NAME,OneDrive.exe"),
"PROCESS-NAME,OneDrive.exe,DIRECT",
)
def test_clash_verge_script_adds_reachable_direct_dns_fallbacks(self) -> None:
script = render_script(["PROCESS-NAME,OneDrive.exe"])
self.assertIn("const directDnsFallbacks", script)
self.assertIn("https://dns.alidns.com/dns-query", script)
self.assertIn("https://doh.pub/dns-query", script)
self.assertIn("config.dns = dns", script)
if __name__ == "__main__":
unittest.main()