Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gemini/ops_changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
| 2026-05-23 16:35:00 | Implement and Refine GitHub Gist Synchronization | src/logic/sync.ts, src/logic/storage.ts, src/options/Options.vue, src/background/main.ts, src/manifest.ts, docs/user-guide/github-sync.md | Add cross-device sync via GitHub Gist with automated background push/pull, timestamp merging, tombstone deletion, and exponential backoff retry (Issue #41, #42, #43) | HEAD | - |
| 2026-06-02 08:55:00 | Refactor Sidepanel into modular Composables and Components | src/sidepanel/Sidepanel.vue, src/sidepanel/composables/*, src/sidepanel/components/*, src/tests/tagTree.spec.ts | Modularize monolithic Sidepanel.vue into 5 domain-specific composables and 5 focused UI components to improve maintainability and testability (Issue #46) | e14fd1d | git checkout main && git branch -D issue-46 |
| 2026-06-02 09:30:00 | Fix Blocking CR issues and sync documents | src/sidepanel/Sidepanel.vue, src/sidepanel/composables/useTagActions.ts, src/sidepanel/composables/useMarkActions.ts, docs/superpowers/* | Resolve tag picker state management bugs, fix race conditions in tagging, improve error handling in deletion, and restore missing spec/plan docs (Issue #46) | HEAD | - |
| 2026-06-10 14:10:00 | Refactor highlight restoration with confidence-based branching | src/contentScripts/restorer.ts, src/contentScripts/index.ts, src/contentScripts/ui.ts, src/contentScripts/state.ts, src/contentScripts/views/Tooltip.vue, src/sidepanel/Sidepanel.vue, src/logic/search.ts, src/logic/config.ts, src/logic/storage.ts | Replace passive disambiguation modal with active recovery status management: high confidence auto-restore, medium confidence pending-confirm style, low confidence sidepanel recalibration list (Issue #47) | HEAD | git checkout main && git branch -D feat/recovery-status-management |

---

Expand Down
181 changes: 181 additions & 0 deletions docs/superpowers/plans/2026-06-10-recovery-status-management-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# 高亮恢复可信度分级与主动状态管理实施计划

> **For agentic workers:** Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 将高亮恢复从"被动弹窗确认"改造为"按可信度分支处理 + 侧边栏主动管理",同时修复 L3 搜索的位置偏移问题。

**Architecture:**
1. **数据层**:Mark 接口扩展 `recoveryStatus` 字段。
2. **恢复层**:`HighlightRestorer` 按 confidence (high/medium/low) 分支处理,不再触发弹窗。
3. **表现层**:`Tooltip.vue` 支持 `pending-confirm` 模式;`Sidepanel.vue` 显示待恢复列表。
4. **交互层**:Content Script 支持 `recalibrate-mark` 消息,进入重新选择模式。

**Tech Stack:** Vue 3, TypeScript, WebExtension API, Rangy, webext-bridge.

---

### Task 1: 数据层扩展

**Files:**
- Modify: `src/logic/storage.ts`
- Modify: `src/logic/config.ts`

- [x] **Step 1: Mark 接口添加 recoveryStatus**
```typescript
recoveryStatus?: 'restored' | 'pending-confirm' | 'needs-recalibration'
```

- [x] **Step 2: 新增 pending-confirm 样式常量**
```typescript
export const highlightPendingConfirmStyle = (color: string) =>
`box-shadow: inset 0 -5px 0 0 ${color}; cursor: pointer; border-bottom: 2px dashed ${color}; opacity: 0.85;`
```

---

### Task 2: 恢复逻辑改造(核心)

**Files:**
- Modify: `src/contentScripts/restorer.ts`
- Modify: `src/contentScripts/state.ts`

- [x] **Step 1: 扩展 SearchRestoreResult 接口**
添加 `confidence?: 'high' | 'medium' | 'low'`

- [x] **Step 2: 改造 restoreBySearch 分支逻辑**
- similarity ≥ 95% → `confidence: 'high'`,应用默认样式
- similarity 85%-95% → `confidence: 'medium'`,应用 pending-confirm 样式
- similarity < 85% → `confidence: 'low'`,不应用高亮
- multiple candidates / no candidates → `confidence: 'low'`

- [x] **Step 3: 改造 applyMarksTwoPhases**
- L1 成功 → `recoveryStatus: 'restored'`
- high → `recoveryStatus: 'restored'`
- medium → `recoveryStatus: 'pending-confirm'`
- low → `recoveryStatus: 'needs-recalibration'`

- [x] **Step 4: 添加 persistRecoveryStatus 辅助方法**
调用 `update-mark-details` 持久化 recoveryStatus,避免重复写入。

- [x] **Step 5: 移除弹窗触发逻辑**
`restoreHighlights` 返回 `void`,不再处理 `ambiguousMarksQueue`。

- [x] **Step 6: state.ts 添加 recalibration 状态**
```typescript
isRecalibrationMode = false
recalibrationMarkId: string | null = null
```

---

### Task 3: 待确认标记交互

**Files:**
- Modify: `src/contentScripts/views/Tooltip.vue`
- Modify: `src/contentScripts/ui.ts`
- Modify: `src/contentScripts/index.ts`

- [x] **Step 1: Tooltip.vue 支持 pending-confirm 模式**
- 新增 `mode` ref 和 `currentMarkId` ref
- `show` 方法接收 `mode` 和 `markId` 参数
- pending-confirm 模式下显示琥珀色提示条 + "位置正确"/"重新选择"按钮

- [x] **Step 2: ui.ts 添加 confirmPosition 和 recalibrate 处理**
- `handleConfirmPosition`:恢复默认样式,更新 recoveryStatus
- `handleRecalibrate`:进入重新选择模式

- [x] **Step 3: index.ts 点击现有高亮时传递 mode**
- 检查 mark.recoveryStatus,若为 pending-confirm 则传递 mode='pending-confirm'

---

### Task 4: 重新选择模式

**Files:**
- Modify: `src/contentScripts/ui.ts`
- Modify: `src/contentScripts/index.ts`

- [x] **Step 1: ui.ts 实现 recalibration 模式**
- `enterRecalibrationMode`:设置状态,显示页面顶部浮动提示
- `exitRecalibrationMode`:清理状态,移除提示
- `updateMarkFromRecalibration`:移除旧高亮,应用新高亮,更新 mark 数据

- [x] **Step 2: index.ts 处理 recalibrate-mark 消息**
- 新增 `onMessage('recalibrate-mark', ...)` 处理器
- `processSelection` 中 isRecalibrationMode 为 true 时,Alt+点击触发重新标记而非新建

---

### Task 5: 侧边栏待恢复列表

**Files:**
- Modify: `src/sidepanel/Sidepanel.vue`

- [x] **Step 1: 添加 pendingRecalibrationMarks 计算属性**
从 `marksByUrl` 过滤所有 `recoveryStatus === 'needs-recalibration'` 的 mark。

- [x] **Step 2: 模板中添加待恢复区域**
- 可折叠的琥珀色卡片,显示标记数量
- 每个标记:原文片段 + "重新选择" + "丢弃"按钮

- [x] **Step 3: 实现 startRecalibration 方法**
激活对应标签页,发送 `recalibrate-mark` 消息到 content script。

- [x] **Step 4: 实现 discardPendingMark 方法**
调用 `remove-mark-by-id` 彻底删除标记。

---

### Task 6: L3 搜索修复(配套)

**Files:**
- Modify: `src/logic/search.ts`

- [x] **Step 1: 移除 div 从 structureBoundaries 查询**
减少大容器 div 对搜索空间的过度过滤。

- [x] **Step 2: 添加回退机制**
当 structureBoundaries 过滤死所有组合时,回退到纯相似度搜索。

- [x] **Step 3: 修复溢出 bug**
- `suggestRange` 结果 clamp 到 `fullText` 长度
- `LocalAligner` 的 `endMin` 防溢出

---

### Task 7: 测试与验证

**Files:**
- Modify: `src/tests/restorer.spec.ts`

- [x] **Step 1: 更新 restorer.spec.ts**
`restoreHighlights` 返回 `void`,更新断言。

- [x] **Step 2: 运行全部测试**
验证所有相关测试通过。

---

### Task 8: 文档与审计

**Files:**
- Modify: `.gemini/ops_changelog.md`
- Create: `docs/superpowers/specs/2026-06-10-recovery-status-management-design.md`
- Create: `docs/superpowers/plans/2026-06-10-recovery-status-management-plan.md`

- [x] **Step 1: 更新审计日志**
- [x] **Step 2: 创建 Spec 文档**
- [x] **Step 3: 创建 Plan 文档**

---

### Task 9: 分支与 PR

- [ ] **Step 1: 切换分支**
`git checkout -b feat/recovery-status-management`

- [ ] **Step 2: 提交代码**
按照 conventional commit 规范提交。

- [ ] **Step 3: 创建 PR**
使用 PR Template,填写变更摘要和质量验证。
120 changes: 120 additions & 0 deletions docs/superpowers/specs/2026-06-10-recovery-status-management-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# 设计规约:高亮恢复可信度分级与主动状态管理

- **状态**:已评审
- **日期**:2026-06-10
- **关联 Issue**:[#47](https://github.com/catx1726/MarkFlow/issues/47)

## 1. 目标 (Goals)

解决当前恢复机制的两大核心问题:

1. **被动弹窗体验差**:L4 歧义消除弹窗让用户在模糊的候选列表中被动选择,仿佛机器已帮用户"预选"了错误位置。
2. **位置偏移不可信**:L3 共识搜索在复杂 DOM 场景下返回的 range 可能偏移,直接应用会导致高亮飘到错误位置。

通过引入**可信度分级**和**恢复状态持久化**,将恢复从"机器猜测 → 弹窗确认"改为"按可信度分支处理 + 侧边栏主动管理"。

## 2. 方案详述 (Detailed Design)

### 2.1 可信度分级策略

| 等级 | 条件 | 处理方式 | 视觉表现 |
|------|------|----------|----------|
| **High** (≥95%) | L1 路径还原成功 或 L3 相似度 ≥95% | 自动恢复,静默成功 | 默认样式(box-shadow 下划线) |
| **Medium** (85%-95%) | L3 相似度 85%-95% | 自动恢复,但标记为"待确认" | 虚线边框 + 半透明 + 提示图标 |
| **Low** (<85%) | L3 相似度 <85% / 无候选 / 多候选 | 不自动恢复,进入待恢复列表 | 无高亮,侧边栏显示"待恢复" |

### 2.2 恢复状态持久化

在 `Mark` 数据结构中新增 `recoveryStatus` 字段:

```typescript
recoveryStatus?: 'restored' | 'pending-confirm' | 'needs-recalibration'
```

- 状态自描述:mark 数据本身携带恢复状态,无需 content script 维护额外队列。
- 状态流转:
- 新建标记 → 默认为 `undefined`(向后兼容,视为 `'restored'`)
- High 恢复成功 → `'restored'`
- Medium 恢复成功 → `'pending-confirm'`
- Low 恢复失败 → `'needs-recalibration'`
- 用户确认 pending-confirm → `'restored'`
- 用户重新选择 → `'restored'`

### 2.3 侧边栏"待恢复"列表

在侧边栏 TagFolder 列表**上方**新增可折叠区域:

- 标题:⚠️ 待恢复标记 (N)
- 每个标记显示:原文片段(截断)+ "重新选择"按钮 + "丢弃"按钮
- 点击"重新选择":激活对应标签页,发送 `recalibrate-mark` 消息到 content script

### 2.4 重新选择模式 (Recalibration Mode)

当用户从侧边栏触发重新选择时,content script 进入特殊模式:

1. 页面顶部显示浮动提示:"请选中原标记「xxx」对应的文本,然后按 Alt+点击确认"
2. 用户选中文本后按 Alt+点击(与新建标记相同手势)
3. 系统不创建新 mark,而是更新现有 mark 的 text/rangySerialized/html/context/recoveryStatus
4. 退出重新选择模式

### 2.5 待确认标记的确认交互

当用户点击 `pending-confirm` 样式的高亮时:

1. Tooltip 显示琥珀色提示条:"此标记位置可能已变化,请确认是否准确"
2. 提供两个按钮:
- **位置正确**:将样式恢复为默认,更新 `recoveryStatus` 为 `'restored'`
- **重新选择**:进入重新选择模式

### 2.6 L3 搜索修复(配套改进)

本次改造同时修复了导致位置偏移的根因:

- `structureBoundaries` 过滤过度:从查询列表中移除 `div`,减少不必要的边界干扰
- 回退机制:当 structureBoundaries 过滤死所有搜索路径时,自动回退到纯相似度搜索
- `suggestRange` 和 `LocalAligner` 的溢出保护

## 3. 技术实现 (Technical Implementation)

### 3.1 关键代码改动

| 文件 | 改动 |
|------|------|
| `src/logic/storage.ts` | Mark 接口添加 `recoveryStatus` |
| `src/logic/config.ts` | 新增 `highlightPendingConfirmStyle` |
| `src/contentScripts/restorer.ts` | 恢复逻辑按 confidence 分支;移除弹窗触发 |
| `src/contentScripts/state.ts` | 添加 `isRecalibrationMode` / `recalibrationMarkId` |
| `src/contentScripts/ui.ts` | 添加 `enterRecalibrationMode` / `updateMarkFromRecalibration` / `handleConfirmPosition` |
| `src/contentScripts/index.ts` | `processSelection` 支持 recalibration;tooltip 传递 mode |
| `src/contentScripts/views/Tooltip.vue` | 新增 `pending-confirm` 模式 UI |
| `src/sidepanel/Sidepanel.vue` | 新增"待恢复标记"区域 |

### 3.2 消息流

```
Sidepanel --recalibrate-mark--> Content Script --update-mark-details--> Background
|
v
Storage (recoveryStatus)
```

### 3.3 向后兼容

- 旧数据没有 `recoveryStatus` 字段 → 恢复时按现有逻辑处理,成功后写入 `'restored'`
- `DisambiguationModal.vue` 保留文件但不再被调用(Phase 6 可彻底移除)

## 4. 测试策略

| 测试类型 | 内容 |
|----------|------|
| 单元测试 | restorer.spec.ts 更新:验证 confidence 返回值和 recoveryStatus 更新 |
| 集成测试 | cross-element.spec.ts, li_deletion.spec.ts:验证各场景下的恢复行为 |
| 手动测试 | 删除 li → 观察待恢复列表;修改少量文字 → 观察 pending-confirm 样式 |

## 5. 风险与对策

| 风险 | 对策 |
|------|------|
| 旧用户数据无 recoveryStatus | 视为 undefined,恢复成功后自动写入 |
| 重新选择模式与新建标记手势冲突 | Recalibration 模式下优先处理重新标记 |
| 待确认样式与页面背景冲突 | 使用虚线边框 + 透明度 + 琥珀色提示,区分度足够 |
44 changes: 34 additions & 10 deletions src/contentScripts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,7 @@ async function initialize() {
ui.ensureMounted()
window.addEventListener('keydown', handleKeyDown)
attachListenersToShadowRoots(document)
const ambiguous = await restorer.restoreHighlights()
if (ambiguous.length > 0 && !state.modalState.visible) {
setTimeout(() => {
if (state.ambiguousMarksQueue.value.length > 0 && !state.modalState.visible) {
state.disambiguationModalApp?.show(state.ambiguousMarksQueue.value)
}
}, 1000)
}
await restorer.restoreHighlights()
{
const hash = window.location.hash
if (hash.startsWith('#__highlight-mark__')) {
Expand Down Expand Up @@ -163,7 +156,7 @@ function findContainingBlock(node: Node): HTMLElement {
return node as HTMLElement
}

function processSelection(event: {
async function processSelection(event: {
target: EventTarget | null
path: EventTarget[]
clientX: number
Expand Down Expand Up @@ -192,6 +185,24 @@ function processSelection(event: {
const isNewSelectionAction = event.altKey && !initialSelection.isCollapsed

if (isNewSelectionAction) {
// 重新选择模式:用当前选区更新已有 mark
if (state.isRecalibrationMode && state.recalibrationMarkId) {
const freshSelection = rangy.getSelection()
if (freshSelection.rangeCount > 0 && !freshSelection.isCollapsed) {
const range = freshSelection.getRangeAt(0)
const capturedText = range.toString().trim()
if (capturedText) {
try {
await ui.updateMarkFromRecalibration(state.recalibrationMarkId, range, capturedText)
ui.exitRecalibrationMode()
} catch (e) {
console.error('[WebMarker] Error during recalibration:', e)
}
}
}
return
}

ui.clearPreviewHighlight()
let range: rangy.RangyRange | null = null
if (event.detail >= 3) {
Expand Down Expand Up @@ -274,7 +285,9 @@ async function showTooltipForExistingMark(markId: string, x: number, y: number)
const color = mark ? mark.color : settings.value.defaultHighlightColor
const tags = mark ? mark.tags : undefined
ui.setOriginalColorForChange(color)
state.tooltipApp?.show(x, y, true, note, color, mark?.text ?? '', tags)
// 根据 recoveryStatus 决定 tooltip 模式
const mode = mark?.recoveryStatus === 'pending-confirm' ? 'pending-confirm' : 'edit'
ui.showTooltip(x, y, true, note, color, mark?.text ?? '', tags || [], mode, markId)
}

// #endregion
Expand Down Expand Up @@ -313,4 +326,15 @@ onMessage('goto-chapter', ({ data }) => {
}
}
})
onMessage('recalibrate-mark', async ({ data }) => {
const { markId, originalText, contextSelector } = data
// 先尝试滚动到原标记上下文附近
if (contextSelector) {
const element = querySelectorDeep(contextSelector)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}
ui.enterRecalibrationMode(markId, originalText)
})
// #endregion
Loading
Loading