Files
Su YijunandClaude Opus 5.5 47674ecd3e feat: 接入 blackmatrix7 / Hagezi / UT1 识别库并定好顺序与权重,修复外部数据源未启用(debug12)
- lists.py:blackmatrix7(应用级,bm7_map.yaml 对应约 560 个目录)、Hagezi / UT1(分类级,
  64 位哈希表约 7MB),运行时下载,不进仓库和镜像;新分类“赌博博彩”与 gen_* 兜底应用
- 顺序:应用级域名(内置 / v2fly / bm7 最长后缀,平局按先后)→ 应用级 IP → 分类级(Hagezi > UT1)
  → 国内域名列表 → IP 段兜底;Tranco 前 1 万能认出类别 19.6% → 35.7%
- 修复:0.6.0 起主域名回填每次启动报错,导致外部数据源、自定义规则、LAN 设置都没加载;
  启动初始化各步互相独立

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:46:59 +08:00

110 lines
6.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""debug12:第三方规则库 blackmatrix7(应用级)、Hagezi / UT1(分类级)的识别顺序与权重。"""
import pytest
from app.classify import Classifier
from app.classify import lists as L
from app.classify.lists import _HashTable, lists
from app.classify.sources import sources as src
@pytest.fixture
def loaded(tmp_path, monkeypatch):
"""小份样本:bm7 三个目录、Hagezi 两个列表、UT1 两个列表、国内域名列表"""
bm7 = tmp_path / "lists" / "bm7"
bm7.mkdir(parents=True)
(bm7 / "BiliBili.list").write_text("# NAME: BiliBili\nDOMAIN-SUFFIX,zz-bili-extra.example\nDOMAIN,cdn.zz-bili-full.example\n"
"DOMAIN-KEYWORD,bili\nPROCESS-NAME,tv.danmaku.bili\n", encoding="utf-8")
# 同样长的后缀:内置库(bilibili.com → bilibili)优先;更长的后缀:bm7 赢
(bm7 / "TencentVideo.list").write_text("DOMAIN-SUFFIX,bilibili.com\nDOMAIN-SUFFIX,vtest.qq.com\n", encoding="utf-8")
(bm7 / "Zalo.list").write_text("DOMAIN-SUFFIX,zalo-test.example\nIP-CIDR,198.18.77.0/24,no-resolve\n"
"IP-CIDR,91.108.4.0/22,no-resolve\n", encoding="utf-8")
hg = tmp_path / "lists" / "hagezi"
hg.mkdir(parents=True)
(hg / "nsfw.txt").write_text("# Hagezi\nadult-test.example\nbilibili.com\n", encoding="utf-8")
(hg / "gambling.medium.txt").write_text("both-lists.example\n*.wild-test.example\n", encoding="utf-8")
(hg / "social.txt").write_text("example.org\n", encoding="utf-8")
ut = tmp_path / "lists" / "ut1"
ut.mkdir(parents=True)
(ut / "games.txt").write_text("game-test.example\nboth-lists.example\ncn-game.example-test.cn\n0123456789.com\n1.2.3.4\n",
encoding="utf-8")
(ut / "shopping.txt").write_text("shop.example.org\n", encoding="utf-8")
(tmp_path / "china-domains.conf").write_text("server=/example-test.cn/114.114.114.114\n", encoding="utf-8")
monkeypatch.setenv("DATA_DIR", str(tmp_path))
monkeypatch.setattr(L, "_enabled", lambda sid: True)
monkeypatch.setattr("app.classify.sources._enabled", lambda sid: True)
src.reset()
src.load()
lists.reset()
lists.load()
yield lists
lists.reset()
src.reset()
def test_bm7_app_level(loaded):
c = Classifier()
# 补内置库 / v2fly 没有的域名
assert c.classify("203.0.113.9", 6, 443, "a.zz-bili-extra.example", None) == "bilibili"
assert c.classify("203.0.113.9", 6, 443, "cdn.zz-bili-full.example", None) == "bilibili"
assert c.classify("203.0.113.9", 6, 443, "x.cdn.zz-bili-full.example", None) == "https" # DOMAIN 是精确匹配
# 后缀一样长:内置库优先;bm7 的后缀更长:bm7 赢
assert c.classify("203.0.113.9", 6, 443, "www.bilibili.com", None) == "bilibili"
assert c.classify("203.0.113.9", 6, 443, "x.vtest.qq.com", None) == "tencent_video"
assert c.classify("203.0.113.9", 6, 443, "x.qq.com", None) == "qq"
# bm7 新增的应用带分类
assert c.classify("203.0.113.9", 6, 443, "chat.zalo-test.example", None) == "bm7_zalo"
assert c.app_info("bm7_zalo").category == "im"
# bm7 的 IP 段:在内置 CIDR 之后(91.108.4.0/22 内置属于 Telegram)
assert c.classify("198.18.77.5", 6, 443, None, None) == "bm7_zalo"
assert c.classify("91.108.4.10", 6, 443, None, None) == "telegram"
def test_category_lists_order(loaded):
c = Classifier()
assert c.classify("203.0.113.9", 6, 443, "adult-test.example", None) == "gen_adult"
assert c.app_info("gen_adult").category == "adult"
assert c.classify("203.0.113.9", 6, 443, "www.game-test.example", None) == "gen_game"
assert c.classify("203.0.113.9", 6, 443, "a.wild-test.example", None) == "gen_gambling" # *. 前缀
# 两个列表都有:Hagezi 优先
assert c.classify("203.0.113.9", 6, 443, "both-lists.example", None) == "gen_gambling"
assert c.app_info("gen_gambling").category == "gambling"
# 命中的后缀更长者赢(不论来自哪个列表)
assert c.classify("203.0.113.9", 6, 443, "a.shop.example.org", None) == "gen_shopping"
assert c.classify("203.0.113.9", 6, 443, "a.example.org", None) == "gen_social"
# 应用级永远优先:bilibili.com 在成人列表里也不影响
assert c.classify("203.0.113.9", 6, 443, "www.bilibili.com", None) == "bilibili"
# 分类级排在国内域名列表之前
assert c.classify("203.0.113.9", 6, 443, "cn-game.example-test.cn", None) == "gen_game"
assert c.classify("203.0.113.9", 6, 443, "other.example-test.cn", None) == "cn_web"
# v2fly 的地区兜底条目由分类列表细化
assert c.classify("203.0.113.9", 6, 443, "www.0123456789.com", None) == "gen_game"
# IP 写在列表里会被忽略;列表外的域名仍走通用端口
assert c.classify("1.2.3.4", 6, 443, None, None) == "https"
assert c.classify("203.0.113.9", 6, 443, "nothing.example", None) == "https"
def test_cache_cleared_on_reload(loaded):
c = Classifier()
assert c.classify("203.0.113.9", 6, 443, "adult-test.example", None) == "gen_adult"
lists.reset()
assert c.classify("203.0.113.9", 6, 443, "adult-test.example", None) == "https"
def test_rows_and_hash_table(loaded):
rows = {r["id"]: r for r in lists.rows()}
assert rows["bm7"]["rules"] == 7 and rows["bm7"]["apps"] == 3
assert rows["hagezi"]["domains"] == 5 and rows["ut1"]["domains"] == 5
t = _HashTable.build([("a", ["x.example", "y.example"]), ("b", ["y.example", "z.example"])])
assert len(t) == 3 and t.get("y.example") == "a" and t.get("z.example") == "b" and t.get("q.example") is None
assert t.lookup("deep.sub.x.example") == ("a", 2)
def test_map_targets_exist():
"""bm7_map.yaml 里并入的应用都存在(v2fly 换代删了某个应用时这里会报出来),新应用的分类都存在"""
c = Classifier(extra_file=None)
full = Classifier()
m = L.load_map()
missing = sorted({a for a in m["owner"].values() if a not in full.apps})
assert missing == []
assert all(a["category"] in c.categories for a in m["apps"].values())