paper-burner/output/BUGFIX_CROSS_BLOCK_ANNOTATI...

9.5 KiB
Raw Blame History

Bug修复: 跨子块批注状态污染

修复日期: 2025-11-12 严重程度: 🔴 高(影响批注核心功能) 影响范围: 批注和高亮系统 修复文件: js/annotations/annotation_logic.js


🐛 Bug 描述

问题表现

用户在详情页进行批注操作时:

  1. 先做一次跨子块高亮(比如选中 8 个子块)
  2. 再做单子块内高亮(比如只选中 109.0 内的文本)
  3. Bug: 第 2 次操作却执行了跨 8 个子块的高亮

用户日志

[跨子块检测] 选择在同一个子块内 ✅ 检测正确
[跨子块检测] 未检测到跨子块选择,继续单子块处理 ✅ 流程正确

// 但是点击高亮按钮后:
[跨子块操作] 执行操作: highlight-block, 涉及 8 个子块 ❌ 使用了旧数据!

🔍 根源分析

问题代码

文件: annotation_logic.js:804-806

const isCrossBlockOperation = annotationContextMenuElement.dataset.contextIsCrossBlock === "true";
if (isCrossBlockOperation) {
    return handleCrossBlockMenuAction(action, color, event);
}

Bug 机制

跨子块操作时第1次

// annotation_logic.js:1694-1699
annotationContextMenuElement.dataset.contextIsCrossBlock = "true";
annotationContextMenuElement.dataset.contextAffectedSubBlocks = JSON.stringify([...8个子块ID]);

正确设置

单子块操作时第2次

// annotation_logic.js:716-738 (修复前)
annotationContextMenuElement.dataset.contextContentIdentifier = ...;
annotationContextMenuElement.dataset.contextTargetIdentifier = ...;
// ... 设置很多属性

// ❌ 问题:没有清除跨子块相关属性!
// contextIsCrossBlock 仍然是 "true"
// contextAffectedSubBlocks 仍然是旧的 8 个子块

点击菜单时

// annotation_logic.js:804
const isCrossBlockOperation = dataset.contextIsCrossBlock === "true";  // ❌ 读到旧值 "true"

if (isCrossBlockOperation) {
    return handleCrossBlockMenuAction(...);  // ❌ 误调用跨子块处理
    // 使用了旧的 contextAffectedSubBlocks8个子块
}

时序图

时间线:
  ┌─────────────────────────────────────────────────┐
  │ 第 1 次操作跨子块高亮8 个子块)              │
  ├─────────────────────────────────────────────────┤
  │ contextIsCrossBlock = "true" ✅                 │
  │ contextAffectedSubBlocks = [8个ID] ✅            │
  └─────────────────────────────────────────────────┘
                      ↓
  ┌─────────────────────────────────────────────────┐
  │ 第 2 次操作单子块高亮109.0 内部分文本)      │
  ├─────────────────────────────────────────────────┤
  │ detectCrossBlockSelection() → false ✅          │
  │ 设置 contextContentIdentifier ✅                │
  │ 设置 contextTargetIdentifier ✅                 │
  │ ❌ 没有清除 contextIsCrossBlock!                │
  │ ❌ 没有清除 contextAffectedSubBlocks!           │
  └─────────────────────────────────────────────────┘
                      ↓
  ┌─────────────────────────────────────────────────┐
  │ 用户点击"高亮"按钮                               │
  ├─────────────────────────────────────────────────┤
  │ 读取 contextIsCrossBlock = "true" ❌ (旧值)     │
  │ 调用 handleCrossBlockMenuAction ❌              │
  │ 使用 contextAffectedSubBlocks = [8个ID] ❌ (旧值│
  │ → 高亮了错误的 8 个子块!                       │
  └─────────────────────────────────────────────────┘

修复方案

修复代码

文件: annotation_logic.js:740-743 (新增)

// 🔧 BUG FIX: 清除跨子块相关属性,避免单子块操作时误用旧的跨子块数据
delete annotationContextMenuElement.dataset.contextIsCrossBlock;
delete annotationContextMenuElement.dataset.contextCrossBlockAnnotationId;
delete annotationContextMenuElement.dataset.contextAffectedSubBlocks;

修复位置

在单子块右键事件处理中(annotation_logic.js:708-766),设置完其他属性后,立即清除跨子块相关属性。

修复后的流程

第 1 次操作:跨子块高亮
  → contextIsCrossBlock = "true" ✅

第 2 次操作:单子块高亮
  → 清除 contextIsCrossBlock ✅
  → 清除 contextAffectedSubBlocks ✅
  → 设置 contextTargetIdentifier = "109.0" ✅

点击高亮按钮
  → contextIsCrossBlock === undefined ✅
  → isCrossBlockOperation = false ✅
  → 执行单子块高亮 ✅

🧪 测试验证

测试场景

  1. 场景 A: 跨子块 → 单子块

    • 先跨 5 个子块高亮
    • 再在单个子块内选择文本高亮
    • 预期: 只高亮选中的文本
  2. 场景 B: 单子块 → 跨子块

    • 先单子块高亮
    • 再跨多个子块高亮
    • 预期: 正确高亮多个子块
  3. 场景 C: 连续单子块操作

    • 连续在不同段落做单子块高亮
    • 预期: 每次都正确高亮

测试步骤

# 1. 清除缓存并刷新
Ctrl + Shift + R

# 2. 打开历史详情页
# 3. 先选中多个段落 → 右键 → 高亮
# 4. 再选中单个段落内的部分文本 → 右键 → 高亮
# 5. 观察控制台日志

预期日志 :

[跨子块检测] 选择在同一个子块内
[AnnotationLogic] 单子块高亮操作...
(不应该出现 "[跨子块操作] 执行操作"

📊 影响评估

严重程度: 🔴

原因:

  • 影响批注核心功能
  • 可能导致错误的高亮范围
  • 用户体验差(高亮了不该高亮的内容)

影响范围

功能 是否受影响 影响程度
跨子块高亮 🔴
单子块高亮 🔴
批注添加 🔴
高亮移除 🟠
其他功能 -

复现条件

  • 必须先做过跨子块操作
  • 然后做单子块操作
  • 两次操作在同一个页面会话中

复现率: 100%(符合条件时)


🚀 部署建议

优先级: 🔴

建议立即部署,原因:

  1. Bug 严重影响批注核心功能
  2. 修复简单3 行代码),风险极低
  3. 不影响其他功能

回归测试清单

[ ] 跨子块高亮 → 单子块高亮
[ ] 单子块高亮 → 跨子块高亮
[ ] 连续多次单子块操作
[ ] 跨子块批注添加
[ ] 单子块批注添加
[ ] 高亮移除
[ ] 切换标签后批注恢复

📝 相关 Issue

用户报告

"我选中某个段落里面有公式的情况,就会出错"

根源不是公式,而是:

  • 用户之前可能选中了包含公式的多个段落(跨子块)
  • 然后选中单个段落内的文本(单子块)
  • 触发了状态污染 bug

公式只是触发场景之一,任何跨子块 → 单子块操作都会触发。


🔮 预防措施

代码模式

原则: 每次设置上下文菜单的 dataset 时,清除所有可能的旧状态

推荐模式:

// 清除所有状态
function clearContextMenuState() {
    const keys = Object.keys(annotationContextMenuElement.dataset);
    keys.filter(k => k.startsWith('context')).forEach(k => {
        delete annotationContextMenuElement.dataset[k];
    });
}

// 设置新状态前先清除
clearContextMenuState();
annotationContextMenuElement.dataset.contextXXX = newValue;

未来改进

建议: 使用状态机管理批注上下文,而不是依赖 dataset

class AnnotationContext {
    constructor() {
        this.reset();
    }

    reset() {
        this.isCrossBlock = false;
        this.affectedSubBlocks = [];
        this.targetIdentifier = null;
        // ...
    }

    setCrossBlock(data) {
        this.reset();
        this.isCrossBlock = true;
        this.affectedSubBlocks = data.subBlocks;
        // ...
    }

    setSingleBlock(data) {
        this.reset();  // 自动清除旧状态
        this.isCrossBlock = false;
        this.targetIdentifier = data.id;
        // ...
    }
}

验收清单

  • Bug 根源分析完成
  • 修复代码实施完成
  • 语法检查通过
  • 功能测试通过
  • 回归测试通过
  • 用户验证通过
  • 部署到生产环境

📌 总结

Bug: 跨子块批注状态污染单子块操作 根源: 单子块处理时未清除跨子块相关的 dataset 属性 修复: 添加 3 行代码清除旧状态 影响: 批注核心功能 优先级: 🔴 高,建议立即部署


修复完成! 🎉

感谢用户的详细反馈,这个 bug 发现得非常及时!