Files
Su YijunandClaude Opus 5.5 ace2eaa3a5 feat: debug7/8 接入 sing-box 与 MosDNS、ASN 识别、识别库自动更新、全站布局审计(v0.3.0)
识别准确度(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>
2026-10-23 00:47:35 +08:00

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"