Wendy's Blog

Blink:把规则仓库做成产品的工程化实践

6 分钟技术向项目

Blink:把规则仓库做成产品的工程化实践

这篇讲实现:同一思想(规则类型即 DNS 语义、domain-first / IP-last 不变式)如何落地。Blink (github.com/Bluetrae/Blink) 是那套思想的完整落地:29 个 App、7 个客户端、 203 个生成产物、每日自动构建、9 道机器门禁。本文按架构层级拆解,重点看 "可验证性"和"变化可控"这两个词是怎么被写进代码的。


0. 一句话概括

Blink 做了一件事:把"个人维护的规则集"从手工劳作变成一条自动流水线, 同时让"正确性"不再是常识,而是机器断言。

它分两层:

  • 规则层(自动维护):审计上游 → canonical 规则模型 → 渲染七种客户端格式,每日更新;
  • 配置层(人工维护):一份意图 + 模板 → 生成七端完整配置文件,人工确认后提交。

生成目录只由构建器写入,绝不手工修改;仓库不含订阅 URL、token、密码或证书。


1. 核心抽象:canonical 规则模型

规则层的第一性原理是:客户端之间共享的是语义,不是文本。

# engine/scripts/build.py
# Canonical rule kinds. The names happen to match Surge vocabulary, but the
# model is client-neutral: renderers own every client-specific serialization.
ALLOWED_RULE_TYPES = (
    "DOMAIN",
    "DOMAIN-SUFFIX",
    "DOMAIN-KEYWORD",
    "USER-AGENT",
    "PROCESS-NAME",
    "IP-CIDR",
    "IP-CIDR6",
)

七个白名单类型是唯一事实源。渲染层负责所有客户端差异:

渲染器 用途
render_classical Surge / Loon / Shadowrocket / Stash 四端,逐字节相同
render_classical_clash 同 classical,去掉 USER-AGENT
render_egern_yaml Egern 自有 YAML schema
render_quantumultx Quantumult X filter 行

这套"数据与序列化分离"的价值在于:任何客户端格式变化, 只改一个 renderer,而不是 29 个 App × 7 个客户端的手工副本。


2. 语义视图:规则类型即 DNS 语义

每个 App 默认输出三类视图,这是上一篇的 domain-first / IP-last 不变式的实现:

  • <App>-domainset.conf — 纯域名段(DOMAIN / DOMAIN-SUFFIX),不触发本地 DNS;
  • <App>-nonip.conf — 非 IP 段(含 keyword / UA / process),不触发 DNS;
  • <App>-ip.conf — IP 段(IP-CIDR / IP-CIDR6),触发 DNS,必须置后

视图的一致性有专门门禁守护:

python engine/scripts/validate_views.py --root .
# 断言:IP 不进 nonip、domain 不进 ip、纯域名 App 不产生空 ip 视图、
#       七端齐全、文件头统计(含显式丢弃)正确

还有一个值得抄的细节:README 用大段 [!WARNING] 说明"文件后缀就是引用方式的答案" (-domainset.confDOMAIN-SET-nonip.conf/-ip.confRULE-SET、IP 段加 no-resolve)——写反后 Surge 不会报错,流量会静默落到 FINAL。 好的文档不只写"怎么做",更写"做错会怎样、如何自查"。


3. 九道机器门禁:把正确性变成断言

engine/docs/MACHINE_GATES.md 是整套工程承诺的机器化清单:

门禁 命令 断言
单元与回归 python -m unittest discover -s engine/tests Parser / renderer / Profile / 变化阈值 / 故障注入
七端等价性 parity_check.py --root . --strict 四端逐字节相同;Clash 去 UA;Egern/QX 去 PROCESS-NAME
产物健康度 health_check.py --root . 非空、合法、无重复、确定性排序、头统计正确
语义多视图一致 validate_views.py --root . 视图类型合法、与 canonical 拆分一致、七端齐全
产物溯源 verify_manifest.py --root . 29 App + 七端 + supplement + 构建器 SHA256 完整一致
Profile 完整性 verify_profiles.py --root . 七端配置可由 intent/templates 逐字节重建
跨 App overlap overlap_check.py --root . 相对人工基线不得出现重叠
敏感模式 secret_scan.py --root . PAT / 私钥 / 代理 URI / 订阅 URL / 本地绝对路径零容忍
实时重建 drift build.py --verify-only --strict-diff 重抓上游并逐字节比对 203 个产物

设计上有几个聪明的取舍:

  1. manifest 不含时间戳、commit、本地路径——只存内容指纹与统计。 同一输入必须产生逐字节相同的 manifest,"它昨天是对的"才可被验证。
  2. 普通 push/PR 只跑离线门禁;实时重建 drift 留给发布前/排障, 避免第三方瞬时网络变成所有 PR 的随机失败。
  3. 不缓存上游文本:发布构建仍实时读取,防止缓存掩盖上游真实变化。

4. 供应链变化门禁:变化必须被看见

规则仓库最大的风险不是"写错",而是上游悄悄变了。Blink 的做法:

  • 写入前,把实时编译的 canonical 规则与已提交产物做集合比较;
  • 默认阻断条件(任一项成立):
    • 新增/删除规则数超过 20;
    • 语义变化比例超过 20%;
    • 出现此前不存在的新规则类型;
  • 失败报告给出 +N/-N、变化比例、新类型和最多五条增删样例;
  • 人工审阅后显式放行:
python engine/scripts/build.py --write --accept-large-change

注意 --accept-large-change 不关闭解析、renderer、parity、health、checksum 校验——它只跳过已经人工审阅的变化量阈值。"确认过的变化"和"没看过的变化"是两种东西。

写入保持原子性:所有 App 成功才更新产物。上游 404 / 超时 = 构建失败, 保留旧产物并暂停更新,而不是用缓存静默生成。


5. 配置层:意图驱动 + 可复现

配置层没有手写七份配置,而是:

engine/sources/profile/intent.yaml   ← 语义意图(策略组/规则引用/能力声明)
        ↓ 模板渲染
Profiles/{Surge,Loon,Stash,Clash,Egern,Shadowrocket,QuantumultX}/...
  • 单一订阅池组织,占位符内置——替换一条订阅 URL 即可复用;
  • 能力映射只允许 FULL / ADAPTED(注释) / UNSUPPORTED(注释) 三态, 禁止静默删除或伪造;
  • verify_profiles.py 断言七端配置可以从 intent + templates 逐字节重建

配置是"意图",所以它不随规则每日更新:规则层自动,配置层人工确认, 自动化边界清晰到不会互相越权。


6. 分发与门户:门户也是数据

engine/portal/ 是一个 React + Vite 的门户,但它不是手写的内容站:

  • 页面数据从生成产物 + 源清单派生(gen_portal_stats.pystats.json);
  • 无时间戳、随产物变化才变化——门户与规则集共享同一事实源,不存在"文案过期"。

接入体验上也下了功夫:每个 App 给一个"完整 .list",想要语义分段再给三视图; README 顶部就是七客户端的 quickstart 表格和官方引用片段。 降低接入成本本身也是产品的一部分。


7. 合规意识:个人项目也可以有边界

  • DISCLAIMER.md:仅供学习研究,使用前阅读;
  • THIRD_PARTY_NOTICES.md:上游来源与许可清单合规;
  • 明确"禁止转载或发布至国内平台";
  • 仓库扫描零敏感信息(这本身就是 secret_scan 门禁的产物)。

一个代理规则仓库,上游归属、许可、使用边界写得清清楚楚——这比多数开源项目 都"成熟"。合规不是大厂的专利,是工程素养。


8. 可以带走的五个工程思维

  1. 语义单一事实源,序列化只是下游。 七种客户端的差异被隔离在 renderer 层, 任何一个都不值得成为"再改一份"的借口。
  2. "正确"必须能被机器复述。 九道门禁全是可执行命令;文档描述的是承诺, 命令执行的是承诺。
  3. 显式大于静默。 降级要计数、不可能的状态要失败、写错引用方式要大声警告。
  4. 变化要审计,而不是感觉。 阈值阻断 + 人工显式放行;已知重叠固化基线, 新重叠自动阻断——"未知的冗余和已知的冗余是两种风险"。
  5. 边界清晰的管线。 规则层自动、配置层人工、门户派生。每一层只有一种职责, 自动化不会偷偷越权。

最后照例感谢 SukkaW——它的博客与 SukkaW/Surge 构建管线是 Blink 设计的思想源头。规则驱动的数据工程,值得每一份个人仓库学习。