状态: 规划定稿(2026-08-21 三轮探讨定稿) 目标: ① 修复 v0.10.0 编辑模式的三处语义矛盾与两处实现缺陷(不推翻 Ctrl+E 机制); ② 让笔记显示图片——存量配图行显示 + 本地图片插入 + 会话图自动插入验证加固。 依据: v0.10.0 交付代码审查(2026-08-21)· v0.10.0 状态一致性规则(版本快照只在显式保存/AI 采纳时建立)· 产品定位"学习的枢纽"(浏览为主场景、内容为机器生成 Markdown 非从零写作) 关联: v0.10.0(本版修复其缺陷)· v0.7.3 屏配图(REQ-160)· v0.8.0 M4 版本化(REQ-144)· assetProtocol 配置
裁决:保留 Ctrl+E 双向切换机制(Obsidian 实证"读用预览、写用源码",toggle 心智简单), 不推翻。复杂度真实来源是叠加的"保存/版本"语义,共发现三处自相矛盾 + 两处实现缺陷:
| # | 问题 | 证据 | 定级 |
|---|---|---|---|
| 1 | 数据串写:编辑中点击左侧其他笔记 → 旧内容覆盖新笔记(NoteEditView 内部 state 不随 prop 重置) | NotesPage 列表点击只 setSelected 不退出编辑 | P0 真实 bug |
| 2 | 「Ctrl+S 建版本」文案假象:前端 createVersion 参数未传后端,4 条保存路径后端行为全同(每次 update_note 无条件建版本) | NoteEditView saveDraft vs commands.rs update_note | P0 规范偏离 |
| 3 | 版本噪音:versioned_save 无内容去重,30s 自动保存/失焦也建版本,污染版本时间线 | db_notes_versions.rs | P1 |
| 4 | 「取消」假取消:按钮语义是"取消"行为是无条件保存草稿后退出 | NoteEditView handleCancel | P1 语义矛盾 |
| 5 | Ctrl+E 实现分散:window/textarea 双监听 + effect 依赖 [selected, editing] 反复重绑 | NotesPage/NoteEditView | P2 |
| 6 | Ctrl+Shift+S 双义:课堂页=截图、编辑器=拆分段(页面互斥下不触发) | ClassroomPage/NoteEditView | P3 登记不改 |
裁决:笔记=机器生成、浏览为主场景 → 浏览态必须从"信息消费"升级为"学习加工"(主动回忆 + 再次出发)。 浏览态增值任务移交 0.11.0(段落级主动回忆标记/回顾流/健康度/勾选进度四候选)。
裁决:一步到位(显示 + 本地插入 + 会话图自动插入验证);图片引用统一"data_dir 相对引用"约定; 解析校验在 Rust 收口(resolve 命令)。
| # | 修复 | 修法 | 提交 |
|---|---|---|---|
| F1 | 串写 | 列表点击/新建时退出编辑态;NoteEditView 卸载时 dirty 自动保存(dirtyRef);key={note.id} 兜底强制重建 | 本次 |
| F2 | 版本参数接通 | update_note 增 create_version: Option;false 走轻量更新(只刷新 title/content/updated_at);前端 saveDraft 传参;任务勾选回写传 false(v0.10.0 规范"勾选不建版本") | 本次 |
| F3 | 版本去重 | update_note 显式保存路径:内容与最新版本相同 → 跳过 versioned_save(防 Ctrl+S 空保存) | 本次 |
| F4 | 取消→完成 | 「取消(Ctrl+E)」改名「完成(Ctrl+E)」(保存草稿后退出,行为不变语义诚实);补 ESC 退出编辑 | 本次 |
| F5 | 实现收敛 | Ctrl+E window 监听 handler 稳定化(ref 持有,消除 [selected, editing] 反复重绑) | 本次 |
| F6 | Ctrl+Shift+S 冲突 | 登记不改(页面互斥;0.11 引入同屏能力前必须处理) | 登记 |
- ✅ 图片生成:关键帧归档
data_dir/session-images/{sid}/full/{ts}.webp - ✅ 配图行内嵌:规则版
- (note_filter);AI 精修 F1 丢图合并;产物转笔记/ - ✅ 会话侧视图显示:baseUrl + convertFileSrc(ArtifactView/ImageGallery 等)
- ❌ v0.10.0 新笔记页:ReactMarkdown img 裸传递,
session-images/...相对引用在 WebView 加载失败 ⚠️ NotePreviewView baseUrl 拼接疑点:session-images/前缀重复拼接(配图行格式与 baseUrl 基准不一致)
| 命令 | 职责 | 安全约束 |
|---|---|---|
resolve_note_image(noteId, src) -> Option<String> |
Markdown src → 可 convert 的本地绝对路径 | 三规则:session-images/ 前缀(sid==笔记 session_id)、notes-images/ 前缀(nid==noteId)、裸 full|thumbs/(按笔记 session_id);拒绝 .. 穿越;不匹配返回 null |
import_note_image(noteId, sourcePath) -> String |
本地图复制进 data_dir/notes-images/{nid}/ 并返回相对引用 |
源存在 + 扩展名白名单(png/jpg/jpeg/webp/gif)+ 大小上限 10MB + 重名序号;零新增 fs 权限(dialog:default 已有) |
app_data_dir() -> String |
暴露 data_dir(NotePreviewView 拼接用) | 只读返回 |
- NotesPage:img 渲染器 → 新组件 NoteImage(http(s)/data: 直出;其余 async resolve → convertFileSrc;失败 → alt + 占位不破版面);点击 → 复用 ImagePreviewOverlay 放大
- NoteEditView:「图片」按钮拆分「🖼 本地图片」(dialog.open → import_note_image → 光标插入
)/「🔗 链接」(保留 prompt URL) - NotePreviewView:配图行前缀判断(
session-images/→ dataDir + src;其他相对 → baseUrl + src;http → 直出)
三路生成管线均已内嵌配图(规则版/AI 精修 F1/产物转笔记),本次验证 + resolve 三规则格式兼容, 确保历史笔记与新建笔记全部可显示。手动笔记无会话图,不适用自动插入。
- 图片文件被删 → resolve 空 → alt + 占位(不破版面)
- 越权引用(A 笔记引 B 会话图)→ 拒绝返回 null
- 插入失败(源不存在/格式不支持/超限)→ 明确错误提示,不落脏数据
| 决策点 | 结论 | 被否方案 |
|---|---|---|
| 图片引用格式 | data_dir 相对引用(session-images/…、notes-images/…) | 绝对路径(不可移植);data URL(膨胀/编辑污染) |
| 解析位置 | Rust resolve 命令收口(前缀白名单+穿越防护+归属校验) | 前端约定拼接(防御弱) |
| 插入入口 | 本地文件 dialog + 复制进 data_dir(硬约束:assetProtocol scope=$APPDATA/**) | 会话图手动选择器(自动已覆盖,0.11 再评) |
| 会话图插入 | 自动(生成管线内嵌已有,验证加固) | 编辑态手动插图 |
| 步骤 | 内容 | 验收 |
|---|---|---|
| M1 | 后端:update_note 版本参数 + 去重 + 三新命令 + 单测 | 单测覆盖:版本参数分支/去重/resolve 三规则+穿越+归属/import 白名单+超限+重名;cargo test 全绿 |
| M2 | 前端:NoteEditView 修复 + NotesPage 修复 + NoteImage + 预览 | 编辑闭环:串写无复现、Ctrl+S 真建版本、自动保存不建版本、ESC 退出、完成按钮 |
| M3 | 图片全链真机验收 | 旧笔记配图显示、新转笔记自动带图、本地插入、点击放大、图片删除降级 |
| M4 | 回归 | cargo clippy 干净;前端 tsc/build 通过;v0.10.0 验收项不劣化 |
- 浏览态增值四候选:A 段落级主动回忆标记(已掌握/待复习/疑问 + 折叠)、B 回顾流基础版(延后 #16)、C 健康度基础版(#17)、D 勾选进度——核心判据:浏览态从"信息消费"升级为"学习加工"
- 遗留决策:即改即存 vs 显式保存长期演进(0.10.1 维持现状);REQ-019/020/027 闪卡排期;图片随导出(#18 联动)