|
| 1 | +# v0.15 笔记体验打磨设计规格(标题切换 / 全站自适应 / 折叠 / 图片落盘 / 剪贴板导入 / 换行一致) |
| 2 | + |
| 3 | +> 状态:✅ 已批准(2026-08-30,6 节逐节确认) |
| 4 | +> 所属版本:v0.15 |
| 5 | +> 范围:v0.14 交付后用户反馈的 6 项笔记相关改进(4 项缺陷 / 2 项体验增强) |
| 6 | +
|
| 7 | +## 1. 背景与目标(痛点 → 根因) |
| 8 | + |
| 9 | +v0.14(编辑器 CM6 / 视觉系统 / 知识体系 / 采集质量)交付后,用户实测反馈 6 项: |
| 10 | + |
| 11 | +| # | 反馈 | 现状根因(代码定位) | |
| 12 | +|---|------|---------------------| |
| 13 | +| 1 | 加 H1 后再点按钮"无取消" | `headingCommand.ts:50` 与 `markdownEdit.ts:82` 对已是标题的行**跳过**(纯 no-op);按钮有光标行级别高亮态(`RichEditorView.tsx:167`)暗示可切换但点了没反应 | |
| 14 | +| 2 | 组件大小不能自适应 | 全站固定宽列硬编码:笔记页 240/320/180(`GroupSidebar.tsx:270`、`NoteListView.tsx:100`、`NoteReadingView.tsx:93`)、会话页 320(`SessionListPanel.tsx:299`)、知识页 260/320(`KnowledgePage.tsx:220`、`KnowledgeDetailPanel.tsx:132`)、课堂页 320(`ClassroomPage.tsx:376`);默认窗口仅 960×720(`tauri.conf.json`)——正文区被挤压;`NoteImage.tsx:86` 固定 240×160 CSS 缩略(原 TODO(v0.12.0)) | |
| 15 | +| 3 | 笔记组、笔记不可收起 | 组侧栏已按 kind 分区折叠(`GroupSidebar.tsx:375`,记忆 localStorage);但中部列表平铺(v0.12.2 决策 1:组行只过滤不展开),组/列表整列无折叠 | |
| 16 | +| 4 | 用户插入的图片需复制一份 | 🖼 文件选择已复制(`import_note_image`,`commands_note_images.rs:79` ✅);但 🌐 链接图只存外链 URL(源站删除即失效)、剪贴板粘贴(待 #5)未落盘、手写/粘贴的绝对路径引用既不显示(`resolve_note_image_path` 只认 4 种前缀)也无副本 | |
| 17 | +| 5 | 编辑模式不支持剪贴板导入图片 | 编辑器(CM 与 textarea 降级)**均无 paste 处理**;快速记录已有剪贴板捕获先例(`GroupSidebar.tsx:147` + `capture_fragment` 的 `image_b64`) | |
| 18 | +| 6 | 编辑态换行、阅读态连上 | CommonMark 软换行(单个 `\n`)= 空格;编辑态按行展示,阅读态 `NoteMarkdown`(react-markdown + remark)把单 `\n` 渲染为空格 → 语义不一致 | |
| 19 | + |
| 20 | +**目标**:编辑与阅读所见一致;图片资源零丢失;笔记页与全站布局随窗口自适应;笔记组/笔记可收起。 |
| 21 | + |
| 22 | +## 2. 方案选型记录(逐条裁决) |
| 23 | + |
| 24 | +### 2.1 H1 切换语义(#1) |
| 25 | + |
| 26 | +| 方案 | 结论 | |
| 27 | +|---|---| |
| 28 | +| **A. set-or-toggle 切换语义(采纳)** | 逐行:行已精确等于所点级别 → 剥掉 `#×level+空格` 还原普通段;其他情况 → 置为该级别。多行选区逐行独立判定。CM 与 textarea 双路径同步修改,配套单测改造 | |
| 29 | +| B. 保留跳过 + 独立"清除格式"按钮 | 按钮更多,发现成本高,否决 | |
| 30 | +| C. 仅保留升/降级(Ctrl+Shift+↑↓) | 与用户"点 H1 取消 H1"预期不符,否决 | |
| 31 | + |
| 32 | +边界:纯空白行保持现有行为(加标记——不具有"取消"语义);光标落位逻辑不变(末行标记后)。 |
| 33 | + |
| 34 | +### 2.2 全站自适应(#2)——用户裁决:**全部页面** |
| 35 | + |
| 36 | +统一策略:**所有固定宽列改为"可拖拽 + 宽度记忆 + 最小/最大约束 + 窄窗自动折叠"**;组件内部自适应(工具栏换行、表格横向滚动已有;图片改为自适应尺寸)。 |
| 37 | + |
| 38 | +### 2.3 收起(#3)——用户裁决:**分组树 + 整列折叠** |
| 39 | + |
| 40 | +中部列表改分组树(组头可收起,记忆);左栏/中栏/右栏大纲全部支持整列折叠(窄条形态);组头"点击名字=过滤"的 v0.12.2 决策 1 **保持不变**(chevron 才有折叠动作)。 |
| 41 | + |
| 42 | +### 2.4 图片落盘(#4)——用户裁决:**每个入口保证复制;不做巡检** |
| 43 | + |
| 44 | +- 🖼 本地文件:已有(`import_note_image`),保持 |
| 45 | +- 剪贴板粘贴(与 #5 同条线):落盘 |
| 46 | +- 🌐 链接图:**自动下载复制**(推荐,用户确认);失败降级插原 URL + 提示 |
| 47 | +- 手写/粘贴绝对路径引用:**不做巡检/不改写**——登记为已知边界(用户裁决:入口全落盘后巡检不再必要;将来若观察到高频粘贴本地路径 Markdown,再在 paste 钩子转换) |
| 48 | +- 删除笔记:顺带清理 `notes-images/{nid}/` 目录(防孤立残留,用户确认;失败不阻断删除) |
| 49 | + |
| 50 | +### 2.5 剪贴板导入(#5) |
| 51 | + |
| 52 | +CM 侧 `EditorView.domEventHandlers.paste` 读 `event.clipboardData`(同步、无权限弹窗);textarea 降级路径 `onPaste` 同款(共享 util)。后端新增 `import_note_image_b64`(base64 IPC,同 `capture_fragment` 先例)。 |
| 53 | + |
| 54 | +### 2.6 换行一致(#6)——用户裁决:**单换行=硬换行渲染 `<br>`** |
| 55 | + |
| 56 | +阅读渲染启用 remark-breaks;零源数据变更(存量内容自动修复);两空格/`\` 强断行语义保留;不加设置项(YAGNI,将来需要再提)。 |
| 57 | + |
| 58 | +## 3. 架构设计 |
| 59 | + |
| 60 | +### 3.1 组件/模块层次 |
| 61 | + |
| 62 | +``` |
| 63 | +NotesPage(编排层:列状态 useColumnWidth + 折叠态) |
| 64 | + ├── GroupSidebar(受控 width/folded) |
| 65 | + ├── NoteListView(分组树版:NoteTreeSection 组头行 + 平铺行) |
| 66 | + ├── NoteReadingView(大纲列并入统一列折叠体系) |
| 67 | + ├── RichEditorView(+ paste 处理 + 🌐 链接图下载导入) |
| 68 | + │ └── NoteEditView(降级:+ paste 处理) |
| 69 | + ├── useClipboardImagePaste(共享 util/hook) |
| 70 | + ├── useColumnWidth + ColumnResizer(全站通用布局装备) |
| 71 | + └── NoteMarkdown(+ remark-breaks) |
| 72 | +SessionsPage / KnowledgePage / ClassroomPage(列迁移,复用同一装备) |
| 73 | +Rust:commands_note_images.rs(+ b64/url 命令 + 删除清理;提取 save_image_bytes) |
| 74 | +``` |
| 75 | + |
| 76 | +### 3.2 新增/修改文件清单 |
| 77 | + |
| 78 | +| 文件 | 变更 | 行数预算 | |
| 79 | +|---|---|---| |
| 80 | +| `hooks/useColumnWidth.ts` | 新:列宽状态 + localStorage 记忆 + 拖拽计算 | ≤120 | |
| 81 | +| `components/ColumnResizer.tsx` | 新:拖拽手柄(方向 L/R) | ≤80 | |
| 82 | +| `components/NoteTreeSection.tsx` | 新:分组树组头行(chevron/计数/色条/过滤) | ≤120 | |
| 83 | +| `hooks/useClipboardImagePaste.ts` | 新:剪贴板图片提取(ClipboardEvent → {blob,mime} + base64) | ≤100 | |
| 84 | +| `components/NoteListView.tsx` | 改:平铺 → 分组树(search/tag/sort 激活时退化平铺) | ≤300(超限拆 NoteTreeSection 后应可控) | |
| 85 | +| `components/NoteReadingView.tsx` | 改:大纲列接入统一折叠(移除独立 outlineCollapsed) | ≤280 | |
| 86 | +| `components/RichEditorView.tsx` | 改:paste 处理 + 链接图命令切换 + 工具栏文案 | ≤280 | |
| 87 | +| `components/NoteEditView.tsx` | 改:onPaste 处理 | ≤300 | |
| 88 | +| `components/NoteMarkdown.tsx` | 改:+ remarkBreaks | ≤250 | |
| 89 | +| `components/NoteImage.tsx` | 改:固定 240×160 → 自适应(maxWidth 100% + maxHeight 60vh + 原始比例) | ≤100 | |
| 90 | +| `pages/NotesPage.tsx` | 改:列状态编排 + 组数据传 NoteListView + 折叠窄条 | ≤300(超限拆 ColumnHost 壳) | |
| 91 | +| `pages/SessionsPage.tsx` / `KnowledgePage.tsx` / `ClassroomPage.tsx` | 改:列迁移(原样宽度作默认值) | — | |
| 92 | +| `commands/headingCommand.ts` | 改:toggle 语义 | ≤120 | |
| 93 | +| `components/markdownEdit.ts` | 改:headingLines toggle 语义 | ≤150 | |
| 94 | +| `app/src-tauri/src/commands_note_images.rs` | 改:提取 save_image_bytes + import_note_image_b64/url + 删除清理 | ≤260 | |
| 95 | +| `app/src-tauri/src/db_notes.rs`(或命令层) | 改:delete_note 后清理图片目录 | ±15 | |
| 96 | + |
| 97 | +### 3.3 依赖变更 |
| 98 | + |
| 99 | +``` |
| 100 | +前端 + remark-breaks@^4(unified 11 系,与 react-markdown 10 / remark-gfm 4 兼容) |
| 101 | +Rust 零新依赖(下载走已有 ureq;校验走已有 image crate) |
| 102 | +``` |
| 103 | + |
| 104 | +## 4. 详细设计 |
| 105 | + |
| 106 | +### 4.1 标题切换(#1) |
| 107 | + |
| 108 | +**纯逻辑**(`computeHeadingChanges` / `headingLines` 同步改,规则一致): |
| 109 | + |
| 110 | +``` |
| 111 | +对每行: |
| 112 | + m = 行首 /^#{1,6}\s/ 匹配 |
| 113 | + 行已等于目标级别(m[0].len === level)→ 变更:删 [行首, 行首+level+1)(剥 `#×level+空格`) |
| 114 | + 其他(其他级别标题或普通段)→ 变更:行首插 `#×level+空格` |
| 115 | + —— 其他级别标题先剥后置(先删原标记再插新标记),净效果=替换 |
| 116 | +``` |
| 117 | + |
| 118 | +- 多行选区逐行独立判定;光标落末行标记后(不变) |
| 119 | +- `headingCommand` 的 `changes.length === 0` 卫兵保留(全行均无变化时无空 transaction) |
| 120 | +- **测试改造**:`headingCommand.test.ts` 原"已是标题的行跳过不叠加"用例改为"同级别行剥除标记/不同级别行置为该级别";`headingLines` 对应用例同步;补 H1→普通段、H2 行点 H1、普通行点 H1 三类 golden |
| 121 | +- 高亮闭环:toggle 后光标行级别变化 → `onHeadingLevelChange` 更新 → 按钮高亮同步消失 |
| 122 | + |
| 123 | +### 4.2 全站自适应(#2) |
| 124 | + |
| 125 | +**布局装备**(三件套,全站复用): |
| 126 | + |
| 127 | +1. `useColumnWidth(key, { default, min, max })`:state(像素)+ `useEffect` 持久化 `layout:col-width:{key}`;暴露 `width/setWidth/reset` |
| 128 | +2. `ColumnResizer`:4px 宽手柄,`onPointerDown` → `setPointerCapture` + 移动回调(方向感知:左列拖右边界 → width 增;右列拖左边界 → 负向);拖拽中 `user-select: none`;最小/最大夹取;双击 = 恢复默认 |
| 129 | +3. 窄窗自动折叠:页面根容器 `ResizeObserver` + 阈值阶梯(<1100 大纲折叠;<860 组侧栏折叠;<700 中列折叠);**用户手动态优先**——窗口回宽后恢复用户手动折叠/展开选择;折叠/展开态持久化 `layout:col-fold:{key}` |
| 130 | + |
| 131 | +**列迁移清单**(默认宽度=现状值,逐列登记): |
| 132 | + |
| 133 | +| 列 | 当前 | min | max | |
| 134 | +|---|---|---|---| |
| 135 | +| GroupSidebar | 240 | 180 | 320 | |
| 136 | +| NoteListView / FeedFragmentList | 320 | 240 | 420 | |
| 137 | +| 大纲 | 180 | 140 | 260 | |
| 138 | +| SessionListPanel | 320 | 240 | 420 | |
| 139 | +| Knowledge 左 | 260 | 200 | 360 | |
| 140 | +| KnowledgeDetailPanel | 320 | 260 | 420 | |
| 141 | +| Classroom 右 | 320 | 240 | 420 | |
| 142 | + |
| 143 | +**折叠形态**:窄条 26px(图标容器,title 提示),点击展开;展开态恢复记忆宽度。折叠/窄条组件 `ColumnBar`(≤80 行,随 ColumnResizer 同文件 or 同包)。 |
| 144 | + |
| 145 | +**组件内自适应**: |
| 146 | +- `NoteImage`:`width: "auto", maxWidth: "100%", maxHeight: "60vh"`(原始比例,窄窗不溢出;点击放大保持不变) |
| 147 | +- 工具栏:现有 `flexWrap` 保留;窗口过窄时提示语("Ctrl+Shift+↑↓层级…")允许省略(`display: none` via 容器查询或保留——低优先) |
| 148 | + |
| 149 | +**编排**:`NotesPage` 持有 3 列状态(侧栏/列表/大纲)+ 折叠态,传给受控子组件;`SessionsPage`/`KnowledgePage`/`ClassroomPage` 各自接入同款(每页 1-2 列,工作量机械)。 |
| 150 | + |
| 151 | +### 4.3 分组树 + 整列折叠(#3) |
| 152 | + |
| 153 | +**分组树(NoteListView 版)**: |
| 154 | + |
| 155 | +- props 增 `groups: NoteGroup[]`(NotesPage 已有 `groups` state,直接复用) |
| 156 | +- 渲染结构:`[📁 未分组]`(`group_id == null`,可折叠)→ 各组按 `GroupSidebar` 同款 kind 排序?**否**——保持 `list_notes`/`search_notes` 返回顺序,仅分组挂载,组顺序=组 id 升序(稳定);组头渲染:chevron(`▸/▾`)+ 组色条(`resolveNoteColor` 已贯通)+ 组名 + `N` 计数 |
| 157 | +- **交互契约**(v0.12.2 决策 1 保持):组头**名字区点击 = 过滤切换**(`groupFilter` toggle);chevron `stopPropagation` 仅折叠/展开;折叠态持久化 `notes:group-fold:{gid}`(未分组区 `notes:group-fold:none`) |
| 158 | +- **退化规则**:`keyword`/`tagFilter` 激活或 `sortMode != "updated-desc"` 时 → 平铺(树上下文搜索无意义);恢复条件清除后还原树 |
| 159 | +- **批量勾选**:树态下"全选"作用于当前可见集合(与现有"只删可见子集"安全边界一致,`NoteListView.tsx:85` 逻辑保持) |
| 160 | +- 空组:**不渲染组头**(无笔记的组不进树;组信息在 GroupSidebar 管理)——避免空头占位噪音 |
| 161 | +- 组件拆分:`NoteTreeSection`(组头 + 子树容器),NoteListView 保持 ≤300 行 |
| 162 | + |
| 163 | +**整列折叠**: |
| 164 | + |
| 165 | +- GroupSidebar / NoteListView / 大纲:`NotesPage` 列状态统一驱动;折叠 = 窄条 26px + 图标(📁/📝/📑)+ 展开 |
| 166 | +- 大纲列:**移除** `NoteReadingView` 内的独立 `outlineCollapsed` 悬浮按钮体系(`NoteReadingView.tsx:48,92-147`),并入统一列折叠(行为等价:✕ 收起 → 窄条;窄条点击展开) |
| 167 | +- 与 4.2 窄窗自动折叠共用同一套 `layout:col-fold:{key}` 状态与 `ColumnBar` 组件 |
| 168 | + |
| 169 | +### 4.4 图片落盘三入口 + 清理(#4) |
| 170 | + |
| 171 | +**4.4.1 后端核心提取**(`commands_note_images.rs`): |
| 172 | + |
| 173 | +``` |
| 174 | +save_image_bytes(data_dir, note_id, bytes, ext_hint) -> Result<String, String> |
| 175 | + ① 字节数 ≤ IMAGE_MAX_BYTES(10MB) |
| 176 | + ② image::guess_format + ImageReader 解码验证(坏图拒绝;不信任 MIME/扩展名) |
| 177 | + ③ 落盘 notes-images/{nid}/{ts}.{ext}(唯一名冲突 _1/_2 复用现有逻辑) |
| 178 | + ④ 返回 "notes-images/{nid}/{name}" |
| 179 | +import_image_file 改为委托(fs 复制→读字节→save_image_bytes) |
| 180 | +``` |
| 181 | + |
| 182 | +**4.4.2 新命令 `import_note_image_b64(note_id, image_b64)`**: |
| 183 | +- base64 解码 → `save_image_bytes`(ext 以解码格式为准) |
| 184 | +- 校验:note 存在、`image_b64` 非空、解码后 ≤10MB |
| 185 | +- 传输:base64 字符串(10MB 图 ≈ 13MB JSON——与 `capture_fragment` 同款先例) |
| 186 | + |
| 187 | +**4.4.3 新命令 `import_note_image_url(note_id, url)`**: |
| 188 | +- 校验 URL → http/https;`ureq` GET(timeout 10s、跟随重定向 ≤3、限流 10MB) |
| 189 | +- Content-Type 白名单(image/*)或按 `image::guess_format` 兜底 |
| 190 | +- `save_image_bytes` 落盘 → 返回相对引用 |
| 191 | +- 失败 → `Err`(前端降级:插原 URL + status 提示"外链图下载失败,原链接已插入") |
| 192 | + |
| 193 | +**4.4.4 前端接线**: |
| 194 | +- 🌐 链接图:`prompt` 后先 invoke `import_note_image_url`,成功插相对引用;失败降级插原 URL(status 提示) |
| 195 | +- 剪贴板(4.4.5/4.5):粘贴 → `import_note_image_b64` |
| 196 | +- 工具栏文案:`🖼 图片`(本地)"/ 🌐 链接图(下载为本地)" |
| 197 | + |
| 198 | +**4.4.5 删除清理**:`delete_note` 命令/Rust 层在 DB 删除成功后 `remove_dir_all(data_dir/notes-images/{id}/)`,失败 eprintln 不阻断(残留无害、无用户可见错误) |
| 199 | + |
| 200 | +**已知边界(登记)**:正文中手写/粘贴的绝对路径/`data:` 引用不改写、不吸收(用户裁决:入口全落盘后巡检不需要;观察将来频度再议)。 |
| 201 | + |
| 202 | +### 4.5 剪贴板导入图片(#5) |
| 203 | + |
| 204 | +- `useClipboardImagePaste`(纯提取,两个编辑器共用): |
| 205 | + ``` |
| 206 | + extractClipboardImage(e: ClipboardEvent | React.ClipboardEvent) -> { blob, mime } | null |
| 207 | + 规则:items 中首个 image/* → blob;多图取第一张;超 10MB 前置拦截(status 提示) |
| 208 | + ``` |
| 209 | +- **CM 侧**:extensions 增 `EditorView.domEventHandlers`: |
| 210 | + ``` |
| 211 | + "paste" (e): |
| 212 | + 图 = extractClipboardImage(e) |
| 213 | + 无图 → return false(默认文本粘贴) |
| 214 | + 有图 → e.preventDefault() → blobToBase64(提取自 GroupSidebar,移入 utils)→ |
| 215 | + invoke import_note_image_b64 → 成功:dispatch insert `\n`(userEvent input.paste) |
| 216 | + 失败:status 提示(文本粘贴不补救——下一帧用户可 Ctrl+V;诚实降级) |
| 217 | + ``` |
| 218 | +- **textarea 侧**:`onPaste` 同款(复用同一 extract + import 函数;插入用 `applyEdit(insertAtCursor(...))`) |
| 219 | +- 落盘后 CM decoration 插件自动重算 → 图片即时内联显示 |
| 220 | +- 权限:`event.clipboardData` 同步读取,WebView2 paste 手势下无需 `navigator.clipboard` 权限弹窗 |
| 221 | + |
| 222 | +### 4.6 换行一致(#6) |
| 223 | + |
| 224 | +- `app/package.json` + `remark-breaks@^4` |
| 225 | +- `NoteMarkdown.tsx`:`remarkPlugins` 增 `remarkBreaks`(置于 remarkGfm/remarkMath/remarkMarkHighlight 之后) |
| 226 | +- 语义:单 `\n` 段落内 → `<br>`;`\` 断行与两空格断行保持;列表/引用内续行同样断行(符合直觉) |
| 227 | +- **存量影响**:既有笔记中的软换行从"空格"变"换行"——正是本次修复目标;**全量 vitest 走查**:`NoteReadingView.test.tsx`/`NotesPage.test.tsx`/`NoteMarkdown` 相关断言若有文本拼接断言需同步 |
| 228 | +- 不加设置项(用户确认直接生效) |
| 229 | + |
| 230 | +## 5. 验收标准 |
| 231 | + |
| 232 | +1. **H1**:光标在 `# 标题` 行点 H1 → 变普通段,按钮高亮消失;H2 行点 H1 → `#` 级;多行选区混合级别逐行正确;textarea 降级路径行为一致 |
| 233 | +2. **自适应**:各页列可拖拽、记忆重启后保持;窗口缩到阈值链 → 大纲/侧栏/列表自动折叠,回宽恢复;图片窄窗不溢出 |
| 234 | +3. **收起**:分组树组头收起/展开 + 记忆;组头点击=过滤不变;三列均可靠窄条折叠/展开 |
| 235 | +4. **图片**:剪贴板粘贴图、链接图、文件选择图全部落盘 `notes-images/{nid}/`;删源文件图不丢;删笔记清空其图片目录;坏图/超限/下载失败有提示不脏内容 |
| 236 | +5. **换行**:编辑态单换行 → 阅读态 `<br>`;强断行(`\`、两空格)不回归 |
| 237 | +6. 非功能:`cargo test`、vitest、`tsc --noEmit`、`vite build` 全绿;新增文件带 `@ai-context`;单文件 ≤300 行(超限登记豁免) |
| 238 | + |
| 239 | +## 6. 边界(本版不做) |
| 240 | + |
| 241 | +- 手写/粘贴绝对路径引用改写(登记,观察后再定) |
| 242 | +- 图片垃圾回收(孤儿图清理仅限删除笔记时;历史孤儿后续单独任务) |
| 243 | +- 设置项:硬换行开关、列宽主题化 |
| 244 | +- 服务端/移动端;无障碍深度打磨 |
| 245 | + |
| 246 | +## 7. 实施顺序(自底向上,AGENTS.md §3.6) |
| 247 | + |
| 248 | +1. **纯逻辑层**(先改先测): |
| 249 | + - `headingCommand.ts` / `markdownEdit.ts` toggle 语义 + 单测改造 |
| 250 | + - `commands_note_images.rs`:`save_image_bytes` 提取 + `import_note_image_b64/url` + 删除清理 + 单测 |
| 251 | + - `NoteMarkdown.tsx` remark-breaks + 渲染断言测试 |
| 252 | + - `useClipboardImagePaste`、`useColumnWidth`、blobToBase64 移入 utils |
| 253 | +2. **业务层**:`NoteTreeSection` 分组树 + NoteListView 改造;NotesPage 列状态编排;RichEditorView/NoteEditView paste 接线 + 链接图命令切换 |
| 254 | +3. **系统/UI 层**:IPC 注册(`lib.rs`);Sessions/Knowledge/Classroom 列迁移;NoteImage 自适应 |
| 255 | +4. **文档**:`docs/versions/v0.15.md` + spec 归档 + CHANGELOG(交付时) |
| 256 | +5. **验证**:全量测试 + 真机走查清单 |
0 commit comments