Your Name 3 주 전
부모
커밋
396525d34f
1개의 변경된 파일1397개의 추가작업 그리고 504개의 파일을 삭제
  1. 1397 504
      TG商户采集系统-需求方案书.md

+ 1397 - 504
TG商户采集系统-需求方案书.md

@@ -1,13 +1,61 @@
-# TG 商户采集系统 — 需求方案书(v2
+# TG 商户采集系统 — 需求方案书(v3.1
 
 > 本文档描述系统要实现的功能和业务逻辑,供开发者从零设计和实现。
-> 版本: v2(精简版,砍掉低 ROI 模块,强调模块化隔离)
+> 版本: v3.1(在 v3 基础上扩展多维度采集 + 商户信息丰富化 + 可控性管理)
+
+---
+
+## 零、版本说明与前置确认清单
+
+### 版本演进
+
+| 版本 | 主要变化 |
+|---|---|
+| v2 | 插件化采集 + 统一清洗 |
+| v3 | 前置确认清单 / shared 基础设施层 / MySQL / 规则细节修正 / 监控评估 |
+| **v3.1** | **多维度采集蓝图(8 插件)/ Enrichment 层 / 商户实体聚合 / 可控性与配额管理** |
+
+### v3.1 相对 v3 的变化
+
+| 变化 | 动机 |
+|---|---|
+| 采集维度从 2 个插件扩到 8 个插件蓝图(分 M1/M2/M3 落地) | 单维度漏网严重,多维度能交叉印证提升置信度 |
+| 新增 **Enrichment 层**(raw→clean→enriched) | clean 只能说"是真商户",enriched 才能说"这是什么样的商户" |
+| 新增 **商户实体聚合** | 同一商户常有多个 TG 号 / 域名,需要聚合成一个实体 |
+| 新增 **可控性与配额管理** | 任何时候都要能回答"系统在做什么 / 花了多少钱 / 能不能立即停" |
+| 数据模型从 9 张扩到 15 张 | 持久化实体、enrichment、配额、审计、审核队列 |
+| 新增 canary 灰度 + 紧急开关(kill switch) | 新插件上线前必须小数据量验证;出合规问题能一键停 |
+
+### 前置确认清单(写代码前必须回答)
+
+> 这三件事不确认就开始写代码,等于在错的地基上盖楼。
+
+**1. 合规边界**
+- 部署所在地?(决定适用法域)
+- 目标客群所在地?(决定 PIPL / GDPR / 所在国法律是否适用)
+- 输出用途?(内部研究 / 冷触达销售 / 二次分发 — 每一种法律风险不同)
+- 目标行业为"机场/VPN/科学上网"时,业务本身的合法性由业务方确认,采集系统只负责技术实现
+
+**2. 月度预算(美元)**
+- 搜索 API:Brave 免费 5000 次/月大概率一周内耗光,需确认是否可切 Serper(建议预算 $10–30/月起步)
+- AI API(TG 插件启用后):按关键词密度估,建议 $20/月起步
+- 代理池:如果启用住宅代理,$50–200/月;不启用则只能单 IP 低并发运行
+- 服务器 + chromedp 内存:建议 2 vCPU / 4 GB 起,chromedp 单页峰值 300 MB
+- Whois / ICP 查询 API:$10/月起(Enrichment 启用后)
+
+**3. 代理池策略**
+- 方案 A:不用代理 → 网页采集日抓量上限约 1000 页,且只能跑中低反爬站
+- 方案 B:自建 IP 池(国内 VPS) → 成本低但维护重
+- 方案 C:买住宅代理(Bright Data / IPRoyal / Oxylabs) → 推荐,按流量计费
+- **day 1 必须预留代理接口**,即使最初跑单 IP,也不要把 http.Client 写死
+
+三个问题没答案前,本文档以下内容按"合规 OK / 有最低预算 / 代理接口先留桩"默认值进行。
 
 ---
 
 ## 一、系统目标
 
-**一句话:用关键词去 Google 搜,把搜到的网页里的商户联系方式扒下来,清洗后输出一张可以直接联系的客户表。**
+**一句话:从多个维度找到 TG 商户,把联系方式和业务画像全扒下来,清洗、聚合、丰富后输出一张可以直接联系的客户表。**
 
 ### 什么是"商户"
 
@@ -23,707 +71,1552 @@
 
 当前只做**机场 / VPN / 科学上网**。行业规则可配置,以后可扩展。
 
-### 输入
+### 输入 / 输出
 
-- 一组**关键词**(比如"机场推荐"、"VPN 订阅"、"科学上网")
-
-### 最终输出
+- **输入**:一组关键词 + 一组种子 TG 频道 + 一份导航站白名单
+- **输出**:商户实体表(merchant_entities)+ 每个实体的 enrichment 画像
 
 | 字段 | 说明 |
 |------|------|
-| 商户名 | 显示名称 |
-| TG 用户名 | @xxx |
-| TG 链接 | https://t.me/xxx |
-| 网站 | 商户官网 |
-| 邮箱 | 联系邮箱 |
-| 电话 | 联系电话 |
-| 来源 | 从哪个网页/渠道发现的 |
+| 实体 ID | 聚合后的唯一商户标识 |
+| 商户名 | 主显示名 |
+| 所有 TG 号 | 主号 + 备用号 + 客服号 |
+| 所有官网 | 主域 + 备用域 |
+| 邮箱 / 电话 | 联系方式 |
+| 来源维度数 | 被多少个维度发现 |
 | 行业标签 | 机场 / VPN 等 |
 | 等级 | Hot / Warm / Cold |
+| 业务画像 | 价格档位 / 支付方式 / 节点地区 / 技术栈 |
+| 活跃度 | 官网存活 / TG 最新消息时间 |
+| 人工备注 | 可选 |
 
 ---
 
-## 二、核心架构:插件式采集 + 统一清洗
-
-### 设计理念
+## 二、整体架构
 
-系统分两大部分:**采集端**和**处理端**。
-
-- **采集端**:负责从各种渠道找商户,每个渠道是一个**独立插件**
-- **处理端**:负责清洗、去重、验证、打标签,**所有插件共用同一套**
+系统分四层:**采集端(插件)** → **共享基础设施** → **处理端(流水线)** → **丰富化**。
 
 ```
-┌─────────────────────────────────────────────────────────────┐
-│                     采集端(插件式)                           │
-│                                                               │
-│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │
-│  │ 插件 A      │  │ 插件 B      │  │ 插件 C      │  ← 互相  │
-│  │ 网页采集    │  │ TG 频道采集 │  │ 未来新增... │  不影响  │
-│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘          │
-│         │                │                │                   │
-│         ▼                ▼                ▼                   │
-│  ┌────────────────────────────────────────────────────┐      │
-│  │          统一入口:商户表 raw                        │      │
-│  │   所有插件的产出格式一样,往同一张表写              │      │
-│  └──────────────────────┬─────────────────────────────┘      │
-└─────────────────────────┼─────────────────────────────────────┘
+┌────────────── 采集端(插件,互不依赖) ──────────────────────┐
+│  web_search  web_directory  tg_channel  tg_snowball          │
+│  github      forum          cert_trans  icp_reverse          │
+│      └──────────────────┬──────────────────┘                 │
+│                         ▼                                    │
+│                 merchants_raw                                 │
+└─────────────────────────┬────────────────────────────────────┘
+                          │
+                          ▼
+┌─────────────── 共享基础设施(shared/) ──────────────────────┐
+│  tgpool / proxypool / searchcache / httpclient / extractor   │
+│  quota / audit / killswitch                                  │
+└─────────────────────────┬────────────────────────────────────┘
+                          │
+                          ▼
+┌────────────── 处理端(固定流水线) ──────────────────────────┐
+│  死号预检 → 黑名单 → 去重 → 实体聚合 → (TG验证) → 打标签      │
+│                                   │                          │
+│                                   ▼                          │
+│                     merchants_clean + merchant_entities      │
+└─────────────────────────┬────────────────────────────────────┘
-┌─────────────────────────┼─────────────────────────────────────┐
-│                     处理端(固定流程)                         │
-│                          ▼                                     │
-│  ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐  │
-│  │ 死号预检 │ → │ 黑名单   │ → │ 去重     │ → │ 打标签   │  │
-│  │ (t.me)   │   │ 过滤     │   │ 合并     │   │ 分等级   │  │
-│  └──────────┘   └──────────┘   └──────────┘   └──────────┘  │
-│                                                     │         │
-│                                                     ▼         │
-│                                          商户表 clean          │
-│                                      (Hot / Warm / Cold)      │
-└───────────────────────────────────────────────────────────────┘
+                          ▼
+┌────────────────── 丰富化(Enrichment) ──────────────────────┐
+│  HTTP探测 / Whois / ICP / TG profile / 文本画像              │
+│                                   │                          │
+│                                   ▼                          │
+│                       merchant_enrichment                    │
+└──────────────────────────────────────────────────────────────┘
 ```
 
-### 为什么这样设计
+### 插件隔离规则
 
-**核心问题:后期加新的采集渠道,不能把前面的东西弄坏。**
+```
+规则 1  每个采集插件是独立的 Go 包(internal/plugins/<name>/)
+规则 2  插件之间零依赖:A 不能 import B 的任何符号
+规则 3  插件和处理端都可以 import internal/shared/* 的共享组件
+规则 4  所有采集插件的产出走同一个标准格式(MerchantData)
+规则 5  新增插件 = 新建目录 + 实现 Collector 接口 + 配置里注册,不改任何已有代码
+规则 6  shared/ 里的包不依赖任何具体插件(否则环形依赖)
+规则 7  新插件上线必须先 canary 模式跑通人工审核(见第十三章)
+```
 
-解决办法:**插件隔离**。
+### 标准产出格式
 
-```
-规则 1: 每个采集插件是独立的代码模块,有自己的目录/文件
-规则 2: 插件之间零依赖,A 插件的代码不能 import B 插件的任何东西
-规则 3: 所有插件的产出格式统一(见下方"标准产出格式")
-规则 4: 插件只管采集,不管清洗/去重/打分 — 那是处理端的事
-规则 5: 新增插件 = 新建一个目录 + 实现标准接口,不改任何已有代码
+```go
+type MerchantData struct {
+    TgUsername   string   // 必填,没有就不入库
+    TgLink       string
+    MerchantName string
+    Website      string
+    Email        string
+    Phone        string
+    SourceType   string   // web_search / web_directory / tg_channel / github / ...
+    SourceName   string
+    SourceURL    string
+    OriginalText string
+    IndustryTag  string
+    FetchedAt    time.Time
+    Canary       bool     // v3.1:canary 数据只进 raw,不进 clean
+}
 ```
 
-### 标准产出格式(所有插件必须遵守)
+**硬约束**:没有 `TgUsername` 的商户不入库。
 
-每个插件采集到商户后,必须按这个格式写入 `merchants_raw` 表:
+### 插件接口
 
-```
-{
-  "merchant_name": "商户名(选填)",
-  "tg_username": "xxx(必填,没有就不入库)",
-  "tg_link": "https://t.me/xxx",
-  "website": "官网地址(选填)",
-  "email": "邮箱(选填)",
-  "phone": "电话(选填)",
-  "source_type": "web / tg_channel / github / ...",
-  "source_name": "具体来源(哪个网页/频道)",
-  "source_url": "来源 URL",
-  "original_text": "原始文本(留底)",
-  "industry_tag": "行业标签(选填)"
+```go
+type Collector interface {
+    Name() string
+    Run(ctx context.Context, cfg map[string]any, emit func(MerchantData)) error
+    Stop() error
 }
 ```
 
-**关键约束**:没有 `tg_username` 的商户不入库。这是核心数据,其他都是锦上添花。
+---
+
+## 三、采集维度蓝图
+
+> 单靠网页+TG 两个维度漏网严重。v3.1 规划 8 个维度,分三期落地。同一商户经常出现在多个维度,多维度互补正是提高召回率和置信度的核心手段。
+
+### 多维度的价值
+
+| 场景 | 单维度的问题 | 多维度的解法 |
+|---|---|---|
+| 商户只在私密 TG 群出现 | Web 搜不到 | TG 滚雪球 |
+| 商户只在 GitHub README | TG 看不到 | github 插件 |
+| 官网被墙但频道活跃 | Web 抓失败 | TG + forum |
+| 主域名被封换备用域 | Web 搜不到新域 | cert_transparency |
+| 验证商户真实性 | 单一来源可能伪造 | 跨维度交叉印证 |
+
+`source_count ≥ 2` 的商户自动进入 Hot,这是多维度的直接收益。
+
+### 插件蓝图(8 个维度)
+
+| # | 插件 | 数据源 | 产出密度 | 合规风险 | 优先级 | 里程碑 |
+|---|---|---|---|---|---|---|
+| 1 | **web_search** | Google/Bing/Brave/Serper 搜索结果 | ★★★★★ | 低 | P0 | M1 |
+| 2 | **web_directory** | 已知导航站白名单主动爬取 | ★★★★★ | 低 | P0 | M1 |
+| 3 | **tg_channel** | TG 频道历史消息 | ★★★ | 中 | P1 | M2 |
+| 4 | **tg_snowball** | 从已发现频道滚雪球(转发源、@mention) | ★★★★ | 中 | P1 | M2 |
+| 5 | **github_search** | GitHub code/README 里的 t.me 链接 | ★★ | 低 | P2 | M2 |
+| 6 | **forum_scraper** | V2EX/hostloc/Reddit 等论坛帖子 | ★★★ | 低 | P2 | M3 |
+| 7 | **cert_transparency** | 证书透明日志反查同组织域名 | ★★ | 低 | P3 | M3 |
+| 8 | **icp_reverse** | 通过 ICP 备案号反查同主体域名 | ★★ | 低 | P3 | M3 |
+
+### 里程碑与验收标准
 
-### 插件的标准接口
+| 里程碑 | 范围 | 周期 | 验收 |
+|---|---|---|---|
+| **M1** | web_search + web_directory + 处理端 + Enrichment 最小集 + 可控性骨架 | 2 周 | Hot 商户 ≥ 100,Precision ≥ 85% |
+| **M2** | + tg_channel + tg_snowball + github_search + 实体聚合 | 1 个月 | 总商户 ≥ 500,source_count≥2 占比 ≥ 30% |
+| **M3** | + forum + cert_transparency + icp_reverse + 全量可控性仪表 | 2 个月 | 按实体去重后 ≥ 800 家商户 |
 
-每个插件需要实现以下接口(伪代码):
+**不跳级**:M1 没稳定前不做 M2;M2 没稳定前不做 M3。每个新插件上线必须先走 canary 模式(见第十三章)。
+
+### 维度互补性示例
 
 ```
-class CollectorPlugin:
-    name: str              # 插件名,比如 "web_collector"
-    
-    async def run(config, callback):
-        """
-        config: 该插件的配置(关键词、URL 列表等)
-        callback(merchant_data): 每找到一个商户就调一次,由框架写入 raw 表
-        """
-        
-    async def stop():
-        """外部可以随时叫停"""
+商户 A:
+  web_search       → 2 个导航站发现
+  web_directory    → airportlist.top 收录
+  tg_snowball      → 频道 @vpn_nav 转发
+  github_search    → awesome-vpn-cn README
+  cert_transparency → 发现备用域名 a-vpn.net
+  source_count = 5 → Hot,置信度 High,实体聚合自动合并
 ```
 
-框架负责:调度插件、写数据库、记日志、控制并发。
-插件负责:采集逻辑,只管找商户,找到就 callback。
-
 ---
 
-## 三、采集插件 A:网页采集(优先开发)
-
-**这是最高优先级的插件,也是系统的核心价值。**
+## 四、采集插件 A:网页搜索采集(web_search,P0)
 
-### 为什么优先
+### 为什么是第一优先级
 
 - 一个导航站几秒出 50 个商户,效率最高
-- 没有限速问题,想跑多快跑多快
-- 导航站上的商户是别人已经整理好的,质量高
+- 没有 TG 账号封禁风险
+- 导航站里的商户是别人整理好的,质量高
 
-### 流
+### 数据
 
 ```
-关键词 → Google 搜索 → 拿到 URL 列表
-                              │
-                    ┌─────────┴─────────┐
-                    ↓                   ↓
-              URL 是 t.me/xxx      URL 是网页
-              直接提取 username     打开网页读 HTML
-                    │                   │
-                    │             ┌─────┴──────┐
-                    │             ↓            ↓
-                    │        找 t.me 链接   找联系方式
-                    │        提取 username  (邮箱/电话/网址)
-                    │             │            │
-                    └──────┬──────┘            │
-                           ↓                   │
-                    写入 merchants_raw ←────────┘
+关键词 → [searchcache 查缓存] → [搜索 API] → URL 列表
+                                                  │
+                                       ┌──────────┴─────────┐
+                                       ▼                    ▼
+                                 URL 是 t.me/xxx        URL 是网页
+                                 直接提取 username      进入抓取流程
+                                       │                    │
+                                       │          ┌─────────┴────────┐
+                                       │          ▼                  ▼
+                                       │    抓 HTML(三层 fallback)  丢弃黑名单域
+                                       │          │
+                                       │     解析 HTML,正则提取
+                                       │     t.me / 邮箱 / 电话
+                                       │          │
+                                       └──────┬───┘
+                                              ▼
+                                       emit → merchants_raw
 ```
 
 ### 详细逻辑
 
 **第一步:关键词搜索**
 
-1. 从关键词表拿关键词(比如"机场推荐 telegram")
-2. 调搜索 API(Serper 或 Brave Search),拿搜索结果
-3. 每个关键词搜 3-5 页,每页 10 条
-4. 关键词之间等几秒,避免被封
+1. 从 `keywords` 表读 enabled=true 的关键词
+2. 先查 `search_cache`:key = (engine, keyword, page),TTL 默认 7 天
+3. 缓存未命中 → 通过 quota 中心拿到额度(见第十三章) → 调搜索 API
+4. 结果写回 `search_cache`
+5. 每个关键词默认 3–5 页 × 10 条;每次请求之间随机等 2–5 秒
 
 **第二步:URL 分拣**
 
-拿到的 URL 分三种:
+| URL 类型 | 判断 | 处理 |
+|---|---|---|
+| `t.me/xxx` | URL 以 `t.me/` 或 `telegram.me/` 开头 | 直接提取 username,emit |
+| `t.me/joinchat/xxx`、`t.me/+xxx` | 邀请链接 | 标记 invalid,丢弃 |
+| 黑名单域(twitter/google/youtube 等 80+) | 域名精确匹配 | 丢弃 |
+| 其他网页 | | 进入第三步 |
+
+**第三步:网页抓取(三层 fallback)**
 
-| URL 类型 | 怎么判断 | 怎么处理 |
-|----------|---------|---------|
-| `t.me/xxx` | URL 以 t.me/ 开头 | 直接提取 username,写 raw 表 |
-| 导航站/有用网页 | 不在黑名单里的网页 | 打开网页,进入第三步 |
-| 垃圾 | 在黑名单里(twitter/google/youtube 等 80 个域名) | 丢弃 |
+由 `shared/httpclient` 统一暴露:
 
-**第三步:网页解析**
+```
+层 1  net/http + colly(默认)
+      超时 10s,失败或 403/429 → 升层
+层 2  utls 自定义 TLS 指纹(绕 Cloudflare 类反爬)
+      超时 15s,失败或 JS 渲染空 body → 升层
+层 3  chromedp(Headless Chrome)
+      超时 30s,失败 → 放弃
+```
 
-1. 用 HTTP 请求抓网页 HTML
-2. HTML 前 3000 字不含中文 → 跳过(不是中文站)
-3. 解析 HTML,找所有外链:
-   - `t.me/xxx` 链接 → 提取 username
-   - `mailto:xxx` → 提取邮箱
-   - 电话号码正则 → 提取电话
-4. 如果页面上有很多 t.me 链接(>5 个),说明这是个导航站,每个链接都是一个商户
-5. 每个提取到的商户按标准格式写入 raw 表
+每层都走同一个 `proxypool.Next()` 拿出口 IP。并发上限见第十二章。
 
-**HTTP 请求失败时的 fallback**(按顺序尝试):
-1. 标准 `net/http`(或 colly 爬虫框架)
-2. 带自定义 TLS 指纹的 HTTP 客户端(绕反爬,如 utls 库)
-3. chromedp / rod(Go 原生浏览器引擎,处理 JS 渲染页面)
+**第四步:HTML 解析**
 
-### 搜索 API 选择
+1. 收到 HTML 后先判断是否中文站(见下方"中文判断"修正)
+2. 用 goquery 遍历:
+   - 所有 `a[href^="https://t.me/"]` 和 `a[href^="tg://"]` → 抽 username
+   - 所有 `a[href^="mailto:"]` → 抽邮箱
+   - 正文纯文本对电话正则匹配(带上下文关键词过滤)
+3. 导航站判断(见下方"导航站启发式"修正)
+4. 每个候选 username 组装 MerchantData → emit
 
-| 方案 | 免费额度 | 付费 | 推荐 |
-|------|---------|------|------|
-| **Brave Search API** | 5000 次/月 | $5/1000 次 | 先用这个测试 |
-| **Serper.dev** | 2500 次(一次性) | $50/50000 次 | Google 结果最准 |
-| **DuckDuckGo** | 无限 | 免费 | 开源库,稳定性差 |
+**中文判断(修正 v2 的 3000 字规则)**
 
-建议:**先用 Brave 免费额度测试**,结果不够好再切 Serper。代码层面做成可配置,换 API 只改配置不改代码。
+```
+策略 1(默认)  解析 HTML 后取 <title> + <meta description> + 前 5000 字符可见文本
+               统计中文字符数 / 总字符数,比例 ≥ 15% 判定为中文站
+策略 2(补充) 若 HTTP 层返回的是 JS 空壳(<body> 少于 200 字符),
+               直接升级到 chromedp 渲染后再判断,不误杀
+告警          若策略 1 和 2 都不过关但页面里有 t.me/@username,
+               仍走 emit,只是在 original_text 里标记 lang=unknown
+```
 
----
+**导航站启发式(修正 v2 的 ">5 个 t.me")**
 
-## 四、采集插件 B:TG 频道采集(第二优先级)
+```
+是导航站的充分条件(满足任一即视为高质量导航站):
+  a) 页面上 ≥ 8 个 t.me 链接 且 分布在不同 DOM 父节点(避免评论区灌水)
+  b) URL / title 含 "导航 / nav / 机场推荐 / 订阅" 等关键词
+  c) 有规律的卡片式布局(<ul><li> 或 <table>,同级节点里重复出现 t.me)
+
+非导航站处理:仍抽取所有 t.me,但标记 SourceType=web_casual
+             清洗阶段这类商户降权(不会直接 Hot)
+```
+
+**电话号码正则(修正 v2 误匹配问题)**
+
+```
+不要用裸 1[3-9]\d{9}。命中时必须满足以下条件之一:
+  a) 正则前/后 20 字符内有关键词:电话|手机|tel|phone|联系|客服
+  b) 命中位置在 <a href="tel:..."> 里
+  c) 位置属于 meta / schema.org Contact 块
+否则丢弃(避免 QQ 号 / 订单号 / 时间戳误判)
+```
+
+### 搜索 API 选择与缓存
 
-**等插件 A 跑稳了再开发这个。** 这是锦上添花,不是核心。
+| 方案 | 免费额度 | 付费 | 用法 |
+|---|---|---|---|
+| **Brave Search API** | 5000 次/月 | $5/1000 次 | 起步用 |
+| **Serper.dev** | 2500 次(一次性) | $50/50000 次 | 免费耗完后切 |
+| **Bing Web Search** | $3/1000 次 | | 备用 |
+| **DuckDuckGo lite** | 无限 | 免费 | 兜底 |
 
-### 为什么第二优先级
+**强制缓存 + 强制配额**:
+- 同一 `(engine, keyword, page)` 在 7 天内只允许调一次 API(search_cache 表)
+- 调用前必须通过 quota 中心检查(见第十三章)
 
-- TG 限速严,一天最多出几十个商户
-- 需要手机号注册账号,被封就废
-- 开发和维护成本比网页采集高得多
+**可配置**:`config.yaml` 里 `search.provider = brave | serper | bing | ddg`。
+
+---
 
-### 流程
+## 五、采集插件 B:TG 频道采集(tg_channel,P1)
+
+**M1 网页插件稳定运行 2 周以上再启动这个。**
+
+### 数据流
 
 ```
-种子频道列表 → 进频道 → 读历史消息(最近 500 条)
-                              │
-                    每条消息看有没有联系方式
-                    正则快扫 → 有 → AI 精确提取
-                              │
-                    写入 merchants_raw(标准格式)
+seed_channels → [tgpool 拿账号] → 进频道 → 读历史消息(最近 500 条)
+                                             │
+                                  每条消息:正则快扫 + 关键词预筛
+                                             │
+                                             ▼
+                                  触发条件 → AI 精确提取
+                                             │
+                                      AI 结果 → 回源校验
+                                             │
+                                             ▼
+                                      emit → merchants_raw
 ```
 
 ### 详细逻辑
 
-1. 从种子列表拿频道(比如 @bbs3000),种子由用户手动添加
-2. 用 TG 客户端库(Go: gotd/td)登录 TG 账号,进入频道
-3. 读最近 500 条消息(支持断点续传,记住上次读到哪条)
-4. 每条消息:
-   - 系统消息 / 非中文 → 跳过
-   - 正则快速扫描有没有 `@xxx`、`t.me/xxx`、邮箱、网址
-   - 有联系方式 → 调 AI 精确提取商户信息
-   - AI 超时(>5 秒)或失败 → 用正则兜底
-5. 提取到的商户按标准格式写入 raw 表
+1. 从 `seed_channels` 表拿 status=pending 的频道
+2. `tgpool.Acquire()` 拿一个可用的 TG 账号
+3. ResolveUsername 前先查 `channels.channel_id` 缓存
+4. 读最近 500 条消息,断点续传记在 `channels.last_message_id`
+5. 消息预筛:
+   - 系统消息 / 非中文 / 长度 < 10 → 跳过
+   - 正则扫 `@\w+` / `t.me/\w+` / 邮箱 / 电话 — 命中任一才进下一步
+6. AI 提取(调 DeepSeek / GLM):
+   - 提示词要求结构化 JSON 输出
+   - 超时 5 秒 / 失败 → 正则兜底
+7. **AI 结果校验(修正 v2 的"正则回原文精确匹配")**:
+   ```
+   校验规则:取 AI 输出的每个联系方式(username / email / phone),
+           去掉标点、空白、@、+86 等前缀,提取核心 token,
+           去原文里做"去格式化后的子串匹配"
+           命中即接受;否则丢弃
+   例:AI 输出 "@abc_123",原文 "加 V:abc_123 / 加Q:456"
+       token = "abc_123" → 子串匹配 → 接受
+   ```
+8. 通过校验的联系方式组装 MerchantData → emit
 
-### TG 账号管理(重要)
+---
 
-TG 账号是稀缺资源,需要专门的调度器:
+## 六、其他采集插件概要
 
-**entity ID 缓存(必须做)**:
-- 第一次 `ResolveUsername` 拿到频道的数字 ID 后存到本地
-- 以后直接用数字 ID 访问,不再调 `ResolveUsername`
-- 这样同一个频道只消耗 1 次 resolve 额度,之后无限次不限速
+本章列出 M2/M3 阶段要做的 6 个补充插件。所有插件共用第七章的 shared 基础设施,统一实现 `Collector` 接口,所以"新增一个维度 = 新建一个目录"。本章只给要点,详细实现在对应里程碑启动时展开。
 
-**限速处理**:
-- 全局请求频率控制(所有模块共享,不超过 30 次/分钟)
-- FloodWait < 60 秒 → 等完继续
-- FloodWait > 60 秒 → 切账号
-- FloodWait > 300 秒 → 停止,下次再来
-- 没有可用账号 → 排队等待,不崩溃
+### 6.1 web_directory — 导航站主动爬取(P0,M1)
 
-**每个 TG 账号需要的信息**:
+**为什么**:web_search 烧 API 额度,但大型导航站列表是已知的(airportlist.top、vpn.nav.vip 等),直接爬比搜索更高效完整。
 
-| 字段 | 说明 |
-|------|------|
-| 手机号 | 注册 Telegram 用的 |
-| api_id | 在 https://my.telegram.org 申请 |
-| api_hash | 同上 |
-| session 文件 | 首次登录后生成 |
+**流程**:
+1. 维护导航站白名单表 `directory_whitelist`
+2. 每个站点配置抓取规则(URL 模板、列表页选择器、卡片 CSS)
+3. 按 cron 定时抓(默认每天一次)
+4. 用 shared/httpclient 抓页面,goquery 按规则解析
+5. 新商户 → emit;已知商户 → 更新 last_seen_at
+
+**维护成本**:规则失效监控(网站改版会导致选择器失效,需要每日产出量监控告警)。
+
+### 6.2 tg_snowball — TG 滚雪球(P1,M2)
+
+**为什么**:种子频道只有少数几个,但 TG 生态内的转发链和 @mention 链能发现大量新频道。
+
+**流程**:
+1. 扫描已采集的 TG 消息,提取所有 `@channel_name` 和"Forwarded from xxx"
+2. 新频道自动加入 `channels` 表,source='discovered',status=pending
+3. 由 tg_channel 插件继续采集
+4. 每日汇总新发现频道数作为指标
+
+**约束**:新频道加入前过滤 bot、违法关键词、非中文频道。
+
+**复用**:不实现新的采集逻辑,只是生产新的种子给 tg_channel。
+
+### 6.3 github_search — GitHub 代码搜索(P2,M2)
+
+**为什么**:大量机场项目放 GitHub,README 直接写 TG 联系方式。GitHub API 免费 5000 req/hour,几乎无限制。
+
+**流程**:
+1. 用 GitHub Code Search API 搜 `"t.me" language:Markdown`、`telegram 机场`、`VPN 订阅` 等
+2. 拿 repo 列表 → 逐个读 README / description / topics
+3. 正则提取 t.me 链接
+4. 关联 repo metadata(star、last_push)作为活跃度信号
+
+**产出特点**:数量不多但质量高(README 一般是项目主动填的)。
+
+### 6.4 forum_scraper — 论坛采集(P2,M3)
+
+**为什么**:V2EX、hostloc、Reddit 的"机场推荐"帖是高质量发现源,尤其携带用户真实评价。
+
+**流程**:
+1. 配置论坛白名单 + 每个论坛的采集规则(API 或 HTML 抓)
+2. 定期抓置顶帖 + 最新主题列表
+3. 帖子正文 + 评论都进 extractor
+4. 用户评价作为"社区口碑"信号供 Enrichment 使用
+
+**产出特点**:密度中等,但携带用户评价这种独家信号。
+
+### 6.5 cert_transparency — 证书透明日志反查(P3,M3)
+
+**为什么**:机场商户常注册多个域名(主域名被墙就换备用)。通过 SSL 证书透明日志(crt.sh)可以反查同一组织申请的其他域名。
+
+**流程**:
+1. 从已发现商户拿主域名
+2. 查 crt.sh:`https://crt.sh/?q=%25example.com&output=json`
+3. 提取 SAN 里的其他域名
+4. 新域名丢给 web_search 做二次采集
+5. 新域名若指向新 TG → emit
+
+**产出特点**:主要用于补全已知商户的备用域名,偶尔发现新商户。
+
+### 6.6 icp_reverse — ICP 备案号反查(P3,M3)
+
+**为什么**:中国境内合规商户需要 ICP 备案。从已知商户的 ICP 号反查同一主体备案的其他域名,能发现关联商户。
+
+**流程**:
+1. 从 Enrichment 层拿商户的 ICP 备案号
+2. 查 ICP 反查 API(beianx.cn 或自建)
+3. 同主体的其他域名 → 标记为 related_domains
+4. 含 TG 的新域名 → emit
+
+**产出特点**:只对备案商户有效(机场类目很多不备案,覆盖率低),但一旦命中置信度高。
 
 ---
 
-## 五、采集插件 C/D/E...:未来扩展
+## 七、共享基础设施(`internal/shared/`)
+
+### 7.1 tgpool — TG 账号池
+
+职责:
+- 管理多个 TG 账号的 session、健康状态、FloodWait 倒计时
+- 提供 `Acquire(ctx) → Account` / `Release(acc)` 接口
+- 调用端不感知限速:超限自动切号,全部账号都限速时挂起调用者
+
+状态持久化到 `tg_accounts` 表:
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | int | 主键 |
+| phone | VARBINARY | AES-GCM 加密存储 |
+| api_id | int | my.telegram.org 申请 |
+| api_hash | VARBINARY | 加密存储 |
+| session_path | string | session 文件路径 |
+| status | string | active / flood_wait / banned / disabled |
+| flood_wait_until | datetime | FloodWait 解除时间 |
+| resolve_count_today | int | 今日 ResolveUsername 计数 |
+| last_used_at | datetime | |
+
+限速策略:
+- 全局请求频率 ≤ 30 次/分钟(所有账号汇总)
+- FloodWait < 60s → 账号原地等
+- FloodWait 60–300s → 账号进 `flood_wait` 状态,切下一个
+- FloodWait > 300s → 标记 `flood_wait`,调度器 5 分钟后重试
+- 所有账号都不可用 → 调用方 block 等待,最长 30 分钟后报错
+
+### 7.2 proxypool — 代理出口池
+
+职责:
+- 抽象"出口 IP"概念,插件和 httpclient 通过 `proxypool.Next()` 拿代理
+- 健康检查:每 5 分钟对每个代理做一次 `GET https://example.com`,连续 3 次失败下线
+- 支持三种后端:
+  - `direct` — 不走代理(默认,方便本地开发)
+  - `static_list` — 静态 IP 列表(YAML 配置)
+  - `bright_data` / `iproyal` — 商用住宅代理(按流量)
+
+**day 1 只实现 `direct`,但接口必须先留好**。
+
+### 7.3 searchcache — 搜索结果缓存
+
+持久化到 `search_cache` 表。见第四章"搜索 API 选择与缓存"。
+
+### 7.4 httpclient — 三层 fallback 客户端
+
+```go
+type Client interface {
+    Get(ctx context.Context, url string, opts ...Option) (*Response, error)
+}
+```
+
+每层通过 `proxypool.Next()` 拿出口。chromedp 层有单独并发闸门(默认 3,见第十二章)。
+
+### 7.5 extractor — 正则提取器
+
+```go
+package extractor
+
+func TgUsernames(text string) []string
+func Emails(text string) []string
+func Phones(text string, ctxWindow int) []string
+func PriceTiers(text string) []PriceTier   // v3.1 Enrichment 使用
+func PaymentMethods(text string) []string
+func ServerRegions(text string) []string
+```
+
+纯函数、无状态、插件和处理端共用。
+
+### 7.6 quota — 配额中心(v3.1 新增)
+
+```go
+type Quota interface {
+    Check(resource string, amount int64) error   // 超限返回 ErrQuotaExceeded
+    Consume(resource string, amount int64) error
+    Usage(resource string) (used, budget int64)
+}
+```
+
+所有外部付费资源(search API / AI tokens / proxy 流量 / tg requests / whois 查询)调用前必须过 `quota.Check`。见第十三章。
+
+### 7.7 audit — 操作审计(v3.1 新增)
 
-以下是以后可能加的插件,**现在不做,但架构要能支持**:
+```go
+type Audit interface {
+    Log(actor, action, targetType, targetID string, payload any) error
+}
+```
+
+系统级动作都写 audit_logs 表。见第十三章。
+
+### 7.8 killswitch — 紧急开关(v3.1 新增)
 
-| 插件 | 数据源 | 什么时候加 |
-|------|--------|-----------|
-| GitHub 搜索 | GitHub README 里的 t.me 链接 | 网页+TG 都稳定后 |
-| TG 频道裂变 | 从种子频道滚雪球发现新频道 | TG 采集稳定后 |
-| 百度搜索 | 百度搜索结果 | 如果 Google 覆盖不够 |
-| Twitter/X | 推文里的 t.me 链接 | 如果有需求 |
+```go
+type KillSwitch interface {
+    IsEngaged(domain string) bool  // domain: "collectors" / "enrichment" / "all"
+    Engage(domain, reason, actor string) error
+    Release(domain, actor string) error
+}
+```
 
-**加新插件的步骤**(这是模块化的价值):
-1. 新建一个目录/文件
-2. 实现 `run()` 和 `stop()` 接口
-3. 按标准格式 callback 产出商户
-4. 在配置里注册插件名
-5. **不改任何已有代码**
+所有插件和 Enrichment 在每一轮循环开始时查询 `IsEngaged("collectors")`,true 就优雅退出。见第十三章。
 
 ---
 
-## 六、处理端:清洗流程
+## 八、Enrichment 层(商户信息丰富化)
+
+> clean 只能告诉你"这是个真商户",enriched 才能告诉你"这是个什么样的商户"。没有 Enrichment,销售拿到的列表只有 TG 号和名字,无法分档、无法个性化触达。
+
+### 输入 / 输出
 
-所有插件的产出都进 `merchants_raw` 表,然后统一过清洗流程。
+- **输入**:`merchants_clean` 里 status=valid 的记录
+- **输出**:`merchant_enrichment` 表(每个 merchant_id 一条)
+
+### 丰富字段(15 项,分四组)
+
+**A. 官网元数据(HTTP 请求)**
+
+| 字段 | 说明 |
+|---|---|
+| `site_alive` | 官网 HTTP 2xx |
+| `site_ssl_days` | SSL 证书剩余天数 |
+| `site_title` | <title> |
+| `site_description` | <meta description> |
+| `tech_stack` | Cloudflare/Nginx/V2Board/SSPanel/Xboard 等 |
+
+**B. 域名元数据(Whois / ICP)**
+
+| 字段 | 说明 |
+|---|---|
+| `domain_registrar` | 注册商 |
+| `domain_registered_at` | 注册时间(越老越可信) |
+| `icp_beian_no` | ICP 备案号(若有) |
+| `icp_subject` | 备案主体名 |
+
+**C. TG 活跃度(可选,需要 tgpool)**
+
+| 字段 | 说明 |
+|---|---|
+| `tg_channel_members` | 频道成员数 |
+| `tg_last_message_at` | 频道最后消息时间 |
+| `tg_is_premium` | TG 号是否 Premium |
 
-### 清洗流水线(4 步,按顺序执行)
+**D. 业务画像(从官网/频道文本提取)**
+
+| 字段 | 说明 |
+|---|---|
+| `price_tiers` | 价格档位数组 ["9.9元/月", "99元/年"] |
+| `payment_methods` | 支付方式(支付宝/USDT/PayPal) |
+| `server_regions` | 节点地区(HK/JP/SG/US) |
+
+### 流水线
 
 ```
-merchants_raw → [死号预检] → [黑名单过滤] → [去重合并] → [打标签分等级] → merchants_clean
+merchants_clean (valid)
+    │
+    ├─[1] 官网 HTTP 探测     → site_* 字段
+    ├─[2] Whois 查询         → domain_* 字段
+    ├─[3] ICP 反查           → icp_* 字段
+    ├─[4] (可选) TG profile  → tg_* 字段
+    ├─[5] 文本画像抽取       → price / payment / region 字段
+    ▼
+merchant_enrichment (upsert by merchant_id)
 ```
 
-### 第一步:t.me 死号预检(免费,无限速)
+### 触发策略
+
+| 场景 | 触发 |
+|---|---|
+| 新发现 Hot 商户 | 立即触发(同步,< 10s) |
+| 新发现 Warm 商户 | 延迟触发(加入 enrichment 队列,< 1h) |
+| 定期刷新已有商户 | 每 7 天一次(site_alive / tg_last_message_at 有时效) |
+| 手动触发 | 前端按钮 |
+
+### 文本画像抽取(最值钱的部分)
+
+价格、支付方式、节点地区是销售判断商户能不能合作的第一手信息。
+
+**价格**:正则 `\d+(\.\d+)?\s*元[//]\s*(月|年|季|天)` + 上下文关键词("套餐/购买/订阅"),命中后取上下文 30 字作为 `price_context` 保留。
+
+**支付方式**:字典匹配 `["支付宝","微信","USDT","PayPal","银行卡","btc","usdt","trc20","erc20","支付宝扫码"]`
+
+**地区**:字典匹配 `["香港","日本","新加坡","美国","台湾","HK","JP","SG","US","TW"]` + 旗帜 emoji `🇭🇰🇯🇵🇸🇬🇺🇸🇹🇼`
+
+**不用 AI 做这个**。AI 幻觉成本太高,规则命中率够用。只在规则完全失败时人工扩词表。
 
-- 用 HTTP 请求访问 `https://t.me/{username}`
-- 看返回 HTML 里有没有 `tgme_page_photo_image` 标记
-- 有头像 = 活号 → 继续
-- 没头像 = 死号 → 标记 invalid,不进后面的步骤
-- **准确率 100%,不花钱,不限速**
-- 建议并发 10 个,每分钟能检 600 个
+### Enrichment 失败的降级
 
-### 第二步:黑名单过滤(本地,秒级)
+- HTTP 探测失败(官网被墙) → `site_alive=false`,其他字段尽力填充
+- Whois 被限速 → 排队 1 小时后重试
+- TG 验证拿不到 → 这一组字段 null,不影响其他字段
+- 整体失败 → 商户仍保留在 merchants_clean,只是没 enrichment 数据
+
+### 实体聚合的触发点
+
+Enrichment 产出的 `icp_subject` 和 `domain_registrar` 是商户实体聚合的主要依据 — 见第九章 9.4。
+
+---
+
+## 九、处理端:清洗流水线
+
+所有插件的产出进 `merchants_raw` 后统一跑清洗。
+
+```
+merchants_raw
+    │
+    ├─[1] t.me 死号预检   ← 免费、HTML 扫描
+    ├─[2] 黑名单 / bot 过滤
+    ├─[3] 去重合并        ← 按 tg_username 聚合
+    ├─[4] 实体聚合         ← v3.1 新增,聚合成 merchant_entity
+    ├─[5] (可选) TG 真实性验证  ← 需要 tgpool
+    ├─[6] 打标签 + 分等级
+    ▼
+merchants_clean + merchant_entities
+```
+
+### 9.1 t.me 死号预检
+
+- HTTP GET `https://t.me/{username}`,解析返回 HTML
+- 用 `tgme_page_photo_image` 标记判断活号,但加**健康监控**:
+  - 每天对 `tg_accounts` 里 1 个已知活号跑基线测试
+  - 基线失败(Telegram 改 HTML)→ 告警,预检结果暂时置 `unknown`
+- 并发 10,通过 `shared/httpclient` 第一层即可
+- 结果写回 `merchants_clean.is_alive`
+
+### 9.2 黑名单 / bot 过滤
 
 | 规则 | 处理 |
-|------|------|
-| TG 用户名是系统 bot(@BotFather、@SpamBot、以 bot 结尾) | 标记 bot |
-| TG 用户名像邀请链接哈希(16-24 位随机字符串) | 标记 invalid |
-| 原始文本不含中文(如果有原始文本的话) | 标记 invalid |
+|---|---|
+| 用户名是系统 bot(`@BotFather`、`@SpamBot`、或以 `bot` 结尾) | status=bot |
+| 用户名形如邀请链接哈希(16–24 位纯随机) | status=invalid |
+| 原始文本完全无中文 且 无英文商业关键词 | status=invalid |
+
+### 9.3 去重合并
+
+- 按 `tg_username` 聚合
+- **raw 入库去重键(修正 v2)**:`UNIQUE(tg_username, source_url_hash, fetched_date)`,fetched_date 是 MySQL 生成列
+- 聚合时按信息丰富度保留最好的一条:`has_website > has_email > has_phone > none`
+- `source_count` 计所有聚合来源
+- `all_sources` 存 JSON 数组
+
+### 9.4 实体聚合(v3.1 新增)
+
+**问题**:同一商户常有 3–5 个 TG 号(主号/备用号/客服号),清洗后是 3–5 条 merchants_clean 记录。销售拿到列表会重复联系。
+
+**解法**:引入 `merchant_entities` 表,把属于同一商户的多个 clean 记录聚合成一个实体。
+
+**聚合规则**(按优先级从高到低):
 
-### 第三步:去重合并(本地,秒级)
+| 规则 | 条件 | 置信度 | 动作 |
+|---|---|---|---|
+| R1 | 同一 `website` 域名 | High | 自动合并 |
+| R2 | 同一 `icp_subject` | High | 自动合并 |
+| R3 | 同一 `email` | Medium | 入 review_queue |
+| R4 | `merchant_name` 归一化后相同 | Medium | 入 review_queue |
+| R5 | `merchant_name` 编辑距离 ≤ 2 且 同 industry_tag | Low | 只生成建议 |
 
-- 同一个 tg_username 可能被多个插件多次发现
-- 按信息丰富度保留最好的一条(有网站 > 没网站,有邮箱 > 没邮箱)
-- 其他标记为 duplicate
-- 合并所有来源信息到保留的那条
+**流程**:
+1. 扫 merchants_clean 里 entity_id IS NULL 的记录
+2. 按 R1-R5 顺序尝试匹配已有 entity
+3. 命中 High → 更新 merchant.entity_id,合并联系方式到 entity
+4. 命中 Medium → 写 review_queue
+5. 未命中 → 新建 entity
+6. 人工可以手动合并 / 拆分(前端按钮 + audit_log)
 
-### 第四步:打标签 + 分等级
+**聚合结果**:一个实体包含所有成员的 TG 号、邮箱、电话、官网(去重合并),level 取成员最高级。
 
-**行业标签**:用关键词匹配(商户名/原始文本里包含"机场""节点""VPN"→ 打标签)。只做机场/VPN 一个行业时,关键词匹配完全够用,不需要 AI。
+### 9.5 可选:TG 真实性验证
 
-**等级划分**(3 个桶,不打分):
+只在 `tgpool` 可用时执行:
+- `ResolveUsername` 验证账号真实性
+- 拿到:显示名、是否 Premium、成员数
+- 写入 `merchants_clean.tg_profile`(JSON)
+- 失败不致命:清洗继续
 
-| 等级 | 条件 | 含义 |
-|------|------|------|
-| **Hot** | 行业匹配 + 有网站或邮箱 | 优先联系,信息最全 |
-| **Warm** | 行业匹配 + 只有 TG 号 | 可以联系,但信息少 |
-| **Cold** | 行业不匹配 / 信息太少 | 暂不联系 |
+### 9.6 打标签 + 分等级
 
-**为什么不用 0-100 打分**:
-- 100 分制需要成员数、Premium、活跃度等数据,但这些要调 TG API 才能拿到
-- TG API 是最大的瓶颈,为了打分去调 API 得不偿失
-- 3 个桶简单直观,销售拿到手就能用
+**行业标签**:关键词匹配(name + original_text 里含"机场 / 节点 / 订阅 / VPN / 科学上网" → 机场标签)。
 
-### 可选增强:TG 验证(需要 TG 账号)
+**等级(3 桶)**:
 
-如果有 TG 账号且未被限速,可以在第三步和第四步之间加一步:
-- 调 `ResolveUsername` 验证账号真实性
-- 拿到:显示名、是否 Premium、最后在线时间
-- 有了这些数据可以更精准地分等级
+| 等级 | 条件 |
+|---|---|
+| **Hot** | 行业匹配 + `is_alive=true` + (有 website 或 email) + source_count ≥ 2(v3.1 提高门槛) |
+| **Warm** | 行业匹配 + `is_alive=true` + 只有 TG 号,或 source_count=1 |
+| **Cold** | 行业不匹配 / 死号 / 信息太少 / SourceType=web_casual |
 
-**但这不是必须的。** 没有 TG 账号,系统照样能跑(靠 t.me 预检 + 黑名单就能过滤大部分垃圾)。
+等级在实体聚合后重算:实体 level = 成员中最高的那个
 
 ---
 
-## 七、数据模型(5 张表)
+## 十、数据模型(15 张表)
 
-### 表 1:关键词表 (keywords)
+### 表 1:keywords — 关键词
 
 | 字段 | 类型 | 说明 |
-|------|------|------|
+|---|---|---|
 | id | int | 主键 |
-| keyword | string | 搜索关键词 |
-| industry_tag | string | 行业标签(机场/VPN) |
-| enabled | bool | 是否启用 |
-| created_at | datetime | 创建时间 |
+| keyword | varchar(128) | 搜索关键词 |
+| industry_tag | varchar(32) | |
+| enabled | bool | |
+| created_at | datetime(3) | |
 
-种子频道也放这个表(`industry_tag = 'seed'`),不单独建表。
+### 表 2:seed_channels — TG 种子频道
 
-### 表 2:商户表 — 原始 (merchants_raw)
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | int | 主键 |
+| username | varchar(64) | UNIQUE |
+| industry_tag | varchar(32) | |
+| enabled | bool | |
+| note | varchar(256) | |
 
-所有插件的产出统一写这张表。
+### 表 3:channels — TG 频道元数据 + 断点
 
 | 字段 | 类型 | 说明 |
-|------|------|------|
+|---|---|---|
 | id | int | 主键 |
-| tg_username | string | **必填**,TG 用户名 |
-| tg_link | string | t.me 链接 |
-| merchant_name | string | 商户名 |
-| website | string | 官网 |
-| email | string | 邮箱 |
-| phone | string | 电话 |
-| source_type | string | 来源类型(web / tg_channel / github) |
-| source_name | string | 具体来源(哪个网页/频道) |
-| source_url | string | 来源 URL |
-| original_text | text | 原始文本 |
-| industry_tag | string | 行业标签 |
-| status | string | raw / processing / done |
-| created_at | datetime | 入库时间 |
-
-**入库去重规则**:同 `tg_username` + 同 `source_url` 不重复插入。不同来源发现同一个 username 允许多条(去重在清洗阶段做)。
-
-### 表 3:商户表 — 已清洗 (merchants_clean)
-
-清洗通过的商户。
+| username | varchar(64) | UNIQUE |
+| channel_id | bigint | 缓存 |
+| access_hash | bigint | 缓存 |
+| status | varchar(16) | pending / scraped / skipped / error |
+| last_message_id | int | 断点续传 |
+| merchants_found | int | |
+| source | varchar(16) | seed / discovered |
+| created_at | datetime(3) | |
+
+### 表 4:tg_accounts — TG 账号
+
+见 7.1。`phone` / `api_hash` 用 VARBINARY 加密存储。
+
+### 表 5:merchants_raw — 原始商户
 
 | 字段 | 类型 | 说明 |
-|------|------|------|
-| id | int | 主键 |
-| tg_username | string | TG 用户名 |
-| tg_link | string | t.me 链接 |
-| merchant_name | string | 商户名 |
-| website | string | 官网 |
-| email | string | 邮箱 |
-| phone | string | 电话 |
-| source_count | int | 被多少个来源发现 |
-| all_sources | text | 所有来源列表(JSON) |
-| industry_tag | string | 行业标签 |
-| level | string | **Hot / Warm / Cold** |
-| status | string | valid / invalid / bot / duplicate |
-| is_alive | bool | t.me 预检结果 |
-| last_checked_at | datetime | 最近一次验证时间 |
-| created_at | datetime | 首次发现时间 |
-
-### 表 4:频道表 (channels)
-
-只有启用了 TG 采集插件才需要这张表。
+|---|---|---|
+| id | bigint | 主键 |
+| tg_username | varchar(64) | **必填** |
+| tg_link | varchar(128) | |
+| merchant_name | varchar(128) | |
+| website | varchar(512) | |
+| email | varchar(128) | |
+| phone | varchar(32) | |
+| source_type | varchar(32) | web_search / web_directory / tg_channel / ... |
+| source_name | varchar(128) | |
+| source_url | varchar(512) | |
+| source_url_hash | char(64) | GENERATED AS SHA2(source_url,256) STORED |
+| original_text | text | |
+| industry_tag | varchar(32) | |
+| status | varchar(16) | raw / processing / done |
+| canary | bool | v3.1 灰度标记 |
+| fetched_at | datetime(3) | |
+| fetched_date | date | GENERATED AS DATE(fetched_at) STORED |
+| created_at | datetime(3) | |
+
+**入库去重**:`UNIQUE KEY uk_raw_dedup (tg_username, source_url_hash, fetched_date)`
+**留存策略**:超过 `raw_retention_days`(默认 90)的 done 记录归档到 `merchants_raw_archive`,或用 RANGE 分区表按月 DROP PARTITION。
+
+### 表 6:merchants_clean — 清洗后商户
 
 | 字段 | 类型 | 说明 |
-|------|------|------|
-| id | int | 主键 |
-| username | string | 频道用户名(唯一) |
-| channel_id | bigint | TG 数字 ID(缓存,避免重复 resolve) |
-| access_hash | bigint | TG access_hash(缓存) |
-| status | string | pending / scraped / skipped |
-| last_message_id | int | 上次采集到哪条(断点续传) |
-| merchants_found | int | 发现了多少商户 |
-| source | string | seed / discovered |
-| created_at | datetime | 入库时间 |
+|---|---|---|
+| id | bigint | 主键 |
+| entity_id | bigint | FK → merchant_entities,v3.1 新增 |
+| tg_username | varchar(64) | UNIQUE |
+| tg_link | varchar(128) | |
+| merchant_name | varchar(128) | |
+| website | varchar(512) | |
+| email | varchar(128) | |
+| phone | varchar(32) | |
+| source_count | int | |
+| all_sources | json | |
+| industry_tag | varchar(32) | |
+| level | varchar(8) | Hot / Warm / Cold |
+| status | varchar(16) | valid / invalid / bot / duplicate |
+| is_alive | bool | |
+| tg_profile | json | |
+| last_checked_at | datetime(3) | |
+| created_at | datetime(3) | |
+
+`INDEX idx_entity_id (entity_id)` / `INDEX idx_level (level)`
+
+### 表 7:search_cache — 搜索 API 缓存
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | bigint | 主键 |
+| engine | varchar(16) | brave / serper / bing / ddg |
+| keyword | varchar(128) | |
+| page | int | |
+| result_json | longtext | 原始响应 |
+| fetched_at | datetime(3) | |
+| expires_at | datetime(3) | |
+
+`UNIQUE KEY (engine, keyword, page)`
+
+### 表 8:task_logs — 任务日志
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | bigint | 主键 |
+| task_type | varchar(32) | |
+| task_instance | varchar(64) | |
+| plugin_name | varchar(32) | |
+| status | varchar(16) | running / success / failed / stopped / paused |
+| items_processed | int | |
+| merchants_added | int | |
+| errors_count | int | |
+| started_at | datetime(3) | |
+| finished_at | datetime(3) | |
+| detail | text | |
+
+### 表 9:eval_samples — 抽检样本(可选)
 
-### 表 5:任务日志表 (task_logs)
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | bigint | 主键 |
+| merchant_id | bigint | FK → merchants_clean |
+| sample_round | varchar(32) | |
+| is_true_positive | bool | |
+| note | text | |
+| reviewed_at | datetime(3) | |
+
+### 表 10:merchant_entities — 商户实体(v3.1 新增)
+
+见 9.4。
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | bigint | 主键 |
+| primary_name | varchar(128) | |
+| primary_website | varchar(512) | |
+| primary_tg_username | varchar(64) | |
+| all_tg_usernames | json | |
+| all_emails | json | |
+| all_phones | json | |
+| industry_tag | varchar(32) | |
+| source_count | int | 去重后来源数 |
+| level | varchar(8) | 实体级别 |
+| merged_clean_ids | json | 聚合的 clean ID |
+| note | text | 人工备注 |
+| created_at | datetime(3) | |
+| updated_at | datetime(3) | |
+
+### 表 11:merchant_enrichment — 丰富化数据(v3.1 新增)
+
+见第八章。
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | bigint | 主键 |
+| merchant_id | bigint | FK → merchants_clean,UNIQUE |
+| entity_id | bigint | FK → merchant_entities(可选) |
+| site_alive | bool | |
+| site_ssl_days | int | |
+| site_title | varchar(512) | |
+| site_description | text | |
+| tech_stack | json | |
+| domain_registrar | varchar(128) | |
+| domain_registered_at | date | |
+| icp_beian_no | varchar(64) | |
+| icp_subject | varchar(256) | |
+| tg_channel_members | int | |
+| tg_last_message_at | datetime(3) | |
+| tg_is_premium | bool | |
+| price_tiers | json | |
+| payment_methods | json | |
+| server_regions | json | |
+| last_enriched_at | datetime(3) | |
+
+### 表 12:directory_whitelist — 导航站白名单(v3.1 新增)
+
+见 6.1。
 
 | 字段 | 类型 | 说明 |
-|------|------|------|
+|---|---|---|
 | id | int | 主键 |
-| task_type | string | web_collect / tg_collect / clean / ... |
-| plugin_name | string | 哪个插件 |
-| status | string | running / success / failed / stopped |
-| items_processed | int | 处理了多少条 |
-| merchants_added | int | 新增了多少商户 |
-| errors_count | int | 错误数 |
-| started_at | datetime | 开始时间 |
-| finished_at | datetime | 结束时间 |
-| detail | text | 详细日志/错误信息 |
+| name | varchar(64) | |
+| url_template | varchar(512) | 列表页模板,支持 `{page}` |
+| selector_rules | json | CSS 选择器 |
+| max_pages | int | |
+| cron_schedule | varchar(32) | |
+| enabled | bool | |
+| last_run_at | datetime(3) | |
+| last_merchants_found | int | |
 
----
+### 表 13:quota_usage — 配额使用统计(v3.1 新增)
+
+见第十三章。
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | bigint | 主键 |
+| resource | varchar(32) | search_brave / search_serper / ai_tokens / proxy_bytes / tg_requests / whois_queries |
+| period | varchar(8) | day / month |
+| period_start | date | |
+| used | bigint | |
+| budget | bigint | |
+| last_updated_at | datetime(3) | |
 
-## 八、前端需求(只做 2 个页面)
+`UNIQUE KEY (resource, period, period_start)`
 
-早期只需要 2 个页面,其他的等有需求了再加。
+### 表 14:audit_logs — 操作审计(v3.1 新增)
 
-### 页面 1:商户列表
+见第十三章。
 
-- 显示 `merchants_clean` 表的数据
-- 按等级筛选(Hot / Warm / Cold)
-- 按行业筛选
-- 按来源筛选
-- 搜索(按商户名、TG 用户名)
-- 排序(按发现时间、来源数)
-- 导出 CSV / Excel
-- 点击 TG 链接可以直接跳转
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | bigint | 主键 |
+| actor | varchar(64) | 用户名 / system |
+| action | varchar(64) | start_task / stop_task / update_config / merge_entity / kill_switch / export |
+| target_type | varchar(32) | |
+| target_id | varchar(64) | |
+| payload | json | 改动前后详情 |
+| created_at | datetime(3) | |
 
-### 页面 2:任务管理
+`INDEX idx_actor (actor)` / `INDEX idx_action (action)` / `INDEX idx_created_at (created_at)`
 
-- 选择插件启动任务(网页采集 / TG 采集 / 清洗)
-- 显示当前运行中的任务
-- 停止任务
-- 查看历史任务和结果
+### 表 15:review_queue — 人工审核队列(v3.1 新增)
 
-### 不做的(延后)
+见第十三章。
 
-| 功能 | 为什么不做 |
-|------|-----------|
-| ~~仪表盘~~ | 数据量小时看列表就够了 |
-| ~~配置管理~~ | 改配置文件比写前端快 |
-| ~~实时日志流~~ | SSH 看日志就行 |
-| ~~种子频道管理~~ | 初期手动维护,量不大 |
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | bigint | 主键 |
+| queue_type | varchar(32) | hot_publish / entity_merge / low_conf_dedup / canary_review |
+| payload | json | 待审项的全部数据 |
+| status | varchar(16) | pending / approved / rejected |
+| assignee | varchar(64) | |
+| decided_at | datetime(3) | |
+| note | text | |
+| created_at | datetime(3) | |
+
+`INDEX idx_queue_status (queue_type, status)`
 
 ---
 
-## 九、外部依赖
+## 十一、外部依赖
 
 ### 技术栈
 
 | 层 | 选型 | 说明 |
 |---|---|---|
-| **后端语言** | **Go** | 高并发、编译型、单二进制部署 |
-| **Web 框架** | Gin 或 Echo | 轻量 HTTP 框架 |
-| **ORM** | GORM | Go 主流 ORM |
-| **数据库** | SQLite(初期)/ PostgreSQL(后期) | 初期单机够用 |
-| **TG 客户端** | gotd/td 或 gotdlib | Go 原生 Telegram MTProto 库 |
-| **HTML 解析** | goquery | 类似 jQuery 的 HTML 解析 |
-| **HTTP 客户端** | net/http + colly | 标准库 + 爬虫框架 |
-| **浏览器引擎** | chromedp 或 rod | Go 原生 Chrome DevTools Protocol(替代 Playwright) |
-| **前端** | Vue 3 + Vite + TypeScript | 不变 |
-| **配置** | YAML(viper 库) | Go 生态标准 |
-| **日志** | zerolog 或 zap | 结构化日志 |
-
-### 必须的外部服务
-
-| 服务 | 用途 | 备注 |
-|------|------|------|
-| **搜索 API** | 关键词搜索 | Brave(免费 5000 次/月)或 Serper($50/50000 次) |
-| **HTTP 客户端** | 抓网页、t.me 预检 | net/http + colly + chromedp 三层 fallback |
-
-### 可选的(TG 采集插件启用后才需要)
-
-| 服务 | 用途 | 备注 |
-|------|------|------|
-| **gotd/td** | TG 频道采集 | Go 原生 MTProto 库,替代 Python Telethon |
-| **AI 大模型 API** | 联系方式提取 | 智谱 GLM 或 DeepSeek,仅 TG 采集时用,HTTP 调用即可 |
+| 后端语言 | Go | 高并发、单二进制部署 |
+| Web 框架 | Gin / Echo | |
+| ORM | GORM | |
+| 数据库 | **MySQL 8.0+** | InnoDB 引擎,utf8mb4 字符集 |
+| TG 客户端 | gotd/td | 原生 MTProto |
+| HTML 解析 | goquery | |
+| HTTP 客户端 | net/http + colly + utls + chromedp | 三层 fallback |
+| 前端 | Vue 3 + Vite + TypeScript | |
+| 配置 | YAML + viper | |
+| 日志 | zerolog / zap | 结构化 |
+| 指标 | prometheus client_golang | 第十四章用 |
+| 定时任务 | robfig/cron | 插件 schedule |
+
+### 外部服务与成本(建议预算)
+
+| 服务 | 用途 | 最低预算 |
+|---|---|---|
+| 搜索 API(Brave/Serper/Bing) | 关键词搜索 | $10–30/月 |
+| 代理池(住宅代理) | 网页抓取出口 | $50–200/月(day 1 可不开) |
+| AI API(DeepSeek/GLM) | TG 消息提取 | $10–20/月 |
+| Whois API | Enrichment 域名查询 | $5–10/月 |
+| ICP 反查 API | Enrichment 备案查询 | $5–10/月(国内) |
+| GitHub API | github_search 插件 | 免费 |
+| 服务器 | chromedp + DB | 2 vCPU / 4 GB 起 |
 
 ### AI 使用策略
 
-**规则优先,AI 只在一个地方用。**
-
 | 环节 | 方法 | 说明 |
-|------|------|------|
-| 网页联系方式提取 | **纯正则** | 网页上的 t.me 链接、邮箱、电话,正则就能 100% 提取 |
-| TG 消息联系方式提取 | **正则 + AI** | 非标准格式("加V:xxx")需要 AI |
-| 行业分类 | **纯关键词匹配** | 只做机场/VPN,关键词够用 |
-| 导航站识别 | **纯规则**(黑名单 + 正向关键词) | 不需要 AI |
+|---|---|---|
+| 网页联系方式提取 | 纯正则 | 不用 AI |
+| TG 消息联系方式提取 | 正则预筛 + AI 精提取 + 去格式化子串校验 | 唯一用 AI 的地方 |
+| 行业分类 | 纯关键词匹配 | |
+| 导航站识别 | 纯规则 | |
+| Enrichment 文本画像 | 纯正则 + 字典匹配 | 不用 AI |
+
+### MySQL 建表要点(必读)
 
-AI 只在 TG 采集插件的联系方式提取环节使用。网页采集完全不需要 AI。
+v3 选 MySQL,建表时必须规避以下坑:
+
+**1. 字符集强制 utf8mb4**
+```sql
+CREATE DATABASE tg_scraper
+  DEFAULT CHARACTER SET utf8mb4
+  DEFAULT COLLATE utf8mb4_unicode_ci;
+```
+每张表显式加 `DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`。TG 消息含 emoji 和四字节汉字,utf8mb3 会报错。
+
+**2. 索引长度限制**
+InnoDB 单列索引 key ≤ 3072 字节,utf8mb4 下单列 VARCHAR 索引 ≤ 768 字符。长列用前缀索引或 SHA-256 哈希列代替。
+
+**3. raw 去重键的实现**
+```sql
+CREATE TABLE merchants_raw (
+  id              BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  tg_username     VARCHAR(64)  NOT NULL,
+  source_url      VARCHAR(512) NOT NULL,
+  source_url_hash CHAR(64) GENERATED ALWAYS AS
+                  (SHA2(source_url, 256)) STORED,
+  fetched_at      DATETIME(3)  NOT NULL,
+  fetched_date    DATE GENERATED ALWAYS AS (DATE(fetched_at)) STORED,
+  ...
+  PRIMARY KEY (id),
+  UNIQUE KEY uk_raw_dedup (tg_username, source_url_hash, fetched_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+```
+
+**4. 时间列统一 UTC**
+```sql
+created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
+updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
+                          ON UPDATE CURRENT_TIMESTAMP(3)
+```
+应用层和 MySQL 都配置 `time_zone='+00:00'`。
+
+**5. 大文本列类型**
+- `original_text` → TEXT
+- `all_sources`、`price_tiers`、`selector_rules` 等 JSON 字段 → 用 MySQL 8 的 `JSON` 类型
+- `result_json` → LONGTEXT
+
+**6. 分区表(上量后启用)**
+`merchants_raw` 按 `fetched_date` RANGE 分区,到期 DROP PARTITION 比 DELETE 快几个数量级。
+
+**7. GORM + MySQL DSN**
+```
+user:pass@tcp(host:3306)/tg_scraper?charset=utf8mb4&parseTime=True&loc=UTC&time_zone=%27%2B00%3A00%27
+```
+
+**8. 连接池**
+```go
+sqlDB.SetMaxOpenConns(50)
+sqlDB.SetMaxIdleConns(10)
+sqlDB.SetConnMaxLifetime(time.Hour)
+```
+
+**9. 敏感字段加密**
+`tg_accounts.phone`、`tg_accounts.api_hash` 应用层 AES-GCM 加密后存 `VARBINARY(255)`。
 
 ---
 
-## 十、运行方式
+## 十二、并发与资源限额
+
+统一在 `config.yaml` 里声明,跨模块生效。
+
+```yaml
+concurrency:
+  http_layer1_colly:   50   # 标准 HTTP
+  http_layer2_utls:    20   # 绕 Cloudflare
+  http_layer3_chrome:  3    # Headless Chrome(内存敏感,硬上限)
+  tme_precheck:        10   # t.me 死号预检
+  search_api_qps:      1    # 搜索 API 串行化
+  tg_global_per_min:   30   # tgpool 全局 QPS
+  ai_extract:          5    # AI 提取并发
+  enrichment:          10   # Enrichment 流水线并发
+task:
+  max_per_instance:
+    web_search:        2
+    web_directory:     1
+    tg_channel:        1
+    tg_snowball:       1
+    github_search:     1
+    forum_scraper:     1
+    clean:             1
+    enrichment:        2
+memory:
+  chrome_page_max_mb:  400
+retention:
+  raw_days:            90
+  search_cache_days:   7
+  task_logs_days:      180
+  audit_logs_days:     365
+```
 
-### 单插件运行
+**关键**:chromedp 并发 3 是硬上限。
 
-每个插件可以独立跑:
-- 只跑网页采集 → 看搜到了什么
-- 只跑清洗 → 处理已有的脏数据
-- 只跑 TG 采集 → 从指定频道挖商户
+---
 
-### 全链路运行
+## 十三、可控性与配额管理(v3.1 新增)
 
-也可以串起来:`网页采集 → 清洗`(两步就够了)
+> **目标**:任何时候都能回答三个问题 — 系统现在在做什么?花了多少钱?能不能立刻停?
 
-### 任务控制
+### 13.1 配额中心 (quota_usage)
+
+所有外部资源消耗走统一配额中心:
+
+| 资源 | 单位 | 周期 | 示例上限 |
+|---|---|---|---|
+| search_brave | 次数 | 月 | 5000 |
+| search_serper | 次数 | 月 | 50000 |
+| search_bing | 次数 | 月 | 10000 |
+| ai_tokens | tokens | 月 | 1000000 |
+| proxy_bytes | 字节 | 月 | 50 GB |
+| tg_requests | 次数 | 日 | 1000 |
+| whois_queries | 次数 | 月 | 1000 |
+| icp_queries | 次数 | 月 | 1000 |
 
-- 每个任务有状态:运行中 / 完成 / 失败 / 已停止
-- 支持手动停止
-- 同类型任务不能同时跑两个
-- 支持测试模式(只跑少量数据)
+调用点在发起请求前先 `quota.Check(resource, amount)`,超限返回错误;调用后 `quota.Consume(resource, amount)` 累加。状态持久化到 `quota_usage` 表。
+
+### 13.2 熔断策略
+
+| 用量 | 状态 | 行为 |
+|---|---|---|
+| < 80% | 正常 | 无限制 |
+| 80–95% | 告警 | 日志 warn + 前端黄灯 + Bark/Slack 通知 |
+| 95–100% | 降级 | 停止该资源的新任务,已跑任务继续 |
+| = 100% | 熔断 | 全部拒绝,触发硬停 |
+
+熔断解除:手动(前端按钮或 API),必须写 audit_logs。
+
+### 13.3 任务生命周期
+
+```
+pending → running → (paused ↔ running) → success
+                                       ↘ failed
+                                       ↘ stopped(人工)
+```
+
+每个任务具备:
+- **优先级** 0-9(数字大优先,默认 5)
+- **可暂停** pause 时保留已抓进度
+- **可恢复** 从 checkpoint 恢复,不重跑
+- **可取消** stopped 状态不能恢复
+- **实例隔离** 同 task_type 可多实例并行(见十二章)
+
+### 13.4 插件开关与独立配置
+
+每个插件在 config.yaml 里有独立开关:
+
+```yaml
+plugins:
+  web_search:
+    enabled: true
+    canary: false
+    schedule: "0 */6 * * *"
+    quota_profile: standard
+  web_directory:
+    enabled: true
+    canary: false
+    schedule: "0 3 * * *"
+  tg_channel:
+    enabled: false
+    canary: true
+    quota_profile: tg_strict
+  tg_snowball:
+    enabled: false
+  github_search:
+    enabled: false
+  forum_scraper:
+    enabled: false
+  cert_transparency:
+    enabled: false
+  icp_reverse:
+    enabled: false
+```
+
+**热加载**:改 config.yaml 发 SIGHUP 不用重启。
+
+### 13.5 Canary 模式(灰度发布)
+
+新插件上线第一次默认 canary=true:
+- 只处理前 1% 数据(比如只跑 1 个关键词,或只抓 1 个导航站)
+- 产出写 `merchants_raw` 但 `canary=true`
+- 清洗阶段 canary 数据不进 clean 表,进 review_queue(queue_type=canary_review)
+- 人工确认无误 → 改 `canary=false` → 全量跑
+
+### 13.6 人工审核队列 (review_queue)
+
+四种审核场景:
+
+| 队列类型 | 触发 | 审核动作 |
+|---|---|---|
+| **hot_publish** | 新商户分级为 Hot | approve → 进 clean;reject → 进 archive |
+| **entity_merge** | Medium 置信度实体合并建议 | approve → 合并;reject → 保持独立 |
+| **low_conf_dedup** | 去重有歧义 | approve / reject |
+| **canary_review** | canary 插件产出 | approve → 插件全量;reject → 回滚配置 |
+
+**非阻塞**:hot_publish 审核可关(`config.review.hot_publish_required = false`),默认开启。
+
+### 13.7 操作审计 (audit_logs)
+
+以下动作强制写 audit:
+- 启动 / 停止 / 暂停 / 恢复 任务
+- 修改任何配置
+- 合并 / 拆分 merchant_entity
+- 触发紧急开关
+- 手动调整 quota 配额
+- 导出数据(含 merchant_count)
+- 审核队列决定
+
+日志不可删,只能归档。
+
+### 13.8 紧急开关(Kill Switch)
+
+配置:`kill_switch.collectors = true` 一瞬间停所有采集插件。清洗和 Enrichment 不停(可独立设 `kill_switch.enrichment`)。
+
+触发场景:
+- 收到合规问询
+- 代理流量突增疑似被当爬虫
+- 目标站返回蜜罐数据
+- 配额严重超支
+
+前端有大红按钮。触发后写 audit_logs 并发告警。
+
+### 13.9 可观测性最小集(/dashboard 页面)
+
+前端 dashboard 必须展示:
+
+1. **各资源本月配额用量**(进度条 + 黄/红告警色)
+2. **各插件今日产出 merchants_added 趋势**(折线图)
+3. **当前运行中任务列表**(task_type / instance / 进度 / 操作)
+4. **最近 24h 错误率**(分插件)
+5. **紧急开关状态**(大按钮)
+6. **最近 10 条 audit_logs**
+7. **review_queue 待审数**(hot_publish / entity_merge / canary)
+8. **实体去重后的商户总数**(按 level 分组)
+
+这 8 条是"系统在不在健康运行"的最低信息。
 
 ---
 
-## 十一、踩过的坑(供参考)
+## 十四、评估与监控
 
-### 1. TG 限速是最大坑
+### 14.1 定期抽检(人工,1 小时/周)
 
-单账号一天最多几百次 `ResolveUsername`,之后被限速 10-24 小时。
+每周从 `merchant_entities` 按分层抽样:
+- Hot 抽 20 条
+- Warm 抽 20 条
+- Cold 抽 10 条
 
-**根治方案**:缓存 `channel_id + access_hash`,同一个频道只调一次 `ResolveUsername`,之后用数字 ID 直接访问。
+写入 `eval_samples` 表,人工打 `is_true_positive`。聚合指标:
+- **Precision**
+- **Hot 级精确率目标 ≥ 85%**
+- **Warm 级精确率目标 ≥ 60%**
 
-### 2. t.me 网页可以免费检测死号
+精确率跌破阈值 → 触发规则审查。
 
-访问 `https://t.me/{username}`,HTML 里有 `tgme_page_photo_image` = 活号,没有 = 死号。准确率 100%,不限速,不花钱。**在调 TG API 之前先做这一步能省 90% 的 API 调用。**
+### 14.2 召回率侧估算
 
-### 3. AI 会编造联系方式
+维护"已知商户黄金集"(人工收集 30–50 个真实商户作为 ground truth),每次跑完全链路检查命中比例。命中率 < 50% → 关键词 / 导航站覆盖不够。
 
-AI 提取后必须用正则回原文二次验证。原文里找不到的,丢弃 AI 的结果。
+### 14.3 运行时监控指标(Prometheus)
 
-### 4. 清洗后数据和原始数据分开存
+| 指标 | 说明 | 告警阈值 |
+|---|---|---|
+| `search_api_calls_total{engine}` | 搜索 API 调用数 | 接近月额度 |
+| `search_cache_hit_ratio` | 搜索缓存命中率 | < 60% |
+| `http_fallback_layer_total{layer}` | 三层 fallback 各自命中数 | layer3 > 20% |
+| `chrome_page_oom_total` | chromedp OOM 次数 | > 0 |
+| `tg_flood_wait_total` | FloodWait 次数 | 持续上升 |
+| `tme_precheck_baseline_fail` | 死号检测基线失败 | > 0 |
+| `merchants_added_per_day{plugin}` | 每日新增 | 连续 3 天 0 |
+| `entity_merged_per_day` | 实体合并数 | 突增/突降 |
+| `enrichment_failure_rate{step}` | Enrichment 各步失败率 | > 30% |
+| `quota_usage_ratio{resource}` | 配额使用率 | > 80% 告警、> 95% 熔断 |
+| `review_queue_pending{type}` | 审核待办数 | > 100 积压 |
 
-用两张表(raw 和 clean),清洗通过的搬到 clean 表。raw 表保留原始数据,可以反复清洗。
+---
+
+## 十五、前端(只做 3 个页面 + 认证)
+
+### 页面 1:Dashboard(v3.1 新增)
 
-### 5. 非中文内容直接跳过
+见 13.9。
 
-系统只做中文商户,非中文的网页/消息直接跳过,节省大量处理时间。
+### 页面 2:商户实体列表
 
-### 6. 网页抓取要有 fallback
+- 显示 `merchant_entities` 的数据(按实体而非 clean 记录)
+- 按等级筛选(Hot / Warm / Cold)
+- 按行业 / 来源维度数 / 技术栈 / 支付方式 / 节点地区筛选
+- 搜索(按名字、TG 号、域名)
+- 排序(发现时间 / 来源维度数 / 最后活跃时间)
+- 点击进详情页:展示所有 TG 号、所有域名、enrichment 画像、来源列表
+- **手动合并 / 拆分**按钮(写 audit_logs)
+- 导出 CSV / Excel
 
-有些网页有反爬(Cloudflare),有些是 JS 渲染。按顺序试:net/http → utls(自定义 TLS 指纹)→ chromedp/rod(浏览器引擎)。
+### 页面 3:任务与审核管理
 
-### 7. 不要一上来就做全链路
+- 启动 / 暂停 / 恢复 / 停止 任务
+- 任务历史与错误详情
+- 审核队列(hot_publish / entity_merge / canary)
+- 配额中心(每资源当月用量 + 调整按钮)
+- 紧急开关
 
-先把一个插件(网页采集)做稳做透,再加第二个(TG)。一上来就做 7 阶段 pipeline,结果哪个都不稳。
+### 认证
+
+- 存个人信息,**必须认证**
+- 单用户 basic auth 起步(env 里存 bcrypt hash)
+- 扩多人时换 JWT
+- 所有 API 路由强制登录
+- 权限分层:`admin`(配置+紧急开关)/ `operator`(启停任务+审核)/ `viewer`(只读)
 
 ---
 
-## 附录 A:模块化目录结构建议(Go)
+## 十六、运行方式
+
+### 单插件运行
+
+每个插件可独立跑:
+- `go run ./cmd/server --plugin=web_search --dry-run`
+- 只跑清洗:`go run ./cmd/server --task=clean`
+- 只跑 Enrichment:`go run ./cmd/server --task=enrichment`
+
+### 全链路运行
+
+`web_search → web_directory → clean → entity_merge → enrichment` 由任务调度器串起来。cron 触发。
+
+### 任务控制
+
+- 状态:running / paused / success / failed / stopped
+- 同 task_type 支持 N 个 task_instance 并行
+- 测试模式:`--dry-run` 跑少量数据不写库
+- 所有任务启停写 audit_logs
+
+---
+
+## 十七、踩过的坑
+
+### v2 原有(保留)
+
+1. **TG 限速是最大坑** — channel_id + access_hash 必须缓存
+2. **t.me 死号可以免费检测** — 扫描 HTML 里的头像标记
+3. **AI 会编造联系方式** — 必须回源校验
+4. **raw/clean 分开存** — 允许反复清洗
+5. **非中文直接跳过** — 但注意 JS 渲染站的误伤
+6. **HTTP 抓取要有 fallback** — 但并发必须有上限
+7. **不要一上来就全链路** — 先把一个插件做透
+
+### v3 新增
+
+8. **搜索 API 免费额度会一周耗光** — 必须有 search_cache
+9. **chromedp 是内存杀手** — 单页峰值 300 MB,并发 3 是硬上限
+10. **电话正则离上下文关键词不超过 20 字符** — 否则 QQ 号全进来
+11. **AI 验证用去格式化子串匹配** — 不用精确匹配
+12. **tgme_page_photo_image 是未文档化的 HTML 标记** — 每天跑基线测试
+13. **raw 去重键要带日期维度** — 跨天同源不被吞
+14. **shared/ 层是解决"应该隔离但实际需要共享"的唯一干净解法**
+
+### v3.1 新增
+
+15. **单维度采集漏网严重** — 同一商户常只出现在特定平台,多维度互补是提高召回率的关键
+16. **实体聚合必须先做再打等级** — 如果在聚合前打等级,同商户的 3 个 TG 号会被分别评级,数据重复
+17. **Enrichment 失败必须降级而非整条删除** — 官网被墙很常见,不该因此丢掉有价值的 TG 商户
+18. **canary 必须默认开启** — 新插件第一次跑常有问题,canary 能避免污染 clean 表
+19. **配额中心必须在请求前检查** — 在请求后检查等于事后算账,账单已经爆了
+20. **kill switch 要在插件主循环每一轮查询** — 不能只在任务启停时查,否则长任务停不下来
+21. **审计日志不可删除** — 合规审计要求,只能按时间归档
+22. **Hot 级别门槛提到 source_count ≥ 2** — 单来源商户大概率是伪造或低质量
+23. **价格 / 支付方式 / 节点地区用规则而非 AI** — Enrichment 场景规则命中率够用,AI 幻觉成本高
+
+---
+
+## 附录 A:目录结构(Go)
 
 ```
 tg-lead-scraper/
 ├── cmd/
-│   └── server/
-│       └── main.go                 # 程序入口
+│   └── server/main.go
 ├── internal/
-│   ├── plugin/                     # 插件框架
-│   │   ├── interface.go            # 插件接口定义(Collector interface)
-│   │   └── registry.go             # 插件注册中心
+│   ├── plugin/
+│   │   ├── interface.go
+│   │   └── registry.go
+│   │
+│   ├── plugins/                  # 采集插件(零依赖)
+│   │   ├── websearch/            # M1
+│   │   ├── webdirectory/         # M1
+│   │   ├── tgchannel/            # M2
+│   │   ├── tgsnowball/           # M2
+│   │   ├── githubsearch/         # M2
+│   │   ├── forumscraper/         # M3
+│   │   ├── certtransparency/     # M3
+│   │   └── icpreverse/           # M3
 │   │
-│   ├── plugins/                    # 采集插件目录(每个插件一个包)
-│   │   ├── webcollector/           # 插件 A:网页采集
-│   │   │   ├── collector.go        # 实现 Collector 接口
-│   │   │   ├── searcher.go         # 调搜索 API
-│   │   │   └── parser.go           # 解析网页 HTML
-│   │   ├── tgcollector/            # 插件 B:TG 频道采集
-│   │   │   ├── collector.go        # 实现 Collector 接口
-│   │   │   ├── scraper.go          # TG 消息采集
-│   │   │   └── account.go          # TG 账号调度
-│   │   └── githubcollector/        # 插件 C:未来新增
-│   │       └── ...
+│   ├── shared/                   # 共享基础设施
+│   │   ├── tgpool/
+│   │   ├── proxypool/
+│   │   ├── searchcache/
+│   │   ├── httpclient/
+│   │   │   ├── layer1_colly.go
+│   │   │   ├── layer2_utls.go
+│   │   │   └── layer3_chrome.go
+│   │   ├── extractor/
+│   │   ├── quota/                # v3.1
+│   │   ├── audit/                # v3.1
+│   │   └── killswitch/           # v3.1
 │   │
-│   ├── processor/                  # 处理端(清洗流程)
-│   │   ├── pipeline.go             # 清洗流水线调度
-│   │   ├── tmechecker.go           # t.me 死号预检
-│   │   ├── blacklist.go            # 黑名单过滤
-│   │   ├── dedup.go                # 去重合并
-│   │   └── tagger.go               # 打标签 + 分等级
+│   ├── processor/                # 处理端
+│   │   ├── pipeline.go
+│   │   ├── tmechecker.go
+│   │   ├── blacklist.go
+│   │   ├── dedup.go
+│   │   ├── entity.go             # v3.1 实体聚合
+│   │   ├── tgverify.go
+│   │   └── tagger.go
 │   │
-│   ├── model/                      # 数据模型
-│   │   ├── merchant.go             # 商户结构体 + GORM model
-│   │   ├── channel.go              # 频道
-│   │   ├── keyword.go              # 关键词
-│   │   └── tasklog.go              # 任务日志
+│   ├── enrichment/               # v3.1 丰富化层
+│   │   ├── pipeline.go
+│   │   ├── site_probe.go
+│   │   ├── whois.go
+│   │   ├── icp.go
+│   │   ├── tg_profile.go
+│   │   └── profile_extract.go    # 价格/支付/地区
 │   │
-│   ├── store/                      # 数据访问层
-│   │   ├── db.go                   # 数据库连接 + 初始化
-│   │   ├── merchant_repo.go        # 商户 CRUD
-│   │   └── keyword_repo.go         # 关键词 CRUD
+│   ├── review/                   # v3.1 审核队列
+│   │   └── queue.go
 │   │
-│   ├── extractor/                  # 联系方式提取器
-│   │   ├── regex.go                # 正则提取
-│   │   └── ai.go                   # AI 提取(调大模型 API)
+│   ├── model/
+│   │   ├── merchant.go
+│   │   ├── entity.go             # v3.1
+│   │   ├── enrichment.go         # v3.1
+│   │   ├── channel.go
+│   │   ├── keyword.go
+│   │   ├── seed_channel.go
+│   │   ├── tg_account.go
+│   │   ├── search_cache.go
+│   │   ├── directory.go          # v3.1
+│   │   ├── quota.go              # v3.1
+│   │   ├── audit.go              # v3.1
+│   │   ├── review.go             # v3.1
+│   │   ├── tasklog.go
+│   │   └── eval_sample.go
 │   │
-│   └── task/                       # 任务调度
-│       └── manager.go              # 任务启停、并发控制
+│   ├── store/
+│   │   ├── db.go
+│   │   └── *_repo.go
+│   │
+│   ├── task/
+│   │   ├── manager.go
+│   │   └── scheduler.go          # v3.1 cron + 优先级
+│   │
+│   └── eval/
+│       └── sampler.go
-├── api/                            # HTTP API
-│   ├── server.go                   # Gin/Echo 初始化
+├── api/
+│   ├── server.go
 │   ├── handler/
-│   │   ├── merchant.go             # 商户列表 API
-│   │   └── task.go                 # 任务管理 API
+│   │   ├── merchant.go
+│   │   ├── entity.go             # v3.1
+│   │   ├── task.go
+│   │   ├── review.go             # v3.1
+│   │   ├── quota.go              # v3.1
+│   │   └── dashboard.go          # v3.1
 │   └── middleware/
-│       └── auth.go                 # 认证中间件
+│       ├── auth.go
+│       └── audit.go              # v3.1 自动写审计
-├── frontend/                       # 前端(Vue 3)
-│   └── ...
+├── frontend/                     # Vue 3(dashboard + 实体列表 + 任务与审核)
 ├── config/
-│   └── config.yaml                 # 全局配置
+│   └── config.yaml
-├── go.mod
-├── go.sum
-└── Makefile
+├── go.mod / go.sum / Makefile / Dockerfile
 ```
 
-### 插件接口定义(Go interface)
-
-```go
-// internal/plugin/interface.go
-package plugin
-
-import "context"
-
-// MerchantData 是所有插件的标准产出格式
-type MerchantData struct {
-    TgUsername    string `json:"tg_username"`
-    TgLink       string `json:"tg_link"`
-    MerchantName string `json:"merchant_name"`
-    Website      string `json:"website"`
-    Email        string `json:"email"`
-    Phone        string `json:"phone"`
-    SourceType   string `json:"source_type"`
-    SourceName   string `json:"source_name"`
-    SourceURL    string `json:"source_url"`
-    OriginalText string `json:"original_text"`
-    IndustryTag  string `json:"industry_tag"`
-}
-
-// Collector 是所有采集插件必须实现的接口
-type Collector interface {
-    // Name 返回插件名,比如 "web_collector"
-    Name() string
-    // Run 启动采集,每找到一个商户就调 callback,ctx 取消时优雅退出
-    Run(ctx context.Context, cfg map[string]any, callback func(MerchantData)) error
-    // Stop 外部可以随时叫停
-    Stop() error
-}
-```
-
-### 加新插件的步骤
-
-1. 在 `internal/plugins/` 下新建包(比如 `baiducollector/`)
-2. 实现 `Collector` 接口的 3 个方法
-3. 在 `registry.go` 注册插件名
-4. 在 `config.yaml` 加插件配置
-5. **不改 internal/plugins/ 外的任何代码**
+**依赖方向**:
+- `plugins/* → shared/*`
+- `processor → shared/*`
+- `enrichment → shared/*`
+- `processor ⊥ plugins`(永远不 import)
+- `enrichment ⊥ plugins`
+- `api → processor / enrichment / task / review`
 
 ---
 
 ## 附录 B:完整数据流图
 
 ```
-┌─────────── 采集端 ───────────┐
-│                                │
-│  关键词 → [网页采集插件]       │
-│              │                 │
-│              ├→ t.me 链接      │
-│              └→ 网页 → 解析    │      ┌─────── 处理端 ──────┐
-│                    │           │      │                       │
-│                    ↓           │      │  [死号预检]           │
-│            merchants_raw  ←────┼──→   │      ↓               │
-│                    ↑           │      │  [黑名单过滤]        │
-│  种子频道 → [TG 采集插件]     │      │      ↓               │
-│              │                 │      │  [去重合并]          │
-│              └→ 消息 → AI提取 │      │      ↓               │
-│                                │      │  [打标签分等级]      │
-│  (未来)  → [GitHub 插件]      │      │      ↓               │
-│  (未来)  → [百度插件]         │      │  merchants_clean     │
-│  (未来)  → [Twitter 插件]     │      │  (Hot/Warm/Cold)     │
-│                                │      │                       │
-└────────────────────────────────┘      └───────────────────────┘
-                                                    │
-                                                    ↓
-                                           前端:商户列表 + 导出
+┌──────────────── 采集端(8 个插件) ──────────────────┐
+│                                                      │
+│  keywords → [web_search]   ┐                         │
+│           → [web_directory]┤                         │
+│                            │                         │
+│  seed     → [tg_channel]   ├──→ merchants_raw ─┐     │
+│           → [tg_snowball]  │                   │     │
+│                            │                   │     │
+│           → [github]       │                   │     │
+│           → [forum]        │                   │     │
+│           → [cert_trans]   │                   │     │
+│           → [icp_reverse]  ┘                   │     │
+└────────────────────────────────────────────────┼─────┘
+                                                 │
+                                                 ▼
+┌──────────────── 处理端 ──────────────────────────────┐
+│  死号预检 → 黑名单 → 去重 → 实体聚合 → TG验证 → 标签  │
+│                                │                     │
+│                                ▼                     │
+│                    merchants_clean + merchant_entities│
+└──────────────────────────┬───────────────────────────┘
+                           │
+                           ▼
+┌──────────────── 丰富化 ──────────────────────────────┐
+│  HTTP探测 / Whois / ICP / TG profile / 文本画像      │
+│                                │                     │
+│                                ▼                     │
+│                      merchant_enrichment             │
+└──────────────────────────┬───────────────────────────┘
+                           │
+           ┌───────────────┴───────────────┐
+           ▼                               ▼
+     前端:dashboard                  评估:抽检 + 黄金集
+     前端:实体列表                    监控:Prometheus
+     前端:任务与审核                  审计:audit_logs
+                                       配额:quota_usage
+                                       熔断:kill_switch
 ```
+
+---
+
+## 附录 C:决策日志
+
+| # | 旧做法 | 现做法 | 原因 | 引入版本 |
+|---|---|---|---|---|
+| 1 | 5 张表 | 15 张表 | 持久化实体、enrichment、配额、审计、审核 | v3→v3.1 |
+| 2 | 种子频道塞 keywords 表 | 拆 seed_channels | 两种语义共用一表 | v3 |
+| 3 | 插件零依赖 / 清洗可调 TG 验证 | 抽 shared/ 层 | 解决隔离与共享矛盾 | v3 |
+| 4 | 搜索 API 无缓存 | search_cache + quota | 免费额度一周耗光 | v3 |
+| 5 | HTML 前 3000 字无中文就跳过 | 比例法 + JS 渲染后复判 | 避免 JS 站误杀 | v3 |
+| 6 | 导航站 >5 个 t.me | 多条件充分集 | 避免评论区灌水 | v3 |
+| 7 | 电话正则无上下文 | 要求附近有关键词 | 消除误匹配 | v3 |
+| 8 | AI 回原文精确匹配 | 去格式化子串匹配 | AI 常做格式归一 | v3 |
+| 9 | 未指定 chromedp 并发 | 硬上限 3 | 避免 OOM | v3 |
+| 10 | 未提代理 | proxypool day 1 留接口 | 上规模必用 | v3 |
+| 11 | 未提评估 | 分层抽检 + 黄金集 | 无指标不知好坏 | v3 |
+| 12 | 未提前端认证 | 强制 + 三级权限 | 个人信息合规 | v3→v3.1 |
+| 13 | 未提合规 | 前置确认清单 | 代码前必答 | v3 |
+| 14 | 未提成本 | 预算表 + 配额中心 | 钱包先炸很常见 | v3→v3.1 |
+| 15 | raw 去重键 (username, url) | +MySQL 生成列 fetched_date | 跨天重采不被吞 | v3 |
+| 16 | 未提 raw 留存 | retention_days=90 + 分区 | 避免无限增长 | v3 |
+| 17 | 未提 tgme HTML 基线检查 | 每日基线 + 告警 | 未文档化标记可能变 | v3 |
+| **18** | **只有 2 个采集插件** | **8 个插件蓝图 + M1/M2/M3** | **单维度漏网严重** | **v3.1** |
+| **19** | **raw → clean 两层** | **+ Enrichment 层(15 字段)** | **clean 只能说"是真商户"** | **v3.1** |
+| **20** | **每条 merchants_clean 独立** | **+ merchant_entities 聚合** | **同商户多 TG 号会被重复联系** | **v3.1** |
+| **21** | **Hot 标准:行业匹配 + 官网/邮箱** | **+source_count ≥ 2** | **单来源大概率低质或伪造** | **v3.1** |
+| **22** | **无灰度** | **canary=true 默认** | **新插件易出错污染 clean 表** | **v3.1** |
+| **23** | **无配额中心** | **quota.Check/Consume** | **请求前检查才能防超支** | **v3.1** |
+| **24** | **无紧急开关** | **kill_switch 三档(collectors/enrichment/all)** | **合规问询要一键停** | **v3.1** |
+| **25** | **无操作审计** | **audit_logs 强制写入** | **合规审计要求** | **v3.1** |
+| **26** | **无审核队列** | **review_queue 四类** | **Hot 商户需人工过一眼** | **v3.1** |
+| **27** | **Enrichment 用 AI 抽价格** | **纯正则 + 字典** | **规则命中率够,AI 幻觉成本高** | **v3.1** |