ISSUE-0089:点分隔电话被坐标过滤器误杀并不可逆删除存量¶
症状¶
法国 / 比利时 / 部分美国站点的电话采集不到;已入库的同类号码在扩展重启后消失。
根因¶
src/utils/scraper.ts:100-109:
export function isLikelyCoordinateNoise(rawPhone: string): boolean {
const norm = String(rawPhone || '').replace(/\s+/g, ' ').trim();
if (!norm) return false;
const dotCount = (norm.match(/\./g) || []).length;
if (dotCount >= 2) return true; // 特征 1
if (/^\d{1,2}\./.test(norm)) return true; // 特征 2
return false;
}
该判据是 0080-phone-regex-captures-gps-coordinates 的修复产物,用于拦截 31.2304,121.4737 这类坐标。
但它对点分隔电话格式同样命中。
伤害边界(自构验证)¶
| 样本 | 国家 | dotCount | 结果 |
|---|---|---|---|
01.42.68.53.00 |
法国 | 4 | 命中特征 1 — 丢弃 |
02.345.67.89 |
比利时 | 3 | 命中特征 1 — 丢弃 |
212.555.0100 |
美国 | 2 | 命中特征 1 — 丢弃 |
01 42 68 53 00 |
法国空格版 | 0 | 存活 |
⇒ 伤害精确限定在点分隔格式;空格 / 横杠格式不受影响。
四个真实调用方(全部验证)¶
| 位置 | 时机 | 后果 |
|---|---|---|
src/utils/scraper.ts:477 |
addPhone 提取时 |
新号码不入库 |
src/utils/contact-pool.ts:88 |
writeContactRecord 写前 |
不进共享池 |
src/utils/contact-pool.ts:136 / :156 |
queryContactPoolLocal / queryContactPoolByUrl 读时 |
已存的读不出来 |
src/utils/contact-pool.ts:283 |
cleanPollutedContactPool 迁移 |
写回覆盖存量 |
迁移触发点:src/utils/engine-manager.ts:278 — 每个 SW session 首次进入 manageQueue() 时
fire-and-forget 执行(dedupeRanThisSession 守卫)。写回语句为
connection.update(... set: { phones: filtered, syncedAt: 0 }),删除后无还原路径。
⇒ 这不是「将来可能丢」,是已经在丢并在删存量。
为什么定 P0¶
对已入库数据的不可逆写入 + 影响面是一个完整市场(外贸营销 SaaS 的欧洲客户)。
修复方向(止损可立即做,判据需拍板)¶
判据不能只看点号数量,至少要区分:
- 坐标特征:含逗号分隔的两个小数、纬度范围
[-90, 90]+ 经度范围[-180, 180]、小数位 ≥ 4 - 电话特征:分段长度模式(法国
2-2-2-2-2)、总位数 8-15、可选国家码前缀 - 来源置信度:
tel:链接(Tier 1,网站主动声明)不应过噪音过滤
止损与判据是两件事(不要混为一谈)¶
| 层 | 内容 | 是否需要拍板 |
|---|---|---|
| 止损 | 停用 cleanPollutedContactPool 的存量销毁 |
不需要 — 停止破坏不等于选定新判据,可立即做 |
| 判据 | 新的坐标 vs 电话判别器往哪边偏 | 需要 Tony 拍板 |
🔴 待 Tony 拍板(判据层):收点分隔电话 = 承担少量 GPS 噪音回流; 不收 = 主动放弃法/比/瑞电话采集。这是产品市场取舍,不是技术选择。 详见 SPEC-010-current-defect-triage §7 与 §8-1。
回归判据¶
- 上表 4 个样本按预期分类
31.2304,121.4737与-33.8688,151.2093仍被拦- 存量迁移在修复前必须停用,避免继续销毁
发现过程¶
2026-08-30 多 agent 对抗审查发现,terra(openai) 与 dshf(deepseek) 跨厂商双路独立复现。 两路均独立指出这是产品取舍而非技术判断。
止损记录(SPEC-010 Phase 0,2026-08-30)¶
只做止损,判据本身本轮不动——isLikelyCoordinateNoise 函数体一个字符没改,
四个调用方(写入 / 读取 ×2)也都保持原样,等 Tony 拍板判据往哪边偏。
src/utils/engine-manager.ts:
- 新增导出常量 ENABLE_CONTACT_POOL_COORDINATE_CLEANUP = false
- manageQueue() 里原来的裸调用 cleanPollutedContactPool().catch(() => {}) 改成
if (ENABLE_CONTACT_POOL_COORDINATE_CLEANUP) { cleanPollutedContactPool().catch(() => {}); }
- 同一个 if (!dedupeRanThisSession) 块里的 revivePollutedDomains() 与
batchDedupeByUrl() 没有被一起停——只停了这一个调用点,其它职责不受影响
- cleanPollutedContactPool 函数本身、contact-pool.ts 里的三处调用方
(写前 / 读时 ×2)均未改动,可恢复:判据修好、验证过后把常量改回 true 即可
回归测试¶
tests/engine-manager-contact-pool-cleanup.test.ts——engine-manager.ts 顶层拉了
wxt/browser / jsstore / scraper-executor.ts 一整套只有真实扩展环境才能跑的依赖,
vitest/node 下 import 会炸,所以不 import 模块,改成读源码文本做接线断言:
- 止损开关常量存在且为 false
- cleanPollutedContactPool() 调用点必须被这个开关的 if 包住,不能裸跑
- 同一个 if 块里 revivePollutedDomains / batchDedupeByUrl 仍是裸调用(没被连坐停用)
- 函数定义本身没被删(import { cleanPollutedContactPool } 还在)
修复前(红):临时把调用改回裸调用重跑测试,「必须被开关包住」与「不能连坐」两条 断言按预期失败;改回止损版本后(绿)4 条全过。
状态¶
止损部分完成;判据部分维持 in-progress,等 Tony 拍板(见上方「修复方向」表格)。