跳转至

信号维度纪律 — 页级信号不能升级到站级状态

核心:抓取/状态/缓存的每个事件都有"作用维度"(行 / URL / 域名 / 全局)。 把小维度的失败信号升级大维度的状态降级 → 必然过度跳过 + 用户误以为产品坏。

ISSUE-0075(row vs URL)+ ISSUE-0076(URL vs domain)是同一类家族 bug。

维度层级(从小到大)

row(单商家行)
  └── URL(具体页面 https://example.com/contact)
        └── domain(example.com)
              └── global(系统所有)

规则:信号默认在原维度生效;要升级到更高维度需要明确的累积证据

反模式(红信号)

反模式 1:单次失败直接升级(无历史感知)

// ❌ domain-state.ts v0.10.114 之前
if (outcome === 'dead') {
  setState(stat, 'dead');   // 一次失败 → 整域名 30 天 TTL
}

jbjcortho.com 案例:首页 fetch ok-contact → 子页 HEAD dead → 整站打死,首页和其他子页全跳过。

反模式 2:页级信号污染域名状态

// ❌ contact 没找到(页级)升级到域名 cold(站级)
if (outcome === 'ok-empty') {
  // 5 次 ok-empty → cold
  setState(stat, 'cold');   // 整域名 90 天跳过
}

moleskidental.com 案例:5 个不同子页都没找到 contact → 整域名 cold → 包括首页都被跳过。

反模式 3:单点 404 不试 root 直接 dead

// ❌ probe.ts v0.10.115 之前
if (status === 404) return { kind: 'dead', reason: '404' };

子页 404 不代表整站死。用户给的 website URL 可能是失效医生页,root 仍正常。


正模式(绿信号)

模式 1:历史感知降级

// ✅ 已 ok 过的需累积证据才降级
if (outcome === 'dead') {
  const hasOkHistory = stat.recent.some(e => e.outcome === 'ok-*');
  if (hasOkHistory) {
    const recentDead = stat.recent.slice(-DEAD_CONSECUTIVE_AFTER_OK);
    if (recentDead.length >= 3 && recentDead.every(e => e.outcome === 'dead')) {
      setState(stat, 'dead');
    }
    return;  // 没满足条件不降级
  }
  setState(stat, 'dead');
}

模式 2:页级信号留在页级缓存

// ✅ contact 找不到 → URL 级缓存(ContactPool url-hash),不升域名
// 域名状态机只记 dead/antibot/friendly 等真正的站级信号

模式 3:fallback 链 — 404 试 root URL

// ✅ probe.ts v0.10.116
if (firstResult.kind === 'dead' && reason === '404') {
  const rootResult = await probeSingleUrl(getRootUrl(url));
  if (rootResult.kind !== 'dead') {
    return { ...rootResult, finalUrl: rootUrl };  // root 活就用 root
  }
}

决策表

信号 维度 该影响什么
HEAD/fetch 网络层失败 可能站级 累积 N 次才升 dead
404/410(明确状态码) 页级 仅当 root 也失败才升站级
5xx / 429 通常站级(瞬时) retry-later,不持久化
antibot 拦截 (cf-mitigated 等) 站级 升 antibot-soft/hard
ok-contact 站级正向 累积升 friendly
ok-empty (contact 找不到) 页级 只更新 ContactPool 缓存(url-hash),不升站级
ContactPool hit URL 级 短路 fetch,不影响域名

必问清单

写新状态机规则 / 缓存层 / 黑名单时:

[ ] 这个事件的作用维度是什么?(行 / URL / 域名 / 全局)
[ ] 我要把它影响到哪个维度?跨维度的话有充分证据吗?
[ ] 单次事件够吗?还是需要累积 N 次?
[ ] 有历史正向证据时,要不要"豁免"单次失败?
[ ] 升级有 TTL 吗?TTL 期间能不能手动复活?

当前合规处(已审)

模块 实践 备注
domain-state.ts dead 加历史感知 / 砍 ok-empty 升 cold / 加复活迁移 v0.10.116 修
probe.ts 404 试 root URL fallback v0.10.116 加
contact-pool.ts URL 级缓存(url-hash),不跨维度污染

历史复发(同家族 bug)

ISSUE 描述 修复版本
ISSUE-0075 row 维度 status 与 URL 维度 page-log 矛盾 v0.10.114
ISSUE-0076 页级 ok-empty 升域名 cold / 子页 dead 升站级 / 404 不试 root v0.10.116

反复 2 次就需要写规则 — 不能等第 3 次。


教训

  • 状态机的复杂度在维度,不在状态数 — 6 个状态 × 错位升级 = 600 种失控场景
  • "5 次失败 = 该跳"听起来合理,但要问"5 次是什么维度的失败"
  • 历史感知 ≥ 一刀切:已成功过的不该轻易降级
  • fallback 链要完整:404 不该是终点;root 试一次成本极低
  • 用户问"5 次是同 URL 还是不同子页?"是黄金洞察 — 设计者陷入"次数信号",用户从"维度"问出来