import { Plugin, addIcon, Notice } from "obsidian"; import * as fs from "fs"; import * as path from "path"; import { execFile, exec, spawn } from "child_process"; import { VIEW_TYPE_PAPERFORGE, PF_ICON_ID, PF_RIBBON_SVG, ACTIONS, DEFAULT_SETTINGS, PaperForgeSettings } from "./constants"; import { t, setLanguage } from "./i18n"; import { PaperForgeSettingTab } from "./settings"; import { PaperForgeStatusView } from "./views/dashboard"; import { resolvePythonExecutable, paperforgeEnrichedEnv } from "./services/python-bridge"; import { resolveVaultPaths } from "./services/memory-state"; export default class PaperForgePlugin extends Plugin { settings!: PaperForgeSettings; private _lastExportMtime = 0; private _lastOcrMtimes: Record = {}; private _autoSyncRunning = false; private _lastSyncTime: string | null = null; private _pollTimer: ReturnType | null = null; private _embedProcess: unknown = null; private _embedProgress = { current: 0, total: 0, key: "" }; private _embedStderr = ""; _memoryStatusText: string | null = null; async onload() { await this.loadSettings(); this.saveSettings(); setLanguage(this.app); this.registerView(VIEW_TYPE_PAPERFORGE, (leaf) => new PaperForgeStatusView(leaf)); try { addIcon(PF_ICON_ID, PF_RIBBON_SVG); } catch (_) {} this.addRibbonIcon(PF_ICON_ID, "PaperForge Dashboard", () => PaperForgeStatusView.open(this as any)); const redoAction = ACTIONS.find(a => a.id === "paperforge-ocr-redo"); if (redoAction) { this.addRibbonIcon("reset", "PaperForge: 重做OCR", () => { const vp = (this.app.vault.adapter as any).basePath as string; new Notice(`PaperForge: 重做OCR starting...`); const { path: py, extraArgs: ex } = resolvePythonExecutable(vp, this.settings, undefined, undefined); execFile(py, [...ex, "-m", "paperforge", "ocr", "redo"], { cwd: vp, timeout: 600000 }, (err, stdout, stderr) => { if (err) { new Notice(`PaperForge: 重做OCR failed`); return; } new Notice(`PaperForge: 重做OCR done`); }); }); } this.addSettingTab(new PaperForgeSettingTab(this.app, this as any)); this.addCommand({ id: "paperforge-status-panel", name: `PaperForge: ${t("guide_open")}`, callback: () => PaperForgeStatusView.open(this as any), }); for (const a of ACTIONS) { this.addCommand({ id: a.id, name: `PaperForge: ${a.title}`, callback: () => { if (a.disabled) { new Notice(`[i] ${a.disabledMsg || 'This action is not yet available.'}`, 6000); return; } const vp = (this.app.vault.adapter as any).basePath as string; new Notice(`PaperForge: running ${a.cmd}...`); const { path: cmdPythonExe, extraArgs: cmdExtra = [] } = resolvePythonExecutable(vp, this.settings, undefined, undefined); const cmdArgs = Array.isArray(a.args) ? [...a.args] : []; execFile(cmdPythonExe, [...cmdExtra, "-m", "paperforge", a.cmd, ...cmdArgs], { cwd: vp, timeout: 300000 }, (err, stdout, stderr) => { if (err) { new Notice(`[!!] ${a.cmd} failed: ${(stderr || err.message).slice(0, 120)}`, 8000); return; } new Notice(`[OK] ${a.okMsg || stdout.trim().split("\n")[0].slice(0, 80)}`); }); }, }); } if (this.settings.auto_update_on_startup === true && this.settings.setup_complete) { setTimeout(() => this._autoUpdate(), 3000); } this._startFilePolling(); this._firstLaunchSnapshotMigration(); } private _firstLaunchSnapshotMigration() { const vp = (this.app.vault.adapter as any).basePath as string; if (!vp) return; const runtimePaths = resolveVaultPaths(vp); const memSnap = runtimePaths.memoryStatePath; if (!fs.existsSync(memSnap)) { const py = resolvePythonExecutable(vp, this.settings, undefined, undefined); const commands: string[][] = [ ['runtime-health', '--json'], ['memory', 'status', '--json'], ['embed', 'status', '--json'], ]; commands.forEach((cmdArgs) => { const args = [...py.extraArgs, '-m', 'paperforge', '--vault', vp, ...cmdArgs]; execFile(py.path, args, { cwd: vp, timeout: 60000, windowsHide: true }, () => {}); }); } } private _autoUpdate() { const vp = (this.app.vault.adapter as any).basePath as string; const { path: pythonExe, extraArgs = [] } = resolvePythonExecutable(vp, this.settings, undefined, undefined); const ver = this.manifest.version; const pypiPkg = `paperforge==${ver}`; const gitUrl = `git+https://github.com/LLLin000/PaperForge.git@${ver}`; const doInstall = (pkg: string, onDone: (ok: boolean) => void) => { const child = spawn(pythonExe, [...extraArgs, '-m', 'pip', 'install', '--upgrade', pkg], { cwd: vp, timeout: 120000, env: paperforgeEnrichedEnv() }); child.on('close', (code) => onDone(code === 0)); }; execFile(pythonExe, [...extraArgs, '-c', 'import paperforge; print(paperforge.__version__)'], { cwd: vp, timeout: 10000 }, (err, stdout) => { const install = (label: string) => { console.log(`[PaperForge] Auto-update: trying PyPI (paperforge==${ver})`); doInstall(pypiPkg, (ok) => { if (ok) { console.log('[PaperForge] Auto-update: installed via PyPI'); new Notice(`[OK] PaperForge CLI ${label}`, 5000); return; } console.warn('[PaperForge] Auto-update: PyPI failed, falling back to git...'); doInstall(gitUrl, (ok2) => { if (ok2) { console.log('[PaperForge] Auto-update: installed via git'); new Notice(`[OK] PaperForge CLI ${label} (via git)`, 5000); } }); }); }; if (err) { install('installed'); return; } const pyVer = stdout.trim(); if (pyVer !== ver) { install(`${pyVer} -> ${ver}`); } }); } private _startFilePolling() { const vaultPath = (this.app.vault.adapter as any).basePath as string; this._pollTimer = setInterval(() => { this._checkExports(vaultPath); this._checkOcr(vaultPath); }, 120000); } private _checkExports(vaultPath: string) { if (this._autoSyncRunning) return; const exportsDir = resolveVaultPaths(vaultPath).exportsDir; if (!fs.existsSync(exportsDir)) return; let newestMtime = 0; try { fs.readdirSync(exportsDir).forEach(f => { if (!f.endsWith('.json')) return; const stat = fs.statSync(path.join(exportsDir, f)); if (stat.mtimeMs > newestMtime) newestMtime = stat.mtimeMs; }); } catch(e) { return; } if (newestMtime > this._lastExportMtime) { this._lastExportMtime = newestMtime; this._autoSync(vaultPath); } } private _autoSync(vaultPath: string) { if (this._autoSyncRunning) return; this._autoSyncRunning = true; const pyResult = resolvePythonExecutable(vaultPath, this.settings, undefined, undefined); if (!pyResult.path) { this._autoSyncRunning = false; return; } const cmd = `"${pyResult.path}" -m paperforge --vault "${vaultPath}" sync`; exec(cmd, { timeout: 120000, encoding: 'utf-8' }, (err, _stdout, _stderr) => { this._autoSyncRunning = false; this._memoryStatusText = null; if (!err) { this._lastSyncTime = new Date().toLocaleTimeString(); } try { const exportsDir = resolveVaultPaths(vaultPath).exportsDir; let newest = 0; fs.readdirSync(exportsDir).forEach(f => { if (!f.endsWith('.json')) return; newest = Math.max(newest, fs.statSync(path.join(exportsDir, f)).mtimeMs); }); this._lastExportMtime = newest; } catch(_e) {} }); } private _checkOcr(vaultPath: string) { if (this._autoSyncRunning) return; const ocrDir = resolveVaultPaths(vaultPath).ocrDir; if (!fs.existsSync(ocrDir)) return; try { fs.readdirSync(ocrDir, { withFileTypes: true }).forEach(entry => { if (!entry.isDirectory()) return; const metaPath = path.join(ocrDir, entry.name, 'meta.json'); if (!fs.existsSync(metaPath)) return; const stat = fs.statSync(metaPath); const prevMtime = this._lastOcrMtimes[entry.name] || 0; if (stat.mtimeMs <= prevMtime) return; this._lastOcrMtimes[entry.name] = stat.mtimeMs; if (this._autoSyncRunning) return; this._autoSyncRunning = true; const pyResult = resolvePythonExecutable(vaultPath, this.settings, undefined, undefined); if (!pyResult.path) { this._autoSyncRunning = false; return; } const cmd = `"${pyResult.path}" -m paperforge --vault "${vaultPath}" sync`; exec(cmd, { timeout: 30000, encoding: 'utf-8' }, () => { this._autoSyncRunning = false; this._memoryStatusText = null; }); }); } catch(_e) {} } readPaperforgeJson(): Record { const vaultPath = (this.app.vault.adapter as any).basePath as string; const pfPath = path.join(vaultPath, 'paperforge.json'); const DEFAULTS: Record = { system_dir: 'System', resources_dir: 'Resources', literature_dir: 'Literature', base_dir: 'Bases', }; try { if (!fs.existsSync(pfPath)) { return DEFAULTS; } const raw = fs.readFileSync(pfPath, 'utf-8'); const data = JSON.parse(raw); const vc = data.vault_config || {}; return { system_dir: vc.system_dir || data.system_dir || DEFAULTS.system_dir, resources_dir: vc.resources_dir || data.resources_dir || DEFAULTS.resources_dir, literature_dir: vc.literature_dir || data.literature_dir || DEFAULTS.literature_dir, base_dir: vc.base_dir || data.base_dir || DEFAULTS.base_dir, }; } catch (e) { console.warn('PaperForge: Failed to read paperforge.json, using defaults', e); return DEFAULTS; } } savePaperforgeJson(pathConfig: Record): void { const vaultPath = (this.app.vault.adapter as any).basePath as string; const pfPath = path.join(vaultPath, 'paperforge.json'); let data: Record = {}; try { if (fs.existsSync(pfPath)) { data = JSON.parse(fs.readFileSync(pfPath, 'utf-8')); } } catch (e) { console.warn('PaperForge: Failed to read paperforge.json for update', e); } if (!data.vault_config || typeof data.vault_config !== 'object') { data.vault_config = {}; } const validPathKeys = ['system_dir', 'resources_dir', 'literature_dir', 'base_dir']; for (const key of validPathKeys) { if (pathConfig[key] !== undefined) { data.vault_config[key] = pathConfig[key]; } } if (!data.schema_version) { data.schema_version = '2'; } for (const key of validPathKeys) { delete data[key]; } try { fs.writeFileSync(pfPath, JSON.stringify(data, null, 2), 'utf-8'); if (this.settings) { const pfConfig = this.readPaperforgeJson(); this.settings.system_dir = pfConfig.system_dir; this.settings.resources_dir = pfConfig.resources_dir; this.settings.literature_dir = pfConfig.literature_dir; this.settings.base_dir = pfConfig.base_dir; } } catch (e) { console.error('PaperForge: Failed to write paperforge.json', e); new Notice('PaperForge: Failed to save configuration to paperforge.json'); } } onunload() { if (this._pollTimer) clearInterval(this._pollTimer); this.app.workspace.detachLeavesOfType(VIEW_TYPE_PAPERFORGE); } async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); if (this.settings.features && DEFAULT_SETTINGS.features) { this.settings.features = Object.assign({}, DEFAULT_SETTINGS.features, this.settings.features || {}); } if (!this.settings.frozen_skills) { this.settings.frozen_skills = {}; } const pfConfig = this.readPaperforgeJson(); this.settings.system_dir = pfConfig.system_dir; this.settings.resources_dir = pfConfig.resources_dir; this.settings.literature_dir = pfConfig.literature_dir; this.settings.base_dir = pfConfig.base_dir; if (this.settings.python_path && this.settings.python_path.trim()) { const pp = this.settings.python_path.trim(); if (!fs.existsSync(pp)) { console.warn(`PaperForge: Saved python_path "${pp}" no longer exists - showing stale warning`); this.settings._python_path_stale = true; } else { this.settings._python_path_stale = false; } } } async saveSettings() { const dataToSave: Record = {}; for (const key of Object.keys(DEFAULT_SETTINGS)) { if (key in this.settings) { dataToSave[key] = this.settings[key]; } } await this.saveData(dataToSave); } }