diff --git a/package.json b/package.json index 9cfe85f..77507ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "smallclaw", - "version": "2.9.0", + "version": "2.9.6", "description": "Local AI agent framework powered by Ollama - OpenClaw alternative", "main": "dist/index.js", "bin": { diff --git a/src/gateway/server-v2.ts b/src/gateway/server-v2.ts index ff5d452..b3fb0ed 100644 --- a/src/gateway/server-v2.ts +++ b/src/gateway/server-v2.ts @@ -9576,6 +9576,43 @@ app.get('/api/semantic/search', requireGatewayAuth, async (req: express.Request, } }); +app.get('/api/europepmc/search', requireGatewayAuth, async (req: express.Request, res: express.Response) => { + try { + const query = String(req.query.q || req.query.query || '').trim(); + const maxResults = Math.min(Number(req.query.max || 20), 50); + const minYear = req.query.min_year ? Number(req.query.min_year) : undefined; + const sort = req.query.sort === 'date' ? 'P_PDATE_D desc' : 'RELEVANCE'; + if (!query) { res.status(400).json({ error: 'q parameter required' }); return; } + + let q = query; + if (minYear) q += ` AND PUB_YEAR:[${minYear} TO 2099]`; + + const url = `https://www.ebi.ac.uk/europepmc/webservices/rest/search?query=${encodeURIComponent(q)}&format=json&resultType=core&pageSize=${maxResults}&sort=${encodeURIComponent(sort)}`; + const resp = await fetch(url, { signal: AbortSignal.timeout(15000) }); + if (!resp.ok) { res.status(502).json({ error: `Europe PMC ${resp.status}` }); return; } + const data = await resp.json() as any; + + const results = (data.resultList?.result || []).map((p: any) => ({ + title: p.title?.replace(/<[^>]+>/g, '').trim() || '', + authors: p.authorString || '', + year: p.firstPublicationDate ? String(p.firstPublicationDate).slice(0, 4) : (p.journalInfo?.yearOfPublication ? String(p.journalInfo.yearOfPublication) : ''), + journal: p.journalInfo?.journal?.title || '', + doi: p.doi || null, + pmid: p.pmid ? String(p.pmid) : null, + pmcid: p.pmcid ? String(p.pmcid).replace(/^PMC/i, '') : null, + pub_types: p.pubTypeList?.pubType?.filter((t: string) => !/^(research|review)-article$/i.test(t)).join(', ') || '', + is_oa: p.isOpenAccess === 'Y', + oa_url: p.isOpenAccess === 'Y' && p.pmcid ? `https://europepmc.org/article/PMC/${p.pmcid}` : null, + abstract: p.abstractText?.replace(/<[^>]+>/g, '') || '', + citations: p.citedByCount || 0, + })); + + res.json({ success: true, data: { results } }); + } catch (err) { + res.status(500).json({ error: String(err) }); + } +}); + const PPTX_BASE = 'pptx'; function pptxProjectSlug(raw: string) { return String(raw || '').trim().replace(/[^a-zA-Z0-9가-힣_\-]/g, '_').replace(/_+/g, '_').replace(/^_+|_+$/g, '').toLowerCase().slice(0, 60); @@ -10095,7 +10132,7 @@ app.get('/api/pptx/project-images', requireGatewayAuth, async (req: express.Requ try { if (fs.statSync(abs).isDirectory()) { scan(abs, relF); continue; } } catch { continue; } - if (IMG_EXT.test(f) && !seenPaths.has(abs)) { + if (IMG_EXT.test(f) && !seenPaths.has(abs) && !relF.includes('/preview/')) { seenPaths.add(abs); images.push({ path: relF, name: f }); } @@ -10109,6 +10146,10 @@ app.get('/api/pptx/project-images', requireGatewayAuth, async (req: express.Requ if (wsRootDir !== projectDir && fs.existsSync(wsRootDir) && fs.statSync(wsRootDir).isDirectory()) { scan(wsRootDir, slugOnly); } + images.sort((a, b) => { + const numOf = (s: string) => { const m = s.match(/(\d+)/g); return m ? m.map(n => n.padStart(6, '0')).join('_') : s; }; + return numOf(a.name).localeCompare(numOf(b.name)); + }); res.json({ images }); } catch (err) { res.status(500).json({ error: String(err) }); diff --git a/src/tools/pdf-extract.ts b/src/tools/pdf-extract.ts index 571d946..60a479e 100644 --- a/src/tools/pdf-extract.ts +++ b/src/tools/pdf-extract.ts @@ -408,22 +408,30 @@ export const pdfExtractImagesTool = { // --- figures mode: render pages then auto-crop figures/tables with OpenCV --- if (mode === 'figures' || mode === 'both') { - const figDpi = Math.min(300, Math.max(100, dpi)); - const ppmArgs = ['-png', '-r', String(figDpi)]; - if (pageFrom) ppmArgs.push('-f', String(pageFrom)); - if (pageTo) ppmArgs.push('-l', String(pageTo)); - ppmArgs.push(resolved, path.join(outDir, 'page')); - try { - await execFileAsync('pdftoppm', ppmArgs, { timeout: 120_000 }); - } catch (err: any) { - errors.push(`pdftoppm: ${String(err.message || err).slice(0, 200)}`); + // Skip expensive re-extraction if figure files already exist in outDir + const existingFigs = fs.existsSync(outDir) + ? fs.readdirSync(outDir).filter(f => f.startsWith('figure_') && f.endsWith('.png')) + : []; + if (existingFigs.length > 0 && !pageFrom && !pageTo) { + extracted.push(...existingFigs.sort().map(f => `${outDirRel}/${f}`)); + } else { + const figDpi = Math.min(300, Math.max(100, dpi)); + const ppmArgs = ['-png', '-r', String(figDpi)]; + if (pageFrom) ppmArgs.push('-f', String(pageFrom)); + if (pageTo) ppmArgs.push('-l', String(pageTo)); + ppmArgs.push(resolved, path.join(outDir, 'page')); + try { + await execFileAsync('pdftoppm', ppmArgs, { timeout: 120_000 }); + } catch (err: any) { + errors.push(`pdftoppm: ${String(err.message || err).slice(0, 200)}`); + } + const { files: figFiles, errors: figErrors } = await extractFigures(outDir, outDir); + errors.push(...figErrors); + for (const f of fs.readdirSync(outDir).filter(f => f.startsWith('page-') && f.endsWith('.png'))) { + try { fs.unlinkSync(path.join(outDir, f)); } catch {} + } + extracted.push(...figFiles.map(f => `${outDirRel}/${f}`)); } - const { files: figFiles, errors: figErrors } = await extractFigures(outDir, outDir); - errors.push(...figErrors); - for (const f of fs.readdirSync(outDir).filter(f => f.startsWith('page-') && f.endsWith('.png'))) { - try { fs.unlinkSync(path.join(outDir, f)); } catch {} - } - extracted.push(...figFiles.map(f => `${outDirRel}/${f}`)); } if (extracted.length === 0) { diff --git a/web-ui/pptx-wizard.html b/web-ui/pptx-wizard.html index 4ab2d36..e68b6bd 100644 --- a/web-ui/pptx-wizard.html +++ b/web-ui/pptx-wizard.html @@ -22,10 +22,21 @@ body.theme-warm{ } .theme-btn{background:none;border:1.5px solid var(--line);border-radius:6px;padding:4px 10px;font-size:12px;color:var(--muted);cursor:pointer;transition:.15s;} .theme-btn:hover,.theme-btn.active{border-color:var(--brand);color:var(--brand2);background:rgba(99,102,241,.08);} -.src-chk{display:flex;align-items:flex-start;gap:6px;font-size:12px;font-weight:600;color:var(--muted);cursor:pointer;background:var(--panel2);border:1.5px solid var(--line);border-radius:7px;padding:7px 12px;transition:.15s;user-select:none;max-width:220px;} -.src-chk:has(input:checked){border-color:var(--brand);background:rgba(99,102,241,.1);color:var(--brand2);} -.src-chk input{accent-color:var(--brand);width:13px;height:13px;cursor:pointer;margin-top:2px;flex-shrink:0;} -.src-chk-desc{display:block;font-size:10px;font-weight:400;color:var(--muted);margin-top:3px;line-height:1.5;} +.src-chk{display:flex;align-items:flex-start;gap:6px;font-size:12px;font-weight:600;color:var(--muted);cursor:pointer;border:1.5px solid var(--line);border-radius:8px;padding:8px 11px;transition:.15s;user-select:none;width:168px;flex-shrink:0;box-sizing:border-box;} +.src-chk input{width:13px;height:13px;cursor:pointer;margin-top:2px;flex-shrink:0;} +.src-chk-desc{display:block;font-size:10px;font-weight:400;color:var(--muted);margin-top:4px;line-height:1.5;} +#src-pubmed{background:rgba(239,68,68,.07);} +#src-pubmed:has(input:checked){background:rgba(239,68,68,.18);border-color:#f87171;color:#fca5a5;} +#src-pubmed input{accent-color:#ef4444;} +#src-openalex{background:rgba(34,197,94,.07);} +#src-openalex:has(input:checked){background:rgba(34,197,94,.18);border-color:var(--green);color:#86efac;} +#src-openalex input{accent-color:#22c55e;} +#src-semantic{background:rgba(251,191,36,.07);} +#src-semantic:has(input:checked){background:rgba(251,191,36,.18);border-color:#fbbf24;color:#fde68a;} +#src-semantic input{accent-color:#f59e0b;} +#src-europepmc{background:rgba(96,165,250,.07);} +#src-europepmc:has(input:checked){background:rgba(96,165,250,.18);border-color:#60a5fa;color:#bfdbfe;} +#src-europepmc input{accent-color:#3b82f6;} *{margin:0;padding:0;box-sizing:border-box;} body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,sans-serif;height:100vh;display:flex;flex-direction:column;overflow:hidden;font-size:14px;} @@ -56,7 +67,9 @@ body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,san .srow{display:flex;gap:8px;margin-bottom:12px;} .srow input{flex:1;background:var(--panel2);border:1px solid var(--line);border-radius:var(--r);padding:8px 12px;font-size:13px;color:var(--text);outline:none;} .srow input:focus{border-color:var(--brand);} -.srow .yin{width:80px;flex-shrink:0;} +.srow .yin{width:52px;flex:none;padding:8px 4px;text-align:center;} +.srow .yin::-webkit-inner-spin-button,.srow .yin::-webkit-outer-spin-button{-webkit-appearance:none;} +.srow .yin{-moz-appearance:textfield;} /* Paper cards */ .pcard{background:var(--panel2);border:1px solid var(--line);border-radius:var(--r);padding:11px 13px;margin-bottom:8px;display:flex;gap:10px;align-items:flex-start;transition:.15s;} @@ -276,22 +289,31 @@ body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,san 검색 소스: +
이후 (선택) - + + +
-
- - -