# 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 存量,沙箱无法完整编译时尤其如此。