识别准确度(debug7) - 可选接入 sing-box Clash API:按来源终端 + 域名 + 字节记账,代理隧道与 FakeIP 流量不重复计算;未接入时代理机器流量标为旁路代理隧道 - 可选接入 MosDNS 审计日志:终端 + IP → 域名 - 地址与密钥由用户在 系统设置 → 外部数据源 填写(或环境变量),不填不启用 - IP→ASN 库(iptoasn.com,PDDL,运行时每天下载),无域名流量按归属识别;实时连接 / 访问目标显示归属与国家 - 健康指数新增旁路代理提示;概览应用数区分今日出现与规则库总数 识别库自动更新(debug8) - 每天检查 v2fly 最新提交,下载转换后自检(抽样域名、应用数),通过后热加载,失败保留旧库 - 识别规则页新增识别库面板(来源、版本、规模、许可证、检查结果、自动更新开关) - 转换逻辑移到 app/classify/v2fly.py,生成文件记录来源提交 布局审计 - 手机上表格隐藏次要列、名称下补充关键信息;子标签横向滑动;统计卡改为卡片样式;平板断点调整 - 系统设置拆为四个子标签;识别规则页整理;重复名称只显示一次;少量数据点的折线显示数据点 - 测试环境不启动后台定时任务,修复整点落库导致的偶发失败 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
56 lines
2.6 KiB
Python
56 lines
2.6 KiB
Python
import os
|
|
|
|
import yaml
|
|
|
|
from app.classify import V2FLY_FILE, Classifier, active_v2fly_file
|
|
from app.classify import updater as upd
|
|
from app.classify import v2fly
|
|
|
|
|
|
def _files():
|
|
"""一份极小的 v2fly data:一个分类 + 两个列表"""
|
|
return {
|
|
"category-games": "include:steam\ninclude:newgame\n",
|
|
"steam": "steampowered.com\nsteamcontent.com\n",
|
|
"newgame": "# 新游戏\nnewgame-example.com\n",
|
|
}
|
|
|
|
|
|
def test_convert_and_source_header(tmp_path):
|
|
doc, st = v2fly.convert(_files())
|
|
assert any(a["name"] == "新游戏" and a["category"] == "game" for a in doc["apps"])
|
|
p = tmp_path / "out.yaml"
|
|
v2fly.write(doc, str(p), {"sha": "abc123", "date": "2030-01-01T00:00:00Z"})
|
|
assert v2fly.read_source(str(p)) == {"sha": "abc123", "date": "2030-01-01T00:00:00Z"}
|
|
assert yaml.safe_load(p.read_text(encoding="utf-8"))["apps"]
|
|
|
|
|
|
def test_updater_rejects_bad_data_and_keeps_old(monkeypatch, tmp_path):
|
|
monkeypatch.setenv("DATA_DIR", str(tmp_path))
|
|
u = upd.RulesUpdater()
|
|
monkeypatch.setattr(v2fly, "latest_commit", lambda: {"sha": "f" * 40, "date": "2099-01-01T00:00:00Z"})
|
|
monkeypatch.setattr(v2fly, "fetch", lambda ref: _files()) # 只有 3 个列表:应用数骤减,自检必须拒绝
|
|
reloaded = []
|
|
st = u.check(force=True, reload=lambda: reloaded.append(1))
|
|
assert st["result"] == "rejected" and "应用数" in st["error"] and not reloaded
|
|
assert not os.path.exists(tmp_path / "apps_v2fly.yaml") and active_v2fly_file() == V2FLY_FILE
|
|
|
|
|
|
def test_updater_installs_newer_rules(monkeypatch, tmp_path):
|
|
monkeypatch.setenv("DATA_DIR", str(tmp_path))
|
|
u = upd.RulesUpdater()
|
|
# 用打包的完整规则 + 一个新应用模拟“上游有更新”
|
|
full = yaml.safe_load(open(V2FLY_FILE, encoding="utf-8"))
|
|
full["apps"].append({"id": "v2_brand_new", "name": "全新应用", "name_en": "Brand new", "category": "game",
|
|
"domains": ["brand-new-game.example"]})
|
|
monkeypatch.setattr(v2fly, "latest_commit", lambda: {"sha": "e" * 40, "date": "2099-01-01T00:00:00Z"})
|
|
monkeypatch.setattr(v2fly, "fetch", lambda ref: {})
|
|
monkeypatch.setattr(v2fly, "convert", lambda files: (full, {"apps": len(full["apps"]), "extend": 1, "domains": 1}))
|
|
reloaded = []
|
|
st = u.check(force=True, reload=lambda: reloaded.append(1))
|
|
assert st["result"] == "updated" and reloaded == [1]
|
|
assert active_v2fly_file() == str(tmp_path / "apps_v2fly.yaml")
|
|
assert Classifier().match_domain("brand-new-game.example") == "v2_brand_new"
|
|
# 同一个提交再检查:已是最新
|
|
assert u.check(force=True)["result"] == "uptodate"
|