@a9i5k4/dsh-literature 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/lib/client.js +17 -12
  2. package/lib/index.js +297 -232
  3. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -1856,12 +1856,237 @@ var init_local_api = __esm({
1856
1856
  }
1857
1857
  });
1858
1858
 
1859
+ // src/node/importer.js
1860
+ var importer_exports = {};
1861
+ __export(importer_exports, {
1862
+ importDir: () => importDir,
1863
+ importFromZotero: () => importFromZotero,
1864
+ startWatcher: () => startWatcher,
1865
+ stopWatcher: () => stopWatcher,
1866
+ titleFromFilename: () => titleFromFilename,
1867
+ zoteroItemToRecord: () => zoteroItemToRecord
1868
+ });
1869
+ import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "node:fs/promises";
1870
+ import { join as join4, resolve as resolve4, extname, basename } from "node:path";
1871
+ import { writeFile as writeFile3, mkdir as mkdir3 } from "node:fs/promises";
1872
+ import { dirname as dirname2 } from "node:path";
1873
+ function titleFromFilename(name2) {
1874
+ let t = String(name2).replace(/\.pdf$/i, "");
1875
+ const doi = /(10\.\d{4,9}[\/_][^\s]+)/i.exec(t);
1876
+ if (doi) return { kind: "doi", value: doi[1].replace(/_/g, "/") };
1877
+ const arxiv = /\b(\d{4}\.\d{4,5})(v\d+)?\b/.exec(t);
1878
+ if (arxiv) return { kind: "arxiv", value: arxiv[1] + (arxiv[2] ?? "") };
1879
+ t = t.replace(/^\[\d+\]\s*/, "").replace(/\s*\(?\d{4}[a-z]?\)?\s*$/i, "").replace(/[_\-]+/g, " ").replace(/^\d+[\s.]+/, "").trim();
1880
+ if (!t) return null;
1881
+ return { kind: "title", value: t };
1882
+ }
1883
+ function pathForPdfKey(key) {
1884
+ return join4(PDF_DIR, `${key.replace(/[^\w.-]+/g, "_")}.pdf`);
1885
+ }
1886
+ async function savePdfBuffer(key, buffer) {
1887
+ const path = pathForPdfKey(key);
1888
+ await mkdir3(dirname2(path), { recursive: true });
1889
+ await writeFile3(path, buffer);
1890
+ return { path, size: buffer.length, source: "library-import" };
1891
+ }
1892
+ async function importDir(dir, { autoResolve = true } = {}) {
1893
+ const target = resolve4(dir || "");
1894
+ if (!target) throw Object.assign(new Error("\u672A\u914D\u7F6E\u5BFC\u5165\u6587\u4EF6\u5939"), { code: "no_dir" });
1895
+ let files;
1896
+ try {
1897
+ files = await readdir2(target);
1898
+ } catch (e) {
1899
+ throw Object.assign(new Error(`\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6\u5939 ${target}`), { code: "no_dir", cause: e });
1900
+ }
1901
+ const imported = [];
1902
+ const skipped = [];
1903
+ for (const name2 of files.filter((f) => extname(f).toLowerCase() === ".pdf")) {
1904
+ const abs = join4(target, name2);
1905
+ const seen = await getImportedFile(abs);
1906
+ let st;
1907
+ try {
1908
+ st = await stat2(abs);
1909
+ } catch {
1910
+ continue;
1911
+ }
1912
+ if (seen && seen.mtimeMs === st.mtimeMs) {
1913
+ skipped.push({ file: name2, reason: "already-imported" });
1914
+ continue;
1915
+ }
1916
+ let item;
1917
+ try {
1918
+ const hit = titleFromFilename(name2);
1919
+ if (!hit) {
1920
+ skipped.push({ file: name2, reason: "unparseable-name" });
1921
+ continue;
1922
+ }
1923
+ const provisional = buildItem({
1924
+ doi: hit.kind === "doi" ? hit.value : "",
1925
+ arxiv: hit.kind === "arxiv" ? hit.value : "",
1926
+ title: hit.kind === "title" ? hit.value : ""
1927
+ });
1928
+ if (!provisional.key) {
1929
+ skipped.push({ file: name2, reason: "no-identity" });
1930
+ continue;
1931
+ }
1932
+ const clash = await getItem(provisional.key);
1933
+ if (clash) {
1934
+ await addImportedFile(abs, st.mtimeMs);
1935
+ skipped.push({ file: name2, reason: "duplicate" });
1936
+ continue;
1937
+ }
1938
+ item = await putItem({
1939
+ ...provisional,
1940
+ kind: hit.kind,
1941
+ rawValue: hit.value,
1942
+ display: hit.value,
1943
+ title: provisional.title || hit.value,
1944
+ state: "discovered",
1945
+ sourceFile: abs,
1946
+ createdAt: Date.now()
1947
+ });
1948
+ emitItem(item);
1949
+ } catch (e) {
1950
+ warn(`import ${name2} failed:`, e.message);
1951
+ skipped.push({ file: name2, reason: e.message });
1952
+ continue;
1953
+ }
1954
+ if (autoResolve) {
1955
+ try {
1956
+ const { resolveItem: resolveItem2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
1957
+ item = await resolveItem2(item.key);
1958
+ } catch (e) {
1959
+ warn(`auto-resolve ${name2} failed:`, e.message);
1960
+ }
1961
+ }
1962
+ await addImportedFile(abs, st.mtimeMs);
1963
+ imported.push(item?.key ?? provisionalKeyOf(item));
1964
+ }
1965
+ return { imported, skipped, dir: target };
1966
+ }
1967
+ function provisionalKeyOf(item) {
1968
+ return item?.key ?? "";
1969
+ }
1970
+ function zoteroItemToRecord(it) {
1971
+ const d = it?.data ?? it ?? {};
1972
+ const year = /^(\d{4})/.exec(String(d.date ?? ""))?.[1];
1973
+ return {
1974
+ source: "zotero",
1975
+ itemType: d.itemType ?? "journalArticle",
1976
+ title: d.title ?? "",
1977
+ authors: (d.creators ?? []).map((c) => ({
1978
+ creatorType: c.creatorType ?? "author",
1979
+ firstName: c.firstName ?? "",
1980
+ lastName: c.lastName ?? ""
1981
+ })),
1982
+ year: year ? Number(year) : null,
1983
+ container: d.publicationTitle ?? d.bookTitle ?? d.proceedingsTitle ?? "",
1984
+ publisher: d.publisher ?? "",
1985
+ volume: d.volume ?? "",
1986
+ issue: d.issue ?? "",
1987
+ pages: d.pages ?? "",
1988
+ doi: d.DOI ?? "",
1989
+ isbn: d.ISBN ?? "",
1990
+ url: d.url ?? "",
1991
+ abstract: d.abstractNote ?? ""
1992
+ };
1993
+ }
1994
+ async function importFromZotero({ limit = 50 } = {}) {
1995
+ const items = await searchItems("", { limit });
1996
+ const imported = [];
1997
+ const skipped = [];
1998
+ for (const it of items) {
1999
+ const rec = zoteroItemToRecord(it);
2000
+ if (!rec.title) {
2001
+ skipped.push({ reason: "no-title" });
2002
+ continue;
2003
+ }
2004
+ const provisional = buildItem(rec);
2005
+ if (!provisional.key) {
2006
+ skipped.push({ reason: "no-identity" });
2007
+ continue;
2008
+ }
2009
+ const existing = await getItem(provisional.key);
2010
+ if (existing) {
2011
+ skipped.push({ reason: "duplicate" });
2012
+ continue;
2013
+ }
2014
+ let pdf = null;
2015
+ try {
2016
+ const children = await getItemChildren(it.key);
2017
+ const attach = (children ?? []).find(
2018
+ (c) => c.itemType === "attachment" && /pdf/i.test(c.contentType ?? c.contentType ?? "")
2019
+ );
2020
+ if (attach?.key) {
2021
+ const { buffer } = await getFileBuffer(attach.key).catch(() => ({ buffer: null }));
2022
+ if (buffer?.length && buffer.subarray(0, 5).equals(PDF_MAGIC2)) {
2023
+ pdf = await savePdfBuffer(provisional.key, buffer);
2024
+ }
2025
+ }
2026
+ } catch (e) {
2027
+ warn(`attachment import failed for ${it.key}:`, e.message);
2028
+ }
2029
+ const item = await putItem({
2030
+ ...provisional,
2031
+ record: rec,
2032
+ state: pdf ? "fetched" : "resolved",
2033
+ pdf,
2034
+ createdAt: Date.now()
2035
+ });
2036
+ emitItem(item);
2037
+ imported.push(item.key);
2038
+ }
2039
+ return { imported, skipped, count: imported.length };
2040
+ }
2041
+ function startWatcher() {
2042
+ if (watcherTimer) return () => stopWatcher();
2043
+ let stopping = false;
2044
+ const sweep = async () => {
2045
+ if (stopping) return;
2046
+ const config = await loadConfig();
2047
+ if (!config.watchImport || !config.importDir) return;
2048
+ try {
2049
+ const r = await importDir(config.importDir, { autoResolve: config.autoResolve !== false });
2050
+ if (r.imported.length) log(`folder watch imported ${r.imported.length} new file(s)`);
2051
+ } catch (e) {
2052
+ warn("folder watch sweep failed:", e.message);
2053
+ }
2054
+ };
2055
+ const initial = setTimeout(sweep, 5e3);
2056
+ initial.unref?.();
2057
+ const timer = setInterval(sweep, 3e4);
2058
+ timer.unref?.();
2059
+ watcherTimer = { timer, initial };
2060
+ return () => stopWatcher();
2061
+ }
2062
+ function stopWatcher() {
2063
+ if (!watcherTimer) return;
2064
+ clearInterval(watcherTimer.timer);
2065
+ clearTimeout(watcherTimer.initial);
2066
+ watcherTimer = null;
2067
+ }
2068
+ var PDF_MAGIC2, watcherTimer;
2069
+ var init_importer = __esm({
2070
+ "src/node/importer.js"() {
2071
+ init_config();
2072
+ init_db();
2073
+ init_sse();
2074
+ init_dedupe();
2075
+ init_metadata();
2076
+ init_local_api();
2077
+ init_log();
2078
+ PDF_MAGIC2 = Buffer.from("%PDF-", "latin1");
2079
+ watcherTimer = null;
2080
+ }
2081
+ });
2082
+
1859
2083
  // src/node/pipeline.js
1860
2084
  var pipeline_exports = {};
1861
2085
  __export(pipeline_exports, {
1862
2086
  arxivBase: () => arxivBase,
1863
2087
  discardItem: () => discardItem,
1864
2088
  fetchItemPdf: () => fetchItemPdf,
2089
+ importDroppedPdf: () => importDroppedPdf,
1865
2090
  importPdf: () => importPdf,
1866
2091
  log: () => log,
1867
2092
  normalizeDoi: () => normalizeDoi,
@@ -1873,8 +2098,8 @@ __export(pipeline_exports, {
1873
2098
  startTask: () => startTask
1874
2099
  });
1875
2100
  import { randomUUID as randomUUID2 } from "node:crypto";
1876
- import { writeFile as writeFile3, unlink, mkdir as mkdir3 } from "node:fs/promises";
1877
- import { join as join4, dirname as dirname2 } from "node:path";
2101
+ import { writeFile as writeFile4, unlink, mkdir as mkdir4 } from "node:fs/promises";
2102
+ import { join as join5, dirname as dirname3 } from "node:path";
1878
2103
  function failure(code, message, extra = {}) {
1879
2104
  return {
1880
2105
  code,
@@ -1885,7 +2110,7 @@ function failure(code, message, extra = {}) {
1885
2110
  };
1886
2111
  }
1887
2112
  function pdfPathFor(key) {
1888
- return join4(PDF_DIR, `${key.replace(/[^\w.-]+/g, "_")}.pdf`);
2113
+ return join5(PDF_DIR, `${key.replace(/[^\w.-]+/g, "_")}.pdf`);
1889
2114
  }
1890
2115
  async function update(key, patch) {
1891
2116
  const next = await patchItem(key, patch);
@@ -2001,8 +2226,8 @@ async function fetchItemPdf(key) {
2001
2226
  { ...config.retry, label: `fetch ${key}` }
2002
2227
  );
2003
2228
  const path = pdfPathFor(key);
2004
- await mkdir3(dirname2(path), { recursive: true });
2005
- await writeFile3(path, result.buffer);
2229
+ await mkdir4(dirname3(path), { recursive: true });
2230
+ await writeFile4(path, result.buffer);
2006
2231
  const updated = await patchItem(key, {
2007
2232
  state: "fetched",
2008
2233
  pdf: { path, size: result.buffer.length, source: result.source, url: result.url },
@@ -2154,8 +2379,8 @@ async function importPdf(key, buffer, { filename = "imported.pdf", autoSave = tr
2154
2379
  throw failure("network", "\u6240\u9009\u6587\u4EF6\u4E0D\u662F\u6709\u6548\u7684 PDF");
2155
2380
  }
2156
2381
  const path = pdfPathFor(key);
2157
- await mkdir3(dirname2(path), { recursive: true });
2158
- await writeFile3(path, buffer);
2382
+ await mkdir4(dirname3(path), { recursive: true });
2383
+ await writeFile4(path, buffer);
2159
2384
  const updated = await update(key, {
2160
2385
  state: "fetched",
2161
2386
  pdf: { path, size: buffer.length, source: "local-import", url: "", filename },
@@ -2163,10 +2388,66 @@ async function importPdf(key, buffer, { filename = "imported.pdf", autoSave = tr
2163
2388
  });
2164
2389
  let saved = updated;
2165
2390
  if (autoSave) {
2166
- saved = await saveItem(key);
2391
+ saved = await saveItem(key, { mode: "builtin" });
2167
2392
  }
2168
2393
  return saved;
2169
2394
  }
2395
+ async function importDroppedPdf(buffer, { filename = "dropped.pdf" } = {}) {
2396
+ if (!buffer || buffer.length < 8 || !buffer.subarray(0, 5).equals(Buffer.from("%PDF-", "latin1"))) {
2397
+ throw failure("network", "\u62D6\u5165\u7684\u6587\u4EF6\u4E0D\u662F\u6709\u6548\u7684 PDF");
2398
+ }
2399
+ const { titleFromFilename: titleFromFilename2 } = await Promise.resolve().then(() => (init_importer(), importer_exports));
2400
+ const hit = titleFromFilename2(filename);
2401
+ const provisional = buildItem(
2402
+ hit ? {
2403
+ doi: hit.kind === "doi" ? hit.value : "",
2404
+ arxiv: hit.kind === "arxiv" ? hit.value : "",
2405
+ title: hit.kind === "title" ? hit.value : ""
2406
+ } : { title: String(filename).replace(/\.pdf$/i, "") }
2407
+ );
2408
+ if (!provisional.key) throw failure("network", "\u65E0\u6CD5\u4ECE\u6587\u4EF6\u540D\u8BC6\u522B\u8BE5\u6587\u732E");
2409
+ const clash = await getItem(provisional.key);
2410
+ if (clash) {
2411
+ const path2 = pdfPathFor(provisional.key);
2412
+ await mkdir4(dirname3(path2), { recursive: true });
2413
+ await writeFile4(path2, buffer);
2414
+ const updated = await patchItem(provisional.key, {
2415
+ state: "fetched",
2416
+ pdf: { path: path2, size: buffer.length, source: "local-import", url: "", filename },
2417
+ error: null,
2418
+ updatedAt: Date.now()
2419
+ });
2420
+ emitItem(updated);
2421
+ return updated;
2422
+ }
2423
+ const path = pdfPathFor(provisional.key);
2424
+ await mkdir4(dirname3(path), { recursive: true });
2425
+ await writeFile4(path, buffer);
2426
+ let item = await putItem({
2427
+ ...provisional,
2428
+ kind: hit?.kind ?? "title",
2429
+ rawValue: hit?.value ?? "",
2430
+ display: hit?.value ?? provisional.title,
2431
+ sourceFile: filename,
2432
+ state: "fetched",
2433
+ pdf: { path, size: buffer.length, source: "drop-import", url: "", filename },
2434
+ createdAt: Date.now()
2435
+ });
2436
+ emitItem(item);
2437
+ try {
2438
+ item = await resolveItem(item.key);
2439
+ } catch {
2440
+ }
2441
+ if (!item.record) {
2442
+ item = await patchItem(item.key, {
2443
+ record: { itemType: "journalArticle", title: item.title || provisional.title, authors: [], year: null },
2444
+ state: "resolved"
2445
+ });
2446
+ }
2447
+ const saved = await saveItem(item.key, { mode: "builtin" });
2448
+ emitItem(saved);
2449
+ return saved;
2450
+ }
2170
2451
  var FAILURE_MESSAGES;
2171
2452
  var init_pipeline = __esm({
2172
2453
  "src/node/pipeline.js"() {
@@ -2367,230 +2648,6 @@ var init_cite = __esm({
2367
2648
  }
2368
2649
  });
2369
2650
 
2370
- // src/node/importer.js
2371
- var importer_exports = {};
2372
- __export(importer_exports, {
2373
- importDir: () => importDir,
2374
- importFromZotero: () => importFromZotero,
2375
- startWatcher: () => startWatcher,
2376
- stopWatcher: () => stopWatcher,
2377
- titleFromFilename: () => titleFromFilename,
2378
- zoteroItemToRecord: () => zoteroItemToRecord
2379
- });
2380
- import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "node:fs/promises";
2381
- import { join as join5, resolve as resolve4, extname, basename } from "node:path";
2382
- import { writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
2383
- import { dirname as dirname3 } from "node:path";
2384
- function titleFromFilename(name2) {
2385
- let t = String(name2).replace(/\.pdf$/i, "");
2386
- const doi = /(10\.\d{4,9}[\/_][^\s]+)/i.exec(t);
2387
- if (doi) return { kind: "doi", value: doi[1].replace(/_/g, "/") };
2388
- const arxiv = /\b(\d{4}\.\d{4,5})(v\d+)?\b/.exec(t);
2389
- if (arxiv) return { kind: "arxiv", value: arxiv[1] + (arxiv[2] ?? "") };
2390
- t = t.replace(/^\[\d+\]\s*/, "").replace(/\s*\(?\d{4}[a-z]?\)?\s*$/i, "").replace(/[_\-]+/g, " ").replace(/^\d+[\s.]+/, "").trim();
2391
- if (!t) return null;
2392
- return { kind: "title", value: t };
2393
- }
2394
- function pathForPdfKey(key) {
2395
- return join5(PDF_DIR, `${key.replace(/[^\w.-]+/g, "_")}.pdf`);
2396
- }
2397
- async function savePdfBuffer(key, buffer) {
2398
- const path = pathForPdfKey(key);
2399
- await mkdir4(dirname3(path), { recursive: true });
2400
- await writeFile4(path, buffer);
2401
- return { path, size: buffer.length, source: "library-import" };
2402
- }
2403
- async function importDir(dir, { autoResolve = true } = {}) {
2404
- const target = resolve4(dir || "");
2405
- if (!target) throw Object.assign(new Error("\u672A\u914D\u7F6E\u5BFC\u5165\u6587\u4EF6\u5939"), { code: "no_dir" });
2406
- let files;
2407
- try {
2408
- files = await readdir2(target);
2409
- } catch (e) {
2410
- throw Object.assign(new Error(`\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6\u5939 ${target}`), { code: "no_dir", cause: e });
2411
- }
2412
- const imported = [];
2413
- const skipped = [];
2414
- for (const name2 of files.filter((f) => extname(f).toLowerCase() === ".pdf")) {
2415
- const abs = join5(target, name2);
2416
- const seen = await getImportedFile(abs);
2417
- let st;
2418
- try {
2419
- st = await stat2(abs);
2420
- } catch {
2421
- continue;
2422
- }
2423
- if (seen && seen.mtimeMs === st.mtimeMs) {
2424
- skipped.push({ file: name2, reason: "already-imported" });
2425
- continue;
2426
- }
2427
- let item;
2428
- try {
2429
- const hit = titleFromFilename(name2);
2430
- if (!hit) {
2431
- skipped.push({ file: name2, reason: "unparseable-name" });
2432
- continue;
2433
- }
2434
- const provisional = buildItem({
2435
- doi: hit.kind === "doi" ? hit.value : "",
2436
- arxiv: hit.kind === "arxiv" ? hit.value : "",
2437
- title: hit.kind === "title" ? hit.value : ""
2438
- });
2439
- if (!provisional.key) {
2440
- skipped.push({ file: name2, reason: "no-identity" });
2441
- continue;
2442
- }
2443
- const clash = await getItem(provisional.key);
2444
- if (clash) {
2445
- await addImportedFile(abs, st.mtimeMs);
2446
- skipped.push({ file: name2, reason: "duplicate" });
2447
- continue;
2448
- }
2449
- item = await putItem({
2450
- ...provisional,
2451
- kind: hit.kind,
2452
- rawValue: hit.value,
2453
- display: hit.value,
2454
- title: provisional.title || hit.value,
2455
- state: "discovered",
2456
- sourceFile: abs,
2457
- createdAt: Date.now()
2458
- });
2459
- emitItem(item);
2460
- } catch (e) {
2461
- warn(`import ${name2} failed:`, e.message);
2462
- skipped.push({ file: name2, reason: e.message });
2463
- continue;
2464
- }
2465
- if (autoResolve) {
2466
- try {
2467
- const { resolveItem: resolveItem2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
2468
- item = await resolveItem2(item.key);
2469
- } catch (e) {
2470
- warn(`auto-resolve ${name2} failed:`, e.message);
2471
- }
2472
- }
2473
- await addImportedFile(abs, st.mtimeMs);
2474
- imported.push(item?.key ?? provisionalKeyOf(item));
2475
- }
2476
- return { imported, skipped, dir: target };
2477
- }
2478
- function provisionalKeyOf(item) {
2479
- return item?.key ?? "";
2480
- }
2481
- function zoteroItemToRecord(it) {
2482
- const d = it?.data ?? it ?? {};
2483
- const year = /^(\d{4})/.exec(String(d.date ?? ""))?.[1];
2484
- return {
2485
- source: "zotero",
2486
- itemType: d.itemType ?? "journalArticle",
2487
- title: d.title ?? "",
2488
- authors: (d.creators ?? []).map((c) => ({
2489
- creatorType: c.creatorType ?? "author",
2490
- firstName: c.firstName ?? "",
2491
- lastName: c.lastName ?? ""
2492
- })),
2493
- year: year ? Number(year) : null,
2494
- container: d.publicationTitle ?? d.bookTitle ?? d.proceedingsTitle ?? "",
2495
- publisher: d.publisher ?? "",
2496
- volume: d.volume ?? "",
2497
- issue: d.issue ?? "",
2498
- pages: d.pages ?? "",
2499
- doi: d.DOI ?? "",
2500
- isbn: d.ISBN ?? "",
2501
- url: d.url ?? "",
2502
- abstract: d.abstractNote ?? ""
2503
- };
2504
- }
2505
- async function importFromZotero({ limit = 50 } = {}) {
2506
- const items = await searchItems("", { limit });
2507
- const imported = [];
2508
- const skipped = [];
2509
- for (const it of items) {
2510
- const rec = zoteroItemToRecord(it);
2511
- if (!rec.title) {
2512
- skipped.push({ reason: "no-title" });
2513
- continue;
2514
- }
2515
- const provisional = buildItem(rec);
2516
- if (!provisional.key) {
2517
- skipped.push({ reason: "no-identity" });
2518
- continue;
2519
- }
2520
- const existing = await getItem(provisional.key);
2521
- if (existing) {
2522
- skipped.push({ reason: "duplicate" });
2523
- continue;
2524
- }
2525
- let pdf = null;
2526
- try {
2527
- const children = await getItemChildren(it.key);
2528
- const attach = (children ?? []).find(
2529
- (c) => c.itemType === "attachment" && /pdf/i.test(c.contentType ?? c.contentType ?? "")
2530
- );
2531
- if (attach?.key) {
2532
- const { buffer } = await getFileBuffer(attach.key).catch(() => ({ buffer: null }));
2533
- if (buffer?.length && buffer.subarray(0, 5).equals(PDF_MAGIC2)) {
2534
- pdf = await savePdfBuffer(provisional.key, buffer);
2535
- }
2536
- }
2537
- } catch (e) {
2538
- warn(`attachment import failed for ${it.key}:`, e.message);
2539
- }
2540
- const item = await putItem({
2541
- ...provisional,
2542
- record: rec,
2543
- state: pdf ? "fetched" : "resolved",
2544
- pdf,
2545
- createdAt: Date.now()
2546
- });
2547
- emitItem(item);
2548
- imported.push(item.key);
2549
- }
2550
- return { imported, skipped, count: imported.length };
2551
- }
2552
- function startWatcher() {
2553
- if (watcherTimer) return () => stopWatcher();
2554
- let stopping = false;
2555
- const sweep = async () => {
2556
- if (stopping) return;
2557
- const config = await loadConfig();
2558
- if (!config.watchImport || !config.importDir) return;
2559
- try {
2560
- const r = await importDir(config.importDir, { autoResolve: config.autoResolve !== false });
2561
- if (r.imported.length) log(`folder watch imported ${r.imported.length} new file(s)`);
2562
- } catch (e) {
2563
- warn("folder watch sweep failed:", e.message);
2564
- }
2565
- };
2566
- const initial = setTimeout(sweep, 5e3);
2567
- initial.unref?.();
2568
- const timer = setInterval(sweep, 3e4);
2569
- timer.unref?.();
2570
- watcherTimer = { timer, initial };
2571
- return () => stopWatcher();
2572
- }
2573
- function stopWatcher() {
2574
- if (!watcherTimer) return;
2575
- clearInterval(watcherTimer.timer);
2576
- clearTimeout(watcherTimer.initial);
2577
- watcherTimer = null;
2578
- }
2579
- var PDF_MAGIC2, watcherTimer;
2580
- var init_importer = __esm({
2581
- "src/node/importer.js"() {
2582
- init_config();
2583
- init_db();
2584
- init_sse();
2585
- init_dedupe();
2586
- init_metadata();
2587
- init_local_api();
2588
- init_log();
2589
- PDF_MAGIC2 = Buffer.from("%PDF-", "latin1");
2590
- watcherTimer = null;
2591
- }
2592
- });
2593
-
2594
2651
  // src/node/index.js
2595
2652
  init_config();
2596
2653
  init_db();
@@ -2821,6 +2878,14 @@ async function handler(req, res) {
2821
2878
  writeJson(res, 200, result);
2822
2879
  return;
2823
2880
  }
2881
+ if (head === "drop" && methodOk(req, "POST")) {
2882
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
2883
+ const filename = url.searchParams.get("filename") || "dropped.pdf";
2884
+ const buffer = await readRawBody(req, 128 * 1024 * 1024);
2885
+ const item = await importDroppedPdf(buffer, { filename });
2886
+ writeJson(res, 200, { item });
2887
+ return;
2888
+ }
2824
2889
  if (head === "import-zotero" && methodOk(req, "POST")) {
2825
2890
  const body = await readJsonBody2(req);
2826
2891
  const { importFromZotero: importFromZotero2 } = await Promise.resolve().then(() => (init_importer(), importer_exports));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a9i5k4/dsh-literature",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "DSH Literature 文献侧窗:在 DeepSeek Harness 侧边栏识别 DOI/arXiv/标题、抓取元数据与全文、写入本地文献库或导出目录,并提供内置 PDF 阅读器(缩放/翻页/目录/搜索/高亮/笔记)。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",