Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cda1bd7fb |
@@ -1,5 +1,7 @@
|
|||||||
# Gitea Actions CI(统一离线 lite 门禁)
|
# Gitea Actions CI 模板(统一极简合规门禁)
|
||||||
# Runner 无外网且无 pip;这里只运行标准库测试和本地合规检查。
|
# 复制为 <repo>/.gitea/workflows/ci.yml 后即可用;无需调整。
|
||||||
|
# 说明:runner 容器无外网且无 pip(Gitea 实例限制),CI 只做本地可完成的合规检查;
|
||||||
|
# 依赖安装与测试一律在本地运行(开发机上完成)。
|
||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -16,18 +18,13 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lite:
|
check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@524e936cd9e579adf00e308bfdf971aebc7de09e
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Gitea 合规校验
|
- name: Gitea 合规校验
|
||||||
run: |
|
run: |
|
||||||
if git ls-tree -r HEAD | awk '$1==160000' | grep -q .; then echo "HEAD 含 gitlink"; exit 1; fi
|
if git ls-tree -r HEAD | awk '$1==160000' | grep -q .; then echo "HEAD 含 gitlink"; exit 1; fi
|
||||||
if git ls-files .DS_Store | grep -q .; then echo "跟踪 .DS_Store"; exit 1; fi
|
if git ls-files .DS_Store | grep -q .; then echo "跟踪 .DS_Store"; exit 1; fi
|
||||||
if git grep -n -I -E 'sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|xox[baprs]-[A-Za-z0-9-]{10,}' HEAD | grep -q .; then echo "检出疑似凭证"; exit 1; fi
|
if git grep -n -I -E 'sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|xox[baprs]-[A-Za-z0-9-]{10,}' HEAD | grep -q .; then echo "检出疑似凭证"; exit 1; fi
|
||||||
echo "Gitea 合规校验通过"
|
echo "Gitea 合规校验通过"
|
||||||
- name: 规则契约测试
|
|
||||||
run: python3 -m unittest discover -s tests -v
|
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
{
|
|
||||||
"schema_version": 1,
|
|
||||||
"module_id": "vpn-rules",
|
|
||||||
"authority": {
|
|
||||||
"scope": "module_metadata",
|
|
||||||
"subject": "vpn-rules",
|
|
||||||
"owner": "vpn-rules-owner",
|
|
||||||
"source": "MODULE_SPEC.yaml",
|
|
||||||
"revision": 1,
|
|
||||||
"effective_from": "2026-08-20T00:00:00+08:00"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"name": "vpn-rules",
|
|
||||||
"workspace_id": null,
|
|
||||||
"type": "infrastructure",
|
|
||||||
"maturity": "operational"
|
|
||||||
},
|
|
||||||
"bounded_context": {
|
|
||||||
"domain": "network-routing-policy",
|
|
||||||
"responsibility": "Own the single source of truth for direct-routing rules and deterministic projections for supported clients.",
|
|
||||||
"prohibited_responsibilities": [
|
|
||||||
"Applying rules to production clients, routers, VPN services, or proxy runtimes",
|
|
||||||
"Owning or exposing proxy subscription credentials, provider tokens, or device secrets",
|
|
||||||
"Claiming generated or checked-in artifacts are active production configuration"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"capabilities": [
|
|
||||||
{
|
|
||||||
"id": "direct-routing-rule-authoring",
|
|
||||||
"summary": "Maintain provider-neutral direct-routing entries in the authoritative mihomo rule-provider source.",
|
|
||||||
"status": "operational"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "clash-verge-script-generation",
|
|
||||||
"summary": "Render source entries as idempotent DIRECT rules for Clash Verge Script.js.",
|
|
||||||
"status": "operational"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "router-rule-projection",
|
|
||||||
"summary": "Generate policy-bearing YAML and policy-free plain-text projections for router consumers.",
|
|
||||||
"status": "operational"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"data": {
|
|
||||||
"owns": [
|
|
||||||
{
|
|
||||||
"asset_id": "direct-routing-rule-source",
|
|
||||||
"kind": "reference",
|
|
||||||
"classification": "restricted"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"asset_id": "generated-router-rule-projections",
|
|
||||||
"kind": "artifact",
|
|
||||||
"classification": "restricted"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"contracts": {
|
|
||||||
"provides": [
|
|
||||||
{
|
|
||||||
"contract_id": "mihomo-direct-rule-provider",
|
|
||||||
"kind": "artifact",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"path": "direct.yaml",
|
|
||||||
"compatibility": "backward"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contract_id": "router-direct-rules",
|
|
||||||
"kind": "artifact",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"path": "router/direct-rules.yaml",
|
|
||||||
"compatibility": "exact"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contract_id": "router-direct-plain-list",
|
|
||||||
"kind": "artifact",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"path": "router/direct-plain.txt",
|
|
||||||
"compatibility": "exact"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"consumes": []
|
|
||||||
},
|
|
||||||
"dependencies": [],
|
|
||||||
"agent_context": {
|
|
||||||
"default_entrypoints": [
|
|
||||||
{
|
|
||||||
"path": "README.md",
|
|
||||||
"purpose": "Repository purpose, supported consumers, generation commands, and maintenance flow."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "direct.yaml",
|
|
||||||
"purpose": "Authoritative direct-routing rule source and provider format."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"excluded_paths": [
|
|
||||||
"router"
|
|
||||||
],
|
|
||||||
"max_default_tokens": 6000
|
|
||||||
},
|
|
||||||
"verification": {
|
|
||||||
"commands": [
|
|
||||||
{
|
|
||||||
"id": "rule-contract-tests",
|
|
||||||
"argv": [
|
|
||||||
"python3",
|
|
||||||
"-m",
|
|
||||||
"unittest",
|
|
||||||
"discover",
|
|
||||||
"-s",
|
|
||||||
"tests",
|
|
||||||
"-v"
|
|
||||||
],
|
|
||||||
"cwd": ".",
|
|
||||||
"network": false,
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,13 +8,8 @@ Clash Verge 和华硕路由器上的 mihomo(Magic Catling2)等消费端同
|
|||||||
```
|
```
|
||||||
vpn-rules/
|
vpn-rules/
|
||||||
├── direct.yaml # 规则真源(mihomo rule-provider 格式)
|
├── direct.yaml # 规则真源(mihomo rule-provider 格式)
|
||||||
├── router/
|
|
||||||
│ ├── direct-rules.yaml # 路由器手动粘贴版(`- ` 前缀 + DIRECT 策略)
|
|
||||||
│ └── direct-plain.txt # 无 `- ` 前缀纯文本版(规则逐行、无策略)
|
|
||||||
└── scripts/
|
└── scripts/
|
||||||
├── gen_clash_verge_script.py # 真源 → Clash Verge Script.js 生成器
|
└── gen_clash_verge_script.py # 真源 → Clash Verge Script.js 生成器
|
||||||
├── gen_router_paste.py # 真源 → 路由器手动粘贴版生成器
|
|
||||||
└── gen_plain_list.py # 真源 → 纯文本版生成器
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 为什么需要它
|
## 为什么需要它
|
||||||
@@ -56,7 +51,7 @@ rules:
|
|||||||
|
|
||||||
> 路由器内网自建 Gitea(192.168.50.2:3000)也可直连:`http://192.168.50.2:3000/ageorge156/vpn-rules/raw/branch/main/direct.yaml`。
|
> 路由器内网自建 Gitea(192.168.50.2:3000)也可直连:`http://192.168.50.2:3000/ageorge156/vpn-rules/raw/branch/main/direct.yaml`。
|
||||||
|
|
||||||
**拉不到 URL 时(手动粘贴版)**:打开 `router/direct-rules.yaml`,把里面的规则行粘贴到路由器现有规则列表最前面即可,无需联网(生成命令 `python3 scripts/gen_router_paste.py`)。若工具不支持 `- ` 前缀,用 `router/direct-plain.txt`(生成命令 `python3 scripts/gen_plain_list.py`)。
|
**拉不到 URL 时(手动粘贴版)**:打开 `router/direct-rules.yaml`,把里面的规则行粘贴到路由器现有规则列表最前面即可,无需联网(生成命令 `python3 scripts/gen_router_paste.py`)。
|
||||||
|
|
||||||
## 维护流程
|
## 维护流程
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
DOMAIN,frp.puyuanfh.cn
|
|
||||||
IP-CIDR,8.133.177.3/32,no-resolve
|
|
||||||
DOMAIN-SUFFIX,bilibili.com
|
|
||||||
DOMAIN-SUFFIX,bilibili.cn
|
|
||||||
DOMAIN-SUFFIX,bilivideo.com
|
|
||||||
DOMAIN-SUFFIX,bili2233.cn
|
|
||||||
DOMAIN-SUFFIX,hdslb.com
|
|
||||||
GEOIP,CN,no-resolve
|
|
||||||
DOMAIN-SUFFIX,deepseek.com
|
|
||||||
DOMAIN-SUFFIX,minimaxi.com
|
|
||||||
DOMAIN-SUFFIX,minimax.chat
|
|
||||||
DOMAIN-SUFFIX,hailuoai.com
|
|
||||||
DOMAIN,minimax-public-cdn.oss-cn-wulanchabu.aliyuncs.com
|
|
||||||
DOMAIN-SUFFIX,hf-mirror.com
|
|
||||||
DOMAIN-SUFFIX,cdn.hf.co
|
|
||||||
DOMAIN-SUFFIX,lmstudio.ai
|
|
||||||
IP-CIDR,127.0.0.0/8,no-resolve
|
|
||||||
IP-CIDR,10.0.0.0/8,no-resolve
|
|
||||||
IP-CIDR,172.16.0.0/12,no-resolve
|
|
||||||
IP-CIDR,192.168.0.0/16,no-resolve
|
|
||||||
IP-CIDR,100.64.0.0/10,no-resolve
|
|
||||||
DOMAIN-SUFFIX,.local
|
|
||||||
DOMAIN-SUFFIX,icloud.com.cn
|
|
||||||
DOMAIN-SUFFIX,ug.link
|
|
||||||
DOMAIN-SUFFIX,ugreen.com
|
|
||||||
DOMAIN-SUFFIX,ugnas.cloud
|
|
||||||
DOMAIN-SUFFIX,ugos.cn
|
|
||||||
DOMAIN-KEYWORD,ugreen
|
|
||||||
DOMAIN-KEYWORD,ugnas
|
|
||||||
DOMAIN-KEYWORD,ugreengroup
|
|
||||||
DOMAIN-SUFFIX,syncthing.net
|
|
||||||
IP-CIDR,110.42.0.0/16,no-resolve
|
|
||||||
IP-CIDR,43.248.128.0/17,no-resolve
|
|
||||||
IP-CIDR,111.170.0.0/16,no-resolve
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""从 direct.yaml(真源)生成无 `- ` 前缀的纯文本规则列表。
|
|
||||||
|
|
||||||
格式:每条规则一行,无 YAML 列表前缀、无注释、无策略字段
|
|
||||||
(同 rule-provider payload 内容,只是去掉 `- `)。
|
|
||||||
|
|
||||||
适用于把规则直接粘贴进不支持 YAML 列表 / 不需要策略前缀的工具。
|
|
||||||
"""
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from gen_clash_verge_script import load_payload
|
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
OUT = REPO_ROOT / "router" / "direct-plain.txt"
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
payload = load_payload(REPO_ROOT / "direct.yaml")
|
|
||||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
OUT.write_text("\n".join(payload) + "\n", encoding="utf-8")
|
|
||||||
print(f"✅ 已生成 {OUT}({len(payload)} 条规则,无 - 前缀)")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
|
|
||||||
class CiContractTests(unittest.TestCase):
|
|
||||||
def test_ci_publishes_one_offline_lite_gate(self) -> None:
|
|
||||||
workflow = (ROOT / ".gitea/workflows/ci.yml").read_text(encoding="utf-8")
|
|
||||||
job_block = workflow.split("jobs:", 1)[1]
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
re.findall(r"(?m)^ ([a-z][a-z0-9_-]*):\s*$", job_block),
|
|
||||||
["lite"],
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"actions/checkout@524e936cd9e579adf00e308bfdf971aebc7de09e",
|
|
||||||
workflow,
|
|
||||||
)
|
|
||||||
self.assertIn("persist-credentials: false", workflow)
|
|
||||||
self.assertIn(
|
|
||||||
"python3 -m unittest discover -s tests -v",
|
|
||||||
workflow,
|
|
||||||
)
|
|
||||||
for forbidden in (
|
|
||||||
"actions/checkout@v",
|
|
||||||
"apt ",
|
|
||||||
"pip ",
|
|
||||||
"curl ",
|
|
||||||
"wget ",
|
|
||||||
"docker pull",
|
|
||||||
):
|
|
||||||
self.assertNotIn(forbidden, workflow)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path, PurePosixPath
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleSpecTests(unittest.TestCase):
|
|
||||||
def test_module_spec_declares_rule_authority_and_safe_context(self) -> None:
|
|
||||||
spec = json.loads(SPEC_PATH.read_text(encoding="utf-8"))
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
set(spec),
|
|
||||||
{
|
|
||||||
"schema_version",
|
|
||||||
"module_id",
|
|
||||||
"authority",
|
|
||||||
"repository",
|
|
||||||
"bounded_context",
|
|
||||||
"capabilities",
|
|
||||||
"data",
|
|
||||||
"contracts",
|
|
||||||
"dependencies",
|
|
||||||
"agent_context",
|
|
||||||
"verification",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(spec["schema_version"], 1)
|
|
||||||
self.assertEqual(spec["module_id"], "vpn-rules")
|
|
||||||
self.assertEqual(
|
|
||||||
spec["authority"],
|
|
||||||
{
|
|
||||||
"scope": "module_metadata",
|
|
||||||
"subject": "vpn-rules",
|
|
||||||
"owner": "vpn-rules-owner",
|
|
||||||
"source": "MODULE_SPEC.yaml",
|
|
||||||
"revision": 1,
|
|
||||||
"effective_from": "2026-08-20T00:00:00+08:00",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
spec["repository"],
|
|
||||||
{
|
|
||||||
"name": "vpn-rules",
|
|
||||||
"workspace_id": None,
|
|
||||||
"type": "infrastructure",
|
|
||||||
"maturity": "operational",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
boundary = spec["bounded_context"]
|
|
||||||
self.assertEqual(boundary["domain"], "network-routing-policy")
|
|
||||||
self.assertIn("single source of truth", boundary["responsibility"].lower())
|
|
||||||
prohibited = " ".join(boundary["prohibited_responsibilities"]).lower()
|
|
||||||
self.assertIn("production", prohibited)
|
|
||||||
self.assertIn("credential", prohibited)
|
|
||||||
self.assertIn("proxy subscription", prohibited)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
{capability["id"] for capability in spec["capabilities"]},
|
|
||||||
{
|
|
||||||
"direct-routing-rule-authoring",
|
|
||||||
"clash-verge-script-generation",
|
|
||||||
"router-rule-projection",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(spec["contracts"]["consumes"], [])
|
|
||||||
self.assertEqual(spec["dependencies"], [])
|
|
||||||
provided = {
|
|
||||||
contract["contract_id"]: contract
|
|
||||||
for contract in spec["contracts"]["provides"]
|
|
||||||
}
|
|
||||||
self.assertEqual(
|
|
||||||
set(provided),
|
|
||||||
{
|
|
||||||
"mihomo-direct-rule-provider",
|
|
||||||
"router-direct-rules",
|
|
||||||
"router-direct-plain-list",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
{contract["version"] for contract in provided.values()}, {"1.0.0"}
|
|
||||||
)
|
|
||||||
for contract in provided.values():
|
|
||||||
self.assertTrue((ROOT / contract["path"]).is_file())
|
|
||||||
|
|
||||||
context = spec["agent_context"]
|
|
||||||
self.assertLessEqual(context["max_default_tokens"], 6000)
|
|
||||||
self.assertEqual(
|
|
||||||
{entry["path"] for entry in context["default_entrypoints"]},
|
|
||||||
{"README.md", "direct.yaml"},
|
|
||||||
)
|
|
||||||
for entry in context["default_entrypoints"]:
|
|
||||||
self.assertTrue((ROOT / entry["path"]).is_file())
|
|
||||||
self.assertIn("router", context["excluded_paths"])
|
|
||||||
for value in context["excluded_paths"]:
|
|
||||||
path = PurePosixPath(value)
|
|
||||||
self.assertFalse(path.is_absolute())
|
|
||||||
self.assertNotIn("..", path.parts)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
spec["verification"],
|
|
||||||
{
|
|
||||||
"commands": [
|
|
||||||
{
|
|
||||||
"id": "rule-contract-tests",
|
|
||||||
"argv": [
|
|
||||||
"python3",
|
|
||||||
"-m",
|
|
||||||
"unittest",
|
|
||||||
"discover",
|
|
||||||
"-s",
|
|
||||||
"tests",
|
|
||||||
"-v",
|
|
||||||
],
|
|
||||||
"cwd": ".",
|
|
||||||
"network": False,
|
|
||||||
"required": True,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_checked_in_router_projections_match_the_rule_source(self) -> None:
|
|
||||||
payload = load_payload(ROOT / "direct.yaml")
|
|
||||||
plain = (ROOT / "router/direct-plain.txt").read_text(encoding="utf-8")
|
|
||||||
router = (ROOT / "router/direct-rules.yaml").read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
self.assertEqual(plain, "\n".join(payload) + "\n")
|
|
||||||
self.assertEqual(
|
|
||||||
router.split("rules:\n", 1)[1],
|
|
||||||
"\n".join(f" - {to_rule(entry)}" for entry in payload) + "\n",
|
|
||||||
)
|
|
||||||
self.assertTrue(all(",DIRECT" not in entry for entry in payload))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
Reference in New Issue
Block a user