Skip to content

feat: refactor highlight restoration with confidence-based branching and recovery status management#48

Open
catx1726 wants to merge 1 commit into
mainfrom
feat/recovery-status-management
Open

feat: refactor highlight restoration with confidence-based branching and recovery status management#48
catx1726 wants to merge 1 commit into
mainfrom
feat/recovery-status-management

Conversation

@catx1726

Copy link
Copy Markdown
Owner

🚀 变更摘要 (Summary)

将高亮恢复机制从"被动弹窗确认"重构为"按可信度分支处理 + 侧边栏主动管理",解决 L3 搜索位置偏移导致的不可信恢复问题,同时修复 structureBoundaries 过度过滤的底层 bug。

关联 Issue: Closes #47

变更类型:

  • 🐛 Bug 修复
  • 🚀 新功能
  • 🔧 重构/优化
  • 📝 文档更新
  • 🧪 测试补充

🧠 智力资产贡献 (Knowledge Points)

  • 📐 Spec 文档: docs/superpowers/specs/2026-06-10-recovery-status-management-design.md
  • 📋 Plan 文档: docs/superpowers/plans/2026-06-10-recovery-status-management-plan.md
  • 📚 经验总结:
    • structureBoundariesdiv 的过度登记是 L3 搜索返回 0.0% 得分的根因,真实页面中嵌套 div 过多会杀死所有搜索路径。
    • 纯文本搜索 + 字符偏移映射回 DOM range 在动态页面上有天花板,复杂场景下必须引入用户确认/重新选择机制。
    • 恢复状态持久化到 mark 数据本身,比 content script 维护临时队列更可靠,侧边栏可直接从 storage 读取。

🛡️ 质量验证 (Validation)

  • 单元测试通过率:79/83(4 个预存的 tagTree URL 解析问题与本次改动无关)
  • 手动测试场景:
    • 删除 li → 进入待恢复列表
    • 修改少量文字 → pending-confirm 虚线边框样式
    • 点击 pending-confirm → tooltip 确认交互
    • 重新选择模式 → 页面顶部提示 + Alt+点击更新
  • 已执行本地预验 (npx vitest run src/tests/)

🔒 安全与审计 (Security & Audit)

  • 已补全审计日志 (.gemini/ops_changelog.md)
  • 无敏感信息泄露
  • 向后兼容:旧数据无 recoveryStatus 字段,恢复成功后自动写入

🛑 AI 暂停点确认 (Human-in-the-Loop)

  • Spec 设计已确认 (brainstorming 后,Driver 批准)
  • Plan 已批准 (writing-plans 后,Driver 确认)
  • 高风险操作已获授权 (meta-safe-executor 检测,Driver 指令)
  • 测试失败已澄清 (verification 阶段,Driver 澄清预期)
  • 资产提纯已审查 (meta-distiller 后,Driver Accept)
  • PR 合并已确认 (Driver 执行 gh pr merge)

📝 补充说明 (Additional Notes)

核心改动文件

文件 改动
src/logic/storage.ts Mark 接口添加 recoveryStatus
src/logic/config.ts 新增 highlightPendingConfirmStyle
src/contentScripts/restorer.ts 恢复逻辑按 confidence 分支;移除弹窗触发
src/contentScripts/ui.ts 重新选择模式 + 确认位置处理
src/contentScripts/views/Tooltip.vue 新增 pending-confirm 模式 UI
src/sidepanel/Sidepanel.vue 新增"待恢复标记"区域
src/logic/search.ts 修复 structureBoundaries 过度过滤 + 回退机制

恢复行为对照表

可信度 条件 处理方式 视觉表现
High >=95% 相似度 自动恢复 默认 box-shadow 下划线
Medium 85-95% 相似度 自动恢复 + 待确认 虚线边框 + 半透明
Low <85% / 无候选 不恢复 无高亮,进入侧边栏待恢复列表

@github-actions

Copy link
Copy Markdown
Contributor

好的,作为 Google 资深工程师,我将根据提供的《审查标准》和《审查清单》对本次变更进行代码审查。

总体评价

本次重构是一次非常成功的设计演进。它将高亮恢复从“被动猜测+弹窗确认”的糟糕体验,彻底改造为“可信度分级+侧边栏主动管理”的健壮体系。设计文档清晰、Plan 详尽、代码实现与设计高度一致,并且修复了 structureBoundaries 过度过滤的底层 Bug。整体代码健康度得到了显著提升。

以下是我的审查意见,按严重程度分类。

严重问题 (Blocking)

1. src/contentScripts/restorer.ts: sanitizeRestoredHighlights 方法存在潜在的竞态条件和逻辑缺陷

  • 问题: 在 restoreHighlights 的入口处,你调用了 sanitizeRestoredHighlights 来清理“已恢复”但实际文本相似度低于 90% 的高亮。然而,此时 this.state.restoredMarkIds 中可能包含的是上一次页面加载时恢复的标记 ID。如果页面在短时间内发生了多次 DOM 变化(例如,通过 MutationObserver 触发了多次 restoreHighlights),sanitizeRestoredHighlights 可能会错误地清理掉刚刚在上一轮恢复中成功的高亮,因为 restoredMarkIds 可能还未被完全更新或清理。

  • 建议: 这个方法的设计意图是好的(健康检查),但执行时机和状态管理存在风险。建议将此逻辑移出 restoreHighlights 的常规流程,或者将其作为恢复流程的最后一步,而不是第一步。更安全的做法是在 applyMarksTwoPhases 中,当 L1 路径还原成功时,直接验证并清理旧高亮,而不是在循环开始前进行全局清理。

    // 更安全的做法:在 L1 成功时,验证并清理
    // 在 applyMarksTwoPhases 的 L1 分支中
    if (range) {
      // ... 应用高亮 ...
      // 清理该 mark 可能存在的旧高亮,而不是全局清理
      this.cleanupExistingHighlightsForMark(mark.id)
      // ...
    }
  • 严重性: 高。这可能导致在动态页面上,用户的高亮被反复清除和重建,造成闪烁甚至数据丢失。

2. src/contentScripts/restorer.ts: persistSearchSuccess 方法缺少 recoveryStatus 的写入

  • 问题: 在 restoreBySearch 方法中,无论是 high 还是 medium 置信度的分支,在成功调用 persistSearchSuccess 后,都会调用 this.persistRecoveryStatus(mark, 'restored')'pending-confirm'。然而,persistSearchSuccess 方法内部通过 sendMessage('update-mark-details', ...) 更新了 mark 的 text, html, rangySerialized 等字段,但没有包含 recoveryStatus。这意味着,persistSearchSuccess 的调用可能会覆盖掉后续 persistRecoveryStatus 的更新,或者导致两次独立的异步写入,存在状态不一致的风险。

  • 建议: 将 recoveryStatus 的更新合并到 persistSearchSuccess 方法中,作为一个原子操作。这样可以减少一次网络/消息通信,并保证数据一致性。

    // 在 persistSearchSuccess 方法中
    await sendMessage('update-mark-details', {
      id: mark.id, url: mark.url, text: candidate.displayTextSnippet,
      html: actualHtml, rangySerialized: newSerialized,
      // ... 其他字段 ...
      recoveryStatus: status, // 新增参数
    } as any, 'background')
  • 严重性: 中。虽然在实际运行中可能不会立即出错,但这是一个数据一致性的隐患,尤其是在高并发或网络延迟的情况下。

轻微建议 (Nit)

1. src/contentScripts/restorer.ts: restoreBySearch 方法中 applier 的创建可以重构

  • 问题: 在 highmedium 两个分支中,你重复创建了 rangy.createClassApplier,唯一的区别是 style 不同。这导致了代码重复。

  • 建议: 可以将 applier 的创建提取到分支判断之前,根据 similarity 决定使用哪种 style。这样代码更简洁,也更容易维护。

    const style = similarity >= 95 ? highlightDefaultStyle(mark.color) : highlightPendingConfirmStyle(mark.color)
    const applier = rangy.createClassApplier(`webext-highlight-${mark.id}`, {
      elementTagName: 'span',
      elementAttributes: { style },
    })

2. src/sidepanel/Sidepanel.vue: startRecalibration 方法中 getNormalizedUrl 的使用

  • 问题: 你从 useMarkActions 中解构出了 getNormalizedUrl,这是一个好的实践。但在 startRecalibration 中,你直接使用了 getNormalizedUrl(mark.url) 来匹配标签页。这依赖于 getNormalizedUrl 的实现细节。如果未来 URL 规范化逻辑发生变化,这里可能会产生不一致。

  • 建议: 可以考虑在 useMarkActions 中提供一个更高级的 findTabForMark 方法,封装 URL 匹配和标签页查找的逻辑,让 Sidepanel 组件更专注于 UI 呈现。

3. src/contentScripts/index.ts: processSelection 中的 recalibration 分支

  • 问题: 在 processSelection 函数中,recalibration 分支的代码是直接内联的。这增加了 processSelection 函数的复杂度,使其承担了“新建标记”和“重新选择”两种职责。

  • 建议: 可以将 recalibration 的处理逻辑提取成一个独立的方法,例如 handleRecalibrationSelection,放在 ui.ts 中。这符合单一职责原则,并使 processSelection 的流程更清晰。

4. src/logic/search.ts: LocalAligner.refineBoundary 中的回退日志

  • 问题: 你添加了一个很好的回退机制日志(console.log)。但在生产环境中,这种日志可能会过于频繁,尤其是在复杂页面上。

  • 建议: 考虑将 console.log 替换为 console.debug,或者添加一个基于 debug 标志的条件判断,以便在需要时才输出这些信息。

5. src/contentScripts/ui.ts: updateMarkFromRecalibration 方法

  • 问题: 这个方法中,你通过 sendMessage('get-mark-by-id', ...) 获取了 mark 对象,但只使用了 mark.colormark.url。对于 mark.url,你其实已经在调用处通过 getCanonicalUrlForMark() 获取了。这看起来是一次不必要的网络/消息往返。

  • 建议: 可以直接将 colorurl 作为参数传递给 updateMarkFromRecalibration 方法,或者从 state 中获取,从而避免这次查询。

SOP 合规检查

  • Issue 关联: PR 摘要中明确关联了 Closes #47,符合要求。
  • 文档检查:
    • docs/superpowers/specs/2026-06-10-recovery-status-management-design.md: 已存在,且内容详实,设计清晰。
    • docs/superpowers/plans/2026-06-10-recovery-status-management-plan.md: 已存在,且 Plan 与最终代码实现高度一致,任务追踪清晰。

总结

本次重构设计精良,代码质量高,文档完备,是一次非常优秀的变更。修复了底层 Bug,并引入了更健壮、用户体验更好的恢复机制。

  • 严重问题 (Blocking): 2
  • 轻微建议 (Nit): 5

请优先处理上述两个 Blocking 问题,它们关系到数据一致性和在动态页面上的稳定性。解决后,我对此 CL 给予 LGTM

@catx1726 catx1726 force-pushed the feat/recovery-status-management branch from 87b613f to ca4ab70 Compare June 10, 2026 06:20
@github-actions

Copy link
Copy Markdown
Contributor

代码审查报告

概述

本次 PR 对高亮恢复机制进行了重要的架构重构,从"被动弹窗确认"改为"按可信度分支处理 + 侧边栏主动管理",同时修复了 L3 搜索位置偏移的底层 bug。整体设计思路清晰,变更范围合理,文档完整。

SOP 合规检查 ✅

  • PR 关联了 Issue (Closes #47)
  • Spec 文档存在 (docs/superpowers/specs/2026-06-10-recovery-status-management-design.md)
  • Plan 文档存在 (docs/superpowers/plans/2026-06-10-recovery-status-management-plan.md)
  • 审计日志已更新 (.gemini/ops_changelog.md)

设计评审

优点

  1. 架构设计合理:可信度分级策略清晰,从 High/Medium/Low 三个级别处理恢复,避免了"一刀切"的弹窗体验。
  2. 状态持久化到数据层:将 recoveryStatus 存储在 Mark 数据本身,而非 content script 维护临时队列,设计更可靠。
  3. 向后兼容处理得当:旧数据无 recoveryStatus 字段,恢复成功后自动写入,无需数据迁移。
  4. L3 搜索修复:移除 divstructureBoundaries 查询是合理的,真实页面中嵌套 div 确实会过度分割搜索空间。

关注点

  1. sanitizeRestoredHighlights 的 90% 阈值缺乏说明:为什么选择 90% 作为重新恢复的阈值?建议在代码注释中说明依据。
  2. persistRecoveryStatus 的幂等性:方法中已经做了 if (mark.recoveryStatus === status) return 检查,但需要注意并发场景下的竞态条件。

逐文件审查

src/logic/storage.ts

Mark 接口添加 recoveryStatus 字段,改动简洁,类型定义清晰。

src/logic/config.ts

新增 highlightPendingConfirmStyle 常量,与现有 highlightDefaultStyle 风格一致。

src/logic/search.ts ⚠️

Blocking: LocalAligner.refineBoundary 中的 BLOCK_SIZE_LIMIT 定义存在潜在问题:

const BLOCK_SIZE_LIMIT = this.markText.length * 2

如果 markText 很短(例如 5 个字符),BLOCK_SIZE_LIMIT 只有 10,这可能会让一些本应被过滤的小块级元素(如 <li>)逃逸。建议增加一个下限值:

const BLOCK_SIZE_LIMIT = Math.max(this.markText.length * 2, 50) // 至少 50 字符

Nit: searchWithBoundaries 函数内部嵌套了三层循环,复杂度较高。建议添加注释说明时间复杂度,或考虑后续优化。

src/contentScripts/restorer.ts ⚠️

Blocking: sanitizeRestoredHighlights 方法中的 querySelectorAllDeep 可能返回跨 Shadow DOM 的元素,但清理逻辑没有考虑 Shadow DOM 边界:

const currentText = existingHighlights.map(el => el.textContent || '').join('').trim()

如果高亮分布在不同的 Shadow DOM 中,textContent 拼接可能产生错误的相似度计算结果。建议对每个 Shadow Root 分别计算。

Nit: persistRecoveryStatus 方法中使用了 as any 类型断言,建议定义明确的 UpdateMarkDetailsPayload 接口:

await sendMessage('update-mark-details', {
  id: mark.id,
  url: mark.url,
  recoveryStatus: status,
} as any, 'background')

src/contentScripts/ui.ts ⚠️

Blocking: handleConfirmPosition 方法直接修改 DOM 元素的 style 属性来恢复默认样式,这可能会与后续的样式更新冲突。建议使用统一的样式管理方式:

// 当前实现
el.style.boxShadow = `inset 0 -5px 0 0 ${mark.color}`
el.style.borderBottom = ''
el.style.opacity = ''

// 建议:使用 Rangy ClassApplier 重新应用默认样式

Nit: showRecalibrationPrompt 方法中直接拼接 HTML 字符串,存在 XSS 风险(虽然 originalText 来自存储,但建议使用 textContent 设置):

el.innerHTML = `
  <span>📝 请选中原标记「${snippet}」对应的文本,然后按 Alt+点击确认</span>
  <button ...>取消</button>
`

src/contentScripts/views/Tooltip.vue

pending-confirm 模式 UI 设计合理,琥珀色提示条清晰传达了"位置可能已变化"的信息。

src/sidepanel/Sidepanel.vue ⚠️

Blocking: startRecalibration 方法中,如果标签页不存在,使用 browser.tabs.create 创建新标签页,但此时 sendMessage 调用已经失败。建议先检查标签页是否存在,再决定发送消息还是创建标签页:

if (tab?.id) {
  await browser.tabs.update(tab.id, { active: true })
  sendMessage('recalibrate-mark', { markId: mark.id, originalText: mark.text }, { context: 'content-script', tabId: tab.id })
} else {
  // 创建标签页后,需要等待页面加载完成再发送消息
  const newTab = await browser.tabs.create({ url: urlWithHash.href, active: true })
  // 这里需要监听页面加载完成事件
}

src/contentScripts/monitor.ts

修复了 MutationObserver 只监听 addedNodes 的问题,现在也监听 removedNodes,这是正确的改进。

src/tests/

测试用例覆盖了关键场景(li 删除、cross-element 恢复),测试代码质量良好。

总结

严重问题 (Blocking): 3

  1. src/logic/search.ts: BLOCK_SIZE_LIMIT 缺少下限值,短文本场景下可能过滤不足。
  2. src/contentScripts/restorer.ts: sanitizeRestoredHighlights 未考虑 Shadow DOM 边界。
  3. src/sidepanel/Sidepanel.vue: startRecalibration 中标签页创建与消息发送的时序问题。

轻微建议 (Nit): 3

  1. src/logic/search.ts: 三层循环嵌套的复杂度注释。
  2. src/contentScripts/restorer.ts: 使用明确的类型定义替代 as any
  3. src/contentScripts/ui.ts: showRecalibrationPrompt 的 XSS 防护。

总体评价

本次重构设计合理,解决了实际痛点(弹窗体验差、位置偏移),代码质量整体良好。建议修复上述 3 个 Blocking 问题后合并。

LGTM (待修复 Blocking 问题后)

@catx1726 catx1726 force-pushed the feat/recovery-status-management branch from ca4ab70 to 88790d3 Compare June 10, 2026 06:26
@github-actions

Copy link
Copy Markdown
Contributor

好的,作为一名 Google 资深工程师,我将根据提供的《审查标准》和《审查清单》,对本次变更 (feat/recovery-status-management) 进行代码审查。

总体评价

这是一个雄心勃勃且设计良好的重构。它将高亮恢复机制从被动的“弹窗确认”模式,优雅地重构为基于可信度的主动管理模型。这个变更清晰地解决了 Issue #47 中描述的核心问题,并且配套的 Spec 和 Plan 文档非常详尽,展现了良好的工程实践。

代码整体质量很高,逻辑清晰,测试覆盖也考虑到了关键场景。我的评论主要集中在几个方面:确保新引入的复杂交互逻辑的健壮性、提升代码的可维护性,以及修复一些潜在的边界情况。


代码审查意见

1. [Blocking] 潜在的数据竞争与状态不一致 (src/contentScripts/restorer.ts)

文件: src/contentScripts/restorer.ts
行号: restoreBySearch 方法中,persistSearchSuccesspersistRecoveryStatus 被多次调用。

问题:
restoreBySearch 方法内部,当找到一个候选者后,会调用 persistSearchSuccess 来更新 mark 的 textrangySerialized 等数据。紧接着,在 applyMarksTwoPhases 中,又会调用 persistRecoveryStatus 来更新 recoveryStatus。这会导致对同一个 mark 的 update-mark-details 消息被连续发送两次。

虽然 persistRecoveryStatus 内部有 if (mark.recoveryStatus === status) return 的检查,但 mark 对象是传入的旧引用,它不会自动更新为 persistSearchSuccess 之后的最新状态。因此,persistRecoveryStatus 几乎总是会执行一次额外的、不必要的后台写入操作。

更严重的是,如果两次消息的到达顺序在后台处理时发生错乱(例如,由于异步队列),persistRecoveryStatus 可能先于 persistSearchSuccess 被处理,导致 recoveryStatus 被写入,但最新的 rangySerialized 等关键数据却丢失了。

建议:
persistSearchSuccesspersistRecoveryStatus 合并为一次原子性的 update-mark-details 调用。在 persistSearchSuccess 中,直接包含 recoveryStatus 字段。

// 在 persistSearchSuccess 中
await sendMessage('update-mark-details', {
  id: mark.id, 
  url: mark.url, 
  text: candidate.displayTextSnippet,
  // ... 其他字段
  recoveryStatus: similarity >= 95 ? 'restored' : 'pending-confirm', // 直接在这里设置
} as any, 'background')

然后,在 applyMarksTwoPhases 中,对于成功的情况,只需要更新本地状态 (this.state.restoredMarkIds.add(mark.id)),不再需要调用 persistRecoveryStatuspersistRecoveryStatus 方法可以保留,但只用于处理 needs-recalibration 这种不需要更新其他数据的场景。

2. [Blocking] sanitizeRestoredHighlights 可能导致高亮闪烁或丢失 (src/contentScripts/restorer.ts)

文件: src/contentScripts/restorer.ts
行号: 314-341

问题:
sanitizeRestoredHighlights 方法在 restoreHighlights 开始时被调用,用于清理文本相似度低于 90% 的“已恢复”高亮。这本身是一个很好的健康检查机制。

然而,这个方法会直接操作 DOM,移除高亮元素 (el) 并 normalize 父节点。如果此时页面上的高亮正在被用户交互(例如,用户正在点击或选择文本),或者页面本身正在进行动画/重排,这种 DOM 操作可能会导致意外的视觉闪烁或状态不一致。

此外,sanitizeRestoredHighlights 的触发时机是每次 restoreHighlights 调用。restoreHighlights 可能由 MutationObserver 频繁触发(尽管有防抖),在页面动态变化时,这可能导致高亮频繁地“消失-重建”,造成不良的用户体验。

建议:

  1. 降低触发频率: 考虑为 sanitizeRestoredHighlights 添加一个独立的、更长的防抖或节流机制,或者仅在页面加载完成后的第一次恢复时执行。
  2. 避免在恢复过程中操作: 如果可能,将这个健康检查放在 applyMarksTwoPhases 的 L1 阶段之前,而不是在 restoreHighlights 的开头。这样,如果某个高亮被判定为“不健康”,它会在整个恢复流程中被重新处理,而不是先被删除,然后在同一个流程中又被重建。
  3. 增加更安全的检查: 在操作 DOM 之前,检查页面是否处于可交互状态(例如,document.readyState === 'complete')。

3. [Blocking] recalibrate-mark 消息处理缺乏错误处理和用户反馈 (src/contentScripts/index.ts)

文件: src/contentScripts/index.ts
行号: 329-341

问题:
当侧边栏发送 recalibrate-mark 消息时,content script 会尝试滚动到上下文并进入重新选择模式。但如果 contextSelector 对应的元素在 DOM 中不存在(例如,页面结构已大变),querySelectorDeep 会返回 nullscrollIntoView 不会执行,用户将看不到任何视觉反馈,只会看到一个顶部提示。这可能会让用户感到困惑。

建议:

  1. 处理 contextSelector 不存在的情况: 如果找不到上下文元素,可以尝试滚动到页面的顶部或底部,并在顶部提示中增加说明:“无法定位到原文位置,请在页面中手动查找”。
  2. 增加超时机制: 如果用户在进入重新选择模式后长时间(例如 30 秒)没有操作,应该自动退出该模式并清理状态,避免模式被“卡住”。
  3. 增加更明确的视觉引导: 除了顶部提示,可以考虑在页面中央显示一个半透明的覆盖层或闪烁的指示器,更明确地引导用户进行下一步操作。

4. [Nit] Tooltip.vuepending-confirm 模式的实现可以更简洁

文件: src/contentScripts/views/Tooltip.vue

问题:
当前实现通过大量的 v-if="mode !== 'pending-confirm'" 来隐藏编辑模式下的 UI 元素。这种方法虽然有效,但使得模板逻辑变得分散且难以维护。未来如果增加新的模式,这种模式会变得更复杂。

建议:
考虑将 pending-confirm 模式抽取为一个独立的、更小的子组件(例如 PendingConfirmTooltip.vue)。然后在 Tooltip.vue 中根据 mode 动态渲染不同的子组件。这样可以将不同模式的逻辑和模板完全隔离,提升代码的可读性和可维护性。

5. [Nit] Sidepanel.vue 中的 startRecalibration 逻辑可以更健壮

文件: src/sidepanel/Sidepanel.vue
行号: 96-113

问题:
startRecalibration 方法通过 URL 匹配来查找已打开的标签页。如果用户打开了多个相同 URL 的标签页,它只会激活找到的第一个,这可能不是用户期望的那个。

建议:

  1. 优先使用当前激活的标签页: 如果当前侧边栏关联的标签页(通过 browser.sidebarAction.getTabId() 或其他方式获取)的 URL 匹配,应优先激活它。
  2. 提供选择: 如果找到多个匹配的标签页,可以考虑弹出一个简单的选择列表让用户决定,或者至少记录一条日志以便调试。

6. [Nit] 测试覆盖可以更全面

文件: src/tests/li_deletion.spec.ts, src/tests/li_deletion_apply.spec.ts

问题:
新增的测试覆盖了核心的搜索和应用场景,非常好。但还缺少一些重要的集成测试:

  • pending-confirm 交互流程: 模拟用户点击 pending-confirm 高亮,然后点击“位置正确”或“重新选择”按钮,验证状态和样式是否正确更新。
  • recalibrate-mark 消息处理: 模拟从侧边栏发送消息,验证 content script 是否正确进入重新选择模式,以及 Alt+点击 后是否能正确更新 mark。
  • sanitizeRestoredHighlights 逻辑: 模拟高亮文本被修改后,验证该健康检查机制是否能正确触发并清理失效的高亮。

建议:
虽然这些测试实现起来可能比较复杂(特别是需要模拟浏览器 API),但它们是确保新交互流程稳定性的关键。可以考虑使用 vitest 配合 jsdomhappy-dom 来模拟 DOM 环境,并使用 vi.mock 来模拟 webext-bridgesendMessage 调用。


总结

  • SOP 合规检查: 通过。PR 关联了 Issue Refactor Sidepanel into Composables and Components #47,并且 docs/superpowers/ 目录下存在对应的 Spec 和 Plan 文档。
  • 严重问题 (Blocking): 3
    1. persistSearchSuccesspersistRecoveryStatus 的两次写入可能导致数据竞争,建议合并为一次原子操作。
    2. sanitizeRestoredHighlights 的频繁触发和 DOM 操作可能导致性能问题和 UI 闪烁,建议降低触发频率或调整执行时机。
    3. recalibrate-mark 消息处理缺乏对 contextSelector 不存在的错误处理,建议增加降级策略和用户反馈。
  • 轻微建议 (Nit): 3
    1. Tooltip.vuepending-confirm 模式的 UI 逻辑建议抽取为独立子组件以提升可维护性。
    2. Sidepanel.vue 中的 startRecalibration 在多个匹配标签页时行为不够明确,建议优化。
    3. 建议增加更多针对新交互流程的集成测试,以确保其稳定性。

LGTM 意见: 由于存在 3 个 Blocking 级别的问题,我不能给出 LGTM。请作者优先解决上述 Blocking 问题,特别是数据竞争和 UI 闪烁问题,它们是影响系统稳定性和用户体验的关键。解决后,我会非常乐意批准这个优秀的变更。

Replace the passive disambiguation modal with active recovery status
management. Restoration now branches by confidence level:

- High (>=95%): auto-restore with default style, mark as 'restored'
- Medium (85-95%): auto-restore with pending-confirm dashed style
- Low (<85% or no candidates): skip highlight, mark as 'needs-recalibration'

Also fixes L3 search boundary filtering issues:
- Remove 'div' from structureBoundaries query to reduce over-filtering
- Add fallback to boundary-free search when all combos are filtered
- Clamp suggestRange results and fix LocalAligner endMin overflow

Issue #47
@catx1726 catx1726 force-pushed the feat/recovery-status-management branch from 88790d3 to fd59ea2 Compare June 10, 2026 06:27
@github-actions

Copy link
Copy Markdown
Contributor

好的,作为一名 Google 资深工程师,我将依据《审查标准》和《审查清单》对本次变更进行全面的代码审查。

代码审查报告

变更概述: 本次 PR 将高亮恢复机制从“被动弹窗确认”重构为“按可信度分支处理 + 侧边栏主动管理”。这是一个架构级别的优化,旨在解决 L3 搜索位置偏移导致的不可信恢复问题,并修复了 structureBoundaries 过度过滤的底层 Bug。

总体评价: 这是一个高质量的变更。设计思路清晰,架构合理,文档详尽,测试覆盖了关键场景。代码质量整体很高,但仍然存在一些可以改进的地方。以下是我的具体审查意见。


1. 设计与架构 (Design)

LGTM. 将恢复逻辑从“猜测-弹窗”的被动模式,改为“按置信度分级-侧边栏主动管理”的主动模式,这是一个非常明智的设计决策。它从根本上解决了用户体验和位置偏移两大核心问题。

  • 数据驱动: 将 recoveryStatus 持久化到 Mark 数据本身,而不是依赖 Content Script 的临时状态,是更可靠、更可维护的设计。这符合“数据是核心”的原则。
  • 关注点分离: 将恢复逻辑 (restorer.ts)、UI 交互 (ui.ts, Tooltip.vue)、侧边栏管理 (Sidepanel.vue) 清晰地分离,职责明确。
  • 渐进增强: 通过置信度分级(High/Medium/Low)实现了渐进式的恢复策略,避免了“一刀切”带来的问题。

2. 功能与逻辑 (Functionality)

LGTM. 核心逻辑看起来是正确的。restoreBySearch 函数根据相似度分数正确地分支到不同的置信度等级,并应用相应的样式和状态。sanitizeRestoredHighlights 函数作为健康检查,防止了因 DOM 变化导致的“僵尸高亮”。

一个潜在的竞态条件 (Race Condition):

restorer.tsapplyMarksTwoPhases 方法中,persistRecoveryStatus 是一个异步操作。如果用户在恢复过程中(例如,在 persistRecoveryStatus 完成之前)与页面交互(例如,点击一个 pending-confirm 的高亮),可能会发生状态不一致。

例如:

  1. applyMarksTwoPhases 为 Mark A 设置 recoveryStatuspending-confirm 并调用 persistRecoveryStatus
  2. persistRecoveryStatusawait 期间,用户点击了 Mark A 的高亮,触发了 handleConfirmPosition
  3. handleConfirmPosition 尝试将 recoveryStatus 更新为 restored
  4. 之后,步骤 1 中的 persistRecoveryStatus 完成,覆盖了步骤 3 的更新,将 Mark A 的状态又改回了 pending-confirm

建议: 虽然这种情况在实际中概率较低,但为了健壮性,建议在 persistRecoveryStatus 中添加一个乐观锁或版本号检查。或者,更简单的方式是,在 handleConfirmPositionhandleRecalibrate 中,先读取最新的 mark.recoveryStatus,再决定是否执行操作。例如,如果状态已经是 restored,则 handleConfirmPosition 应该是一个空操作。


3. 复杂度 (Complexity)

LGTM. 代码整体复杂度控制得很好。restoreBySearch 函数虽然变长了,但逻辑分支清晰,可读性尚可。persistSearchSuccess 方法的提取是一个很好的实践,减少了代码重复。

一个关于 LocalAligner 的 Nit:

src/logic/search.ts 中,LocalAligner.refineBoundary 方法内部新增了 searchWithBoundaries 闭包函数。这个函数内部的嵌套循环和 if 判断使得方法体变得有些长。虽然逻辑正确,但可以考虑将其提取为一个独立的私有方法,例如 _searchRange,以提升代码的可读性和可测试性。

// Nit: 考虑将 searchWithBoundaries 提取为一个私有方法
private _searchRange(fullText: string, startMin: number, startMax: number, endMin: number, endMax: number, ignoreBoundaries: boolean) {
  // ... 循环逻辑 ...
}

4. 测试 (Tests)

LGTM. 测试覆盖了核心场景,包括 li_deletioncross-element 集成测试,这非常好。测试用例设计得很有针对性,能够验证新逻辑的正确性。

Nit: li_deletion.spec.tsli_deletion_apply.spec.ts 中的测试用例非常相似,都测试了删除第一个 <li> 的场景。可以考虑将它们合并或明确区分其测试目标(例如,一个测试搜索,一个测试应用高亮)。


5. 命名与注释 (Naming & Comments)

LGTM. 命名清晰,例如 pendingRecalibrationMarksenterRecalibrationMode 等,很好地表达了其意图。注释也很有用,特别是关于 structureBoundaries 中移除 div 的原因说明。


6. 文档 (Documentation)

LGTM. 本次变更的文档工作非常出色。

  • SOP 合规: 已关联 Issue #47,并在 docs/superpowers/ 下创建了对应的 Spec 和 Plan 文档,完全符合 SOP 要求。
  • 审计日志: 已更新 .gemini/ops_changelog.md
  • 变更摘要: PR 描述清晰,包含了核心改动文件、恢复行为对照表等关键信息,便于其他开发者理解。

7. 其他细节

  • src/contentScripts/monitor.ts: 将 hasAddedNodes 改为 hasStructuralChange,并检查 removedNodes,这是一个很好的改进,可以更准确地触发恢复。
  • src/contentScripts/index.ts: processSelection 函数中,重新选择模式的逻辑处理得当,与新建标记的流程清晰分离。
  • src/sidepanel/Sidepanel.vue: 侧边栏的 UI 设计友好,hover 时显示的解释性 tooltip 非常贴心。

总结

本次重构是一次非常成功的架构升级,解决了长期存在的问题,并引入了更优雅、更可靠的恢复机制。代码质量高,文档完善,测试充分。

  • 严重问题 (Blocking): 0
  • 轻微建议 (Nit): 2
    1. 考虑在 restorer.ts 中处理 persistRecoveryStatus 的潜在竞态条件。
    2. 建议将 LocalAligner 中的 searchWithBoundaries 提取为私有方法。

LGTM. 代码整体上显著提升了系统的代码健康度,我批准合并。请作者考虑以上 Nit 建议,并在后续迭代中优化。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant