mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
feat: paperforge status --json, plugin panel with OCR progress bar
This commit is contained in:
parent
11ebc85c4e
commit
f617054d5a
6 changed files with 182 additions and 131 deletions
|
|
@ -148,7 +148,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
)
|
||||
|
||||
# status
|
||||
sub.add_parser("status", help="Run the literature pipeline status check")
|
||||
status_p = sub.add_parser("status", help="Run the literature pipeline status check")
|
||||
status_p.add_argument("--json", action="store_true", dest="json_output", help="Output JSON")
|
||||
|
||||
# sync (new unified command)
|
||||
p_sync = sub.add_parser("sync", help="Sync Zotero selection and refresh literature index")
|
||||
|
|
|
|||
|
|
@ -40,4 +40,4 @@ def run(args: argparse.Namespace) -> int:
|
|||
vault = resolve_vault(cli_vault=getattr(args, "vault", None))
|
||||
|
||||
run_status = _get_run_status()
|
||||
return run_status(vault, verbose=getattr(args, "verbose", False))
|
||||
return run_status(vault, verbose=getattr(args, "verbose", False), json_output=getattr(args, "json_output", False))
|
||||
|
|
|
|||
|
|
@ -4,34 +4,18 @@ const { exec } = require('node:child_process');
|
|||
const VIEW_TYPE_PAPERFORGE = 'paperforge-status';
|
||||
|
||||
const COMMANDS = [
|
||||
{
|
||||
id: 'paperforge-sync',
|
||||
name: 'PaperForge: 同步文献并生成笔记',
|
||||
cmd: 'sync',
|
||||
okMsg: 'Sync complete',
|
||||
},
|
||||
{
|
||||
id: 'paperforge-ocr',
|
||||
name: 'PaperForge: 运行 OCR',
|
||||
cmd: 'ocr',
|
||||
okMsg: 'OCR started — check status for completion',
|
||||
},
|
||||
{ id: 'paperforge-sync', name: 'PaperForge: 同步文献并生成笔记', cmd: 'sync', okMsg: 'Sync complete' },
|
||||
{ id: 'paperforge-ocr', name: 'PaperForge: 运行 OCR', cmd: 'ocr', okMsg: 'OCR started' },
|
||||
];
|
||||
|
||||
class PaperForgeStatusView extends ItemView {
|
||||
constructor(leaf) {
|
||||
super(leaf);
|
||||
}
|
||||
constructor(leaf) { super(leaf); }
|
||||
|
||||
getViewType() { return VIEW_TYPE_PAPERFORGE; }
|
||||
getDisplayText() { return 'PaperForge'; }
|
||||
getIcon() { return 'book-open'; }
|
||||
|
||||
async onOpen() {
|
||||
this._buildPanel();
|
||||
this._fetchStats();
|
||||
}
|
||||
|
||||
async onOpen() { this._buildPanel(); this._fetchStats(); }
|
||||
onClose() {}
|
||||
|
||||
_buildPanel() {
|
||||
|
|
@ -41,85 +25,95 @@ class PaperForgeStatusView extends ItemView {
|
|||
|
||||
root.createEl('div', { cls: 'paperforge-status-header' }, (el) => {
|
||||
el.createEl('h3', { text: 'PaperForge' });
|
||||
const refreshBtn = el.createEl('button', {
|
||||
cls: 'paperforge-status-refresh',
|
||||
text: 'Refresh',
|
||||
});
|
||||
refreshBtn.addEventListener('click', () => this._fetchStats());
|
||||
const btn = el.createEl('button', { cls: 'paperforge-status-refresh', text: 'Refresh' });
|
||||
btn.addEventListener('click', () => this._fetchStats());
|
||||
});
|
||||
|
||||
this._metricsEl = root.createEl('div', { cls: 'paperforge-metrics' });
|
||||
this._statsEl = root.createEl('div', { cls: 'paperforge-stats' });
|
||||
this._ocrEl = root.createEl('div', { cls: 'paperforge-ocr-section' });
|
||||
this._actionsEl = root.createEl('div', { cls: 'paperforge-actions' }, (el) => {
|
||||
el.createEl('h4', { text: 'Quick Actions' });
|
||||
const btnGrp = el.createEl('div', { cls: 'paperforge-actions-buttons' });
|
||||
el.createEl('h4', { text: 'Actions' });
|
||||
const bg = el.createEl('div', { cls: 'paperforge-actions-buttons' });
|
||||
for (const c of COMMANDS) {
|
||||
const btn = btnGrp.createEl('button', {
|
||||
cls: 'paperforge-action-btn',
|
||||
text: c.name,
|
||||
});
|
||||
btn.addEventListener('click', () => this._runCommand(c));
|
||||
const b = bg.createEl('button', { cls: 'paperforge-action-btn', text: c.name });
|
||||
b.addEventListener('click', () => this._runCmd(c));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_fetchStats() {
|
||||
this._metricsEl.empty();
|
||||
this._metricsEl.createEl('div', {
|
||||
text: 'Loading...',
|
||||
cls: 'paperforge-status-loading',
|
||||
});
|
||||
this._statsEl.empty();
|
||||
this._ocrEl.empty();
|
||||
this._statsEl.setText('Loading...');
|
||||
|
||||
const vaultPath = this.app.vault.adapter.basePath;
|
||||
exec('python -m paperforge status', { cwd: vaultPath, timeout: 30000 }, (err, stdout) => {
|
||||
this._metricsEl.empty();
|
||||
const vp = this.app.vault.adapter.basePath;
|
||||
exec('python -m paperforge status --json', { cwd: vp, timeout: 30000 }, (err, stdout) => {
|
||||
this._statsEl.empty();
|
||||
if (err) {
|
||||
this._metricsEl.createEl('div', {
|
||||
text: 'Error connecting to PaperForge. Is it installed?',
|
||||
cls: 'paperforge-status-error',
|
||||
});
|
||||
this._statsEl.createEl('div', { text: 'Error connecting to PaperForge', cls: 'paperforge-status-error' });
|
||||
return;
|
||||
}
|
||||
this._renderMetrics(stdout);
|
||||
try {
|
||||
const d = JSON.parse(stdout);
|
||||
this._renderStats(d);
|
||||
this._renderOcr(d);
|
||||
} catch {
|
||||
this._statsEl.createEl('div', { text: 'Invalid response from paperforge', cls: 'paperforge-status-error' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_renderMetrics(statusText) {
|
||||
const lines = statusText.split('\n').filter(Boolean);
|
||||
const stats = {};
|
||||
|
||||
for (const line of lines) {
|
||||
const m = line.match(/^\S+/);
|
||||
if (!m) continue;
|
||||
const key = m[0].toLowerCase();
|
||||
stats.total = stats.total || 0;
|
||||
if (key === 'found') {
|
||||
const n = parseInt(line.match(/\d+/)?.[0] || '0');
|
||||
stats.total += n;
|
||||
}
|
||||
}
|
||||
|
||||
_renderStats(d) {
|
||||
const metrics = [
|
||||
{ label: 'Papers', value: stats.total || '—', color: 'var(--color-cyan)' },
|
||||
{ label: 'Papers', value: d.total_papers, color: 'var(--color-cyan)' },
|
||||
{ label: 'Notes', value: d.formal_notes, color: 'var(--color-blue)' },
|
||||
{ label: 'Exports', value: d.exports, color: 'var(--color-purple)' },
|
||||
];
|
||||
|
||||
for (const metric of metrics) {
|
||||
const card = this._metricsEl.createEl('div', { cls: 'paperforge-metric-card' });
|
||||
card.createEl('div', { text: metric.value.toString(), cls: 'paperforge-metric-value' });
|
||||
card.createEl('div', { text: metric.label, cls: 'paperforge-metric-label' });
|
||||
if (metric.color) {
|
||||
card.style.setProperty('--metric-color', metric.color);
|
||||
}
|
||||
const grid = this._statsEl.createEl('div', { cls: 'paperforge-metrics' });
|
||||
for (const m of metrics) {
|
||||
const card = grid.createEl('div', { cls: 'paperforge-metric-card' });
|
||||
card.style.setProperty('--metric-color', m.color);
|
||||
card.createEl('div', { cls: 'paperforge-metric-value', text: m.value?.toString() || '—' });
|
||||
card.createEl('div', { cls: 'paperforge-metric-label', text: m.label });
|
||||
}
|
||||
}
|
||||
|
||||
_runCommand(c) {
|
||||
const vaultPath = this.app.vault.adapter.basePath;
|
||||
_renderOcr(d) {
|
||||
const ocr = d.ocr || {};
|
||||
const total = ocr.total || 0;
|
||||
const done = ocr.done || 0;
|
||||
const pending = ocr.pending || 0;
|
||||
const failed = ocr.failed || 0;
|
||||
const pct = total > 0 ? Math.round(done / total * 100) : 0;
|
||||
|
||||
const section = this._ocrEl;
|
||||
section.createEl('h4', { text: 'OCR Progress' });
|
||||
|
||||
const bar = section.createEl('div', { cls: 'paperforge-progress-bar' });
|
||||
bar.createEl('div', { cls: 'paperforge-progress-fill', attr: { style: `width:${pct}%` } });
|
||||
bar.createEl('span', { cls: 'paperforge-progress-label', text: `${done}/${total} (${pct}%)` });
|
||||
|
||||
const detail = section.createEl('div', { cls: 'paperforge-progress-detail' });
|
||||
if (pending > 0) {
|
||||
detail.createEl('div', { cls: 'paperforge-progress-row' }, (el) => {
|
||||
el.createEl('span', { text: 'Processing', cls: 'paperforge-progress-row-label' });
|
||||
el.createEl('span', { text: pending.toString(), cls: 'paperforge-progress-row-value' });
|
||||
});
|
||||
}
|
||||
if (failed > 0) {
|
||||
detail.createEl('div', { cls: 'paperforge-progress-row' }, (el) => {
|
||||
el.createEl('span', { text: 'Failed', cls: 'paperforge-progress-row-label' });
|
||||
el.createEl('span', { text: failed.toString(), cls: 'paperforge-progress-row-value paperforge-progress-row-failed' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_runCmd(c) {
|
||||
const vp = this.app.vault.adapter.basePath;
|
||||
new Notice(`PaperForge: running ${c.cmd}...`);
|
||||
exec(`python -m paperforge ${c.cmd}`, { cwd: vaultPath, timeout: 300000 }, (err, stdout, stderr) => {
|
||||
exec(`python -m paperforge ${c.cmd}`, { cwd: vp, timeout: 300000 }, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
const msg = stderr
|
||||
? stderr.split('\n').filter(Boolean).slice(-2).join(' | ')
|
||||
: err.message;
|
||||
const msg = stderr ? stderr.split('\n').filter(Boolean).slice(-2).join(' | ') : err.message;
|
||||
new Notice(`[!!] ${c.cmd} failed: ${msg}`, 8000);
|
||||
return;
|
||||
}
|
||||
|
|
@ -130,45 +124,25 @@ class PaperForgeStatusView extends ItemView {
|
|||
|
||||
static async open(plugin) {
|
||||
const leaves = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_PAPERFORGE);
|
||||
if (leaves.length > 0) {
|
||||
plugin.app.workspace.revealLeaf(leaves[0]);
|
||||
return;
|
||||
}
|
||||
if (leaves.length > 0) { plugin.app.workspace.revealLeaf(leaves[0]); return; }
|
||||
const leaf = plugin.app.workspace.getRightLeaf(false);
|
||||
if (leaf) {
|
||||
await leaf.setViewState({ type: VIEW_TYPE_PAPERFORGE, active: true });
|
||||
plugin.app.workspace.revealLeaf(leaf);
|
||||
}
|
||||
if (leaf) { await leaf.setViewState({ type: VIEW_TYPE_PAPERFORGE, active: true }); plugin.app.workspace.revealLeaf(leaf); }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = class PaperForgePlugin extends Plugin {
|
||||
async onload() {
|
||||
this.registerView(VIEW_TYPE_PAPERFORGE, (leaf) => new PaperForgeStatusView(leaf));
|
||||
|
||||
this.addRibbonIcon('book-open', 'PaperForge Status', () => {
|
||||
PaperForgeStatusView.open(this);
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'paperforge-status-panel',
|
||||
name: 'PaperForge: 打开状态面板',
|
||||
callback: () => PaperForgeStatusView.open(this),
|
||||
});
|
||||
|
||||
this.addRibbonIcon('book-open', 'PaperForge Status', () => PaperForgeStatusView.open(this));
|
||||
this.addCommand({ id: 'paperforge-status-panel', name: 'PaperForge: 打开状态面板', callback: () => PaperForgeStatusView.open(this) });
|
||||
for (const c of COMMANDS) {
|
||||
this.addCommand({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
id: c.id, name: c.name,
|
||||
callback: () => {
|
||||
const vaultPath = this.app.vault.adapter.basePath;
|
||||
const vp = this.app.vault.adapter.basePath;
|
||||
new Notice(`PaperForge: running ${c.cmd}...`);
|
||||
exec(`python -m paperforge ${c.cmd}`, { cwd: vaultPath, timeout: 300000 }, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
const msg = stderr ? stderr.split('\n').filter(Boolean).slice(-2).join(' | ') : err.message;
|
||||
new Notice(`[!!] ${c.cmd} failed: ${msg}`, 8000);
|
||||
return;
|
||||
}
|
||||
exec(`python -m paperforge ${c.cmd}`, { cwd: vp, timeout: 300000 }, (err, stdout, stderr) => {
|
||||
if (err) { new Notice(`[!!] ${c.cmd} failed: ${(stderr || err.message).slice(0, 120)}`, 8000); return; }
|
||||
new Notice(`[OK] ${c.okMsg || stdout.trim().split('\n')[0].slice(0, 80)}`);
|
||||
});
|
||||
},
|
||||
|
|
@ -176,7 +150,5 @@ module.exports = class PaperForgePlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.app.workspace.detachLeavesOfType(VIEW_TYPE_PAPERFORGE);
|
||||
}
|
||||
onunload() { this.app.workspace.detachLeavesOfType(VIEW_TYPE_PAPERFORGE); }
|
||||
};
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
.paperforge-metric-card {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
min-width: 90px;
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
|
|
@ -65,6 +65,67 @@
|
|||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ── OCR Progress Bar ── */
|
||||
.paperforge-ocr-section h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: var(--font-ui-small);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.paperforge-progress-bar {
|
||||
position: relative;
|
||||
height: 22px;
|
||||
background: var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.paperforge-progress-fill {
|
||||
height: 100%;
|
||||
background: var(--interactive-accent);
|
||||
border-radius: var(--radius-s);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.paperforge-progress-label {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-ui-smaller);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-on-accent);
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
|
||||
.paperforge-progress-detail {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.paperforge-progress-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
|
||||
.paperforge-progress-row-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.paperforge-progress-row-value {
|
||||
color: var(--text-normal);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.paperforge-progress-row-failed {
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
||||
/* ── Status rows ── */
|
||||
.paperforge-status-row {
|
||||
display: flex;
|
||||
|
|
@ -79,22 +140,7 @@
|
|||
border-bottom: none;
|
||||
}
|
||||
|
||||
.paperforge-status-row-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.paperforge-status-row-value {
|
||||
color: var(--text-normal);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
/* ── Error / Loading ── */
|
||||
.paperforge-status-loading {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.paperforge-status-error {
|
||||
color: var(--text-error);
|
||||
font-size: var(--font-ui-small);
|
||||
|
|
|
|||
|
|
@ -452,8 +452,10 @@ def _is_junction(path: Path) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def run_status(vault: Path, verbose: bool = False) -> int:
|
||||
def run_status(vault: Path, verbose: bool = False, json_output: bool = False) -> int:
|
||||
"""Print a compact Lite install/runtime status."""
|
||||
import json as _json
|
||||
|
||||
paths = pipeline_paths(vault)
|
||||
cfg = load_vault_config(vault)
|
||||
config = load_domain_config(paths)
|
||||
|
|
@ -464,15 +466,23 @@ def run_status(vault: Path, verbose: bool = False) -> int:
|
|||
base_count = sum(1 for _ in paths["bases"].glob("*.base")) if paths["bases"].exists() else 0
|
||||
ocr_done = 0
|
||||
ocr_total = 0
|
||||
ocr_pending = 0
|
||||
ocr_failed = 0
|
||||
if paths["ocr"].exists():
|
||||
for meta_path in paths["ocr"].glob("*/meta.json"):
|
||||
ocr_total += 1
|
||||
try:
|
||||
meta = read_json(meta_path)
|
||||
except Exception:
|
||||
ocr_failed += 1
|
||||
continue
|
||||
if str(meta.get("ocr_status", "")).strip().lower() == "done":
|
||||
status = str(meta.get("ocr_status", "")).strip().lower()
|
||||
if status == "done":
|
||||
ocr_done += 1
|
||||
elif status in ("pending", "processing"):
|
||||
ocr_pending += 1
|
||||
else:
|
||||
ocr_failed += 1
|
||||
env_paths = [vault / ".env", paths["pipeline"] / ".env"]
|
||||
env_found = [str(path.relative_to(vault)).replace("\\", "/") for path in env_paths if path.exists()]
|
||||
|
||||
|
|
@ -487,18 +497,40 @@ def run_status(vault: Path, verbose: bool = False) -> int:
|
|||
except Exception:
|
||||
continue
|
||||
|
||||
if json_output:
|
||||
data = {
|
||||
"vault": str(vault),
|
||||
"system_dir": cfg["system_dir"],
|
||||
"resources_dir": cfg["resources_dir"],
|
||||
"exports": len(export_files),
|
||||
"domains": len(config.get("domains", [])),
|
||||
"total_papers": record_count,
|
||||
"formal_notes": note_count,
|
||||
"bases": base_count,
|
||||
"ocr": {
|
||||
"done": ocr_done,
|
||||
"pending": ocr_pending,
|
||||
"failed": ocr_failed,
|
||||
"total": ocr_total,
|
||||
},
|
||||
"path_errors": path_error_count,
|
||||
"env_configured": len(env_found) > 0,
|
||||
}
|
||||
print(_json.dumps(data, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
print("PaperForge status")
|
||||
print(f"- vault: {vault}")
|
||||
print(f"- system_dir: {cfg['system_dir']}")
|
||||
print(f"- resources_dir: {cfg['resources_dir']}")
|
||||
print(f"- literature_dir: {cfg['literature_dir']}")
|
||||
print(f"- literature_dir: cfg['literature_dir']")
|
||||
print(f"- control_dir: {cfg['control_dir']}")
|
||||
print(f"- exports: {len(export_files)} JSON file(s)")
|
||||
print(f"- domains: {len(config.get('domains', []))}")
|
||||
print(f"- library_records: {record_count}")
|
||||
print(f"- formal_notes: {note_count}")
|
||||
print(f"- bases: {base_count}")
|
||||
print(f"- ocr: {ocr_done}/{ocr_total} done")
|
||||
print(f"- ocr: {ocr_done}/{ocr_total} done (pending: {ocr_pending}, failed: {ocr_failed})")
|
||||
print(f"- path_errors: {path_error_count}")
|
||||
if path_error_count > 0:
|
||||
print(" Tip: Run `paperforge repair --fix-paths` to attempt resolution")
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import pytest
|
|||
CAPTURED_CALLS: list[tuple[str, Path]] = []
|
||||
|
||||
|
||||
def stub_run_status(vault: Path, verbose: bool = False) -> int:
|
||||
def stub_run_status(vault: Path, verbose: bool = False, json_output: bool = False) -> int:
|
||||
CAPTURED_CALLS.append(("run_status", vault))
|
||||
return 0
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue