9.5 KiB
9.5 KiB
Bug修复: 跨子块批注状态污染
修复日期: 2025-11-12 严重程度: 🔴 高(影响批注核心功能) 影响范围: 批注和高亮系统 修复文件:
js/annotations/annotation_logic.js
🐛 Bug 描述
问题表现
用户在详情页进行批注操作时:
- 先做一次跨子块高亮(比如选中 8 个子块)
- 再做单子块内高亮(比如只选中 109.0 内的文本)
- 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(...); // ❌ 误调用跨子块处理
// 使用了旧的 contextAffectedSubBlocks(8个子块)
}
时序图
时间线:
┌─────────────────────────────────────────────────┐
│ 第 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 ✅
→ 执行单子块高亮 ✅
🧪 测试验证
测试场景
-
场景 A: 跨子块 → 单子块
- 先跨 5 个子块高亮
- 再在单个子块内选择文本高亮
- 预期: 只高亮选中的文本 ✅
-
场景 B: 单子块 → 跨子块
- 先单子块高亮
- 再跨多个子块高亮
- 预期: 正确高亮多个子块 ✅
-
场景 C: 连续单子块操作
- 连续在不同段落做单子块高亮
- 预期: 每次都正确高亮 ✅
测试步骤
# 1. 清除缓存并刷新
Ctrl + Shift + R
# 2. 打开历史详情页
# 3. 先选中多个段落 → 右键 → 高亮
# 4. 再选中单个段落内的部分文本 → 右键 → 高亮
# 5. 观察控制台日志
预期日志 ✅:
[跨子块检测] 选择在同一个子块内
[AnnotationLogic] 单子块高亮操作...
(不应该出现 "[跨子块操作] 执行操作")
📊 影响评估
严重程度: 🔴 高
原因:
- 影响批注核心功能
- 可能导致错误的高亮范围
- 用户体验差(高亮了不该高亮的内容)
影响范围
| 功能 | 是否受影响 | 影响程度 |
|---|---|---|
| 跨子块高亮 | ✅ 是 | 🔴 高 |
| 单子块高亮 | ✅ 是 | 🔴 高 |
| 批注添加 | ✅ 是 | 🔴 高 |
| 高亮移除 | ✅ 是 | 🟠 中 |
| 其他功能 | ❌ 否 | - |
复现条件
- ✅ 必须先做过跨子块操作
- ✅ 然后做单子块操作
- ✅ 两次操作在同一个页面会话中
复现率: 100%(符合条件时)
🚀 部署建议
优先级: 🔴 高
建议立即部署,原因:
- Bug 严重影响批注核心功能
- 修复简单(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 发现得非常及时!