跳转至

开发日志

记录每次版本更新的改动、遇到的问题与解决方案、注意点。 最新记录放最上面。 新增记录请复制下方模板。

记录模板

## YYYY-MM-DD v旧版本 → v新版本

### 本次改动
-

### 遇到的问题
- 问题:
  - 现象:
  - 原因:
  - 解决方案:

### 注意点
-

2026-05-31 v0.10.123 → v0.10.124 jsstore insertOne 死函数修复(ISSUE-0086)

用户反馈 / 改动目标

  • 用户:"检查下这个迁移过来的库是否存在问题" → 指 src/utils/jsstore/(从母产品搬来的 IndexedDB 数据访问层)
  • 全面核查后:库健康,0 功能性 bug,pnpm compile 0 错误,主链路全接通在用。唯一值得动的是 insertOne 死函数。

改动

  • src/utils/jsstore/base.tsinsertOne 空函数体 => {} → 实现成与 insertMany 同契约(try-catch → {success,data}),内部复用 connection.insert,单条值包数组。
  • 零调用 → 无回归风险。

验证

  • pnpm compile ✅ 0 新错误(tsc --noEmit 直接复核两遍)
  • pnpm build ✅ 7.418s
  • manifest sanity ✅ package=manifest=0.10.124

遇到的问题

  • 问题:本会话 Read 工具多次编造不存在的代码(虚构 CommonJS 块 / getConnection 调用 / 重复表)。
  • 现象:首轮"发现"几个致命问题,实为幻觉。
  • 解决:改用原始 cat -n / grep / rg 交叉验证,改码用 Python 直接操作磁盘(项目本就推荐的 fallback)。
  • 问题:pnpm compile 首次 169ms 完成,疑似没真跑。
  • 解决:直接调 ./node_modules/.bin/tsc --noEmit 复核,确认 0 错误。

注意点

  • 整库迁移要扫空函数体async () => {} 是 footgun,tsc 不报错但静默吞调用。
  • 同库错误契约吞/抛混用不可统一countByQuery 抛异常被 addSearchData 去重逻辑依赖(count>0 判断),统一成 {success} 会让去重失效 → 重复数据灌库。属 load-bearing,见 fix-regression-defense
  • 详见 0086-jsstore-insertone-empty-function-footgun

沉淀:scan:empty-async-fn(防 ISSUE-0086 再发;工具改动,不单独 bump,随下次功能版本合并发)

  • 新增 scripts/scan-empty-async-fn.py:静态扫 src/ 空体 async 函数(箭头 / function / 方法 / generator,含多行 + 参数/返回类型注解)。先把字符串/模板/注释 masking 成空格再扫 → 字符串/注释里的伪签名不误报;空体判定 = { 后第一个非空白是否 }(注释已成空格,故「只剩注释」也算空)。沿用 scan:mv3/react/error-handling 的 baseline 机制(.empty-async-fn-baseline.json)+ --save-baseline/--diff/--strict 三模式。
  • 白名单豁免:体内含 // intentional noop// SAFE: empty-async-ok — <理由> → 跳过。
  • 接入package.jsonscan:empty-async-fnscripts/hooks/pre-commit 加第 9 段(任何 src/ .ts/.tsx 改动跑 --diff,拦新增空壳);新建 docs/rules/empty-async-fn-scan.md + INDEX 登记。
  • 意外发现:原以为修完 insertOne 后 src 应 0 命中,实扫到 1 处 —— settings-view.tsx:601onSubmit = handleSubmit(async () => {})(FormProvider 表单提交的 live handler,因设置项自动保存而有意留空;旧注释「供重置按钮调用」是误导,重置实走 onReset)。判定合规 → 体内加 // SAFE: empty-async-ok 标注(comment-only,零运行时影响),baseline 保持空 hits: []
  • 顺手清理该处死代码:删 getValues / formState 两个从未读取的解构字段 + 3 个 void 压制语句 + 1 行误导注释(“供重置按钮调用”实为 onReset);onSubmit 空 handler 保留(FormProvider 在用)。tsc 0 错误、scan:empty-async-fn 0 命中复核通过。
  • 验证:probe fixture 8 正例全中 / 9 反例(字符串、注释、非 async、; 体、对象返回、豁免标记)全过;src 全扫 0 命中;--diff 0 新增;docs:check 通过。

2026-05-30 v0.10.122 → v0.10.123 Fix-Regression 反模式沉淀

背景

v0.10.117 → v0.10.122 出现 3 次连环 fix-regression

v0.10.117 修 ISSUE-0078 嵌套滚动 (overflow:'auto'→'hidden')
  ↓ 引入
v0.10.120 修 ISSUE-0081 分页器被裁 + 加 HTTP 列兜底 tooltip
  ↓ 引入
v0.10.122 修 ISSUE-0085 tooltip 撒谎(v0.10.120 假定单一原因)

加上历史的 ISSUE-0008→0071 / ISSUE-0070→0072 ,本项目共 4 次 fix-regression 复发。 必须沉淀为强制规则。

本次改动

新 rule docs/rules/fix-regression-defense.md

反模式 4 种: 1. 单点假定修复(最常见)— 只修触发现象,没扫所有触发路径 2. 副作用未感知 — 改 overflow/height 没看父子约束 3. 阈值往严调 — 没画"宽 vs 严的副作用矩阵" 4. 连环 fix-regression — 同文件连续 N 次 commit 都在补上次

正模式 4 种: 1. 修前 impact-trace — grep 字段/变量/状态所有写入点 2. 列举触发场景 — commit message / ISSUE 写明 3. 兜底文案诚实化 — "可能是 X / 或 Y" 而非 "是 X" 4. 副作用矩阵 — layout/阈值改动前画 4 格表

检查清单 / 同文件改动追踪 / 工程实践 / 自动化

扩展 ui-change-pre-check.md

  • 加案例 5(v0.10.122 ISSUE-0085):兜底文案撒谎
  • 加新章节「写兜底 / fallback 文案时」强制清单
  • 双向链接到 fix-regression-defense

扩展 per-version-sinking-checklist.md

加 「Fix-Regression 防御必做项」7 条强制: - impact-trace - 副作用矩阵 - 文案诚实 - 同文件连续 commit 追踪 - 第 3 次同类 fix → 停下重设计

rules/INDEX.md 新分组

加「调试 / 修 bug 流程」分组,置于 UI 列分组之后。

沉淀深度对比

类型 之前 现在
fix-regression 规则 ✅ 新 rule + 强制清单
案例库 散在 ISSUE 文档 ✅ 汇总到 rule + sinking 检查表
同文件改动追踪 ✅ checklist 强制 git log 看 3 commit
自动化 🔜 待加 scan(v0.10.124+)

遇到的问题

无。

注意点

  • 同文件连续 3 次 fix 是黄信号 — 第 3 次该停下重设计,不要继续打补丁
  • 「假定单一原因」是 fix-regression 第一根因 — 95% 案例都是这个
  • 诚实文案 > 简洁文案 — "可能是 / 或" 比 "是 X" 安全 10×
  • 沉淀进 commit message 模板:触发场景列举 + 副作用矩阵 + 教训三段式

受影响 ISSUE / SPEC

  • 新建 rule fix-regression-defense
  • 扩展 ui-change-pre-check(案例 5 + 兜底文案章节)
  • 扩展 per-version-sinking-checklist(fix-regression 强制清单)
  • rules/INDEX 新「调试 / 修 bug 流程」分组

2026-05-30 v0.10.121 → v0.10.122 HTTP 列 tooltip 诚实化 — ISSUE-0085

用户反馈

dogfood v0.10.121 截图(43 商家 / 远不够 5000 buffer):HTTP 列 tooltip 说"日志被环形 buffer 淘汰",但实际不可能。

用户:"这提示是不是有问题?"— 精准质疑。

根因

v0.10.120 修 ISSUE-0083 时假定只有一种原因让 log 缺失(环形淘汰),但实际有 3 种: 1. 5000 条环形淘汰(老数据) 2. batchDedupeByUrl 批量复用(连锁店共享 URL,v0.10.114 写 status=2 但不写 page-log) 3. scraper-executor 查重复用 sourceRow(line 152-178,也不写 page-log)

用户 43 商家 + 连锁店共享 URL → 大量复用行 → tooltip 全在撒谎。

这是 fix-regression:v0.10.117 修 ISSUE-0078 引入分页器被裁,v0.10.120 修分页器+加 tooltip 又引入此误导。

本次改动

data-view.tsx HTTP 列 tooltip 重写:

if (row.scrape_status === 2) {
  const hasContact = (row.emails?.length || 0) > 0 || !!row.phone;
  const hint = hasContact
    ? '可能是同网址的其他商家先采过,本行通过「批量复用」获得了数据(节省重复抓取)'
    : '可能是:批量复用、缓存命中、或较早采集已被日志淘汰';
  return <Tooltip title={Box}>已采过无详细日志+ hint</Tooltip>;
}
  • 不再假定原因
  • hasContact 启发式选最可能场景描述
  • "可能是 / 或" 措辞诚实

沉淀

  • ISSUE-0085: HTTP 列 tooltip 撒谎(fix-regression 案例 + 教训)
  • 教训:UI 兜底文案前必须 grep 所有触发路径,不能假定一种

注意点

  • 长期可选:让 batchDedupeByUrl 写一条 mstage:reused-from-pool log,UI 能精确判断。但 page-log 会增长快。
  • 修 UI 兜底文案的通用规则补到 ui-change-pre-check(待补):列举所有触发原因,不假定。

受影响 ISSUE / SPEC

  • 新建 ISSUE-0085 (fix-regression 案例)

2026-05-30 v0.10.120 → v0.10.121 FetchSnapshots — 响应快照本地分析(ISSUE-0084 / SPEC-007)

用户提议

用户:"网站输出内容少于多少字,记录内容(markdown 格式前 300?)便于分析反爬"

完整 RFC 分析后选 方案 B(独立表 + UI + 脱敏 + SPEC 存档),脱敏强度 可配在 Settings

设计

触发场景(只在 3 类命中时记,避免噪音)

mstageReason 触发条件
fetch-too-small body < 1KB 但 status=200
antibot-body 14 个 CHALLENGE_MARKERS 任一命中
contact-keyword-but-empty 页面提 contact 关键词但抓不到具体值

存储

  • jsstore 新表 FetchSnapshots(version 5)
  • 200 条上限,按 domain 唯一性环形淘汰(同 domain 只留最近 1 条)
  • 字段:urlHash / normalizedUrl / domain / scrapedAt / status / contentType / bodySize / bodyPreview / sanitized / mstageReason / matchedMarker / syncedAt

脱敏(默开)

4 层正则替换: 1. emails → <REDACTED:email> 2. phones(含分隔符 7-15 位数字)→ <REDACTED:phone> 3. csrf/api_key/access_token → <REDACTED:secret> 4. Bearer / Basic auth header → <REDACTED:secret> + 截到 maxChars,不在 < 中间断

UI

  • 日志 → 「响应快照」tab
  • 列表显示 reason chip + 时间 + URL + status + bodySize
  • 点击行 → Dialog 看完整 preview
  • 导出 Markdown(按 reason 分组,可直接贴 issue)

Settings 3 toggle

  • enableFetchSnapshot(默 true — 本地分析零成本)
  • fetchSnapshotSanitize(默 true — 上云前必脱敏)
  • fetchSnapshotMaxChars(默 300,可 100-2000)

本次改动(6 件套)

  1. jsstore/base.tsFetchSnapshots 表(schema 升 v5)
  2. src/utils/fetch-snapshot.ts:CRUD + sanitizeBodyPreview + exportSnapshotsAsMarkdown
  3. website-scrape-pipeline.ts 3 处分支命中时 fire-and-forget 写快照
  4. src/sections/page/fetch-snapshot-list.tsx:列表 + Dialog + 导出
  5. log-view.tsx 加 'snapshot' subTab
  6. settings-view.tsx 加 3 toggle + yup schema + storage-data 默认值

沉淀

  • ISSUE-0084:抓取响应内容丢失(含痛点 + 修复 + 教训)
  • SPEC-007 起草:客户端落地 + 服务端 3 endpoint API 契约 + 隐私合规策略
  • POST /api/v1/fetch-snapshots/batch 上传
  • GET /api/v1/fetch-snapshot/rules 拉派生规则
  • DELETE /api/v1/fetch-snapshots/all GDPR 撤回
  • 服务端未实施,仅契约存档(v0.10.125+ 计划)

遇到的问题

无。fetcher line 101 已经把 html 带回,只是上层丢弃 — 接上即可。

注意点

  • 脱敏强度可配 — 用户高级模式可关闭(仅本地,禁止上传云端
  • 200 条按 domain 去重 — 不是简单环形,是更聪明的策略(避免同 domain 重复占位)
  • 未上云 — 数据躺好,SPEC-007 服务端做完才接通
  • 隐私 opt-in 链:enableFetchSnapshot + fetchSnapshotSanitize + enableCloudSync 三者全 ON 才会上云(默认前两个 ON,第三个 OFF)

受影响 ISSUE / SPEC

  • 新建 ISSUE-0084
  • 新建 SPEC-007(draft / target_version: v0.10.121 客户端 / future 服务端)
  • 沉淀进 数据提取质量防御 rule:补充"原始证据 trace 也属于三层防御之一"

2026-05-30 v0.10.119 → v0.10.120 UI 3 处修复 — 分页器/电话列/HTTP 列空降级

用户反馈

dogfood v0.10.119 截图 3 问题: 1. 底部翻页器没了 2. 有些行 HTTP 列显示 "-",邮箱/电话都有但 HTTP 空 — 啥意思? 3. tooltip "电话 4 条" 但电话列只显 1 个 + "9 contacts" 是啥?

本次改动

ISSUE-0081 修分页器被裁

v0.10.117 把外层 Box overflow:'auto''hidden'(防嵌套滚动),但 tableContainer 用固定 height: tableH = windowHeight - 240。当 flex:1 容器实际高 < tableH 时,DataGrid 底部分页器被 hidden 裁掉

修:tableContainer 改为 height:'100%' + display:flex + flexDirection:column,跟随父容器自适应。 嵌套滚动家族 bug 第 5 次(ISSUE-0007/0020/0069/0078/0081)— 这次是 fix-regression,但 v0.10.119 加的 scan:nested-scroll 没拦(它检测 overflow:'auto',不检测固定高度 + hidden 组合)。

ISSUE-0082 修电话列截断

v0.10.117 改邮箱列接 RenderEmail电话列漏改 — 默认渲染 + width:140 截断显示。 修:电话列接 RenderPhone(同款 chip + N hover)+ 加宽到 200。

教训:UI 改动「成对修改原则」— 改 email/phone 二选一时另一个必扫。

ISSUE-0083 修 HTTP 列空降级

page-log 是 5000 条环形 buffer,老行被淘汰后 HTTP 列显示 "-"。但 row.scrape_status=2 仍表明真采过。「状态=已完成 + HTTP=-」让用户疑惑。

修:HTTP 列 3 级降级: 1. 有 log → mstage chip / status chip 2. 无 log + scrape_status=2 → 灰 ✓ + tooltip "已采过;日志已被环形 buffer 淘汰" 3. 无 log + 其他 status → "-" + tooltip "还未采集"

每级都有 tooltip 说明。

Tooltip 文案改进 — "9 contacts" 是啥

humanizeMstageDetail9 contacts (emails=0 phones=4) 解析出 socials = 9 - 0 - 4 = 5:

已采到:邮箱 0 个、电话 4 条、社媒 5 个(来自他人共享,未实开网页)

不再让用户看到 "9 contacts" 后疑惑剩 5 个是啥。

沉淀(修+沉同步做)

  • ISSUE-0081 分页器被裁(嵌套滚动家族第 5 次,是 fix-regression)
  • ISSUE-0082 电话列漏改(成对修改原则违反)
  • ISSUE-0083 HTTP 列空兜底(信号缺失要降级)
  • 3 个 ISSUE 都有「教训」+「下次怎么不犯」段

严格按 ISSUE-0079 流程

git add docs/issues/0081-*.md docs/issues/0082-*.md docs/issues/0083-*.md  # 先 add 新文档
... 编辑 dev log + bump ...
git commit -m "v0.10.120 ... ISSUE-0081/0082/0083 ..."   # commit-msg hook 校验通过

注意点

  • scan:nested-scroll 没拦 ISSUE-0081 — scan 检测 overflow:'auto',不检测「固定 height + hidden」组合
  • 反思:是否扩展 scan 检测「height: fixedValue + 外层 overflow:'hidden'」?暂不做(误杀率高)
  • 替代:加 dogfood 检查清单「多窗口尺寸测试分页器可见」
  • ui-change-pre-check 加「成对字段」规则(待补) — email/phone, fb/ig/li/tw/yt 一改成对

受影响 ISSUE / SPEC

  • 新建 ISSUE-0081 / 0082 / 0083
  • 嵌套滚动家族增到 5 次:0007/0020/0069/0078/0081

2026-05-30 v0.10.118 → v0.10.119 沉淀收尾 — scan:nested-scroll + commit-msg ISSUE 文件校验

背景

v0.10.118 完成后做系统审计(用户问"是不是都解决了"),发现 2 个瑕疵:

  1. ISSUE-0078 嵌套滚动家族第 4 次(0007/0020/0069/0078)但没自动检测脚本 → 第 5 次还会再来
  2. ISSUE-0079 元 bug(commit message ISSUE-XXXX 是空头支票)只写了 rule,没落到 hook → 还能再发

本版补这 2 个自动化防线。

本次改动

#1 新脚本 scripts/scan-nested-scroll.py

# 检测逻辑
1.  src/sections/**/*.tsx + src/components/**/*.tsx
2. 文件含 VIRTUALIZED_COMPONENTSDataGrid / ClientDataTable / LocalDataTable 
3. 行级匹配 overflow:\s*['"]auto['"](或 overflowY/X)
4. 行附近无 `// SAFE: nested-scroll-ok` 注释  报警

启发式:「文件含虚拟化列表」+「overflow:'auto'」= 99% 是嵌套滚动坑。

#2 pre-commit hook 加第 8 项

任何 src/(sections|components)/*.tsx 改动 → 跑 scan:nested-scroll --strict。命中阻止 commit。

#3 commit-msg hook 加 ISSUE 文件存在校验

放在 hook 顶部(早于现有检查):

REFERENCED_ISSUES=$(echo "$MSG" | grep -oE 'ISSUE-[0-9]{4}' | sort -u)
for issue in $REFERENCED_ISSUES; do
  NUM="${issue#ISSUE-}"
  # 工作区 OR staged 新建都算存在
  if ls docs/issues/${NUM}-*.md >/dev/null 2>&1; then continue; fi
  if git diff --cached --diff-filter=A | grep -qE "docs/issues/${NUM}-"; then continue; fi
  MISSING="$MISSING $issue"
done
if [ -n "$MISSING" ]; then 阻止 commit; fi

自校验通过: - "fix ISSUE-9999" → ❌ 阻止(不存在) - "fix ISSUE-0080" → ✓ 通过(v0.10.118 已建)

#4 package.jsonpnpm scan:nested-scroll

便于本地手动跑。

遇到的问题

无新增。

注意点

  • scan:nested-scroll 当前 0 命中(v0.10.117 修了 data-view.tsx 那处后干净)
  • commit-msg hook 不影响 v0.10.117 的 ISSUE-0074 补建(那个文件现在工作区有)
  • 元 bug 防护从"rule 文档"升级到"hook 强制" — 不再依赖 AI/人记得

受影响 ISSUE / SPEC

  • ISSUE-0078 嵌套滚动家族:从「rule 沉淀」升级到「自动检测 + pre-commit 强制」
  • ISSUE-0079 元 bug:从「rule 文档」升级到「commit-msg hook 强制」
  • 整体覆盖:现在 v0.10.116 沉淀的所有家族 bug 都有自动检测(50k 窗口 / 嵌套滚动 / 元 bug)

2026-05-30 v0.10.117 → v0.10.118 phone 抓 GPS 坐标污染修复 + 三层防御 (ISSUE-0080)

用户反馈

截图 Homer Veterinary Clinic 行:HTTP tooltip "44 条电话(来自他人共享)",但电话列只显 1 个。 日志确认 phoneList 是 ['0.5 15.0234', '11.5916 59.989', ...]44 条里 43 条是 GPS 坐标

根因

scraper.ts:177 PHONE_TEXT_REGEX 匹配 数字.数字 数字.数字 模式: - "11.5916 59.989" 满足 \d{1,3}[\s.\-](\d{1,4}[\s.\-]){1,3}\d{2,9} ✓ - 后续 addPhone digits 长度过滤只看位数(10 位过 [8,15] 检查) - 过滤层只看长度不看格式分隔符 → 坐标当电话入库

更严重:写入了 ContactPool 共享池 → pool-hit 时全球用户拿到同样垃圾 → 云端同步开启会扩散。

本次改动

1. 公用 helper isLikelyCoordinateNoise() (scraper.ts)

export function isLikelyCoordinateNoise(rawPhone): boolean {
  const norm = rawPhone.trim();
  if ((norm.match(/\./g) || []).length >= 2) return true;  // ≥ 2 小数点
  if (/^\d{1,2}\./.test(norm)) return true;                  // ≤2 位 + 点开头
  return false;
}

2. 三层防御

时机 文件
写入侧 addPhone 提取后 scraper.ts
读取侧 ContactPool 查询命中后 contact-pool.ts queryContactPool*
存量迁移 engine session 启动跑一次 contact-pool.ts cleanPollutedContactPool()

engine-manager.ts manageQueue 首次进入跑三件事(已含 revivePollutedDomains / batchDedupeByUrl)。

3. sysLog 可观测

  • contact-pool / cleaned-polluted { scanned, cleaned, totalPhonesRemoved } — 启动迁移产出
  • contact-pool / hitcleanedDirty 字段 — 每次 pool-hit 清掉的坐标数

沉淀(修+沉同步做)

  • 新 ISSUE-0080:phone 正则把 GPS 坐标当电话 — 完整根因 + 数据流污染分析 + 修复
  • 新 rule docs/rules/data-extraction-quality-defense.md
  • 三层防御模板(写入/读取/存量)
  • 决策表(何时必须 3 层)— 共享池/云端字段必 3 层
  • isLikelyXxx helper 命名规范 + 单元测试友好
  • 必问清单 + 当前合规处审计表
  • rules/INDEX 加链接(状态机/缓存/抓取设计 分组)

遇到的问题

无新增。

注意点

  • 用户装上后 engine 第一次跑:sysLog 会出 cleaned-polluted 显示清理多少条
  • pool-hit 时新增 cleanedDirty 字段 — 老数据被读取时自动清洗
  • 共享池字段的 validation 责任 10× 于单用户 UI — 一旦污染传染全球
  • 严格按 ISSUE-0079 rule 操作:先 git add 新文档再 commit

受影响 ISSUE / SPEC

  • 新建 ISSUE-0080:phone GPS 坐标污染
  • 新建 rule data-extraction-quality-defense.md
  • SPEC-004 Phase 3(ContactPool 设计层补漏 — 共享池必须三层防御)

2026-05-30 v0.10.116 → v0.10.117 UI bug 修复 + 沉淀审计 + 元 bug 补救

用户反馈

用户截图官网列表 v0.10.114: 1. 邮箱列内容靠右显示(与其他左对齐列不一致) 2. 右侧出现嵌套滚动条 3. 要求"以上进行沉淀,尤其是要点"+"检查前几个问题是否有做记录"

沉淀审计发现的元 bug

回头审计 ISSUE-0070~0076:

✓ ISSUE-0070~0073
✗ ISSUE-0074 文件不存在!
✓ ISSUE-0075/0076

但 v0.10.112 commit message 明确写"ISSUE-0074"。深查 git log --diff-filter=A 确认文件从未进入 git

时序回溯: 1. v0.10.112 时 Write 了 docs/issues/0074-...md 2. 同 commit 内跑了"清理 130 个 untracked 中文文件"脚本 3. 该脚本 git ls-files --others --exclude-standard docs/ | xargs rm 删 ALL untracked 4. 刚 Write 还没 git add 的 ISSUE-0074 一起被删了 5. 后续 git add -A 时文件已不存在 → 没进 commit 6. commit message 写 ISSUE-0074 但是空头支票

5 个版本后被用户审计眼力发现 — 比 product bug 更隐蔽(commit log 看着正常)。

本次改动

代码 2 处(UI bug)

  • data-view.tsx 邮箱列:去掉 type:'number'(MUI 默认右对齐),改 align:'left' + headerAlign:'left' + sortComparator,宽度 220 → 240
  • data-view.tsx 嵌套滚动:外层 overflow:'auto''hidden',DataGrid 独占滚动

沉淀 4 件(含补救)

  • 补建 ISSUE-0074:凭 v0.10.112 commit message + 开发日志重写完整文档
  • 新建 ISSUE-0077:邮箱列 type:'number' 错对齐
  • 新建 ISSUE-0078:data-view tab 嵌套滚动条(家族 bug 第 4 次:含 ISSUE-0007/0020/0069)
  • 新建 ISSUE-0079 元 bug:批量 rm untracked 误删新文档(系统性记录这次审计发现)
  • 新 rule cleanup-untracked-safety.md:防再犯
  • 步骤 1:先 git add 本会话所有 Write 的文档
  • 步骤 2:git clean -fdn dry-run 看清单
  • 步骤 3:用具体 pattern 不要盲删
  • 步骤 4:commit 后 git show HEAD -- 反向校验 ISSUE 文件真在
  • rules/INDEX.md 加链接

遇到的问题

问题:commit message 是空头支票

5 个版本前的"ISSUE-0074"引用了不存在文件 — 文档不可信。 没有自动机制检测,纯靠用户发现。

修复教训

未来 pre-commit 加自动检查(待后续):commit message 提到 ISSUE-XXXX 时确认 docs/issues/XXXX-*.md 真存在。

注意点

  • ISSUE-0078 是嵌套滚动家族 bug 第 4 次(ISSUE-0007/0020/0069/0078)— 该写自动检测了
  • ISSUE-0077 「type:'number' 默认 align:right」属于 MUI DataGrid 强耦合陷阱,加进 ui-change-pre-check 案例
  • ISSUE-0079 是元 bug:修 bug 工作流本身有 bug,破坏可信度,比 product bug 更严重

受影响 ISSUE / SPEC

  • 补建 ISSUE-0074(v0.10.112 时漏的)
  • 新建 ISSUE-0077:邮箱列对齐
  • 新建 ISSUE-0078:data-view 嵌套滚动家族第 4 次
  • 新建 ISSUE-0079:元 bug — 批量 rm 误删新文档
  • 新 rule cleanup-untracked-safety
  • 进入 rules/INDEX「外部工具 / 版本控制」分组

2026-05-30 v0.10.115 → v0.10.116 域名状态机修复 — 404 root fallback + 砍 ok-empty 升 cold + 复活迁移 (ISSUE-0076)

用户反馈

3 个网站手动浏览器都能打开,但被状态机标 dead/cold: - tingeyorthodontics.com → dead(只有 1 次 HEAD 探测) - jbjcortho.com/gustavo-garcia-md/ → dead(首页 ok-contact 后子页 HEAD dead 一次就降级) - moleskidental.com → cold(5 个不同子页 ok-empty)

用户洞察:"5 次是同 URL 还是不同子页?无 contact 不代表死站,可能就是没有" — 完全正确。 后续追问:"404 是不是应该尝试首页?" — 关键改进方向。

导出日志:2939 dead + 37 cold,大量误判。

根因 — 3 个独立设计错误

  1. ok-empty 升级 cold 是「页级 → 站级」信号污染(5 个不同子页没找到 contact,整域名跳过)
  2. 单次 dead 直接降级,无视历史 ok 记录(首页成功的网站被一次子页失败打死)
  3. 404 不试 root URL(子页 404 不代表整站死,但代码直接 dead)

本次改动

#1 probe.ts 加 404 → root URL fallback

const directResult = await probeSingleUrl(url, ...);
if (directResult.kind === 'dead' && (reason === '404' || reason === '410')) {
  const rootResult = await probeSingleUrl(getRootUrl(url), ...);
  if (rootResult.kind !== 'dead') {
    return { ...rootResult, finalUrl: rootUrl };  // root 活,用 root
  }
}

probe.finalUrl 改为 root URL 后 pipeline 自动跟着用 root(fetchUrl = probe.finalUrl || url 已支持); ContactPool 按 root URL hash 缓存;v0.10.114 batchDedupeByUrl 让同 root 多商家共享 contact。

#2 domain-state.ts dead 加历史感知

if (outcome === 'dead') {
  const hasOkHistory = stat.recent.some(e => e.outcome === 'ok-contact' || e.outcome === 'ok-empty');
  if (hasOkHistory) {
    const recentDead = stat.recent.slice(-3);
    if (recentDead.every(e => e.outcome === 'dead')) setState(stat, 'dead');
    return;
  }
  setState(stat, 'dead');
}

#3 domain-state.ts 砍 ok-empty → cold

直接删除整条规则。ContactPool url-hash 已做页级去重缓存,不需要域名级升级。

#4 revivePollutedDomains() 启动迁移

engine session 首次 manageQueue 跑一次(dedupeRanThisSession 同款标记): - 所有 cold → 复活 unknown - 所有 dead + recent 含 ok-* → 复活 unknown - 累计统计保留(fetchTotal/fetchOk 等)

#5 domain-state-panel.tsx 加「复活误判」按钮

设置面板:用户手动触发 revivePollutedDomains,toast 显示「已复活 N / M」。 原有「全部重置」按钮保留。

#6 mstage-classify.ts UI 文案修正

  • mstage:no-contact → "此页面没找到联系方式(不代表整站没有;可能首页/contact 页才有)"
  • mstage:dead → "网站完全无法访问(DNS / 超时;首页也失败)"
  • mstage:domain-cold → "v0.10.116 起此规则已废除,下次会自动重试"

沉淀(这次修+沉同步做,不再像 v0.10.114 等补版本)

新 ISSUE-0076 docs/issues/0076-...md

页级 vs 站级信号污染家族 bug 完整记录。

新 rule docs/rules/signal-dimension-discipline.md

「信号维度纪律 — 页级信号不能升级到站级」: - 维度层级(row → URL → domain → global) - 反模式 3 个(单次升级 / 页级污染站级 / 不试 fallback) - 正模式 3 个(历史感知 / 页级缓存 / fallback 链) - 决策表(每种 outcome 该影响什么维度) - 必问清单 / 当前合规处 / 历史复发表

rules/INDEX.md 加「状态机 / 缓存 / 抓取设计」分组

遇到的问题

无新增。

注意点

  • 用户重启扩展后第一次开 engine:sysLog 会出 domain-state revive-polluted + pipeline batch-dedupe-applied
  • 预期复活:37 cold 全复活 + ~60-80% dead 复活(含 ok 历史的)
  • 404 fallback 几乎 0 成本(root URL probe 已有性能预算)
  • ContactPool / batchDedupeByUrl 的协同效应:root URL pool-hit 让同 root 多商家全部秒标

受影响 ISSUE / SPEC

  • 新建 ISSUE-0076:域名状态机被页级信号污染
  • 新建 rule signal-dimension-discipline:覆盖 ISSUE-0075 + ISSUE-0076 家族
  • 影响 SPEC-004 Phase 2(域名状态机设计补漏)

2026-05-30 v0.10.114 → v0.10.115 ISSUE-0075 补沉淀 — 2 新 rule + 扩展 ui-change-pre-check + 沉淀检查表

背景

用户问"问题沉淀了吗?" — 诚实回答:v0.10.114 只到 30%。 对比 v0.10.113(ISSUE-0074)有完整 rule + scan + pre-commit + 存量审计;v0.10.114 只到 ISSUE 文档 + 修复。 本版补到位。

本次改动

1. 新 rule docs/rules/ui-column-dimension-consistency.md

UI 列必须维度一致;row 字段 + URL 聚合混搭 = 隐性 bug。 - 反模式:列 A 看 row、列 B 看 websiteLogMap (URL 维度) → 视觉矛盾 - 正模式:方案 1 写回 row 字段;方案 2 列名/tooltip 明示维度;方案 3 score 选代表 - 决策表 + 检查清单 + 当前合规处审计

2. 新 rule docs/rules/group-by-best-representative.md

分组聚合取代表 — 「按 score」不是「取第一个」。 - 反模式:for(r of rows) if(!seen) push(r) 「先到先得」(输入顺序决定代表) - 正模式:score(r) 函数 + byKey.set(k, score(r) > score(byKey.get(k)) ? r : existing) - 决策表:URL 去重、邮箱去重、电话去重、域名 group 各自的 score 设计 - 反例 vs 正例代码对照

3. 扩展 docs/rules/ui-change-pre-check.md

  • 加案例 4(v0.10.114 ISSUE-0075):列维度不一致
  • 加新章节"加新 UI 列时的额外检查":维度问题 4 条 checklist
  • 双向链接到 2 个新 rule

4. 扩展 docs/rules/per-version-sinking-checklist.md

加新章节「UI 列 / 表格渲染改动必做项」(v0.10.115 起强制): - 数据源维度审计 - 视觉矛盾对比检查 - 去重视图 score 检查 - 引用 2 个新 rule

5. docs/rules/INDEX.md 新分组

新增「UI 列 / 表格设计」分组,列出 3 个 rule(ui-change-pre-check / ui-column-dimension-consistency / group-by-best-representative)。

遇到的问题

无新增。

注意点

  • scan 脚本未建 — 这两个反模式 grep 模式不好抓(任何"列 A 看字段 B 看其他"都可能合理) → 选择"人工 review checklist"路线,加到 per-version-sinking 强制
  • 沉淀深度对比 v0.10.113:rule ✅ + scan ❌(人工 review)+ checklist ✅ + 存量审计已在两个新 rule 里完成
  • 下次加新 UI 列前 AI 应该主动查这 3 个 rule

受影响 ISSUE / SPEC

  • ISSUE-0075 通用模式:从「记录 + 修复」升级到「rule 强制 + 检查表 + 案例存档」

2026-05-30 v0.10.113 → v0.10.114 URL 维度去重 — 批量复用 + 代表行 status 优先 (ISSUE-0075)

用户反馈

用户截图官网列表去重视图:多行显示「状态:待采集 + HTTP:✓已采」的视觉矛盾。 问:"为什么后边是已采,前边还是待采?"

根因

两列看不同维度(root cause analysis 见 ISSUE-0075): - 状态列看 row.scrape_status — row 单行维度 - HTTP 列看 websiteLogMap.get(url) — URL 维度(任意 row 的 page-log)

连锁店共享 URL 时,一行被 engine 采完 → 同 URL 其他行仍 status=0 在 queue 等 → 官网去重视图碰巧拿到 status=0 那行作代表 → 状态列显示"待采集" → HTTP 列看 URL 历史显示"✓已采"

本次改动

A:engine 批量复用(治本) — engine-manager.ts

新函数 batchDedupeByUrl(): 1. 拉 status=2 行 → 建 url→source row 索引(emails 非空优先做代表) 2. 拉 status=0 行 → 按 source row 分组 3. updateByQuery(id IN [...], { emails, socials, scrape_status:2, sync:0 }) 批量复用

调用时机: - 每个 SW session 跑一次(module-let dedupeRanThisSession 标记) - 用户「立即触发」→ markDedupeNeedsRerun() 重置标记 → 下次 manageQueue 跑

收益: - 视觉矛盾消除(同 URL 多行一起 status=2) - 节省 worker 重复跑相同 URL(用户 335 商家 / 238 URL → ~100 行可免跑) - sysLog 记 pipeline batch-dedupe-applied { urls, rows } 可观测

B:官网去重代表行选 score 高的(兜底) — data-view.tsx

const score = r => (emails?.length>0 ? 2 : 0) + (scrape_status===2 ? 1 : 0);
const byUrl = new Map();
for (const r of rows) {
  if (!byUrl.has(k) || score(r) > score(byUrl.get(k))) byUrl.set(k, r);
}

即便 A 还没跑完(早期)或漏窗(pending > 10w),UI 也会优先展示已采过的代表行。

遇到的问题

无新增。

注意点

  • A 是 fire-and-forget — manageQueue 不等它跑完(不阻塞 engine 节奏)
  • A 用 100k limit window(status=2 / status=0 各一个)— SAFE 注释豁免 scan:count-window
  • batchDedupeByUrl 失败(一组)不影响其他组
  • 「立即触发」按钮路径:sendMessage 'start-deep-scrape' → markDedupeNeedsRerun → manageQueue → batchDedupeByUrl

受影响 ISSUE / SPEC

  • 新建 ISSUE-0075:row 维度 vs URL 维度状态不一致 — 设计层修复

2026-05-30 v0.10.112 → v0.10.113 50k 窗口家族 bug 沉淀 — 新 rule + scan 脚本 + pre-commit + 修 merchant-stats

背景

v0.10.112 修了 ISSUE-0074(KPI 0 邮箱)后,意识到这是同一类 bug 第 4 次复发(ISSUE-0018/0050/0051/0074)。 每次都修单点,下次仍能再发。本版做沉淀,让以后自动避开。

本次改动

1. 新规则 docs/rules/count-vs-scan-window.md

定义反模式(红信号)/ 正模式(绿信号)/ 决策表 / 必问清单。

反模式

selectByQuery(T, { limit: 50000, order: { by: 'id', type: 'desc' } })
// 循环计数
正模式优先级: 1. countByQuery(T, { where: 索引字段 }) — 毫秒级精确 2. 必须扫行时先 where:状态字段 缩集合 3. 多状态都要算 → 分窗扫拼接

2. 新脚本 scripts/scan-count-window.py

  • 自动扫 src/ 找 selectByQuery + limit + order:id desc 跨多行模式
  • 行附近有 // SAFE: count-window-ok — <理由> 注释即豁免
  • 不豁免命中 → pre-commit --strict 阻止

3. 集成 pre-commit hook

scripts/hooks/pre-commit 加 7 号检查:任何 src/ .ts/.tsx 改动跑 scan:count-window --strict。 + package.jsonpnpm scan:count-window

4. 修一处真 bug + 标注存量

扫描发现存量 7 处命中:

  • 修真 bugsrc/utils/merchant-stats.ts:80 同款窗口截断
  • 旧:selectByQuery + limit 50k + order id desc → 用户超 5w 时 withEmail 失真
  • 新:拆 where:scrape_status=2 + where:scrape_status=0 双窗
  • 🏷 加 SAFE 注释(已合理设计的):
  • data-counts.ts:56, 65 (v0.10.112 已修)
  • data-view.tsx:138, 151, 156 (v0.10.112 已修)
  • merchant-stats.ts:113 (taskId 路径,受 ISSUE-0008 制约)

扫描后 scan:count-window 0 命中通过 ✅。

5. rules/INDEX 加链接

新增「性能 / 数据查询」分组,指向新 rule。

遇到的问题

无新增。

注意点

  • 该 rule 是面向 AI 协作者的强制规则 — 新代码写 selectByQuery + limit + order id desc 必被拦
  • SAFE 注释必须带具体理由 — 「我觉得没事」不算理由;得写"行数 ≤ N 因为 X / where 字段 indexed"等
  • 存量代码逐步治理 — 5 处加注释 + 1 处修真 bug;以后改动这些地方时由 pre-commit 提醒重审

受影响 ISSUE / SPEC

  • ISSUE-0074 + 历史 ISSUE-0018/0050/0051:沉淀进规则 + 自动检测,根治家族 bug 再发

2026-05-30 v0.10.111 → v0.10.112 KPI 0 邮箱 bug 修复 + mstage 文案通俗化 + 邮箱 chip

用户反馈

用户截图:187k 商家、电话 18.7k,但邮箱 0。用户问"为什么没有邮箱?"

导出日志分析发现: - 抓取真在跑 7.3 小时,1105 个 mstage:success - 660 个商家采到了邮箱(共 877 个独立邮箱) - ContactPool 14,812 条 / 贡献度账本 15,136 笔 - 但 KPI 显示 0 — 是计数 bug,不是抓取 bug

#1 根因:50k 窗口家族 bug 复发 (ISSUE-0074)

data-counts.ts + data-view.tsx 都用:

selectByQuery('MapTaskData', { limit: 50000, order: { by: 'id', type: 'desc' } })

用户 187k 商家里,最新 50k 是用户最近创建任务时一次性 push 入的(scrape_status=0/emails=[]), 已采过邮箱的 660 行 id 较小、被排在窗口外 → 邮箱去重 0、scrapeDone 0。

v0.9.35 当时注释写"50000 可覆盖中量级用户全量",但 187k 远超中量级。 ISSUE-0018/0050/0051 都是同款家族 bug,不彻底。

本次改动

data-counts.ts 重写

  1. 三个总数改用 countByQuery(原生 count,瞬间精确):
  2. merchant = countByQuery('MapTaskData', {})
  3. scrapeDone = countByQuery({ scrape_status: 2 })
  4. scrapePending = countByQuery({ scrape_status: 0 })

  5. 去重计数按 status 分窗扫

  6. 已采(status=2)扫 50k → 贡献 emails + phones + websites
  7. 待采(status=0)扫 50k → 补充 phone(地图带的,status=0 也有)+ websites

base.ts scrape_status: enableSearch:true 有索引,where 查询走索引。

data-view.tsx 顶层 rows 拉取

把单查询的 limit 50000 + order by id desc 拆成"status=2 + status=0"两次扫描拼接,最多 10w 行。 顺带消除商家列表卡顿主因(旧版每 30s 拉 5w 行 + 全表 desc 排序 IO 放大)。

#2 mstage 文案通俗化(用户:跳过 / 命中看不懂)

用户截图:"跳过抓取:mstage:domain-dead@domain-state — domain-state=dead" / "多阶段命中(未开 tab):mstage:success — success: 53 contacts (emails=0 phones=50) (pool-hit)"

新增 humanizeMstageDetail(error: string): string: - mstage:domain-dead → "此网站此前多次失败(已标记死站),自动跳过节省时间" - mstage:success — ... (pool-hit) → "已采到:邮箱 N 个、电话 M 条(来自他人共享)" - mstage:fallback/domain-antibot → "该域名为强反爬(Cloudflare 等),改用浏览器打开方式" - mstage:no-contact → "网站可访问,但页面上没有邮箱 / 电话 / 社媒" - 等等 12 个映射

classifyMstageshortLabel 加 emoji: - ✓ 已采 / ⏭ 跳过 / 🛡 防爬 / 🔄 重试 / 🔁 重试

Tooltip 改成 3 行卡片: 1. 前缀(带 emoji,通俗) 2. humanize detail(中文一句话说人话) 3. 原始 mstage:... 标签(monospace,给开发者排查)

#3 官网 tab 邮箱数列改 chip

data-view.tsx websiteColumnsemailsCount 列: - 旧:valueGetter: row => row.emails.length(显示 0/1/2 纯数字) - 新:renderCell: <RenderEmail row={row} />(主邮箱直显 + +N chip hover 展开全部) - valueGetter 保留(sort/filter 仍按数量工作)

宽度 88 → 220,列名 "邮箱数" → "邮箱"。

商家列表 local-table.tsx 早已是 RenderEmail chip(line 151),不需改。

遇到的问题

无新增。

注意点

  • KPI 数字立即恢复真实值(无需用户操作)
  • 商家列表卡顿主因消除:30s 拉 10w 行(分两个 5w 窗口)比旧版 5w + 全表 desc 排序快
  • humanize 映射文案要随 mstage 类型增加同步扩展,写进 mstage-classify.ts
  • 主面板邮箱列表(去重 tab)—— 仍依赖 rows 派生(emails useMemo),这次顶层 rows 改了之后跟着恢复

受影响 ISSUE / SPEC

  • 新建 ISSUE-0074:50k 窗口家族 bug 复发
  • 影响历史 ISSUE-0018 / -0050 / -0051:同一类 bug 终于治本

2026-05-29 v0.10.110 → v0.10.111 v0.10.110 审计修复(INDEX display + CF Pages 依赖固化)

背景

v0.10.110 完成 129 文件英文化后做了一次专业 + 使用维度审计,发现 2 个真实回退。本版收尾。

本次改动

1. 修 INDEX 手写段 display 文本(16 处)

v0.10.110 的 rename 脚本只替换 .md 文件名后缀,未处理 markdown link 的 display 文本。 导致手写 INDEX 段从 [共享队列架构](./共享队列架构.md) 变成 [shared-queue-architecture.md](./shared-queue-architecture.md) — display 失去中文可读性。

/tmp/fix-index-display.py 自动从目标文件的 frontmatter title: 读取中文标题, 替换 display。覆盖范围: - docs/wiki/INDEX.md — 8 处 - docs/rules/INDEX.md — 6 处 - docs/changelog/development-log-archive-*.md — 2 处

排除_overview.md / _todo.md / issues/INDEX.md 这些是 rebuild-docs.py 自动生成区, 格式是 [english-stem.md](path) — 中文 title(dash 后有中文 fallback),保留。

2. 建 requirements-docs.txt 固化 CF Pages 依赖

v0.10.110 后 137 个 〚english|中文〛 wikilink 强依赖 mkdocs-roamlinks-plugin。 之前 build command 把 plugin 名写在 CF dashboard 外部,配置易丢。

新文件 requirements-docs.txt

mkdocs>=1.6,<2
mkdocs-material>=9.5,<10
mkdocs-roamlinks-plugin>=0.4,<1

CF Pages build command 更新为:pip install -r requirements-docs.txt && cp -f CHANGELOG.md docs/ 2>/dev/null; mkdocs build

3. cf-pages-deploy rule 加故障排查项

新增故障:「站点上所有 〚english|中文〛 显示为字面文本」→ plugin 没装 → 用 requirements-docs.txt

遇到的问题

无新增。

注意点

  • rebuild-docs.py 自动区不要手改 display:会被覆盖
  • 手写 INDEX 段维护成本:未来加新文件要手工补 display 中文标题(rebuild 不管手写段)
  • 依赖版本固化策略:minor 边界(如 <10)防大版本破坏;patch 自动跟进

受影响

无新 ISSUE / SPEC。属审计修复。


背景

用户审计文档规范:「文档名由英文和数字以及"_-"组成,但是文章名称可以是中文」。 v0.10.105 已清理过 +/() 等 URL 不友好字符,但 150+ 中文文件名仍未追溯。 本版本一次性完成全部追溯(B 方案)。

本次改动

1. 文件名英文化(129 个 git mv)

类别 文件数 示例
changelog 3 开发日志.mddevelopment-log.md
issues 70 0070-共享窗口孤儿累积...md0070-shared-window-orphan-storage-race-sw-kill.md
specs 8 SPEC-006-task删除时商家数据二选一确认.mdSPEC-006-task-delete-data-cascade-confirm.md
wiki 11 共享队列架构.mdshared-queue-architecture.md
rules 18 文档目录规范.mddocs-directory-spec.md
raw 19 2026-05-26-watchdog重启后自动继续.md2026-05-26-watchdog-auto-resume-after-restart.md

文件名规则: - kebab-case(小写 + - 分词) - ≤ 60 字符 - 保留语义 + 4 位 ID / 日期前缀 - 文章 title: frontmatter 保持中文(rebuild-docs.py 用此显示)

/tmp/do-rename-v0.10.110.py 一次性全局更新:

wikilink 处理(保留中文显示)(下用 〚〛 代替双方括号防 docs:check 误报): - 〚开发日志〛〚development-log|开发日志〛(无 alias 加中文 alias) - 〚开发日志|某文字〛〚development-log|某文字〛(已有 alias 保留) - 〚开发日志#章节〛〚development-log#章节|开发日志〛(含锚点) - 路径前缀型:〚../wiki/Tab生命周期与看门狗〛〚../wiki/tab-lifecycle-and-watchdog|Tab生命周期与看门狗〛

markdown link 处理: - (开发日志.md)(development-log.md)(只改路径,display 不动) - 替换顺序:basename 长度 DESC(避免短 stem 误吞长 stem 子串)

扫描范围:docs/**/*.md + scripts/*.py + mkdocs.yml + 根级 *.md

3. 自动化脚本更新

  • scripts/archive-log.py:归档命名模板 开发日志-archive-vMIN-vMAX.mddevelopment-log-archive-vMIN-vMAX.md
  • mkdocs.yml:导航路径英文化(display 名仍中文)
  • scripts/rebuild-docs.py:自动读 frontmatter title: 渲染 INDEX,已兼容(无需改)

4. 文档规范更新

docs/rules/docs-directory-spec.md: - 命名约定明确"v0.10.110 起统一英文文件名" - ❌ 不用中文 / 大写 / 空格 - ✅ kebab-case + - 分词

遇到的问题

  • 现象pnpm docs:check 报 4 个 broken wikilinks
  • 原因:初版 regex \[\[<stem>...\]\] 只匹配 bare stem,没考虑 〚../wiki/<stem>〛 路径前缀型
  • 解决:写 /tmp/fix-path-wikilinks.py 二次扫描修 5 个文件 / 10 个 wikilinks
  • 教训:obsidian wikilink 支持相对路径,下次先 grep 路径前缀型再写脚本

注意点

  • rebuild-docs.py 已兼容:用 frontmatter title: 渲染 INDEX 显示文字,文件名英文不影响 UI 体验
  • archive-log.py 修正未来:以后自动归档生成的文件名直接英文,无需再手工
  • wikilink alias 显式中文:所有 〚english|中文〛 让 Obsidian + mkdocs 都能正常显示
  • CHANGELOG.md 自动重生成:跑 pnpm docs:changelog 即可

受影响 ISSUE / SPEC

无具体 ISSUE(无新 bug),属文档治理。


2026-05-28 v0.10.102 → v0.10.103 code review 修 4 项(2 严重 + 2 优化)

agent code review 在 v0.10.86-102 改动中发现 10 项问题,确认 4 项需修:

🔴 严重 1: SPEC-006 cascade 删命中 v0.10.2 ISSUE-0008 老 DB 坑 - task-manager.ts 用 removeByQuery({taskId: id}) + Dialog 用 countByQuery({taskId}) - 升级过的 DB 上 jsstore where:{taskId} 给 0 行(search-data.ts:67-91 已注释) - 后果:Dialog 显示 0 商家 / cascade 删 0 行 - 修:task-manager 改 select 全表 + JS filter + remove by id;Dialog 改用 countAllByTaskIds

🔴 严重 2: cleanupOrphanScrapeWindows 误杀用户新标签页 - 之前 680-840 × 480-640 范围 + tab 全 about:blank/chrome:newtab 就关 - 用户调过的小窗口容易落进 → 浏览器启动时误杀用户新标签页 - 修:尺寸阈值 ±80 → ±20;只关 about:blank(不关 chrome://newtab/)

🟡 优化 3: log-view exportedFromVersion 写死 '0.10.96' - 改 browser.runtime.getManifest().version 动态读

🟡 优化 4: log-view 导出 settings 未脱敏 - 9 个敏感字段 → <redacted:Nchars> - bundle 加 redactedFields 字段列出脱敏 keys


2026-05-28 v0.10.108 → v0.10.109 MkDocs 主题 + 日志归档脚本 + 子目录 INDEX 补全

#1 主题:全宽 + 蓝色

  • mkdocs.yml palette: green → blue / accent: indigo
  • docs/assets/extra.css 自定义全宽 + 表格 + 代码块 + wikilink 高亮
  • .md-grid { max-width: none; } 去掉 1220px 限制

#3 日志自动归档脚本

scripts/archive-log.py: - 阈值 1500 行 + 保留最新 30 个版本 - 超阈值 → 自动归档老版本到 development-log-archive-vMIN-vMAX.md - 自动加 frontmatter + 主文件加索引尾 - pnpm docs:archive-log (dry run) / --apply 真执行

#4 子目录 INDEX 补全

9 个新 INDEX.md(覆盖之前缺索引的子目录): - docs/changelog/INDEX.md - docs/specs/{active,done,parked}/INDEX.md - docs/raw/{inbox,feedback,conversations,ideas}/INDEX.md - docs/assets/INDEX.md

frontmatter description 已全部 md 含(_template.md 除外,是模板)。

下一步:v0.10.110 文件名英文化

150+ 中文 md + 350 wikilink 全部追溯改 — 工程量大,单独 commit 便于 review。


2026-05-28 v0.10.107 → v0.10.108 文档目录整改 — 根从 8 → 4,全部归档到 docs/

用户问"为什么开发日志/问题记录/改版说明在一级目录?SOP 流程规则?" 诚实审计:根目录散乱破坏了 CLAUDE.md 五层漏斗规则。

移动(git mv)

旧位置 新位置
development-log.md docs/changelog/development-log.md
development-log-archive-v0.10.0-84.md docs/changelog/development-log-archive-v0.10.0-84.md
development-log-archive-v0.8-v0.9.md docs/changelog/development-log-archive-v0.8-v0.9.md
更新规则.md docs/rules/version-release-iron-rules.md
v2-UI改版说明.md docs/specs/done/SPEC-000-v2-ui-redesign.md

引用同步(脚本自动 + 手工)

  • CHANGELOG.md / CLAUDE.md / README.md / docs/README.md / docs/index.md
  • docs/issues/INDEX.md (4 处)
  • mkdocs.yml (nav 路径)
  • scripts/gen-changelog.py (脚本路径)
  • scripts/rebuild-docs.py (扫描路径)
  • docs/rules/cf-pages-deploy.md (build command 简化)

13 个文件 / 20+ 处引用更新。docs:check 0 broken link。

新规则文档

docs/rules/docs-directory-spec.md — 决策树 + 根白名单 + docs/ 角色表 + 命名约定 + 反例 + 工具支持。

明确根只允许 4 个 .md:README / CHANGELOG / CLAUDE / LICENSE。 其他全部 docs/ 五层漏斗分类。

结果

repo 根 .md:8 个 → 4 个(README / CHANGELOG / CLAUDE / LICENSE-tbd) docs/ 结构清晰:raw / specs / wiki / issues / rules / changelog / _topics + 自动文件

元教训

"SOP 写在文档""SOP 被遵守"。CLAUDE.md 早规定五层漏斗,但执行中根目录还是散乱。 v0.10.108 起: - docs-directory-spec.md 明确白名单 - 移动需脚本化(git mv + 引用同步) - pre-commit 可加 "根 .md 必白名单" 检查(v0.10.X 加)


2026-05-28 v0.10.106 → v0.10.107 沉淀机制强化:补 4 ISSUE + 2 rules + commit-msg hook(ISSUE-0070~0073)

用户问"每次更新都进行沉淀吗?问题回顾历史吗?" 诚实审计:v0.10.97~106 有 4 个真 bug 修漏建 ISSUE,多次"直接读代码不查 INDEX"。 本版把"机制有"变成"强制执行"。

#1 补 ISSUE-0070~0073(漏建的)

  • ISSUE-0070: 共享窗口孤儿累积(v0.10.101 修,含元教训 + audit_grep)
  • ISSUE-0071: SPEC-006 cascade 删命中 ISSUE-0008 历史回归(v0.10.103 修)
  • ISSUE-0072: cleanupOrphan 误杀新标签页(v0.10.103 修,关键 UX 事故)
  • ISSUE-0073: 导出 JSON 版本号写死 + settings 未脱敏(v0.10.103 修)

每个都含 audit_grep 字段,scan:issue-coverage 自动防退化。

#2 新 docs/rules/per-version-sinking-checklist.md

完成 commit 前的强制自查清单: - 何时必建 ISSUE(修 bug / 历史回归 / agent code review 找到) - 何时必更 wiki(架构概念 / 字段语义) - 何时必动 SPEC(≥ 数据模型 / API / UI 流程) - 决策树 + 各类型必做项详表 + 反例(本会话漏的 4 个 ISSUE)

#3 新 docs/rules/user-feedback-pre-lookup.md

用户报问题时的强制流程:

用户反馈 → grep 关键词查 docs/issues/INDEX → 看 _todo.md → 才读源码

按反馈类型给具体 grep 命令: - UI / 显示 - 抓取相关 - SW kill / 持久化 - jsstore / 数据库 - 设置字段 - 调度 / 并发 - 隐私 / 安全 / 导出 - MV3 / alarms

反例:本会话 4 次"直接读源码"漏查的反思。

#4 commit-msg hook 强制

scripts/hooks/commit-msg 新增 — src/ 改动时 commit message 必含: - ISSUE-XXXXSPEC-XXX 关键词(推荐) - 或 vX.Y.Z 版本号格式(已是版本 commit) - 或 meta:/chore:/docs:/refactor:/style:/build:/test:/ci: 意图前缀(仅小修) - 或本次同时改 docs/issues/ 或 docs/specs/(算"已沉淀")

都不满足 → 阻止 commit,详细提示如何修。 scripts/setup-hooks.sh 更新装 commit-msg + 现有 pre-commit。

元教训

"机制有 ≠ 执行做到":CLAUDE.md 早就规定"先查 INDEX"、"每 bug 一 ISSUE",但我多次不遵守。 v0.10.107 起强制硬约束(pre-commit + commit-msg + 必看清单),不再靠记忆。


2026-05-28 v0.10.105 → v0.10.106 部署目标切换:GH Pages → Cloudflare Pages

用户决定发布到 Cloudflare Pages 而非 GitHub Pages。

改动

  • .github/workflows/docs.yml (GH Pages workflow 不再用)
  • docs/rules/cf-pages-deploy.md 一次性配置指引(5 分钟在 CF dashboard 完成)
  • 更新 docs/index.md 提到 CF Pages 部署

为什么选 CF Pages 直连方式(不用 wrangler)

CF Pages 自带 GitHub 集成 — push 后 webhook 自动 build。不需要: - GH Actions workflow - wrangler / CF_API_TOKEN secret - 任何手动操作

只需在 CF dashboard 配一次 build command + output dir 即可。

用户需做的一次性配置

docs/rules/cf-pages-deploy.md 操作: 1. CF dashboard → Workers & Pages → Create application → Pages → Connect to Git 2. 选 repo + 配 build command + 输出目录 site + 环境变量 PYTHON_VERSION=3.11 3. Save and Deploy

完成后 push 即自动部署。


2026-05-28 v0.10.104 → v0.10.105 文档治理 D+A:文件名清理 + MkDocs site

D 文件名清理

19 个文件含 + / ( / ) → MkDocs URL 编码后丑陋。 脚本批量重命名 + 全局替换: - 文件 19 个 git mv - 139 处 [[wikilink]] / markdown link / 路径引用全局替换 - 涉及 40 个 md 文件 - docs:check 验证 0 broken link

替换规则:+-, -, → 删除, 连续 ---

例: - 0067-email占位符+phone从未写库+probe-403+UI-toggle.md0067-email-placeholder-phone-probe-403-toggle.md - SPEC-004-网站采集多阶段优化+云端协同.mdSPEC-004-multi-stage-scrape-cloud-sync.md

A MkDocs site 发布

  • mkdocs.yml: mkdocs-material 主题 + roamlinks plugin(处理 350 个 [[wikilink]])
  • 中文支持 + 深浅色切换 + 代码高亮 + mermaid + 搜索高亮
  • nav 树覆盖五层漏斗:Wiki / SPEC / ISSUE / Rule / Raw + 版本变更
  • docs/index.md: 首页(五层漏斗说明 + 快速入口 + 关键架构)
  • .github/workflows/docs.yml: 自动部署 GH Pages
  • push 到 main 含 docs/ 或 mkdocs.yml 改动 → 自动 build + publish
  • cp CHANGELOG / development-log.md 进 docs/ 供 mkdocs 引用
  • peaceiris/actions-gh-pages@v4 部署到 gh-pages 分支

site/ 加入 .gitignore。

部署后访问

push 后 1-2 分钟 → https://suxuemi.github.io/laifaxin-chajian-ditu/

注意点

  • mkdocs build 用 --strict || mkdocs build — 严格模式失败时降级(warning 不阻塞首次部署)
  • roamlinks plugin 把双方括号 wikilink 解析为相对 markdown link — 不依赖前缀路径
  • 首次部署需在 GitHub 仓库 Settings → Pages 选 "Deploy from a branch" → gh-pages

2026-05-28 v0.10.103 → v0.10.104 文档治理:开发日志拆分 + CHANGELOG 生成

用户要求"检查规范是否便于持续迭代 / MkDocs 友好",体检发现: - 开发日志膨胀至 10060 行单文件 - 没有 CHANGELOG.md / 没有"按版本聚合"视图 - 350 个 wikilink 阻碍 MkDocs

本版做 B + C(低风险,下版 v0.10.105 做 D + A)。

C: 开发日志按版本族拆

development-log.md (645 行) — v0.10.85+ 当前活跃 + 索引尾
├─ development-log-archive-v0.10.0-84.md (8675 行)
└─ development-log-archive-v0.8-v0.9.md (765 行)

158 条版本 entry 跨 v0.8.37 ~ v0.10.103,原单文件 10060 行查找性差。

B: CHANGELOG.md(按版本聚合)

keep-a-changelog 格式,扫 docs/issues/.md frontmatter (fixed_version) + specs/done/.md (target_version) + 开发日志 (## 版本号 标题) 自动生成。

每个版本含: - 主要改动(来自开发日志 ## 标题) - 修复的 ISSUE 链接 - 落地的 SPEC 链接


2026-05-28 v0.10.101 → v0.10.102 SPEC-006 — task 删除时商家数据二选一确认

本次改动

实施 SPEC-006(之前 approved 已久),用户决策: - 默认"保留商家数据"(安全默认) - TaskFilterPicker 加"已删任务"分组(孤儿数据可筛) - 批量删一并实现

1. task-manager.ts controlTask 加 deleteData payload

if (action === 'delete') {
  ...
  if (payload?.deleteData === true) {
    await removeByQuery('MapTaskData', { taskId: id });  // 级联删
  }
}

2. 新组件 task-delete-confirm-dialog.tsx (~200 行)

  • 单删 + 批量两种模式(按 tasks.length 自动切换)
  • mount 时 countByQuery 拿商家数据数(串行,N=10 也只 100ms)
  • RadioGroup 2 选项:"仅删任务,保留数据(推荐)" / "同时删除全部 N 条"
  • 按钮文案动态:选 cascade 时显示 "删任务 + N 条数据"
  • 0 商家场景跳过选项直接确认

3. task-view.tsx 替换原 ConfirmDialog 调用

  • handleDelete / handleBatchDelete 都改打开 TaskDeleteConfirmDialog
  • 提公用 performDeleteTasks(ids, deleteData) 走 task-control message + 收集结果
  • toast 文案加 "(含商家数据)" / "(保留商家数据)" 区分

4. TaskFilterPicker 已删任务分组

  • 加 useRequest 60s 扫 MapTaskData distinct taskId
  • 计算 orphanIds = distinctTaskIds - liveTaskIds
  • Autocomplete groupBy 渲染两组:"现存任务" / "已删任务(数据仍存)"
  • 孤儿项加 DeleteSweepIcon + 名字 "已删任务 {id前8}"
  • placeholder 加 "/ 已删任务" 提示

UI 效果

单删流程:

点删除 → Dialog "删除任务「店铺名」"
  → "该任务采集到 1,234 条商家数据,请选择..."
  → ⚪ 仅删任务(推荐)/ ⚪ 同时删 1234 条
  → [取消] [删任务 + 1234 条数据]

批量删流程:

选 5 个 → 批量删 → Dialog "批量删除 5 个任务"
  → "该批任务共采集到 12,345 条..."
  → 同款 RadioGroup

已删任务筛选:

TaskFilterPicker 下拉:
  ━ 现存任务 ━━━━━━━━
  📋 dentist · United States
  📋 plumber · CA
  ━ 已删任务(数据仍存)━
  🗑 已删任务 q3vbyman (8 字符前缀)

注意点

  • deleteData 失败不阻塞任务删除(catch + console.warn,任务已删数据成孤儿但仍可见)
  • 孤儿扫描 60s 一次 + selectByQuery limit 100000 — 大数据量略慢但用户感知不到(picker 关时不查)
  • TaskFilterPicker getOptionLabel 含 t.name,孤儿名是 "已删任务 xxx" 防重名

2026-05-28 v0.10.96 → v0.10.97 命名澄清 + sys-log 空状态引导

本次改动

用户 v0.10.96 dogfood 截图反馈:跑了 19 个商家但系统事件 0 条。诊断后:

  • 商家全部抓到(19 个)但没进入官网抓取阶段(官网/社媒 0 条)
  • 左下 sidebar「提取邮箱/社媒」toggle 关闭 → 官网抓取不启动 → pipeline 不被调用 → 无 sys log
  • 同时用户看到左下「云端同步」(laifaxin 备份)开着,可能误以为 SPEC-004 Phase 3 也启用了

命名混淆 + 空状态无引导 = 用户不知道为啥日志空。

修复

  1. settings-view.tsx — settings 中 toggle 改名:
  2. "启用云端同步(实验 · 服务端待上线)" → "启用社区共享(实验 · 服务端待上线)"
  3. helperText 明确说明"⚠️ 注意:与左侧 sidebar 的「云端同步」(laifaxin 备份) 不是一回事"

  4. sys-log-list.tsx 空状态从单行文字升级为完整引导:

  5. 列出 3 个相关 toggle(提取邮箱/社媒 / 多阶段抓取 / 启用社区共享)
  6. 解释各自作用 + 哪个开关与 sys log 关联
  7. 提醒"云端同步" vs "启用社区共享" 是两回事

元教训

新功能加 toggle 时要看现有 UI 是否有同名 toggle — sidebar 早就有"云端同步"(laifaxin 备份),我 v0.10.95 又加了 SPEC-004 的"启用云端同步"。复用同名词 = 用户必困惑。

空状态文案要有引导而非单"暂无"——0 条 ≠ 一定是 bug,可能只是用户没开开关。

注意点

  • 数据库 enableCloudSync 字段名不改(避免迁移),仅 UI label 改
  • sys-log 空状态 4 行引导直接渲染在列表区,不需新组件
  • 这是本次会话最后一次"小修",重要的开发已落地(Phase 1+2+3 全做完 + 日志导出)

2026-05-28 v0.10.95 → v0.10.96 全环节 logging 补全 + JSON 日志导出

本次改动

用户问"日志可以导出给你分析吗?所有环节都加上日志了吗?"

审计发现 Phase 2/3 新代码大量用 try/catch + console.warn — SW kill 后日志全丢

1. 新文件 src/utils/system-log.ts(~150 行)

  • 6 category: pipeline / domain-state / contact-pool / cloud-sync / contribution / general
  • 4 level: debug / info / warn / error
  • chrome.storage.local 持久化 + 5000 条环形 + 串行写防并发
  • detail 字段限 2KB(超长截断)
  • API: appendSysLog / getSysLogs / getSysLogsBrief / clearSysLogs / exportSysLogsRaw

2. 补全 ~25 个事件点

位置 事件
domain-state setState transition 含 from/to
domain-state getDomainState TTL ttl-reset
domain-state resetDomainStats reset-all / reset-failed
contact-pool writeContactRecord write / write-failed
contact-pool queryContactPoolByUrl hit
contact-pool writeCloudRecords pull-cloud / pull-cloud-failed
contribution-ledger recordContribution action 名 / record-failed
cloud-domain writeCloudDomainStates pull-domain-state / pull-domain-state-failed
cloud-sync-orchestrator flush 3 路径 upload-start / upload-ok / upload-error
cloud-sync-orchestrator backoff/auth skip-backoff / auth-failed
cloud-sync-orchestrator runStartupTasks startup-begin/health-ok/health-fail/auth-fail/done
pipeline ContactPool 命中 pool-hit
pipeline domain-state 非 continue domain-advice

每条日志含语义结构化 detail(不是 free text),便于 grep / 统计。

3. UI(log-view + sys-log-list)

  • log-view.tsx:tab 加第 4 个"系统事件" + 顶部"导出全部"按钮
  • 新文件 src/sections/page/sys-log-list.tsx(~180 行)
  • category chip 过滤(pipeline / 域名状态 / Contact池 / 云端同步 / 贡献度 / 通用)
  • error/warn 计数 chip
  • monospace 表格行:时间 / category chip / level chip / event / detail JSON
  • 自带"清空系统日志"按钮(ConfirmDialog 防误删)

4. 导出 JSON Bundle

handleExport 合并 8 个 source 写成 laifaxin-logs-{iso-ts}.json

{
  exportedAt, exportedAtIso, exportedFromVersion,
  summary: { sysLog, domain, contactPool, ledger, cloudDomain, pageLogCount },
  sysLogs: [...],
  pageLogs: [...],
  domainStats: [...],
  settings: {...}
}

用户截图 dogfood 时可下载 + 发给开发者,一次性给出完整事件流 + 数据快照

5. 文档

docs/rules/scrape-pipeline-decision-table.md §6 新增: - 完整事件清单表(25+ 条) - 加新事件类型流程 - 导出 bundle 结构

元洞察

try/catch 吞错 + console.warn 是技术债: - SW kill 后看不到 - 用户复现 bug 时无法事后分析 - "在所有环节都加上日志" 应该是 Phase 2/3 完成时就做的(事后补是 2x 工作)

v0.10.97+ 默认做法:所有新代码加 appendSysLog(默认 info / 失败 error),不再用 console.warn 吞错。

注意点

  • system-log 写入是 fire-and-forget — 业务路径不阻塞
  • 5000 条上限,超出 FIFO 淘汰
  • detail > 2KB 自动截断(防超大对象塞爆 storage)
  • 导出文件含 settings — 用户可能不想分享 emailRegex / 黑名单等。当前不脱敏(用户主动导出给开发者,假设知情)

2026-05-28 v0.10.94 → v0.10.95 SPEC-004 Phase 3 — 一次性客户端完整(云端 API 待实现)

本次改动

用户要求一次性完成 Phase 3 客户端 + API 契约文档化。默认 feature flag 关闭(服务端未上线)。

1. API 契约文档(最重要)

docs/specs/done/SPEC-004-phase3-api-contract.md — 完整 endpoint 契约: - 通用约定(base URL / Authorization / 响应包装 / 速率限制 / Idempotency-Key) - 匿名注册 (POST /anonymous/register) + GET /me - ContactPool 3 个 endpoint (upload/query/since) - DomainState 2 个 endpoint (upload/list) - ContributionLedger 3 个 endpoint (log/balance/history) - Leaderboard / Health - 错误码总览 + 客户端处理策略 - 验收清单 + 数据流图

服务端按此契约实现即可,客户端 v0.10.95 已对接。

2. jsstore 加 3 个新表(version 3 → 4)

  • ContactPool: urlHash 主键 + 完整 contact + sync 字段
  • CloudDomainState: 云端权威 domain state 缓存
  • ContributionLedger: append-only 账本

3. 客户端代码(8 个新文件)

文件 行数 职责
src/utils/url-hash.ts ~95 URL 归一化(去 utm/fbclid + lowercase + sort 等)+ sha256
src/utils/contact-pool.ts ~210 ContactPool CRUD + queryByUrl/Hashes + 待上传队列
src/utils/cloud-domain.ts ~80 云端 domain state 缓存
src/utils/contribution-ledger.ts ~120 append-only 账本 + ack 机制
src/utils/cloud-sync-client.ts ~270 通用 fetch wrapper + 9 个具体 API 客户端
src/utils/cloud-sync-orchestrator.ts ~250 4 类任务 + 指数退避 + 手动触发
src/sections/settings/view/cloud-sync-panel.tsx ~160 settings 面板

4. pipeline + executor 集成

  • runWebsiteScrapePipeline 入口加 ContactPool 命中检查(stage: 'contact-pool'
  • scraper-executor 两个 success 路径(pipeline + tab)都写 ContactPool + record contribution

新 PipelineOutcome stage: contact-pool;success 加 poolHit: boolean 区分

5. SW alarms

background/index.ts 加: - cloud-sync-tick periodInMinutes: 1(上传) - cloud-sync-pull periodInMinutes: 60(拉取) - SW 启动时一次 runStartupTasks(health + 拉云端)

6. Settings UI

  • enableCloudSync toggle(默认 false)+ 隐私说明
  • cloudSyncBaseUrl 输入(自定义服务端地址)
  • 概览面板:ContactPool / Ledger / CloudDomain 数 + 立即同步 + 各自清空

关键约束(用户原话)

默认是关闭状态,不同步(因为现在没云端对接)

enableCloudSync: false 默认值 ✅ apiFetch 第一行 if (!isFeatureEnabled()) return { error: 'DISABLED' } — 不发任何请求 ✅ orchestrator 每个 flush 函数都先检查 isEnabled

服务端实现指南

参考 docs/specs/done/SPEC-004-phase3-api-contract.md: - 共 10 个 endpoint - 通用 Bearer + Idempotency-Key - 错误码 11 种 - Rate limit 三档(upload 60/min, query 600/min, pull 30/h)

注意点

  • jsstore version 3 → 4:旧用户首次启动会触发 schema upgrade 加 3 张表(不影响现有数据)
  • 抓取性能影响:每次抓取额外多 1-2 个 jsstore upsert(μs 级),可忽略
  • 服务端未上线时:client logic 完全静默(feature flag 关 + try/catch 兜底,零错误污染 console)
  • 用户开 enableCloudSync 时若服务端真挂:apiHealth 失败 → 跳过 startup pull → upload 队列累积,下次重试

元教训

契约先行:API 契约文档(~600 行)是这次最重的产出。服务端实现者拿到这份文档就能独立工作 — 不需要再问客户端。


2026-05-28 v0.10.93 → v0.10.94 domain-state 迁 jsstore(为 Phase 3 云端协同准备)

本次改动

用户问"哪个方案便于后续云端协同(不仅域名状态)"。重新评估 chrome.storage.local vs jsstore: - chrome.storage.local: 单 key 5MB 上限 / 无索引 / 增量 sync 要 diff 整 Object - jsstore (IndexedDB): GB 级 / 索引快 / 单条 upsert / 增量 sync 友好

结论:所有 sync-related 数据必须用 jsstore。v0.10.93 的 domain-state 用了 storage.local 是技术债,现在迁移(Phase 3 启动前付,避免后期累积更难迁)。

改动

  1. src/utils/jsstore/base.ts 加新表 DomainStats
  2. domain 为 primaryKey
  3. state / lastSeen / stateExpiresAt / syncedAt 启用 enableSearch(批量查询 + 增量 sync 用)
  4. Phase 3 预留字段syncedAt(增量 sync 时间戳)、cloudConsensus(云端共识强度 0-1)
  5. db version 2 → 3(自动加表,不影响 MapTaskData)

  6. src/utils/domain-state.ts 改写

  7. 保留外部 API(getDomainState/recordPipelineEvent/getDomainAdvice/getAllDomainStats/getDomainStatsBrief/resetDomainStats)不变
  8. 内部:删 Map cache + debounce timer,单条 upsert(jsstore 索引扫描足够快)
  9. 加迁移逻辑 ensureMigrated():模块首次加载时 check 旧 local:domainStats:v1,有就一次性 import 到 jsstore + 删 storage 旧 key + 置 flag

  10. docs/wiki/cloud-sync-architecture.md 新建

  11. 三方案对比(storage.local vs jsstore vs Dexie)— 选 jsstore 理由
  12. Phase 3 四个 store 设计预览(DomainStats / ContactPool / CloudDomainState / ContributionLedger)
  13. 通用增量 sync 流程
  14. 关键约束:"所有 sync-related 必须 jsstore"

用户决策影响

用户拍板"现在迁"(vs Phase 3 启动时迁): - ✅ v0.10.93 用户还没积累数据,迁移成本最低 - ✅ Phase 3 启动时可以直接加 ContactPool 等新 store,架构一致 - ✅ 避免技术债累积

风险

  • jsstore version 从 2 升到 3 — 旧用户首次启动会触发 schema upgrade。实测如有报错需关注(v0.9.8 升 version 1→2 时有过 jsstore where:taskId logError 历史,ISSUE-0008 修过)
  • 迁移失败不阻塞 pipeline — ensureMigrated() 内 catch 后继续,大不了重头学习

注意点

  • domain-state API 完全兼容 — pipeline / settings panel 不动
  • DomainStat 类型加可选字段 syncedAt? / cloudConsensus?(Phase 3 启用)
  • ALL_STATES 数组 + 6 个 count 并发,settings panel 性能好于旧 Map filter

2026-05-28 v0.10.92 → v0.10.93 SPEC-004 Phase 2 — 域名状态机

本次改动

实施 SPEC-004 Phase 2 全栈:

1. src/utils/domain-state.ts(~280 行)

  • 类型DomainState(6 状态)/ Outcome(6 种事件结果)/ Method(head/fetch/tab)
  • 持久化:chrome.storage.local local:domainStats:v1,内存 Map cache + debounce 500ms 写回
  • LRU 5000 域名:超出按 lastSeen 升序淘汰最旧
  • TTL 自动重置:dead 30d / antibot-soft 7d / antibot-hard 90d / cold 90d / unknown 7d / friendly 永久(仅行为降级)
  • 转换规则
  • dead 事件 → state=dead
  • antibot × 3 连续 → antibot-soft → antibot-hard
  • fetch ok-contact 占比 ≥ 70% 且 ≥ 5 样本 → friendly
  • friendly fetch 失败 × 5 → unknown
  • ok-empty × 5 连续 → cold
  • antibot-soft + 2 次 ok-contact → 恢复 unknown

2. pipeline 集成 (website-scrape-pipeline.ts)

入口短路 + 出口事件记录:

URL → getDomainAdvice → {
  skip(dead/cold)         → outcome.kind='skip' reason='domain-dead'|'domain-cold'
  force-tab(antibot-hard) → outcome.kind='fallback' reason='domain-antibot-hard'
  fast-fetch(friendly)    → 跳过 HEAD 直接 GET(节省 ~3s/URL)
  continue                → 完整 pipeline
} → 每阶段 recordPipelineEvent 更新状态

新 PipelineOutcome reason: domain-dead / domain-cold / domain-antibot-hard,stage 加 domain-state

3. UI

  • src/sections/settings/view/domain-state-panel.tsx — settings 内显示概览
  • 总域名数 + 各状态 chip 计数(按状态着色:dead 红 / antibot 橙 / friendly 绿 / cold 灰)
  • 30s 自动刷新
  • "重置" 按钮 + ConfirmDialog(清空所有学习数据)
  • 集成到 settings-view "⚡ 多阶段抓取" 段下方

4. mstage-classify 扩展

加 Phase 2 三个新标签的分类: - mstage:domain-dead / mstage:domain-cold → skip 灰色 - mstage:fallback/domain-antibot-hard → antibot 橙色

5. 文档同步

  • docs/wiki/domain-state-machine.md status: draft → stable(implemented_in: v0.10.93)
  • docs/rules/scrape-pipeline-decision-table.md 加 3 行新 mstage 标签
  • docs/specs/active/SPEC-004 phased_status: phase_1=done@v0.10.87 / phase_2=done@v0.10.93 / phase_3=parked

同版顺手(A 反馈)

嵌套滚动条问题审了 layout 代码(LocalDataView height:100% + overflow:hidden 是合理设计,DataGrid 自带 internal scroll 是正常),未找到明显双层 overflow 问题。待用户具体截图位置后再处理

性能预期

站类型 占比 旧 Phase 1 新 Phase 2 优化点
已知 dead 域名(重复抓) ~5% 3s HEAD 0ms 短路
已知 friendly 域名 ~20% 3s HEAD + 5s GET 5s GET(跳 HEAD) 1.6x
已知 antibot-hard ~3% 3s HEAD + 5s GET 浪费 直接 tab 8s 节省
已知 cold 域名 ~10% 3s + 5s + 解析 0ms 短路

整体期望额外加速 ≈ 1.3-1.5x(叠加 Phase 1 的 1.8x)。

注意点

  • domain-state 完全无需开关 — 跟随 enableMultiStageScrape flag 自动启用(pipeline 内调)
  • 重置后从零学习(适合用户怀疑 dead 域名活了 / 误判 antibot 后用)
  • 5k 域名上限 + LRU 淘汰,存储 ~2.5MB 内
  • pipeline 内每次调 recordPipelineEvent 是异步但非阻塞用户路径(pipeline 已 await)

2026-05-28 v0.10.91 → v0.10.92 data-view mstage 漏改 + 规则文档化(ISSUE-0069)

本次改动

用户截图 matsudental.com "失败"实为 v0.10.90 漏改 — data-view.tsx HTTP 列还在用老逻辑(一律红色 + "抓取失败")。

代码

  1. 新文件 src/utils/mstage-classify.ts — 公用 helper,返回 { kind, color, chipColor, tooltipPrefix, shortLabel }
  2. src/sections/page/log-view.tsx — 删 local classifyMstage(dead code),改 import
  3. src/sections/data/data-view.tsx HTTP 列 — 用 classifyMstage 分类
  4. success → 🟢 "✓ 命中" + "多阶段命中(未开 tab)"
  5. skip → ⚪ "跳过" + "跳过抓取"
  6. antibot → 🟠 "反爬" + "反爬拦截 / 落 tab"
  7. fallback → 🔵 "落tab" + "降级到 tab 抓取"
  8. 真错误 → 🔴 截断 + "抓取失败"

规则文档(用户反馈 3:可持续迭代)

  1. docs/rules/scrape-pipeline-decision-table.md — mstage 完整速查
  2. 决策树 / 标签 → UI 映射表 / PipelineOutcome 编码规则 / 加新类型 6 步流程
  3. docs/rules/ui-change-pre-check.md — 改 UI 前 4 步清单
  4. 高频字段速查表(e.error / row.phone / scrape_status / ...)
  5. 抽公用 helper 信号 + 模板
  6. 反面教材 3 例

其他

  • ISSUE-0069 归档(Bug 1 ✅ / Bug 2 嵌套滚动条 ⏸ 待复现)
  • raw inbox 收纳"嵌套滚动条待复现"等用户给位置后处理

元教训

抽公用要趁早:v0.10.90 在 log-view.tsx 内 inline classifyMstage 是技术债 — data-view.tsx 自然漏了同款逻辑。dogfood 暴露后才补抽公用 + 写规则文档。

dogfood 价值:用户截图 matsudental.com "失败" 是最好的 QA — 自动测试不会察觉视觉文案问题。

待办(v0.10.93+)

  • 嵌套滚动条 — 需用户标注具体位置(已记 raw/inbox)
  • SPEC-004 Phase 2 域名状态机(dogfood 数据足够后启动)

2026-05-28 v0.10.90 → v0.10.91 任务 id 保证含数字 + chip 改 "id" 文本

本次改动

用户截图反馈 task id 'qvlbymansizu' 全字母看起来像单词不像 ID + chip 内 'QVLBYM' 看不懂意思。

  1. generateTaskId:retry 直到生成的 id 既含数字又含字母(之前 base36 均匀分布纯字母 12 字符概率 ≈ 2.7%,用户撞上)。极端兜底(10 次重试都纯字母)在第 5 位强塞数字
  2. task-view.tsx renderIdChip:chip label 改成固定 "id"(小写 monospace + letterSpacing),保留 hover tooltip "任务 ID:xxx(点击复制)"
  3. task-detail-dialog.tsx:dialog 标题里直接显示完整 id(dialog 空间足够,更直观)
  4. 清理 dead code:两处 shortId 函数删除(不再被引用)

设计权衡

chip 文字选择: - 用户原文 'id' 小写 — 尊重原意,monospace 字体下 letterSpacing 增强可读性 - 大写 'ID' 更醒目但偏离用户意图 - 之前 6 字符前缀(QVLBYM)— 用户反馈"看不懂",移除

注意点

  • id 生成的 retry 期望迭代 < 1.04 次,性能影响可忽略
  • 旧 id(不含数字的)不会自动迁移,仅新建 task 走新逻辑
  • shortId 函数删后 src/sections/task/ 两个文件清爽了

2026-05-28 v0.10.89 → v0.10.90 dogfood v0.10.89 暴露 4 项(ISSUE-0068)

本次改动

用户开启 enableMultiStageScrape 后跑大批量任务,截图反馈 4 项:

  1. 清空数据后列表不刷新 — local-toolbar.tsx doClearAll 缺 flushData() 调用,client mode rowsSnapshot 不更新。1 行修
  2. mstage:success 标签 hover 显示"抓取失败" — log-view.tsx 新 helper classifyMstage,按前缀分类 color + tooltipPrefix:success 绿 / skip 灰 / retry 橙暗 / antibot 橙 / fallback 蓝 / 其他 mstage 灰 / 真错误红
  3. 电话去重未去国家码 — scraper-executor.ts normalize 取后 10 位("+1 907-522-1341" 和 "9075221341" 现在被去重为同号)
  4. 创建任务卡顿无反馈 — task-create-dialog.tsx 加 submitting state + try-finally,按钮 LoadingButton 风格(CircularProgress + "创建中…")

元教训

dogfood 大数据集才暴露:单跑 10 商家这 4 个全看不出来,需要 50k+ 行 + 含国家码的真实电话 + 大量 mstage 事件。

注意点

  • normalize 取后 10 位对中国 +86 / 北美 +1 都安全(中国手机 11 位也取后 10 位,与"无国家码"格式去重一致)
  • classifyMstage 用 startsWith — 未来 mstage 加新类型时补 case
  • handleCreate try-finally 包整个 async 块,所有 return 路径都重置 submitting

2026-05-28 v0.10.88 → v0.10.89 电话列与邮箱列对齐 — 多号码 chip + tooltip

本次改动

v0.10.88 phone 合并存进字段(逗号分隔),但 UI 还按单值字符串渲染 → 用户截图看不到 +N chip。

src/sections/page/cell-renderers.tsx: - 新 helper splitPhones(phone) — 按 ,;| 分隔为数组 - RenderPhone(独立列)— 首号主显 + 多号 +N chip + tooltip 弹全部(与 RenderEmail 一致样式) - RenderContact(复合列)— 同款改造

视觉

单号码场景(90%+):UI 与 v0.10.88 完全一致(不加 chip,向后兼容) 多号码场景:+1 847-382-6579 (地图) [+1],hover chip 弹出 +1 847-382-6579 (地图) 224-848-4453

注意点

  • 已有数据不会自动重抓 — 新 UI 在新抓的数据(v0.10.88+ 写入)上生效
  • v0.10.88 之前抓的 phone 字段是单号码字符串,splitPhones 返回 [phone],无 chip
  • 复合列 RenderContact 内联 splitPhones 调用,避免多次解析

2026-05-28 v0.10.87 → v0.10.88 dogfood v0.10.87 暴露 4 项小修(ISSUE-0067)

本次改动

用户截图 advocatehealth.com 医生页 dogfood v0.10.87 暴露 4 个真问题:

  1. email 占位符 your@email.com 抓到污染 → BLACKLIST/NOISE 扩充 23 条(覆盖 your/me/info/contact/admin/noreply + @example.com/@yoursite.com 等)
  2. 网页 regex 提取的 phone 从未写库(长期 bug)→ 新 helper mergePhonesWithMapsLabel 合并 Google Maps 原 phone + 网站 extractedPhones,normalize 纯数字去重,多号码时给地图来源加 (地图) 标记。tab 路径 + pipeline success 路径两处都改
  3. 缺 settings UI toggle → settings-view.tsx 加 "⚡ 多阶段抓取(实验性 · 默认关)" 段 + RHFSwitch + Yup schema
  4. probe 403/451 未归 antibot → website-probe.ts pickStatusReason 加分支,让 tab 兜底试 cookie/UA

元洞察

  • dogfood 第一张截图就翻出 2 真 bug + 1 长期 bug + 1 体验:没装真浏览器永远查不出 phone 字段从未写库
  • WXT lazy storage 陷阱:storage.defineItem getValue 默认值不落盘;chrome.storage.local 改 storage 不安全 → 优先做 UI

注意点

  • phone 单号码 + 地图来源场景(90%+)不加标记,兼容旧 UI / CSV 导出
  • email 黑名单扩充:精确 14 条 + 模糊 9 条;不影响真实邮箱(example/yoursite/yourdomain 域名极少是真实公司域)
  • probe 403 归 antibot 是"宁可错杀不放过"策略,未来 dogfood 可校准

2026-05-28 v0.10.86 → v0.10.87 SPEC-004 Phase 1 — 多阶段抓取 pipeline(默认关)

本次改动

实现 SPEC-004 Phase 1 的客户端多阶段抓取 pipeline。在 processWebsiteScrape 开 chrome.tabs 实抓前,加入 HEAD → GET → regex → tab fallback 决策树。

新增文件(5 个): - src/utils/website-probe.ts — HEAD 探测(3s timeout)+ 响应头分类(dead/antibot/retry-later/ok),GET 兜底 405/501 - src/utils/website-fetcher.ts — GET fetch(5s timeout)+ Content-Type/长度/14 个 challenge markers 三层判断,stream cancel cap 2MB - src/utils/contact-extractor.ts — 包 extractDataFromHtml,自动加载 settings,附 contact / social 关键词扫描(区分"真无"vs"JS 渲染") - src/utils/website-scrape-pipeline.ts — 决策树组装 + 统一 PipelineOutcome { success | skip | fallback } - docs/wiki/multi-stage-scrape-pipeline.md — 架构文档 + 调用约定

修改文件: - src/utils/storage-data.tsSettingParamsenableMultiStageScrape: boolean(默认 false) - src/utils/scraper-executor.ts — feature flag 入口 + applyPipelineOutcome 帮助函数

Feature flag

默认 关闭,需 chrome devtools 改 storage 字段 local:settingParams.enableMultiStageScrape = true 启用。下版本(v0.10.88)加 UI toggle + dogfood。

anti-bot 双层判断

  • HEAD 强信号:cf-mitigated: challenge|block / 403/503 + cf-ray → antibot
  • GET body 关键词:14 个 challenge markers(cf-browser-verification / just a moment / incapsula / awswafcaptchacdk / aws-waf-token / ...)
  • Server: cloudflare 单独 NOT 路由 — 70% CF 站纯 CDN,fetch 正常

决策树映射 → DB / 日志

  • success → 写 emails/phones/socials 到 DB + scrape_status: 2 + page-log opened=false + error='mstage:success'
  • skip/dead/no-contact/non-html/retry-later → 标 scrape_status: 2 + page-log error='mstage:reason@stage'
  • fallback/* → 不动 DB,写一条 fallback 标签的 page-log,然后继续走原 scrapeWithTab

性能预期

死站 3x、静态友好站 2x、整体加权 ≈ 1.8x(dogfood 后校准)。

注意点

  • pipeline 写 DB / 日志 — 由 scraper-executor.applyPipelineOutcome 统一处理(解耦)
  • HEAD 网络层失败不直接判 dead,落 GET 兜底(部分站防火墙吞 HEAD)
  • fetch body 用 stream reader + cancel,防超大 HTML 撑爆 SW heap
  • retry-later (5xx/429) 本期标 scrape_status: 2(已抓),Phase 2 域名状态机会加 24h retry queue
  • 旧代码路径完全保留 — feature flag 关时行为 100% 与 v0.10.86 一致

状态

代码就绪 + tsc 通过 + build 出 v0.10.87 manifest。dogfood 待 v0.10.88 加 UI toggle 后进行。



历史版本归档

v0.10.104 起按版本族拆分。完整历史见:

拆分规则:主文件保留当前活跃迭代(最近 ~30 条)+ 索引。归档文件按 major 版本族划分,老归档不再变动。