Files
PathEditor/docs/superpowers/plans/2026-05-26-v4.1-bugfix-and-cleanup-plan.md

836 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
# 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<Vec<String>, 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<String> {
```
```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<String>) -> Result<String, String> {
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<Map<string, ValidationState>>(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<string>();
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<AppState>) => 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<AppState>((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<AppState>((set, get) => ({...}))` 改为 `create<AppState>((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<String> {
```
```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
```