|
|
@@ -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** |
|