docs: 添加 v4.1 bug 修复与代码清理的设计文档和实现计划

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 22:48:07 +08:00
parent 605105da09
commit 63c8ed424b
3 changed files with 1200 additions and 0 deletions
@@ -0,0 +1,294 @@
# v4.1 第二轮代码清理 — 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 修复第二轮深度审查发现的 7 个代码质量问题
**Architecture:** 7 个独立小修,互不冲突。PathTable.tsx 有 3 项相关改动,放在一个 Task 里。
**Tech Stack:** TypeScript strict + React + Rust + Tauri IPC
---
### Task 1: PathTable 三合一(并发限制 + 双重触发 + 类型断言)
**Files:**
- Modify: `src/components/path-list/PathTable.tsx:25, 53, 78, 88-96, 111, 121`
- [ ] **Step 1: 提取 `DEFAULT_VALIDATION_STATE` 常量,消除类型断言**
```typescript
// 在组件外部定义(文件顶部 import 之后)
const DEFAULT_VALIDATION_STATE: ValidationState = 'valid';
```
第 121 行改为:
```typescript
state: validationCache.get(path) ?? DEFAULT_VALIDATION_STATE,
```
- [ ] **Step 2: 环境变量展开加 20 并发上限**
第 88-96 行,在 `toExpand` 之后加批次限制:
```typescript
const toExpand = paths.filter(
(p) => p.includes('%') && !expandedCache.has(p),
);
if (toExpand.length === 0) return;
const batch = toExpand.slice(0, 20); // ← 新增:限制并发 20
Promise.all(
batch.map(async (p): Promise<[string, string]> => {
```
- [ ] **Step 3: 消除 useEffect 双重触发**
问题:`validationCache``expandedCache` 在依赖数组中,setState 后触发 effect 再次执行(空跑一轮)。
用 ref 跟踪"是否已经触发过验证"effect 只依赖 `paths`
```typescript
// 新增两个 ref(放在 useState 声明之后)
const validatedRef = useRef<Set<string>>(new Set());
const expandedRef = useRef<Set<string>>(new Set());
```
验证 effect 改为:
```typescript
useEffect(() => {
let cancelled = false;
const toValidate = paths.filter((p) => !validatedRef.current.has(p));
if (toValidate.length === 0) return;
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'];
}
}),
).then((results) => {
if (cancelled) return;
for (const [p] of results) validatedRef.current.add(p);
setValidationCache((prev) => {
const next = new Map(prev);
for (const [p, v] of results) next.set(p, v);
return next;
});
});
return () => { cancelled = true; };
}, [paths]); // ← 移除 validationCache 依赖
```
展开 effect 同理:
```typescript
useEffect(() => {
let cancelled = false;
const toExpand = paths.filter((p) => p.includes('%') && !expandedRef.current.has(p));
if (toExpand.length === 0) return;
const batch = toExpand.slice(0, 20);
Promise.all(
batch.map(async (p): Promise<[string, string]> => {
try {
const expanded: string = await invoke('expand_env_vars', { path: p });
return [p, expanded !== p ? expanded : ''];
} catch {
return [p, ''];
}
}),
).then((results) => {
if (cancelled) return;
for (const [p] of results) expandedRef.current.add(p);
setExpandedCache((prev) => {
const next = new Map(prev);
for (const [p, v] of results) next.set(p, v);
return next;
});
});
return () => { cancelled = true; };
}, [paths]); // ← 移除 expandedCache 依赖
```
`validations` 和渲染逻辑不变。
- [ ] **Step 4: 添加 `useRef` 到 import**
```typescript
// 第 1 行
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
```
- [ ] **Step 5: 编译 + 测试**
```bash
npx tsc --noEmit && npx vitest run
```
- [ ] **Step 6: Commit**
```bash
git add src/components/path-list/PathTable.tsx
git commit -m "fix: PathTable — 环境变量展开限流20并发、消除useEffect双重触发、类型断言改为常量"
```
---
### Task 2: backup.rs — use 语句移到文件顶部 + 消除路径字符串重复
**Files:**
- Modify: `src-tauri/src/commands/registry.rs:4-5`
- Modify: `src-tauri/src/commands/backup.rs:1-3, 22-23, 34-42`
- [ ] **Step 1: registry.rs 常量改为 pub(crate)**
```rust
// src-tauri/src/commands/registry.rs — 第 4-5 行,加 pub(crate)
pub(crate) const SYS_REG_PATH: &str = "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
pub(crate) const USER_REG_PATH: &str = "Environment";
pub(crate) const PATH_VALUE: &str = "Path";
```
- [ ] **Step 2: backup.rs — 移动 use 到文件顶部,消除重复字符串**
```rust
// src-tauri/src/commands/backup.rs — 文件顶部(第 1 行之后)
use chrono::Local;
use std::path::PathBuf;
use winreg::enums::*; // ← 从函数体内移出
// 第 4 行之后新增:
use crate::commands::registry::{self, SYS_REG_PATH, USER_REG_PATH};
```
删除函数体内的 `use`(第 22-23 行),更新路径引用:
```rust
// backup.rs — 第 34-42 行,用常量替换字符串字面量
let sys_paths = registry::load_paths(
HKEY_LOCAL_MACHINE,
SYS_REG_PATH, // ← 用常量
"系统",
)?;
let user_paths = registry::load_paths(
HKEY_CURRENT_USER,
USER_REG_PATH, // ← 用常量
"用户",
)?;
```
- [ ] **Step 3: 编译 + clippy**
```bash
cd src-tauri && cargo check && cargo clippy -- -D warnings
```
- [ ] **Step 4: Commit**
```bash
git add src-tauri/src/commands/registry.rs src-tauri/src/commands/backup.rs
git commit -m "refactor: backup.rs — use 语句移至文件顶部,注册表路径复用常量消除重复"
```
---
### Task 3: AppShell — `as any` 拖拽路径类型安全
**Files:**
- Modify: `src/components/layout/AppShell.tsx:95`
- [ ] **Step 1: 定义 TauriFile 接口并消除 as any**
在 AppShell 组件定义之前(第 16 行之后)加:
```typescript
/** Tauri 的 File 对象扩展了标准 File,额外提供文件系统路径 */
interface TauriFile extends File {
path: string;
}
```
第 95 行改为:
```typescript
const file = e.dataTransfer.files[i] as TauriFile;
if (file.path) useAppStore.getState().addPath(file.path, activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM);
```
- [ ] **Step 2: 编译检查**
```bash
npx tsc --noEmit
```
- [ ] **Step 3: Commit**
```bash
git add src/components/layout/AppShell.tsx
git commit -m "fix: AppShell 拖拽路径消除 as any,用 TauriFile 接口类型安全访问 path"
```
---
### Task 4: app-store — undo/redo 加注释说明为何不用 markDirty()
**Files:**
- Modify: `src/store/app-store.ts:210-226`
- [ ] **Step 1: 在 undo/redo 的 isModified 行添加注释**
```typescript
// app-store.ts — 第 213 行之前加注释
undo: () => {
const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
const result = undoRedo.undo(sysPaths, userPaths);
if (result) {
set({
sysPaths: result[0], userPaths: result[1], selectedIndices: [],
// 内联 isModified 计算而非调用 markDirty(),避免两次 set() 渲染
isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
});
}
},
redo: () => {
const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
const result = undoRedo.redo(sysPaths, userPaths);
if (result) {
set({
sysPaths: result[0], userPaths: result[1], selectedIndices: [],
// 内联 isModified 计算而非调用 markDirty(),避免两次 set() 渲染
isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
});
}
},
```
- [ ] **Step 2: 编译检查**
```bash
npx tsc --noEmit
```
- [ ] **Step 3: Commit**
```bash
git add src/store/app-store.ts
git commit -m "docs: undo/redo 添加注释说明为何内联 isModified 而非调用 markDirty()"
```
---
## 执行顺序
Task 1 → 2 → 3 → 4,互不依赖但建议按序执行。
全部完成后运行完整验证:
```bash
npx tsc --noEmit && npx vitest run && cd src-tauri && cargo check && cargo clippy -- -D warnings
```