# v4.1 Bug 修复与代码清理 — 实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 修复 3 个 bug + 7 个代码质量问题 **Architecture:** 集中在 core/undo-redo.ts、store/app-store.ts、Rust commands/backup.rs、PathTable.tsx、import-export.ts。改动互不冲突,按依赖排序。 **Tech Stack:** TypeScript strict + Rust + Tauri IPC + Vitest --- ### Task 1: B1 — OpRecord 新增 indices 字段 + undo/redo 修复 **Files:** - Modify: `src/core/undo-redo.ts:13-20, 56-60, 97-101` - Modify: `tests/unit/undo-redo.test.ts` (新增测试) - [ ] **Step 1: 更新 OpRecord 接口和 undo/redo 逻辑** ```typescript // src/core/undo-redo.ts — 修改 OpRecord 接口(第 13-20 行替换) export interface OpRecord { type: OperationType; target: TargetType; index: number; count: number; oldPaths: string[]; newPaths: string[]; /** DELETE 操作专用:被删除的各路径的原始 index(已排序) */ indices?: number[]; } ``` - [ ] **Step 2: 更新 DELETE 的 undo 逻辑** ```typescript // src/core/undo-redo.ts — 替换第 56-60 行 case OperationType.DELETE: if (rec.indices) { // 精确恢复到原始位置 for (let i = 0; i < rec.indices.length; i++) { target.splice(rec.indices[i], 0, rec.oldPaths[i]); } } else { for (let i = 0; i < rec.count; i++) { target.splice(rec.index + i, 0, rec.oldPaths[i]); } } break; ``` - [ ] **Step 3: 更新 DELETE 的 redo 逻辑** ```typescript // src/core/undo-redo.ts — 替换第 97-101 行 case OperationType.DELETE: if (rec.indices) { for (let i = rec.indices.length - 1; i >= 0; i--) { target.splice(rec.indices[i], 1); } } else { for (let i = rec.count - 1; i >= 0; i--) { target.splice(rec.index + i, 1); } } break; ``` - [ ] **Step 4: 新增非连续删除 undo/redo 测试** ```typescript // tests/unit/undo-redo.test.ts — 在最后一个 it() 之后、闭合 }); 之前插入 it('非连续多选 DELETE 撤销恢复到原始位置', () => { const old = [...sys, 'C:\\Extra1', 'C:\\Extra2']; sys = old; // 删除 indices [1, 3](C:\Program Files 和 C:\Extra2) const removed = [sys[1], sys[3]]; mgr.push(makeRecord(OperationType.DELETE, TargetType.SYSTEM, 1, 2, removed, [])); sys.splice(3, 1); sys.splice(1, 1); const u = mgr.undo(sys, user)!; expect(u[0]).toEqual(old); const r = mgr.redo(...u)!; expect(r[0]).toEqual(['C:\\Windows', 'C:\\Extra1']); }); ``` - [ ] **Step 5: 运行测试确认通过** ```bash npx vitest run tests/unit/undo-redo.test.ts ``` - [ ] **Step 6: Commit** ```bash git add src/core/undo-redo.ts tests/unit/undo-redo.test.ts git commit -m "fix: 非连续删除 undo 恢复到错误位置 — OpRecord 新增 indices 精确记录原始位置" ``` --- ### Task 2: B1 — app-store deletePaths 传入 indices **Files:** - Modify: `src/store/app-store.ts:104-123` - Modify: `tests/unit/app-store.test.ts` (新增测试) - [ ] **Step 1: deletePaths 传入 sorted indices** ```typescript // src/store/app-store.ts — 替换第 104-123 行 deletePaths: (indices, target) => { if (indices.length === 0) return; const state = get(); const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths; const sorted = [...indices].sort((a, b) => b - a); const sortedAsc = [...indices].sort((a, b) => a - b); const oldPaths = sortedAsc.map((i) => list[i]); state.undoRedo.push({ type: OperationType.DELETE, target, index: sortedAsc[0], count: sortedAsc.length, oldPaths, newPaths: [], indices: sortedAsc, }); const toRemove = new Set(sorted); const newList = list.filter((_, i) => !toRemove.has(i)); if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [] }); else set({ userPaths: newList, selectedIndices: [] }); get()._markDirty(); }, ``` - [ ] **Step 2: 新增非连续多选删除测试** ```typescript // tests/unit/app-store.test.ts — 在 "deletePaths 多选删除" 测试后插入 it('deletePaths 非连续多选删除后可 undo 恢复到正确位置', () => { const store = useAppStore.getState(); store.addPath('A', TargetType.SYSTEM); store.addPath('B', TargetType.SYSTEM); store.addPath('C', TargetType.SYSTEM); store.addPath('D', TargetType.SYSTEM); store.deletePaths([1, 3], TargetType.SYSTEM); expect(useAppStore.getState().sysPaths).toEqual(['A', 'C']); // undo 应恢复到原始顺序 useAppStore.getState().undo(); expect(useAppStore.getState().sysPaths).toEqual(['A', 'B', 'C', 'D']); }); ``` - [ ] **Step 3: 运行测试** ```bash npx vitest run tests/unit/app-store.test.ts tests/unit/undo-redo.test.ts ``` - [ ] **Step 4: Commit** ```bash git add src/store/app-store.ts tests/unit/app-store.test.ts git commit -m "fix: deletePaths 传入 indices 数组以支持非连续多选删除的精确 undo" ``` --- ### Task 3: B2 — Rust 端 backup_registry 内部读取注册表 **Files:** - Modify: `src-tauri/src/commands/registry.rs:8, 21, 66-80` (改可见性) - Modify: `src-tauri/src/commands/backup.rs:22-56` (重写函数签名) - [ ] **Step 1: 将 split_path / join_path 改为 pub(crate)** ```rust // src-tauri/src/commands/registry.rs — 第 8 行,fn load_paths 也改为 pub(crate) pub(crate) fn load_paths(root: winreg::HKEY, sub_path: &str, label: &str) -> Result, String> { ``` ```rust // src-tauri/src/commands/registry.rs — 第 21 行 pub(crate) fn save_paths(root: winreg::HKEY, sub_path: &str, label: &str, paths: &[String]) -> Result<(), String> { ``` ```rust // src-tauri/src/commands/registry.rs — 第 66 行 pub(crate) fn split_path(raw: &str) -> Vec { ``` ```rust // src-tauri/src/commands/registry.rs — 第 73 行 pub(crate) fn join_path(paths: &[String]) -> String { ``` - [ ] **Step 2: 重写 backup_registry** ```rust // src-tauri/src/commands/backup.rs — 替换整个 backup_registry 函数(第 22-56 行) /// 备份当前注册表中的系统 PATH 和用户 PATH /// 在保存前调用,备份的是注册表中的当前值(保存前的状态) #[tauri::command] pub fn backup_registry(custom_dir: Option) -> Result { use crate::commands::registry; use winreg::enums::*; let backup_dir = match custom_dir { Some(ref dir) if !dir.is_empty() => std::path::PathBuf::from(dir), _ => backup_base_dir(), }; std::fs::create_dir_all(&backup_dir) .map_err(|e| format!("无法创建备份目录: {}", e))?; // 读取当前注册表中的值 let sys_paths = registry::load_paths( HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", "系统", )?; let user_paths = registry::load_paths( HKEY_CURRENT_USER, "Environment", "用户", )?; let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S_%3f"); let filename = format!("path_backup_{}.txt", timestamp); let filepath = backup_dir.join(&filename); let mut content = String::new(); content.push_str(&format!( "PathEditor Backup - {}\n", chrono::Local::now().format("%Y-%m-%d %H:%M:%S") )); content.push_str("\n[System PATH]\n"); for path in &sys_paths { content.push_str(&format!("{}\n", path)); } content.push_str("\n[User PATH]\n"); for path in &user_paths { content.push_str(&format!("{}\n", path)); } std::fs::write(&filepath, &content) .map_err(|e| format!("无法写入备份文件: {}", e))?; let result = filepath.to_string_lossy().to_string(); log::info!("备份已保存到: {}", result); Ok(result) } ``` - [ ] **Step 3: 编译检查** ```bash cd src-tauri && cargo check ``` - [ ] **Step 4: Commit** ```bash git add src-tauri/src/commands/registry.rs src-tauri/src/commands/backup.rs git commit -m "fix: backup_registry 改为内部读取注册表当前值,不再依赖前端传入数据" ``` --- ### Task 4: B2 — 更新前端 savePaths 和 lib.rs **Files:** - Modify: `src-tauri/src/lib.rs:27` (移除旧的参数) - Modify: `src/store/app-store.ts:261-263` (简化调用) - Modify: `tests/unit/app-store.test.ts:244-280` (更新 mock) - [ ] **Step 1: lib.rs 无需修改(命令签名更新后自动适配)** lib.rs 中 `commands::backup::backup_registry` 注册已存在,函数签名变更后自动适配。 - [ ] **Step 2: 简化前端 savePaths 中的 backup 调用** ```typescript // src/store/app-store.ts — 替换第 261-263 行 // 备份当前注册表(保存前备份旧值,失败仅警告不中断) invoke('backup_registry', { customDir: null }) .catch(() => set({ statusMessage: i18n.t('status.warning_backup') })); ``` - [ ] **Step 3: 更新 app-store 测试中的 mock** ```typescript // tests/unit/app-store.test.ts — 修改 "保存成功" 测试中的 mock(第 244-251 行) it('保存成功', async () => { mockedInvoke.mockResolvedValue(undefined); await useAppStore.getState().savePaths(); const s = useAppStore.getState(); expect(s.isSaving).toBe(false); expect(s.isModified).toBe(false); expect(s.statusMessage).toBe('保存成功'); }); ``` ```typescript // tests/unit/app-store.test.ts — 修改 "部分失败" 测试中的 mock(第 253-262 行) it('部分失败时报告具体 hive', async () => { mockedInvoke .mockResolvedValueOnce(undefined) // backup_registry(现在无参数) .mockResolvedValueOnce(undefined) // save_system_paths .mockRejectedValueOnce('权限不足'); // save_user_paths await useAppStore.getState().savePaths(); const s = useAppStore.getState(); expect(s.isSaving).toBe(false); expect(s.statusMessage).toContain('用户 PATH 保存失败'); }); ``` ```typescript // tests/unit/app-store.test.ts — 修改 "isSaving 守卫" 测试中的 mock(第 264-280 行) it('isSaving 守卫:并发第二次调用直接返回', async () => { let resolveAll: (v: unknown) => void; const pending = new Promise((r) => { resolveAll = r; }); mockedInvoke.mockReturnValue(pending as any); const p1 = useAppStore.getState().savePaths(); const r2 = useAppStore.getState().savePaths(); await expect(r2).resolves.toBeUndefined(); resolveAll!(undefined); await p1; }); ``` - [ ] **Step 4: 运行测试** ```bash npx vitest run tests/unit/app-store.test.ts ``` - [ ] **Step 5: Commit** ```bash git add src/store/app-store.ts tests/unit/app-store.test.ts git commit -m "fix: 前端 backup 调用不再传递 paths,由 Rust 端自行读取注册表" ``` --- ### Task 5: B3 — 验证异常返回"未知"而非"有效" **Files:** - Modify: `src/components/path-list/PathTable.tsx:26-27, 59-61, 119-120` - [ ] **Step 1: 改缓存类型和异常处理** ```typescript // src/components/path-list/PathTable.tsx — 替换第 26-27 行 type ValidationState = 'valid' | 'invalid' | 'unknown'; const [validationCache, setValidationCache] = useState>(new Map()); ``` ```typescript // src/components/path-list/PathTable.tsx — 替换第 54-62 行 const batch = toValidate.slice(0, 20); Promise.all( batch.map(async (p): Promise<[string, ValidationState]> => { try { if (p.includes('%')) return [p, 'valid']; const valid: boolean = await invoke('validate_path', { path: p }); return [p, valid ? 'valid' : 'invalid']; } catch { return [p, 'unknown']; } }), ``` - [ ] **Step 2: 更新 UI 渲染逻辑** ```typescript // src/components/path-list/PathTable.tsx — 替换第 112-125 行的 validations useMemo const validations = useMemo(() => { const seen = new Set(); return filtered.map(({ path }) => { const lower = path.toLowerCase(); const isDuplicate = seen.has(lower); seen.add(lower); return { state: validationCache.get(path) ?? 'valid' as ValidationState, isDuplicate, isEnvVar: path.includes('%'), }; }); }, [filtered, validationCache]); ``` ```typescript // src/components/path-list/PathTable.tsx — 替换第 170-173 行的颜色逻辑 const v = validations[rowIdx]; const isSelected = selectedIndices.includes(index); let textColor = 'var(--app-fg)'; if (v.state === 'invalid') textColor = '#dc3545'; else if (v.isDuplicate) textColor = '#fd7e14'; else if (v.state === 'unknown') textColor = 'var(--app-fg)'; ``` - [ ] **Step 3: 类型检查** ```bash npx tsc --noEmit ``` - [ ] **Step 4: Commit** ```bash git add src/components/path-list/PathTable.tsx git commit -m "fix: 验证 IPC 异常时返回 unknown 状态,不再错误标记为有效路径" ``` --- ### Task 6: C1 — 删除 AppError 死代码 **Files:** - Delete: `src-tauri/src/error.rs` - Modify: `src-tauri/src/lib.rs:2` - [ ] **Step 1: 从 lib.rs 移除 mod error** ```rust // src-tauri/src/lib.rs — 删除第 2 行,第 1 行保留 mod commands; ``` - [ ] **Step 2: 删除 error.rs 文件** ```bash rm src-tauri/src/error.rs ``` - [ ] **Step 3: 编译检查** ```bash cd src-tauri && cargo check ``` - [ ] **Step 4: Commit** ```bash git add src-tauri/src/lib.rs src-tauri/src/error.rs git commit -m "refactor: 删除未使用的 AppError 死代码" ``` --- ### Task 7: C2 — importPaths 重命名为 replacePaths **Files:** - Modify: `src/store/app-store.ts:37, 171-183` - Modify: `src/hooks/use-app-actions.ts:95, 97, 162, 163` - Modify: `tests/unit/app-store.test.ts:131-137` - [ ] **Step 1: app-store.ts 重命名** ```typescript // src/store/app-store.ts — 第 37 行 replacePaths: (target: TargetType, newPaths: string[]) => void; ``` ```typescript // src/store/app-store.ts — 替换第 171-183 行 replacePaths: (target, newPaths) => { if (newPaths.length === 0) return; const state = get(); const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths; state.undoRedo.push({ type: OperationType.IMPORT, target, index: 0, count: newPaths.length, oldPaths: [...list], newPaths: [...newPaths], }); if (target === TargetType.SYSTEM) set({ sysPaths: [...newPaths], selectedIndices: [] }); else set({ userPaths: [...newPaths], selectedIndices: [] }); get()._markDirty(); }, ``` - [ ] **Step 2: use-app-actions.ts 更新所有调用** ```typescript // src/hooks/use-app-actions.ts — 替换第 95 行 useAppStore.getState().replacePaths(TargetType.SYSTEM, result.system); ``` ```typescript // src/hooks/use-app-actions.ts — 替换第 97 行 useAppStore.getState().replacePaths(TargetType.USER, result.user); ``` ```typescript // src/hooks/use-app-actions.ts — 替换第 162 行 if (flat.system.length > 0) useAppStore.getState().replacePaths(TargetType.SYSTEM, flat.system); ``` ```typescript // src/hooks/use-app-actions.ts — 替换第 163 行 if (flat.user.length > 0) useAppStore.getState().replacePaths(TargetType.USER, flat.user); ``` - [ ] **Step 3: 更新测试** ```typescript // tests/unit/app-store.test.ts — 替换第 131-137 行 it('replacePaths 整体替换列表', () => { const store = useAppStore.getState(); store.addPath('old1', TargetType.USER); store.addPath('old2', TargetType.USER); store.replacePaths(TargetType.USER, ['new1', 'new2', 'new3']); expect(useAppStore.getState().userPaths).toEqual(['new1', 'new2', 'new3']); }); ``` - [ ] **Step 4: 类型检查和测试** ```bash npx tsc --noEmit && npx vitest run ``` - [ ] **Step 5: Commit** ```bash git add src/store/app-store.ts src/hooks/use-app-actions.ts tests/unit/app-store.test.ts git commit -m "refactor: importPaths 重命名为 replacePaths,反映全量替换语义" ``` --- ### Task 8: C3 — detectExportFormat 修正 **Files:** - Modify: `src/core/import-export.ts:5, 13-16` - Modify: `tests/unit/import-export.test.ts:115-124` - [ ] **Step 1: 改类型和函数** ```typescript // src/core/import-export.ts — 第 5 行 export type ExportFormat = 'json' | 'csv' | 'txt'; ``` ```typescript // src/core/import-export.ts — 替换第 13-16 行 export function detectExportFormat(filepath: string): ExportFormat { const lower = filepath.toLowerCase(); if (lower.endsWith('.csv')) return 'csv'; if (lower.endsWith('.txt')) return 'txt'; return 'json'; } ``` - [ ] **Step 2: 更新测试** ```typescript // tests/unit/import-export.test.ts — 替换第 115-124 行 describe('detectExportFormat', () => { it('.csv 检测为 CSV', () => { expect(detectExportFormat('data.CSV')).toBe('csv'); }); it('.txt 检测为 TXT', () => { expect(detectExportFormat('data.txt')).toBe('txt'); }); it('其他扩展名检测为 JSON', () => { expect(detectExportFormat('data.json')).toBe('json'); }); }); ``` - [ ] **Step 3: 运行测试** ```bash npx vitest run tests/unit/import-export.test.ts ``` - [ ] **Step 4: Commit** ```bash git add src/core/import-export.ts tests/unit/import-export.test.ts git commit -m "fix: detectExportFormat 对 .txt 返回 'txt' 而非 'json'" ``` --- ### Task 9: C4 — _markDirty 私有化 **Files:** - Modify: `src/store/app-store.ts:47, 223-226` (移除接口定义,改为闭包私有函数) - Modify: `tests/unit/app-store.test.ts:188-208` (删除 _markDirty 测试小节) - [ ] **Step 1: 从接口移除 _markDirty,改为闭包内私有函数** ```typescript // src/store/app-store.ts — 删除第 47 行 // (从 AppState 接口中删除 _markDirty: () => void; 这一行) ``` ```typescript // src/store/app-store.ts — 在 create() 之前插入模块级私有函数 function arraysEqual(a: readonly string[], b: readonly string[]): boolean { return a.length === b.length && a.every((v, i) => v === b[i]); } ``` ```typescript // src/store/app-store.ts — 删除第 223-226 行的 _markDirty 实现,替换为 create() 外部的私有函数 // 在 create() 调用之前插入(arraysEqual 后面): const _markDirty = (get: () => AppState, set: (partial: Partial) => void) => { const { _savedSys, _savedUser, sysPaths, userPaths } = get(); set({ isModified: !(arraysEqual(sysPaths, _savedSys) && arraysEqual(userPaths, _savedUser)) }); }; ``` ```typescript // src/store/app-store.ts — 替换所有 get()._markDirty() 调用为 _markDirty(get, set) // 例如第 85 行: _markDirty(get, set); // 例如第 101 行: _markDirty(get, set); // 等等(共 8 处) ``` 等等,这个改法会让每个 CRUD 方法的参数变复杂。更简洁的做法是用闭包捕获: 更好的做法:把 `_markDirty` 放在 `create()` 内部、`return` 之前,作为一个局部函数,所有 CRUD 方法通过闭包访问它。 ```typescript // src/store/app-store.ts — 整体结构变为: export const useAppStore = create((set, get) => { // 私有函数,不暴露到 store 接口 const markDirty = () => { const { _savedSys, _savedUser, sysPaths, userPaths } = get(); set({ isModified: !(arraysEqual(sysPaths, _savedSys) && arraysEqual(userPaths, _savedUser)) }); }; return { // ... 所有状态和方法,内部调用 markDirty() 而非 get()._markDirty() }; }); ``` - [ ] **Step 2: 重写 app-store.ts 为闭包私有 markDirty** 完整改动涉及将 `create((set, get) => ({...}))` 改为 `create((set, get) => { const markDirty = ...; return {...}; })`。 具体:移除 `AppState` 接口中的 `_markDirty`,删除第 223-226 行的实现,在 create 回调函数体顶部定义 `markDirty` 局部函数,将所有 8 处 `get()._markDirty()` 替换为 `markDirty()`。 - [ ] **Step 3: 删除测试中的 _markDirty 小节** ```typescript // tests/unit/app-store.test.ts — 删除第 188-208 行(整个 describe('_markDirty', ...) 块) ``` `_markDirty` 的行为通过 CRUD 测试中的 `isModified` 断言间接覆盖。 - [ ] **Step 4: 类型检查和测试** ```bash npx tsc --noEmit && npx vitest run ``` - [ ] **Step 5: Commit** ```bash git add src/store/app-store.ts tests/unit/app-store.test.ts git commit -m "refactor: _markDirty 改为 store 闭包内私有函数,不暴露到公共接口" ``` --- ### Task 10: C5 — PATH 长度阈值统一 **Files:** - Modify: `src/config/default.json:15-17` - [ ] **Step 1: 更新阈值** ```json // src/config/default.json — 替换第 15-17 行 "maxSystemLength": 32767, "maxUserLength": 32767, "maxCombinedLength": 32767 ``` - [ ] **Step 2: Commit** ```bash git add src/config/default.json git commit -m "fix: 前端 PATH 长度阈值与 Rust 端统一为 32767 字符" ``` --- ### Task 11: O1 — BOM 只在首行检查 **Files:** - Modify: `src/core/import-export.ts:68-74, 177-179` - [ ] **Step 1: importFromCsv 首行 BOM 处理** ```typescript // src/core/import-export.ts — 替换第 62-97 行(整个 importFromCsv 函数) export function importFromCsv(content: string): ImportResult { const result: ImportResult = { system: [], user: [] }; const lines = content.split(/\r?\n/); let hasHeader = false; for (let i = 0; i < lines.length; i++) { let line = lines[i]; // BOM 仅出现在第一行 if (i === 0 && line.startsWith('')) { line = line.slice(1); } if (line.trim() === '') continue; const fields = parseCsvLine(line); if (fields.length < 2) continue; if (!hasHeader && isHeaderRow(fields[0], fields[1])) { hasHeader = true; continue; } const type = fields[0].trim().toLowerCase(); const path = fields[1].trim(); if (path.length === 0) continue; if (type === 'system') { result.system.push(path); } else if (type === 'user') { result.user.push(path); } } return result; } ``` - [ ] **Step 2: importFromTxt 首行 BOM 处理** ```typescript // src/core/import-export.ts — 替换第 173-188 行 export function importFromTxt(content: string): string[] { const paths: string[] = []; const lines = content.split(/\r?\n/); for (let i = 0; i < lines.length; i++) { let line = lines[i]; if (i === 0 && line.startsWith('')) { line = line.slice(1); } const trimmed = line.trim(); if (trimmed.length === 0 || trimmed.startsWith('#')) continue; paths.push(trimmed); } return paths; } ``` - [ ] **Step 3: 运行测试** ```bash npx vitest run tests/unit/import-export.test.ts ``` - [ ] **Step 4: Commit** ```bash git add src/core/import-export.ts git commit -m "perf: BOM 检查从每行移到仅首行" ``` --- ### Task 12: O2 — split_path 同步注释 **Files:** - Modify: `src-tauri/src/commands/registry.rs:66` - Modify: `src/core/validation.ts:30` - [ ] **Step 1: 两边加注释** ```rust // src-tauri/src/commands/registry.rs — 在 split_path 函数上方加一行 /// 将分号分隔的 PATH 字符串拆分为数组。 /// 注意:TS 端 src/core/validation.ts 有相同逻辑的 split_path,修改时需同步两端。 pub(crate) fn split_path(raw: &str) -> Vec { ``` ```typescript // src/core/validation.ts — 在 split_path 函数上方加一行 /** 分割 PATH 字符串。 * 注意:Rust 端 src-tauri/src/commands/registry.rs 有相同逻辑的 split_path,修改时需同步两端。 */ export function split_path(raw: string): string[] { ``` - [ ] **Step 2: Commit** ```bash git add src-tauri/src/commands/registry.rs src/core/validation.ts git commit -m "docs: split_path 添加同步提醒注释(Rust + TS 双端实现)" ``` --- ## 执行说明 按 Task 1→12 顺序执行,每个 Task 内 Step 按序执行。Task 之间互有依赖(app-store.ts 被 Task 2、4、7、9 修改),顺序不能乱。 全部完成后运行完整测试: ```bash npx vitest run && cd src-tauri && cargo test && cargo clippy -- -D warnings ```