@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.
- package/lib/client.js +17 -12
- package/lib/index.js +297 -232
- 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
|
|
1877
|
-
import { join as
|
|
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
|
|
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
|
|
2005
|
-
await
|
|
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
|
|
2158
|
-
await
|
|
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