跳转至

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 的欧洲客户)。

修复方向(止损可立即做,判据需拍板)

判据不能只看点号数量,至少要区分:

  1. 坐标特征:含逗号分隔的两个小数、纬度范围 [-90, 90] + 经度范围 [-180, 180]、小数位 ≥ 4
  2. 电话特征:分段长度模式(法国 2-2-2-2-2)、总位数 8-15、可选国家码前缀
  3. 来源置信度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 拍板(见上方「修复方向」表格)。