Files
new-markdown/docs/29-全项目审计.md
Muse 4fca4b7143 fix(rust): spawn_blocking 改走 tauri::async_runtime(工程无 tokio 依赖,Windows 构建 E0433)
6 处 tokio::task::spawn_blocking(Git 同步 4 处 + docs/29 资源
命令 2 处)全部换成工程既有写法 tauri::async_runtime::spawn_blocking;
全量核对源码外部 crate 前缀与 Cargo.toml 依赖一致。docs/29 补勘误。
2026-11-14 18:09:30 +08:00

201 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# docs/29 — 全项目审计(性能 + 逻辑,三线并行 + App 编排层自审)
时间:2026-11-14 晚。范围:src/editor/ 全部、EditorPane/SourceEditor、
src/services/、src/components/、src/hooks/、src/utils/、App.tsx 编排层、
src-tauri/src/ 全部(1289 行)。先说结论:发现 **1 个高危数据问题**
(已修)、中低问题一批(已修 20 余处),其余记录待办;三线均未再
发现新的高危。搜索结果上限(每文件 20 条/共 300 文件)顺带核实存在。
## 已修复
### 高危
- **H1 分片载入未完成时窗格卸载,部分文档被静默回写并截断**:
EditorPane 卸载 cleanup 无条件回写最终序列化,而实时回写路径有
muted(载入中)守卫、唯独卸载路径漏了。载入中被保活淘汰(打开
第 7 个标签)或关标签时,回写的是已追加的部分文本;
syncEditorContent 对干净标签会把 savedContent 一起对齐到部分
文本——不标脏、无提示,下一次保存即把截断内容写入磁盘。修复:
cleanup 在 muted 时跳过回写(状态里本就是磁盘完整原文);载入
失败路径已主动解除 muted 并如实回写,不受守卫影响。同处顺带修:
`editor.create()` 完成回调无取消守卫,卸载后仍会把已销毁编辑器
的 API 注册回 App → 补 cancelled 检查并销毁。回归测试见
tests/editor-pane-unload.test.tsx(对照组);中途卸载的交错在
jsdom 里无法自动化复现(React act 会抽干异步追加,三种卸载方式
实测均如此),该守卫靠代码复核保证:muted 仅在追加完成/失败的
收尾回调里解除。
### 性能(中)
- **P1 Sidebar 的 memo 被三个不稳定 props 击穿,击键时整棵文件树
重渲染**:gitSync 内联对象、onCollapseSidebar 内联回调、随正文
变化重建的 outline 数组。三处稳定化:gitSyncUi 用 useMemo、
collapseSidebar 用 useCallback、非大纲模式传模块级空数组常量。
两条审计线独立撞到同一问题,属实。
- **P2 光标行列每次选区变化从文档头全量遍历**:Milkdown
selectionUpdated 每事务同步分发,600K 文档光标在文末时单次上万
节点。改为 rAF 合并:闭包缓存最新一次计算、逐帧最多算一次
(行列与表格上下文同帧合并)。
- **P3 图片显示解析无缓存**:同一图片出现 N 次/切标签重挂/epoch
重建都重复读盘转 base64。App 层加(文档路径|src)→Promise 缓存,
上限 60 条 FIFO。
- **P4 表格工具栏滚动监听无节流**:每次 scroll 都量 DOM + setState。
改 rAF 合并 + 位置未变不 setState。打字机模式逐键重启平滑滚动
改 behavior:'auto' 防抖动。
- **P5 分片切分的行偏移 O(切点数×行数)**:改行偏移前缀表 O(1) 查表。
- **P6 导出 embed 图片串行逐张解析**:改 Promise.all 并发;
exportHtml 高亮器初始化失败后 rejected Promise 被永久缓存、
整会话高亮失效 → 失败时清缓存,下次导出重试。
- **P7(Rust)list_dir 排序比较器内每次比较分配两个小写字符串**:
改 sort_by_cached_key 预计算键。
### 逻辑/健壮性(中)
- **L1(Rust)资源复制/保存命令是同步命令且无大小上限**:
copy_to_assets 对任意文件整读(冻 UI 线程 + 内存整载),
save_bytes_to_assets 同样无上限。两命令改 async + spawn_blocking,
统一 32MB 上限(与 read_file_base64 对齐)。写入侧补 64MB 上限,
与读侧对称。
- **L2(Rust)工作区搜索跟随 junction/符号链接逃出工作区**:去重
只防环,指向外部的 junction 是「新目录」会被整扫、耗尽配额并
越界读文件。scan_dir 加 canonical 包含校验,越界即跳过。顺带:
元数据取不到时由 fail-open 改为跳过;隐藏文件与隐藏目录同口径
跳过。
- **L3(Rust)recovery 草稿表无任何上限**:单条可达数十 MB、条数
无限增长、启动时全量正文过 IPC。recovery_save 加单条 10MB 上限
(超限静默跳过,草稿是副本、.md 是正本,不报错刷屏)+ 只保留
最近 20 条;workspaces 表同样只留最近 20 条。
- **L4(Rust)启动期数据库失败直接 panic 且循环复现**:DB 损坏/
磁盘异常时用户连界面都进不去。改为:打开/建表失败时把坏库
(含 -wal/-shm)改名 `.broken-<时间戳>` 备份后新建空库继续
启动;正文在磁盘 .md 中不受影响。连接另设 WAL + busy_timeout
3000(多开写操作排队而非直接 SQLITE_BUSY)。
- **L5 路径比较未做大小写归一(Windows 语义)**:pathsEqual/
isPathInside 大小写敏感,盘符/目录大小写不一致时 DnD 守卫漏判、
活动文件高亮消失、恢复键/Git 设置键/最近列表键同根裂键。新增
pathCompareKey(Windows 风格路径折叠小写,POSIX 保持敏感),
pathsEqual/isPathInside/文件树高亮/recoveryKey/Git 设置键与
租约键/legacy recent 去重共用。注意:升级后旧恢复记录键与新键
不一致,旧草稿可能多提示一次、随后被 20 条修剪清掉,无数据
损失。顺带修 parentDirOf('C:/a.md') 返回盘符相对路径 'C:' →
返回 'C:/'。
- **L6 文件树与标签页右键菜单无视口夹取**:靠边弹出溢出屏幕、尾
部条目够不到(编辑区菜单已有夹取,这两处漏了)。抽共享
clampMenuPosition(utils/menuPos.ts),三处菜单共用。
- **L7 逻辑小项打包**:①工作区搜索无乱序保护,慢的旧请求会覆盖
新结果 → 序号守卫;②换工作区时文件树短暂显示旧目录(expanded
按绝对路径残留)→ FileTree 按 root 加 key 重挂;③TabBar 拖拽
dragover 以浏览器节律重复 dispatch 同一移动 → (dragId:下标)
去重;④Dropdown 给 useDismiss 传内联 onClose,打开期间每次渲染
重注册 5 个全局监听 → useCallback 稳定;⑤Git 租约 check-then-
write 双窗口同启可同时取得 → 写入后回读确认持有者;⑥
windowClaims 注册表坏条目(keys 非数组)可让启动扫描抛错 →
prune 时整条丢弃坏条目;⑦store 的 UI 偏好 Web 模式只写不读 →
loadUiPref Web 分支读 localStorage;⑧多行粘贴尾随换行多建一个
空段落 → split 后丢弃末尾空项。
### 本轮新功能面(Git 同步)复审补强
- validate_branch 补:尾随 `.`、开头 `/`、连续 `//`、`@{`;
validate_remote_url 补 ext::/fd:: 可执行 transport 黑名单
(fetch 时会执行命令,用户自填场景威胁弱,属防患)。两函数已
抽出在沙箱实跑断言(合法/非法各一组,全过)。
- git_init_repo:.gitignore 播种提前到绑远程之前,部分失败不再
留「有仓库无忽略规则」的半成品;已有 origin 改 set-url 不再
报错。默认 .gitignore 补 `.*.tmp-*`:原子写临时文件在崩溃
残留时原本会被 `git add -A` 提交推送。
- git_sync_now 游离 HEAD 时不再猜分支名 "main"(会把成果推错
地方),直接报错提示先切回分支。
### 一致性
- FindBar 与表格工具栏的原生 title 提示迁到自绘 Tooltip(跟随
主题,与大王既定偏好对齐;路径类完整信息 title 保留);FindBar
开关按钮补 aria-pressed。
- GitSyncModal 补 Esc/点背幕关闭(忙碌时禁用),与 Stage5 弹窗
范式对齐。
- extractOutline 围栏判定改按字符+长度跟踪:四反引号围栏内的
三反引号行不再误翻转状态(与分片切分的围栏跟踪口径一致)。
### 已实测澄清(非问题)
- **分片切分会把「列表内缩进围栏」误判为顶层切点**(审计推导项):
已构造 >60K 含列表内嵌围栏的文档实测——旧逻辑下分片序列化与
整篇**确实不等**,属实。修复:围栏作切点时只认顶格;回归测试
已加入 tests/chunk-load.test.ts(关掉修复即失败,判别有效)。
- **替换文本含换行符**:实测 replaceAll('line1\nline2') 序列化为
段落内软换行,不拆段、不丢内容。语义与「粘贴多行建多段」不
同,属可接受差异,维持现状。
## 记录待办(本轮未改,附理由)
- D1(中)非 UTF-8 文件首次保存被静默转码为 UTF-8(GBK/UTF-16
文档存一次即变 UTF-8):编辑器内部统一 UTF-8 是既定设计,但
落盘编码变更应至少首次提示一次。需前端交互拍板,未擅自加。
→ **已于当晚按用户拍板实现**:saveTab 在落盘前拦截,首次保存
弹 EncodingConvertModal 说明将转 UTF-8(按路径记已确认、本次
运行只提示一次);自动保存不弹窗也不静默转码,跳过该文档等
手动保存时提示;确认后以冻结快照完成落盘,标签 encoding 如实
更新为 UTF-8。弹窗渲染置于所有弹窗之后(避免被关窗确认框
盖住)。vitest 123+2(含弹窗契约测试)。真机验证点:拿一个
GBK 文档改字后 Ctrl+S,应先弹提示、确认后才落盘。
- D2(中)专注模式跨块时全量重建装饰集:大文档每次回车重建与
顶层块数等量的装饰对象。改增量方案需重构插件,单独立项。
- D3(低)源码模式每次击键物化全文回写 App(无渲染模式的 200ms
防抖等价物):加防抖要先理清源码模式下保存读的是哪份状态,
贸然加会存旧文本,单独立项处理。
- D4(低)查找索引每次 findAll 全量重建(600K 约 5MB 瞬时):
可按 doc 引用缓存,未改。
- D5(低)专注模式开关是模块级全局变量,多窗格/隐藏解析编辑器
共享:目前靠各窗格写同一 App 值维持一致,「按窗格模式」时
需改为注入式。
- D6(低)文件树刷新时全部展开层级同时重拉(IPC burst)、拖拽
悬停整树重渲染:TreeLevel 未 memo 化,属结构性优化,单独立项。
- D7(低,Rust)git_status 串行拉起 6–7 个 git 子进程(每次约
百毫秒级):可改 `git status --porcelain=v2 --branch` 一次拿
全,解析重写风险大于当前收益,未改。
- D8(低,Rust)rename_path:仅大小写不同的改名被 exists 预检
误判冲突;跨卷移动直接报错而非复制+删除。前端入口目前多在
同盘同树内,影响有限,未改。
- D9(低,Rust)schema 无版本号/迁移机制:今后给表加列时
CREATE TABLE IF NOT EXISTS 不会迁移老库(配合 L4 的坏库重建,
最坏情况是偏好重置、正文无损)。加列前须先落 user_version。
- 沿袭:docs/28 的 B1–B4(空仓变基、git 子进程无超时、设置与
同步交错、SSH 主机密钥)与 C1(CSP=null)状态未变。
## 复核通过(无问题,不展开)
- Rust:原子写主链(同目录临时文件 + sync_all + rename,失败
清理)、编码探测顺序与 BOM 处理、sanitize_file_name 对
Windows 非法字符/保留名的处理、SQL 全部参数化、搜索四重
上限、git 同步链错误路径(变基失败先 abort 再报错)。
- 前端:useDismiss/MenuBar 监听清理完整;windowClaims 心跳
10s/过期 45s 与 Git 租约 10s 续租/25s TTL 配比合理;
exportHtml 代码块替换的降序 splice 与路径重写逻辑无误;
SourceEditor replaceAll 倒序 changes 数组经 CM6 实测正确。
- App 编排层:外部修改检测基线在打开/保存/重载各点维护完整,
不会误报自身写入;本轮另给 4 秒轮询加了防重叠(上轮未完跳过
本轮)。自动保存与手动保存并发同标签经原子写 + 幂等成立,
安全。菜单定义随击键重建属小开销,可接受。
## 验证边界
- vitest 122+2(新增:EditorPane 卸载对照、列表内嵌围栏等价、
路径/大纲新例、recoveryKey 新契约),tsc 与生产构建通过。
- Rust:沙箱仍无法跑完整 Tauri 构建(缺 GTK/WebKit 系统库);
四个改动文件已过 rustfmt 语法解析,validate_branch/
validate_remote_url 已抽出实跑断言全过;命令数与注册数对账
28=28 不变(两个命令改 async 不影响注册)。async 化、WAL、
坏库重建、搜索包含校验的实效待 Windows 构建与真机确认。
- H1 的守卫交错无法在 jsdom 自动化复现(见上),真机侧可用
「打开 >60K 文档后立刻连开多个标签挤掉其窗格,再回看该文档
是否完整」一验。
- **勘误(2026-11-14 晚,Windows 构建回报)**:本轮 M1 与 docs/27
的 Git 命令共 6 处用了 `tokio::task::spawn_blocking`,但工程并
没有 tokio 直接依赖(Cargo.toml 只有 tauri 等 7 个),Windows
构建报 E0433。已全部改为工程既有写法的
`tauri::async_runtime::spawn_blocking`(read/write 命令一直在
用),并全量核对源码中所有外部 crate 前缀与 Cargo.toml 依赖
一致、无其他幻影 crate。教训:写 Rust 时新引入的 crate 路径
必须先对 Cargo.toml 存量,沙箱无法完整编译时尤其如此。