feat: add folder-level batch summarization command

Register parallel-reader-batch-generate command. It prompts the user for a
folder path via a Modal, finds all .md files in that folder (non-recursive),
skips files whose cache is still valid, and runs runForFile() sequentially
on the rest, showing per-file progress notices. Add i18n keys for the new
UI strings in both zh and en.

https://claude.ai/code/session_016QvEfqw6YZ3RjwBHrJ4w8S
This commit is contained in:
Claude 2026-04-26 06:18:58 +00:00
parent 10d811f197
commit 02fc5678ae
No known key found for this signature in database
2 changed files with 69 additions and 1 deletions

58
main.ts
View file

@ -1,5 +1,5 @@
'use strict';
import { MarkdownView, Notice, Plugin, requestUrl, TFile } from 'obsidian';
import { MarkdownView, Modal, Notice, Plugin, requestUrl, TFile } from 'obsidian';
import { findLineForAnchor } from './src/anchor';
import { serializeCacheFile, shouldConfirmRegenerate, touchCacheEntry } from './src/cache';
import { CacheManager } from './src/cache-manager';
@ -208,6 +208,11 @@ class ParallelReaderPlugin extends Plugin {
name: this.t('cmdCardJump'),
callback: () => this.jumpActiveCard(),
});
this.addCommand({
id: 'parallel-reader-batch-generate',
name: this.t('cmdBatchGenerate'),
callback: () => this.runBatchForFolder(),
});
this.addSettingTab(new ParallelReaderSettingTab(this.app, this));
@ -551,6 +556,57 @@ class ParallelReaderPlugin extends Plugin {
});
}
async runBatchForFolder() {
const plugin = this;
const folderPath = await new Promise<string | null>((resolve) => {
class FolderPromptModal extends Modal {
private input!: HTMLInputElement;
onOpen() {
this.contentEl.createEl('p', { text: plugin.t('batchSelectFolder') });
this.input = this.contentEl.createEl('input', { type: 'text' });
this.input.placeholder = plugin.t('batchFolderPrompt');
this.input.style.width = '100%';
this.input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
resolve(this.input.value.trim());
this.close();
}
});
this.contentEl.createEl('button', { text: 'OK' }).addEventListener('click', () => {
resolve(this.input.value.trim());
this.close();
});
}
onClose() {
resolve(null);
}
}
new FolderPromptModal(plugin.app).open();
});
if (folderPath === null) return;
const allFiles = this.app.vault.getMarkdownFiles().filter((f) => {
if (folderPath === '') return !f.path.includes('/');
return f.parent?.path === folderPath;
});
if (allFiles.length === 0) {
new Notice(this.t('batchNoMarkdown'));
return;
}
let skipped = 0;
for (let i = 0; i < allFiles.length; i++) {
const file = allFiles[i];
new Notice(this.t('batchProgress', { current: i + 1, total: allFiles.length }));
const content = await this.app.vault.read(file);
const entry = this.cacheManager.get(file.path);
if (entry && cacheEntryMatches(entry, content, this.settings)) {
skipped++;
continue;
}
await this.runForFile(file, false);
}
new Notice(this.t('batchDone', { total: allFiles.length, skipped }));
}
resolveCardAnchors(content: string, rawCards: RawCard[]) {
const resolved: ResolvedCard[] = (rawCards || []).map((c: RawCard) => ({
title: c.title,

View file

@ -145,6 +145,12 @@ export const STRINGS: Record<string, Record<string, string>> = {
clearAllCacheButton: '清除所有缓存',
settingStreamingName: '流式输出',
settingStreamingDesc: '启用后生成时实时显示 LLM 输出进度(仅 OpenAI Chat 和 Anthropic 格式支持)',
cmdBatchGenerate: '批量生成对照笔记(当前文件夹)',
batchSelectFolder: '请输入要批量处理的文件夹路径(留空为 Vault 根目录)',
batchFolderPrompt: '文件夹路径',
batchNoMarkdown: '该文件夹下没有 Markdown 文件',
batchProgress: '批量生成:{current}/{total}',
batchDone: '批量生成完成:共处理 {total} 篇,跳过缓存 {skipped} 篇',
},
en: {
appTitle: 'Parallel Reader',
@ -295,6 +301,12 @@ export const STRINGS: Record<string, Record<string, string>> = {
settingStreamingName: 'Streaming output',
settingStreamingDesc:
'Show real-time LLM output progress during generation (OpenAI Chat and Anthropic formats only)',
cmdBatchGenerate: 'Batch generate parallel notes (current folder)',
batchSelectFolder: 'Enter the folder path to batch-process (leave blank for Vault root)',
batchFolderPrompt: 'Folder path',
batchNoMarkdown: 'No Markdown files found in that folder',
batchProgress: 'Batch generating: {current}/{total}',
batchDone: 'Batch complete: processed {total}, skipped {skipped} (cached)',
},
};