- 在平台适配器中统一暴露 getCurrentDirectoryKey() 方法,用于确定当前目录范围 - 新增 LastRenameOperation 数据模型和核心操作函数,支持单条撤销记录的创建、合并和范围校验 - 在 file-selector-panel 中集成撤销逻辑,支持执行后保存记录、retry 成功后合并记录以及撤销操作 - 在 config-panel 底部操作区新增撤销按钮,根据当前目录范围动态显示/隐藏 - 添加多语言支持,包含撤销相关文案 - 添加单元测试和集成测试,确保撤销功能的正确性和稳定性
11 KiB
Last Batch Undo Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: 为批量重命名面板增加“仅撤销当前目录最近一次批量重命名”的能力,并让 retry 成功项并入同一条可撤销记录。
Architecture: 在平台适配器层统一暴露 getCurrentDirectoryKey(),在 file-selector-panel 维护单条 LastRenameOperation,通过纯函数帮助模块完成记录创建、合并、scope 校验和失败项收窄,再由 config-panel 提供单按钮 UI。执行与撤销都复用 BatchExecutor,避免重复实现节流、进度和错误汇总逻辑。
Tech Stack: TypeScript, Lit Web Components, Vitest, Playwright(回归可选)
Task 1: 增加统一目录 scope API
Files:
- Modify:
src/types/platform.ts - Modify:
src/adapters/base/adapter.interface.ts - Modify:
src/adapters/quark/quark.ts - Modify:
src/adapters/aliyun/aliyun-adapter.ts - Modify:
src/adapters/baidu/baidu-adapter.ts - Modify:
tests/unit/executor.test.ts - Modify:
tests/integration/batch-execution.test.ts - Modify:
tests/integration/folder-batch-execution.test.ts - Modify:
tests/performance/benchmark.test.ts
Step 1: 写一个最小失败测试(或先让 typecheck 暴露接口缺失)
在 tests/unit/executor.test.ts 的 mock adapter 中先补一条注释,准备添加:
getCurrentDirectoryKey(): string {
return 'root';
}
Step 2: 修改平台接口与抽象基类
在 src/types/platform.ts 增加:
getCurrentDirectoryKey(): string;
在 src/adapters/base/adapter.interface.ts 增加抽象声明:
abstract getCurrentDirectoryKey(): string;
Step 3: 在三个平台适配器中实现该方法
目标实现:
getCurrentDirectoryKey(): string {
return this.getCurrentFolderId(); // Quark
}
getCurrentDirectoryKey(): string {
return this.getParentIdFromUrl(); // Aliyun
}
getCurrentDirectoryKey(): string {
return this.getCurrentPath(); // Baidu
}
Step 4: 更新所有测试 mock adapter
给所有 implements PlatformAdapter 的 mock 增加:
getCurrentDirectoryKey(): string {
return 'root';
}
Step 5: 运行定向类型检查/测试
Run:
npm run typecheck
npx vitest run tests/unit/executor.test.ts tests/integration/batch-execution.test.ts
Expected: 新接口编译通过,mock 不再缺少 getCurrentDirectoryKey()。
Step 6: Commit
git add src/types/platform.ts src/adapters/base/adapter.interface.ts src/adapters/quark/quark.ts src/adapters/aliyun/aliyun-adapter.ts src/adapters/baidu/baidu-adapter.ts tests/unit/executor.test.ts tests/integration/batch-execution.test.ts tests/integration/folder-batch-execution.test.ts tests/performance/benchmark.test.ts
git commit -m "refactor: expose current directory scope on adapters"
Task 2: 提取单条 undo 记录帮助模块(TDD)
Files:
- Create:
src/types/undo.ts - Create:
src/core/last-rename-operation.ts - Create:
tests/unit/last-rename-operation.test.ts
Step 1: 先写失败测试
在 tests/unit/last-rename-operation.test.ts 写出以下核心用例:
it('creates a last rename operation from success results', () => {
const op = createLastRenameOperation('quark', 'dir-1', [
{ fileId: '1', original: 'a.txt', renamed: 'b.txt', index: 0 },
]);
expect(op?.items).toHaveLength(1);
expect(op?.items[0].original).toBe('a.txt');
});
it('merges retry success items into the existing operation', () => {
const merged = mergeLastRenameOperation(existing, [
{ fileId: '2', original: 'c.txt', renamed: 'd.txt', index: 1 },
]);
expect(merged.items.map(i => i.fileId)).toEqual(['1', '2']);
});
it('shrinks the operation to failed undo items only', () => {
const next = retainFailedUndoItems(existing, new Set(['2']));
expect(next?.items.map(i => i.fileId)).toEqual(['2']);
});
Step 2: 运行测试确认失败
Run:
npx vitest run tests/unit/last-rename-operation.test.ts
Expected: FAIL,提示模块或导出不存在。
Step 3: 实现最小类型与纯函数
在 src/types/undo.ts 增加:
export interface UndoRenameItem {
fileId: string;
original: string;
renamed: string;
index: number;
}
export interface LastRenameOperation {
platform: PlatformName;
directoryKey: string;
createdAt: number;
updatedAt: number;
items: UndoRenameItem[];
}
在 src/core/last-rename-operation.ts 实现:
createLastRenameOperation(...)mergeLastRenameOperation(...)isLastRenameOperationInScope(...)retainFailedUndoItems(...)
Step 4: 再次运行测试
Run:
npx vitest run tests/unit/last-rename-operation.test.ts
Expected: PASS
Step 5: Commit
git add src/types/undo.ts src/core/last-rename-operation.ts tests/unit/last-rename-operation.test.ts
git commit -m "test: cover last rename undo operation helpers"
Task 3: 在 file-selector-panel 中接入记录、retry 合并与 undo 主逻辑
Files:
- Modify:
src/content/components/file-selector-panel.ts - Modify:
src/types/core.ts(如需补充 undo 结果或辅助类型,否则保持不动)
Step 1: 先写失败测试或至少列出要补的帮助方法
如果暂不引入组件测试,先在代码注释中列出 4 个目标方法:
private getCurrentUndoScope(): { platform: PlatformName; directoryKey: string }
private clearLastRenameOperationIfOutOfScope(): void
private updateLastRenameOperationFromExecute(results: BatchResults): void
private async handleUndoLastRename(): Promise<void>
Step 2: 增加状态字段
在 file-selector-panel.ts 增加:
@state()
private lastRenameOperation: LastRenameOperation | null = null;
@state()
private undoBusy = false;
Step 3: 在关键入口先做 scope 校验
在这些方法开头调用:
this.clearLastRenameOperationIfOutOfScope();
目标入口:
updated()(当open变为 true 时)handleExecute()handleRetryFailed()handleUndoLastRename()
Step 4: 在主执行后创建单条记录
在 handleExecute() 成功路径里:
if (results.success.length > 0) {
this.lastRenameOperation = createLastRenameOperation(
this.adapter.platform,
this.adapter.getCurrentDirectoryKey(),
results.success
);
}
Step 5: 在 retry 成功后合并记录
在 handleRetryFailed() 成功路径里:
if (results.success.length > 0 && this.lastRenameOperation) {
this.lastRenameOperation = mergeLastRenameOperation(
this.lastRenameOperation,
this.adapter.platform,
this.adapter.getCurrentDirectoryKey(),
results.success
);
}
Step 6: 实现 undo 执行
核心逻辑:
const tasks = this.lastRenameOperation.items.map((item, index) => ({
file: this.allFiles.find(file => file.id === item.fileId) ?? {
id: item.fileId,
name: item.renamed,
ext: parseFileName(item.renamed).ext,
parentId: '',
size: 0,
mtime: Date.now(),
},
newName: item.original,
index,
}));
然后复用 BatchExecutor:
const executor = new BatchExecutor(tasks.map(t => t.file), this.ruleConfig, this.adapter, {
requestInterval: this.adapter.getConfig().requestInterval,
maxConcurrent: this.adapter.getConfig().maxConcurrent,
tasks,
onProgress: (progress) => this.handleProgress(progress),
});
Step 7: 处理撤销后的记录状态
- 全部成功:
this.lastRenameOperation = null - 部分失败:只保留失败 fileId 对应的项
const failedIds = new Set(results.failed.map(item => item.fileId));
this.lastRenameOperation = retainFailedUndoItems(this.lastRenameOperation, failedIds);
Step 8: 运行定向测试与类型检查
Run:
npm run typecheck
npx vitest run tests/unit/last-rename-operation.test.ts tests/unit/executor.test.ts
Expected: PASS
Step 9: Commit
git add src/content/components/file-selector-panel.ts src/types/core.ts
git commit -m "feat: add single-operation undo flow"
Task 4: 在 config-panel 加入撤销按钮和多语言文案
Files:
- Modify:
src/content/components/config-panel.ts - Modify:
src/locales/zh_CN.json - Modify:
src/locales/en.json - Modify:
src/locales/zh_TW.json
Step 1: 先加 props 和事件
在 config-panel.ts 增加:
@property({ type: Boolean }) canUndo = false;
@property({ type: Boolean }) undoBusy = false;
以及:
private handleUndo(): void {
this.dispatchEvent(new CustomEvent('undo', { bubbles: true, composed: true }));
}
Step 2: 在执行区渲染按钮
在 renderExecutionActions() 的非 executing 分支中加入:
<button
class="button button-default"
?disabled=${!this.canUndo || this.undoBusy}
@click=${this.handleUndo}
>
${this.undoBusy ? I18nService.t('undo_in_progress') : I18nService.t('undo_last_rename')}
</button>
并在 file-selector-panel.ts 的 render() 中传入:
.canUndo=${Boolean(this.lastRenameOperation)}
.undoBusy=${this.undoBusy}
@undo=${this.handleUndoLastRename}
Step 3: 增加 i18n 文案
至少新增:
undo_last_renameundo_in_progressundo_failed_prefixundo_failed_manual_hintundo_scope_hint
Step 4: 运行定向测试/构建检查
Run:
npm run typecheck
npm run lint
Expected: PASS
Step 5: Commit
git add src/content/components/config-panel.ts src/locales/zh_CN.json src/locales/en.json src/locales/zh_TW.json
git commit -m "feat: add undo action to rename panel"
Task 5: 完成失败提示与整体验证
Files:
- Modify:
src/content/components/file-selector-panel.ts - Modify:
tests/integration/batch-execution.test.ts(如需新增集成回归样例) - Optional:
tests/e2e/batch-rename.spec.ts
Step 1: 在 undo 失败分支拼接提示文本
示例:
const failedLines = results.failed.map(item => `- ${item.file.name}: ${item.error}`);
alert([
I18nService.t('undo_failed_prefix'),
...failedLines,
'',
I18nService.t('undo_failed_manual_hint'),
].join('\n'));
Step 2: 增加一条集成回归样例(推荐)
目标断言:
- 首次执行成功 + retry 成功后,唯一记录包含两批成功项
- undo 后两批文件都恢复原名
Step 3: 运行完整验证
Run:
npm run test:ci
npm run typecheck
npm run lint
Expected: 全部通过
Step 4: 手工验证当前目录 scope
Run:
npm run dev
手工检查:
- 在同目录执行批量重命名;
- 关闭面板,再打开,undo 按钮仍可用;
- 切换目录,再打开面板,undo 按钮消失;
- 回原目录(若页面未刷新且采用懒清理,则记录已被清空,这是符合当前规则的);
- retry 成功的文件也会被 undo 回退。
Step 5: Commit
git add src/content/components/file-selector-panel.ts tests/integration/batch-execution.test.ts tests/e2e/batch-rename.spec.ts
git commit -m "test: verify last batch undo workflow"