feat: validate batch folder input

Change-Id: I137fa046e77781e063ee782af4321b7277d64d51
This commit is contained in:
wujunchen 2026-04-26 18:12:38 +08:00
parent d8dc82362b
commit 9098de6884
6 changed files with 93 additions and 27 deletions

52
main.js

File diff suppressed because one or more lines are too long

15
main.ts
View file

@ -6,6 +6,7 @@ import {
batchProgressVars,
createBatchRunState,
createBatchStats,
hasUnsafeBatchFolderSegments,
markBatchFileRunning,
normalizeBatchFolderInput,
recordBatchProcessed,
@ -13,6 +14,7 @@ import {
requestBatchCancel,
selectBatchFiles,
shouldSkipBatchFile,
validateBatchFolderInput,
} from './src/batch';
import { serializeCacheFile, shouldConfirmRegenerate, touchCacheEntry } from './src/cache';
import { CacheManager } from './src/cache-manager';
@ -585,7 +587,16 @@ class ParallelReaderPlugin extends Plugin {
new FolderPromptModal(plugin.app).open();
});
if (folderPath === null) return;
const allFiles = selectBatchFiles(this.app.vault.getMarkdownFiles(), folderPath);
const validation = validateBatchFolderInput(folderPath, (path) => {
const target = this.app.vault.getAbstractFileByPath(path);
return !!target && !(target instanceof TFile);
});
if (!validation.valid) {
const noticeKey = validation.reason === 'unsafe' ? 'batchInvalidFolderInput' : 'batchFolderNotFound';
new Notice(this.t(noticeKey, { path: validation.folderPath || folderPath }));
return;
}
const allFiles = selectBatchFiles(this.app.vault.getMarkdownFiles(), validation.folderPath);
if (allFiles.length === 0) {
new Notice(this.t('batchNoMarkdown'));
return;
@ -772,6 +783,7 @@ export const __test = {
createBatchStats,
generationFingerprint,
getApiBaseUrl,
hasUnsafeBatchFolderSegments,
markBatchFileRunning,
modelForApi,
normalizeBatchFolderInput,
@ -793,6 +805,7 @@ export const __test = {
translate,
tokenLimitFieldForOpenAiChat,
updateCardAt,
validateBatchFolderInput,
visibleTopProbeY,
supportsStreaming,
deltaExtractorForFormat,

View file

@ -19,6 +19,14 @@ export interface BatchRunState {
currentPath: string;
}
export type BatchFolderValidationReason = 'ok' | 'unsafe' | 'missing';
export interface BatchFolderValidation {
valid: boolean;
reason: BatchFolderValidationReason;
folderPath: string;
}
export function normalizeBatchFolderInput(input: string): string {
return (input || '')
.trim()
@ -28,6 +36,23 @@ export function normalizeBatchFolderInput(input: string): string {
.join('/');
}
export function hasUnsafeBatchFolderSegments(input: string): boolean {
return (input || '')
.split('/')
.map((part) => part.trim())
.some((part) => part === '.' || part === '..');
}
export function validateBatchFolderInput(
input: string,
folderExists: (folderPath: string) => boolean,
): BatchFolderValidation {
const folderPath = normalizeBatchFolderInput(input);
if (hasUnsafeBatchFolderSegments(input)) return { valid: false, reason: 'unsafe', folderPath };
if (folderPath && !folderExists(folderPath)) return { valid: false, reason: 'missing', folderPath };
return { valid: true, reason: 'ok', folderPath };
}
export function isFileInBatchFolder(file: BatchFileLike, folderPath: string): boolean {
if (!folderPath) return !file.path.includes('/');
return file.parent?.path === folderPath;

View file

@ -151,6 +151,8 @@ export const STRINGS: Record<string, Record<string, string>> = {
batchFolderPrompt: '文件夹路径',
batchAlreadyRunning: '批量生成正在运行',
batchCancelRequested: '已请求取消批量生成;当前文件结束后停止',
batchInvalidFolderInput: '文件夹路径无效:{path}',
batchFolderNotFound: '文件夹不存在:{path}',
batchNoMarkdown: '该文件夹下没有 Markdown 文件',
batchProgress: '批量生成:{current}/{total}',
batchDone: '批量生成完成:共处理 {processed}/{total} 篇,跳过缓存 {skipped} 篇',
@ -311,6 +313,8 @@ export const STRINGS: Record<string, Record<string, string>> = {
batchFolderPrompt: 'Folder path',
batchAlreadyRunning: 'Batch generation is already running',
batchCancelRequested: 'Batch cancellation requested; it will stop after the current note',
batchInvalidFolderInput: 'Invalid folder path: {path}',
batchFolderNotFound: 'Folder not found: {path}',
batchNoMarkdown: 'No Markdown files found in that folder',
batchProgress: 'Batch generating: {current}/{total}',
batchDone: 'Batch complete: processed {processed}/{total}, skipped {skipped} (cached)',

View file

@ -74,6 +74,7 @@ assert.strictEqual(typeof t.findLineForAnchor, 'function');
assert.strictEqual(typeof t.folderPathsForTarget, 'function');
assert.strictEqual(typeof t.getApiBaseUrl, 'function');
assert.strictEqual(typeof t.generationFingerprint, 'function');
assert.strictEqual(typeof t.hasUnsafeBatchFolderSegments, 'function');
assert.strictEqual(typeof t.CacheManager, 'function');
assert.strictEqual(typeof t.GenerationJobManager, 'function');
assert.strictEqual(typeof t.createBatchRunState, 'function');
@ -90,6 +91,7 @@ assert.strictEqual(typeof t.serializeCacheFile, 'function');
assert.strictEqual(typeof t.shouldConfirmRegenerate, 'function');
assert.strictEqual(typeof t.translate, 'function');
assert.strictEqual(typeof t.updateCardAt, 'function');
assert.strictEqual(typeof t.validateBatchFolderInput, 'function');
const baseSettings = {
backend: 'api',

View file

@ -226,6 +226,28 @@ assert.deepStrictEqual(t.folderPathsForTarget('a/../../b'), ['a', 'a/b'], 'strip
assert.strictEqual(t.normalizeBatchFolderInput('/Reading//Articles/'), 'Reading/Articles', 'normalizes folder input');
assert.strictEqual(t.normalizeBatchFolderInput('../Inbox/./Daily'), 'Inbox/Daily', 'strips unsafe folder segments');
assert.strictEqual(t.hasUnsafeBatchFolderSegments('../Inbox/./Daily'), true, 'unsafe folder segments are detected');
assert.strictEqual(t.hasUnsafeBatchFolderSegments('/Reading//Articles/'), false, 'normal folder paths are safe');
assert.deepStrictEqual(
t.validateBatchFolderInput('', () => false),
{ valid: true, reason: 'ok', folderPath: '' },
'empty batch folder targets vault root',
);
assert.deepStrictEqual(
t.validateBatchFolderInput('Reading', (path) => path === 'Reading'),
{ valid: true, reason: 'ok', folderPath: 'Reading' },
'existing folder input is valid',
);
assert.deepStrictEqual(
t.validateBatchFolderInput('../Reading', () => true),
{ valid: false, reason: 'unsafe', folderPath: 'Reading' },
'unsafe folder input is invalid even if normalized target exists',
);
assert.deepStrictEqual(
t.validateBatchFolderInput('Missing', () => false),
{ valid: false, reason: 'missing', folderPath: 'Missing' },
'missing folder input is invalid',
);
const batchFiles = [
{ path: 'root.md', parent: null },
{ path: 'Reading/note.md', parent: { path: 'Reading' } },