mirror of
https://github.com/fancive/obsidian-parallel-reader.git
synced 2026-07-22 06:53:43 +00:00
chore: improve engineering quality and hardening
- Fix CI to run all 4 test files (was missing direct-modules.test.js) - Remove unused ESLint dependencies (eslint, @typescript-eslint/*, eslint-plugin-obsidianmd) - Enable noImplicitReturns in tsconfig.json for stricter type safety - Make applyApiProviderPreset return new object instead of mutating input - Add per-file error handling in batch processing loop with error count in summary - Narrow all catch clause types to unknown with proper instanceof narrowing Change-Id: I76975dbd5ebdb5645d18c5858c8a52fd6da3405e
This commit is contained in:
parent
b1e35fc8fc
commit
5f12113fcd
15 changed files with 75 additions and 5156 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
run: npm run typecheck
|
||||
|
||||
- name: Test
|
||||
run: node tests/main.test.js && node tests/generation-job-manager.test.js && node tests/modules.test.js
|
||||
run: node tests/main.test.js && node tests/generation-job-manager.test.js && node tests/modules.test.js && node tests/direct-modules.test.js
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
|
|
|||
40
main.js
40
main.js
File diff suppressed because one or more lines are too long
26
main.ts
26
main.ts
|
|
@ -9,6 +9,7 @@ import {
|
|||
hasUnsafeBatchFolderSegments,
|
||||
markBatchFileRunning,
|
||||
normalizeBatchFolderInput,
|
||||
recordBatchError,
|
||||
recordBatchProcessed,
|
||||
recordBatchSkip,
|
||||
requestBatchCancel,
|
||||
|
|
@ -139,7 +140,10 @@ class ParallelReaderPlugin extends Plugin {
|
|||
name: this.t('cmdClearCurrent'),
|
||||
callback: async () => {
|
||||
const active = this.getActiveView();
|
||||
if (!active?.file) return new Notice(this.t('noCurrentNote'));
|
||||
if (!active?.file) {
|
||||
new Notice(this.t('noCurrentNote'));
|
||||
return;
|
||||
}
|
||||
await this.cacheManager.delete(active.file.path);
|
||||
new Notice(this.t('cacheClearedFile', { name: active.file.basename }));
|
||||
},
|
||||
|
|
@ -228,7 +232,7 @@ class ParallelReaderPlugin extends Plugin {
|
|||
if (this._settingsSaveTimer) clearTimeout(this._settingsSaveTimer);
|
||||
this._settingsSaveTimer = setTimeout(() => {
|
||||
this._settingsSaveTimer = null;
|
||||
this.saveSettings().catch((e) => console.error('[parallel-reader] failed to save settings', e));
|
||||
this.saveSettings().catch((e: unknown) => console.error('[parallel-reader] failed to save settings', e));
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
|
|
@ -539,7 +543,7 @@ class ParallelReaderPlugin extends Plugin {
|
|||
}),
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
.catch((e: unknown) => {
|
||||
if (e instanceof GenerationJobAlreadyRunningError) {
|
||||
new Notice(e.message);
|
||||
return;
|
||||
|
|
@ -550,9 +554,10 @@ class ParallelReaderPlugin extends Plugin {
|
|||
return;
|
||||
}
|
||||
const kind = classifyGenerationError(e);
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error(e);
|
||||
if (view && this.viewIsShowingFile(view, file)) view.renderError(file, e.message || String(e));
|
||||
new Notice(this.t('generationFailed', { kind: kind === 'unknown' ? '' : ` (${kind})`, error: e.message || e }));
|
||||
if (view && this.viewIsShowingFile(view, file)) view.renderError(file, msg);
|
||||
new Notice(this.t('generationFailed', { kind: kind === 'unknown' ? '' : ` (${kind})`, error: msg }));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -615,8 +620,13 @@ class ParallelReaderPlugin extends Plugin {
|
|||
stats = recordBatchSkip(stats);
|
||||
continue;
|
||||
}
|
||||
await this.runForFile(file, false);
|
||||
stats = recordBatchProcessed(stats);
|
||||
try {
|
||||
await this.runForFile(file, false);
|
||||
stats = recordBatchProcessed(stats);
|
||||
} catch (e: unknown) {
|
||||
stats = recordBatchError(stats);
|
||||
console.error('[parallel-reader] batch error for', file.path, e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (this.activeBatch === batch) this.activeBatch = null;
|
||||
|
|
@ -624,6 +634,7 @@ class ParallelReaderPlugin extends Plugin {
|
|||
const batchVars: Record<string, string | number> = {
|
||||
processed: stats.processed,
|
||||
skipped: stats.skipped,
|
||||
errors: stats.errors,
|
||||
total: stats.total,
|
||||
};
|
||||
if (batch.cancelled) new Notice(this.t('batchCancelled', batchVars));
|
||||
|
|
@ -790,6 +801,7 @@ export const __test = {
|
|||
normalizeStreamingTimeoutMs,
|
||||
nextCardIndex,
|
||||
pruneCacheEntries,
|
||||
recordBatchError,
|
||||
recordBatchProcessed,
|
||||
recordBatchSkip,
|
||||
requestBatchCancel,
|
||||
|
|
|
|||
5102
package-lock.json
generated
5102
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -15,11 +15,7 @@
|
|||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.13",
|
||||
"@types/node": "^22.10.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
||||
"@typescript-eslint/parser": "^8.59.0",
|
||||
"esbuild": "^0.24.2",
|
||||
"eslint": "^10.2.1",
|
||||
"eslint-plugin-obsidianmd": "github:obsidianmd/eslint-plugin",
|
||||
"obsidian": "^1.7.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export interface BatchStats {
|
|||
total: number;
|
||||
processed: number;
|
||||
skipped: number;
|
||||
errors: number;
|
||||
}
|
||||
|
||||
export interface BatchRunState {
|
||||
|
|
@ -68,7 +69,7 @@ export function batchProgressVars(index: number, total: number): { current: numb
|
|||
}
|
||||
|
||||
export function createBatchStats(total: number): BatchStats {
|
||||
return { total, processed: 0, skipped: 0 };
|
||||
return { total, processed: 0, skipped: 0, errors: 0 };
|
||||
}
|
||||
|
||||
export function recordBatchProcessed(stats: BatchStats): BatchStats {
|
||||
|
|
@ -79,6 +80,10 @@ export function recordBatchSkip(stats: BatchStats): BatchStats {
|
|||
return { ...stats, processed: stats.processed + 1, skipped: stats.skipped + 1 };
|
||||
}
|
||||
|
||||
export function recordBatchError(stats: BatchStats): BatchStats {
|
||||
return { ...stats, processed: stats.processed + 1, errors: stats.errors + 1 };
|
||||
}
|
||||
|
||||
export function shouldSkipBatchFile(entry: CacheEntry | null, content: string, settings: PluginSettings): boolean {
|
||||
return !!entry && cacheEntryMatches(entry, content, settings);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export class CacheManager {
|
|||
this._timer = setTimeout(() => {
|
||||
this._timer = null;
|
||||
if (!this._dirty) return;
|
||||
this.save().catch((e) => console.error('[parallel-reader] failed to save cache', e));
|
||||
this.save().catch((e: unknown) => console.error('[parallel-reader] failed to save cache', e));
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export class GenerationJobManager {
|
|||
const result = await runner(job);
|
||||
job.throwIfCancelled();
|
||||
return result;
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
if (job.cancelled && !(err instanceof GenerationJobCancelledError)) {
|
||||
throw new GenerationJobCancelledError(key);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,8 +158,8 @@ export const STRINGS: Record<string, Record<string, string>> = {
|
|||
batchFolderNotFound: '文件夹不存在:{path}',
|
||||
batchNoMarkdown: '该文件夹下没有 Markdown 文件',
|
||||
batchProgress: '批量生成:{current}/{total}',
|
||||
batchDone: '批量生成完成:共处理 {processed}/{total} 篇,跳过缓存 {skipped} 篇',
|
||||
batchCancelled: '批量生成已取消:已处理 {processed}/{total} 篇,跳过缓存 {skipped} 篇',
|
||||
batchDone: '批量生成完成:共处理 {processed}/{total} 篇,跳过缓存 {skipped} 篇,失败 {errors} 篇',
|
||||
batchCancelled: '批量生成已取消:已处理 {processed}/{total} 篇,跳过缓存 {skipped} 篇,失败 {errors} 篇',
|
||||
},
|
||||
en: {
|
||||
appTitle: 'Parallel Reader',
|
||||
|
|
@ -324,8 +324,8 @@ export const STRINGS: Record<string, Record<string, string>> = {
|
|||
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)',
|
||||
batchCancelled: 'Batch cancelled: processed {processed}/{total}, skipped {skipped} (cached)',
|
||||
batchDone: 'Batch complete: processed {processed}/{total}, skipped {skipped} (cached), errors {errors}',
|
||||
batchCancelled: 'Batch cancelled: processed {processed}/{total}, skipped {skipped} (cached), errors {errors}',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ async function requestJsonBodyWithStructuredFallback(
|
|||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
return await requestJsonBody(requestUrlImpl, label, url, headers, structuredBody, settings);
|
||||
} catch (e) {
|
||||
} catch (e: unknown) {
|
||||
if (!fallbackBody || !shouldRetryWithoutStructuredOutput(e)) throw e;
|
||||
console.warn(`[parallel-reader] ${label} structured output rejected; retrying without structured output`, e);
|
||||
return requestJsonBody(requestUrlImpl, label + ' fallback', url, headers, fallbackBody, settings);
|
||||
|
|
|
|||
|
|
@ -85,7 +85,10 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
|
|||
.onChange(async (v) => {
|
||||
this.plugin.settings.backend = v;
|
||||
if (v === 'api' && !this.plugin.settings.apiBaseUrl) {
|
||||
applyApiProviderPreset(this.plugin.settings, this.plugin.settings.apiProvider || 'anthropic');
|
||||
this.plugin.settings = applyApiProviderPreset(
|
||||
this.plugin.settings,
|
||||
this.plugin.settings.apiProvider || 'anthropic',
|
||||
);
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
|
|
@ -121,7 +124,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
|
|||
d.addOption(id, entry.label);
|
||||
}
|
||||
return d.setValue(this.plugin.settings.apiProvider).onChange(async (v) => {
|
||||
applyApiProviderPreset(this.plugin.settings, v);
|
||||
this.plugin.settings = applyApiProviderPreset(this.plugin.settings, v);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ export function modelForApi(settings: PluginSettings): string {
|
|||
return raw;
|
||||
}
|
||||
|
||||
export function applyApiProviderPreset(settings: PluginSettings, providerId: string) {
|
||||
export function applyApiProviderPreset(settings: Readonly<PluginSettings>, providerId: string): PluginSettings {
|
||||
const previousPreset = getApiPreset(settings);
|
||||
const preset = API_PROVIDER_PRESETS[providerId] || API_PROVIDER_PRESETS.anthropic;
|
||||
const previousModel = (settings.model || '').trim();
|
||||
|
|
@ -319,12 +319,15 @@ export function applyApiProviderPreset(settings: PluginSettings, providerId: str
|
|||
previousModel === DEFAULT_SETTINGS.model ||
|
||||
(!!previousPreset.model && previousModel === previousPreset.model);
|
||||
|
||||
settings.apiProvider = providerId;
|
||||
settings.apiFormat = preset.format;
|
||||
settings.apiBaseUrl = preset.baseUrl;
|
||||
settings.apiAuthType = preset.authType || 'auto';
|
||||
settings.apiKeyEnvVar = preset.envVar || '';
|
||||
if (shouldSwapModel) settings.model = preset.model || '';
|
||||
return {
|
||||
...settings,
|
||||
apiProvider: providerId,
|
||||
apiFormat: preset.format,
|
||||
apiBaseUrl: preset.baseUrl,
|
||||
apiAuthType: preset.authType || 'auto',
|
||||
apiKeyEnvVar: preset.envVar || '',
|
||||
...(shouldSwapModel ? { model: preset.model || '' } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeSettings(settings: PluginSettings): PluginSettings {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export async function ensureVaultFolder(app: App, folderPath: string) {
|
|||
if (app.vault.getAbstractFileByPath(folder)) continue;
|
||||
try {
|
||||
await app.vault.createFolder(folder);
|
||||
} catch (e) {
|
||||
} catch (e: unknown) {
|
||||
if (!app.vault.getAbstractFileByPath(folder)) throw e;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -264,9 +264,11 @@ assert.deepStrictEqual(t.batchProgressVars(1, 3), { current: 2, total: 3 }, 'pro
|
|||
const batchStats = t.createBatchStats(3);
|
||||
const skippedStats = t.recordBatchSkip(batchStats);
|
||||
const processedStats = t.recordBatchProcessed(skippedStats);
|
||||
assert.deepStrictEqual(batchStats, { total: 3, processed: 0, skipped: 0 }, 'batch stats are immutable');
|
||||
assert.deepStrictEqual(skippedStats, { total: 3, processed: 1, skipped: 1 }, 'batch skip updates progress');
|
||||
assert.deepStrictEqual(processedStats, { total: 3, processed: 2, skipped: 1 }, 'batch processed count accumulates');
|
||||
const errorStats = t.recordBatchError(processedStats);
|
||||
assert.deepStrictEqual(batchStats, { total: 3, processed: 0, skipped: 0, errors: 0 }, 'batch stats are immutable');
|
||||
assert.deepStrictEqual(skippedStats, { total: 3, processed: 1, skipped: 1, errors: 0 }, 'batch skip updates progress');
|
||||
assert.deepStrictEqual(processedStats, { total: 3, processed: 2, skipped: 1, errors: 0 }, 'batch processed count accumulates');
|
||||
assert.deepStrictEqual(errorStats, { total: 3, processed: 3, skipped: 1, errors: 1 }, 'batch error count accumulates');
|
||||
|
||||
const batchState = t.createBatchRunState();
|
||||
assert.deepStrictEqual(batchState, { cancelled: false, currentPath: '' }, 'batch state starts idle');
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"noImplicitReturns": false,
|
||||
"noImplicitReturns": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
|
|
|
|||
Loading…
Reference in a new issue