mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
feat: allow skipping OCR key during setup
This commit is contained in:
parent
07d032e420
commit
18e90a4f6f
5 changed files with 129 additions and 35 deletions
File diff suppressed because one or more lines are too long
|
|
@ -195,6 +195,10 @@ const LANG: Record<string, Record<string, string>> = {
|
|||
wizard_step3: 'Platform & Keys',
|
||||
wizard_step4: 'Install',
|
||||
wizard_step5: 'Done',
|
||||
wizard_skip_ocr_desc: 'OCR will not be available until you configure a valid PaddleOCR API key. You can continue setup now and configure it later in Settings.',
|
||||
wizard_skip_ocr_continue: 'Continue without OCR key',
|
||||
wizard_skip_ocr_back: 'Back to configure',
|
||||
wizard_api_hint_skip: 'OCR key is optional — you may skip it and configure later.',
|
||||
wizard_sys_hint: 'These folders live at the vault root, outside the resources directory:',
|
||||
wizard_title: 'PaperForge Setup Wizard',
|
||||
},
|
||||
|
|
@ -391,6 +395,10 @@ const LANG: Record<string, Record<string, string>> = {
|
|||
wizard_step3: 'Agent',
|
||||
wizard_step4: '安装',
|
||||
wizard_step5: '完成',
|
||||
wizard_skip_ocr_desc: 'OCR 功能在配置有效的 PaddleOCR API 密钥之前不可用。您可以继续完成设置,稍后在设置中配置。',
|
||||
wizard_skip_ocr_continue: '继续,稍后配置密钥',
|
||||
wizard_skip_ocr_back: '返回配置',
|
||||
wizard_api_hint_skip: 'OCR 密钥为选填项 — 可跳过,稍后在设置中配置。',
|
||||
wizard_sys_hint: '独立于资源目录的系统文件:',
|
||||
wizard_title: 'PaperForge 安装向导',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { PaperForgeSettings } from "../constants";
|
|||
import { resolveVaultPaths, getCachedPython } from "../services/memory-state";
|
||||
import { resolvePythonExecutable, resolveGitDir, paperforgeEnrichedEnv } from "../services/python-bridge";
|
||||
import type { PythonResult } from "../services/python-bridge";
|
||||
import { shouldBlockStep3 } from "./step3-gate";
|
||||
|
||||
// ── Interfaces ──
|
||||
|
||||
|
|
@ -210,6 +211,7 @@ export class PaperForgeSetupModal extends Modal {
|
|||
private _pendingSave: ReturnType<typeof setTimeout> | null = null;
|
||||
private _apiKeyValidated!: boolean;
|
||||
private _apiKeyStatus!: HTMLElement;
|
||||
private _showSkipConfirm: boolean = false;
|
||||
|
||||
constructor(app: App, plugin: IPluginRef) {
|
||||
super(app);
|
||||
|
|
@ -263,13 +265,24 @@ export class PaperForgeSetupModal extends Modal {
|
|||
const nav = this.contentEl.createEl('div', { cls: 'paperforge-step-nav' });
|
||||
if (this._step > 1) {
|
||||
nav.createEl('button', { cls: 'paperforge-step-btn', text: t('nav_prev') })
|
||||
.addEventListener('click', () => { this._step--; this._render(); });
|
||||
.addEventListener('click', () => { this._step--; this._showSkipConfirm = false; this._render(); });
|
||||
}
|
||||
if (this._step < 5) {
|
||||
const nextBtn = nav.createEl('button', { cls: 'paperforge-step-btn mod-cta', text: t('nav_next') });
|
||||
nextBtn.addEventListener('click', () => {
|
||||
if (this._step === 3 && !this._validateStep3()) return;
|
||||
if (this._step === 3) {
|
||||
const result = this._validateStep3();
|
||||
if (result.blocked) {
|
||||
if (result.reason === 'zotero') return;
|
||||
if (result.reason === 'ocr') {
|
||||
this._showSkipConfirm = true;
|
||||
this._render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._step++;
|
||||
this._showSkipConfirm = false;
|
||||
this._render();
|
||||
});
|
||||
} else {
|
||||
|
|
@ -278,36 +291,35 @@ export class PaperForgeSetupModal extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
_validateStep3() {
|
||||
_validateStep3(): { blocked: boolean; reason?: 'ocr' | 'zotero' } {
|
||||
const s = this.plugin.settings;
|
||||
|
||||
// Check API key validated
|
||||
if (!this._apiKeyValidated) {
|
||||
new Notice('\u8BF7\u5148\u9A8C\u8BC1 PaddleOCR API \u5BC6\u94A5');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check Zotero data directory: required + exists + is directory + has storage/
|
||||
// Hard block: Zotero data directory (required, exists, is directory, has storage/)
|
||||
if (!s.zotero_data_dir || !s.zotero_data_dir.trim()) {
|
||||
new Notice('Zotero \u6570\u636E\u76EE\u5F55\u4E3A\u5FC5\u586B\u9879\uFF0C\u8BF7\u586B\u5199\u8DEF\u5F84');
|
||||
return false;
|
||||
return { blocked: true, reason: 'zotero' };
|
||||
}
|
||||
const zotPath = s.zotero_data_dir.trim();
|
||||
if (!fs.existsSync(zotPath)) {
|
||||
new Notice('Zotero \u6570\u636E\u76EE\u5F55\u8DEF\u5F84\u4E0D\u5B58\u5728');
|
||||
return false;
|
||||
return { blocked: true, reason: 'zotero' };
|
||||
}
|
||||
if (!fs.statSync(zotPath).isDirectory()) {
|
||||
new Notice('Zotero \u6570\u636E\u76EE\u5F55\u8DEF\u5F84\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55');
|
||||
return false;
|
||||
return { blocked: true, reason: 'zotero' };
|
||||
}
|
||||
const storagePath = path.join(zotPath, 'storage');
|
||||
if (!fs.existsSync(storagePath) || !fs.statSync(storagePath).isDirectory()) {
|
||||
new Notice('Zotero \u6570\u636E\u76EE\u5F55\u4E2D\u672A\u627E\u5230 storage/ \u5B50\u76EE\u5F55');
|
||||
return false;
|
||||
return { blocked: true, reason: 'zotero' };
|
||||
}
|
||||
|
||||
return true;
|
||||
// Soft block: OCR key not validated (confirmation, not hard block)
|
||||
if (!this._apiKeyValidated) {
|
||||
return { blocked: true, reason: 'ocr' };
|
||||
}
|
||||
|
||||
return { blocked: false };
|
||||
}
|
||||
|
||||
/* ── Step 1: Overview ── */
|
||||
|
|
@ -394,6 +406,12 @@ export class PaperForgeSetupModal extends Modal {
|
|||
/* ── Step 3: Keys, Zotero & Agent ── */
|
||||
_stepKeys(el: HTMLElement) {
|
||||
el.createEl('h2', { text: t('wizard_step3') });
|
||||
|
||||
if (this._showSkipConfirm) {
|
||||
this._renderSkipConfirm(el);
|
||||
return;
|
||||
}
|
||||
|
||||
const s = this.plugin.settings;
|
||||
|
||||
el.createEl('p', { text: t('wizard_agent_hint'), cls: 'paperforge-modal-hint' });
|
||||
|
|
@ -443,6 +461,8 @@ export class PaperForgeSetupModal extends Modal {
|
|||
if (this._pendingSave) clearTimeout(this._pendingSave);
|
||||
this._pendingSave = setTimeout(() => { this.plugin.saveSettings(); this._pendingSave = null; }, 500);
|
||||
|
||||
el.createEl('p', { text: t('wizard_api_hint_skip'), cls: 'paperforge-modal-hint' });
|
||||
|
||||
// Zotero data directory (now required)
|
||||
const zotRow = el.createEl('div', { cls: 'paperforge-modal-field' });
|
||||
zotRow.createEl('label', { cls: 'paperforge-modal-label', text: t('field_zotero_data') });
|
||||
|
|
@ -460,7 +480,7 @@ export class PaperForgeSetupModal extends Modal {
|
|||
|
||||
_validateApiKey(key: string, btn: HTMLButtonElement) {
|
||||
if (!key || key.length < 10) {
|
||||
this._apiKeyStatus.textContent = '\u5BC6\u94A5\u683C\u5F0F\u4E0D\u6B63\u786E';
|
||||
this._apiKeyStatus.textContent = '\u5BC6\u94A5\u683C\u5F0F\u4E0D\u6B63\u786E\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002';
|
||||
this._apiKeyStatus.className = 'paperforge-apikey-status error';
|
||||
return;
|
||||
}
|
||||
|
|
@ -495,16 +515,16 @@ export class PaperForgeSetupModal extends Modal {
|
|||
this._apiKeyStatus.className = 'paperforge-apikey-status ok';
|
||||
this._apiKeyValidated = true;
|
||||
} else if (res.statusCode === 401) {
|
||||
this._apiKeyStatus.textContent = '\u9A8C\u8BC1\u5931\u8D25\uFF1A\u5BC6\u94A5\u65E0\u6548';
|
||||
this._apiKeyStatus.textContent = '\u9A8C\u8BC1\u5931\u8D25\uFF1A\u5BC6\u94A5\u65E0\u6548\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002';
|
||||
this._apiKeyStatus.className = 'paperforge-apikey-status error';
|
||||
this._apiKeyValidated = false;
|
||||
} else {
|
||||
this._apiKeyStatus.textContent = '\u9A8C\u8BC1\u5931\u8D25\uFF1AAPI \u8FD4\u56DE ' + res.statusCode;
|
||||
this._apiKeyStatus.textContent = '\u9A8C\u8BC1\u5931\u8D25\uFF1AAPI \u8FD4\u56DE ' + res.statusCode + '\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002';
|
||||
this._apiKeyStatus.className = 'paperforge-apikey-status error';
|
||||
this._apiKeyValidated = false;
|
||||
}
|
||||
} catch (e) {
|
||||
this._apiKeyStatus.textContent = '\u9A8C\u8BC1\u5931\u8D25\uFF1A\u65E0\u6CD5\u89E3\u6790\u54CD\u5E94';
|
||||
this._apiKeyStatus.textContent = '\u9A8C\u8BC1\u5931\u8D25\uFF1A\u65E0\u6CD5\u89E3\u6790\u54CD\u5E94\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002';
|
||||
this._apiKeyStatus.className = 'paperforge-apikey-status error';
|
||||
this._apiKeyValidated = false;
|
||||
}
|
||||
|
|
@ -513,7 +533,7 @@ export class PaperForgeSetupModal extends Modal {
|
|||
req.on('error', (e) => {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '\u9A8C\u8BC1';
|
||||
this._apiKeyStatus.textContent = '\u9A8C\u8BC1\u5931\u8D25\uFF1A\u65E0\u6CD5\u8FDE\u63A5 (' + e.message + ')';
|
||||
this._apiKeyStatus.textContent = '\u9A8C\u8BC1\u5931\u8D25\uFF1A\u65E0\u6CD5\u8FDE\u63A5 (' + e.message + ')\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002';
|
||||
this._apiKeyStatus.className = 'paperforge-apikey-status error';
|
||||
this._apiKeyValidated = false;
|
||||
});
|
||||
|
|
@ -521,6 +541,23 @@ export class PaperForgeSetupModal extends Modal {
|
|||
req.end();
|
||||
}
|
||||
|
||||
_renderSkipConfirm(el: HTMLElement) {
|
||||
el.createEl('p', { text: t('wizard_skip_ocr_desc'), cls: 'paperforge-modal-desc' });
|
||||
|
||||
const btnRow = el.createEl('div', { cls: 'paperforge-modal-actions' });
|
||||
btnRow.createEl('button', { cls: 'paperforge-step-btn mod-cta', text: t('wizard_skip_ocr_continue') })
|
||||
.addEventListener('click', () => {
|
||||
this._showSkipConfirm = false;
|
||||
this._step++;
|
||||
this._render();
|
||||
});
|
||||
btnRow.createEl('button', { cls: 'paperforge-step-btn', text: t('wizard_skip_ocr_back') })
|
||||
.addEventListener('click', () => {
|
||||
this._showSkipConfirm = false;
|
||||
this._render();
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Modal form helpers ── */
|
||||
_modalField(el: HTMLElement, label: string, value: string, disabled: boolean) {
|
||||
const row = el.createEl('div', { cls: 'paperforge-modal-field' });
|
||||
|
|
|
|||
11
paperforge/plugin/src/views/step3-gate.ts
Normal file
11
paperforge/plugin/src/views/step3-gate.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export type Step3BlockResult = { blocked: boolean; reason?: 'ocr' | 'zotero' };
|
||||
|
||||
export function shouldBlockStep3(keyValidated: boolean, zoteroDataDir: string): Step3BlockResult {
|
||||
if (!zoteroDataDir || !zoteroDataDir.trim()) {
|
||||
return { blocked: true, reason: 'zotero' };
|
||||
}
|
||||
if (!keyValidated) {
|
||||
return { blocked: true, reason: 'ocr' };
|
||||
}
|
||||
return { blocked: false };
|
||||
}
|
||||
38
paperforge/plugin/tests/modals.test.ts
Normal file
38
paperforge/plugin/tests/modals.test.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { shouldBlockStep3 } from '../src/views/step3-gate';
|
||||
|
||||
describe('shouldBlockStep3', () => {
|
||||
it('validated key + valid Zotero dir → no block', () => {
|
||||
expect(shouldBlockStep3(true, '/valid/zotero')).toEqual({ blocked: false });
|
||||
});
|
||||
|
||||
it('unvalidated key + valid Zotero dir → confirmation needed (NOT hard block)', () => {
|
||||
const r = shouldBlockStep3(false, '/valid/zotero');
|
||||
expect(r.blocked).toBe(true);
|
||||
expect(r.reason).toBe('ocr');
|
||||
});
|
||||
|
||||
it('unvalidated key + missing Zotero dir → hard block (Zotero)', () => {
|
||||
const r = shouldBlockStep3(false, '');
|
||||
expect(r.blocked).toBe(true);
|
||||
expect(r.reason).toBe('zotero');
|
||||
});
|
||||
|
||||
it('validated key + missing Zotero dir → hard block (Zotero)', () => {
|
||||
const r = shouldBlockStep3(true, '');
|
||||
expect(r.blocked).toBe(true);
|
||||
expect(r.reason).toBe('zotero');
|
||||
});
|
||||
|
||||
it('unvalidated key + whitespace Zotero dir → hard block', () => {
|
||||
const r = shouldBlockStep3(false, ' ');
|
||||
expect(r.blocked).toBe(true);
|
||||
expect(r.reason).toBe('zotero');
|
||||
});
|
||||
|
||||
it('Zotero check takes priority over OCR check', () => {
|
||||
const r = shouldBlockStep3(false, '');
|
||||
expect(r.blocked).toBe(true);
|
||||
expect(r.reason).toBe('zotero');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue