@a9i5k4/dsh-literature 0.2.3 → 0.2.5
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 +35 -13
- package/lib/index.js +265 -11
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -554,6 +554,12 @@ function extractIdentifiers(text, options = {}) {
|
|
|
554
554
|
while ((m = ISBN.exec(source)) !== null) {
|
|
555
555
|
push(out, seen, { kind: "isbn", value: m[1].replace(/[-\s]/g, ""), display: m[1], index: offset + m.index, confidence: 0.85 + confidenceBonus });
|
|
556
556
|
}
|
|
557
|
+
URL_RE.lastIndex = 0;
|
|
558
|
+
while ((m = URL_RE.exec(source)) !== null) {
|
|
559
|
+
const raw = trimRight(m[0]);
|
|
560
|
+
if (/doi\.org|arxiv\.org|dx\.doi/i.test(raw)) continue;
|
|
561
|
+
push(out, seen, { kind: "url", value: raw, display: raw, index: offset + m.index, confidence: 0.9 + confidenceBonus });
|
|
562
|
+
}
|
|
557
563
|
};
|
|
558
564
|
scan(src);
|
|
559
565
|
for (const url of linkTargets) scan(url, 0, 0.02);
|
|
@@ -571,7 +577,7 @@ function extractIdentifiers(text, options = {}) {
|
|
|
571
577
|
}
|
|
572
578
|
return out.sort((a, b) => a.index - b.index);
|
|
573
579
|
}
|
|
574
|
-
var TRIM_RIGHT, CJK_PUNCT, DOI_TAIL, DOI_CORE, DOI_HINT, DOI_LABEL, ARXIV_NEW, ARXIV_OLD, ARXIV_URL, PMID, ISBN, QUOTED, QUOTED_FALLBACK;
|
|
580
|
+
var TRIM_RIGHT, CJK_PUNCT, DOI_TAIL, DOI_CORE, DOI_HINT, DOI_LABEL, ARXIV_NEW, ARXIV_OLD, URL_RE, ARXIV_URL, PMID, ISBN, QUOTED, QUOTED_FALLBACK;
|
|
575
581
|
var init_identifiers = __esm({
|
|
576
582
|
"src/node/extract/identifiers.js"() {
|
|
577
583
|
TRIM_RIGHT = /[.,;:!?。、,;:!?…—>'">)》\]]+$/;
|
|
@@ -582,6 +588,7 @@ var init_identifiers = __esm({
|
|
|
582
588
|
DOI_LABEL = new RegExp(`\\bDOI\\s*[:\uFF1A]\\s*(10\\.\\d{4,9}\\/${DOI_TAIL})`, "gi");
|
|
583
589
|
ARXIV_NEW = /\barXiv\s*[:. ]?\s*(\d{4}\.\d{4,5})(v\d+)?\b/gi;
|
|
584
590
|
ARXIV_OLD = /\barXiv\s*[:. ]?\s*([a-z][a-z-]*(?:\.[A-Z]{2})?\/\d{7})(v\d+)?\b/gi;
|
|
591
|
+
URL_RE = /\bhttps?:\/\/[^\s<>"']+/gi;
|
|
585
592
|
ARXIV_URL = /arxiv\.org\/(?:abs|pdf)\/([^\s"'?#>]+?)(?:\.pdf)?(?=[\s"'?#>()]|$)/gi;
|
|
586
593
|
PMID = /\bPMID\s*[::]?\s*(\d{1,8})\b/gi;
|
|
587
594
|
ISBN = /\bISBN(?:-1[03])?\s*[::]?\s*((?:97[89][-\s]?)?(?:\d[-\s]?){9}[\dXx])\b/gi;
|
|
@@ -1856,6 +1863,136 @@ var init_local_api = __esm({
|
|
|
1856
1863
|
}
|
|
1857
1864
|
});
|
|
1858
1865
|
|
|
1866
|
+
// src/node/metadata/search.js
|
|
1867
|
+
var search_exports = {};
|
|
1868
|
+
__export(search_exports, {
|
|
1869
|
+
searchCandidates: () => searchCandidates
|
|
1870
|
+
});
|
|
1871
|
+
function crossrefType(t) {
|
|
1872
|
+
switch (t) {
|
|
1873
|
+
case "journal-article":
|
|
1874
|
+
return "journalArticle";
|
|
1875
|
+
case "book":
|
|
1876
|
+
return "book";
|
|
1877
|
+
case "book-chapter":
|
|
1878
|
+
return "bookSection";
|
|
1879
|
+
case "proceedings-article":
|
|
1880
|
+
return "conferencePaper";
|
|
1881
|
+
case "posted-content":
|
|
1882
|
+
return "preprint";
|
|
1883
|
+
case "dissertation":
|
|
1884
|
+
return "thesis";
|
|
1885
|
+
case "report":
|
|
1886
|
+
return "report";
|
|
1887
|
+
case "webpage":
|
|
1888
|
+
return "webpage";
|
|
1889
|
+
default:
|
|
1890
|
+
return "journalArticle";
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
async function searchCandidates(query, { rows = 8 } = {}) {
|
|
1894
|
+
const q = String(query ?? "").trim();
|
|
1895
|
+
if (!q) return [];
|
|
1896
|
+
const url = `https://api.crossref.org/works?query.bibliographic=${encodeURIComponent(q)}&rows=${Math.max(1, Math.min(rows, 20))}&select=DOI,title,author,issued,container-title,type,volume,issue,page`;
|
|
1897
|
+
let data;
|
|
1898
|
+
try {
|
|
1899
|
+
data = await httpGetJson(url);
|
|
1900
|
+
} catch {
|
|
1901
|
+
return [];
|
|
1902
|
+
}
|
|
1903
|
+
const out = [];
|
|
1904
|
+
for (const it of data?.message?.items ?? []) {
|
|
1905
|
+
const title = it.title?.[0];
|
|
1906
|
+
if (!title) continue;
|
|
1907
|
+
out.push({
|
|
1908
|
+
doi: it.DOI ?? "",
|
|
1909
|
+
title,
|
|
1910
|
+
authors: (it.author ?? []).map((a) => ({ firstName: a.given ?? "", lastName: a.family ?? "" })),
|
|
1911
|
+
year: it.issued?.["date-parts"]?.[0]?.[0] ?? null,
|
|
1912
|
+
container: it["container-title"]?.[0] ?? "",
|
|
1913
|
+
volume: it.volume ?? "",
|
|
1914
|
+
issue: it.issue ?? "",
|
|
1915
|
+
pages: it.page ?? "",
|
|
1916
|
+
itemType: crossrefType(it.type)
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
return out;
|
|
1920
|
+
}
|
|
1921
|
+
var init_search = __esm({
|
|
1922
|
+
"src/node/metadata/search.js"() {
|
|
1923
|
+
init_net();
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
// src/node/metadata/url.js
|
|
1928
|
+
var url_exports = {};
|
|
1929
|
+
__export(url_exports, {
|
|
1930
|
+
resolveUrlPage: () => resolveUrlPage
|
|
1931
|
+
});
|
|
1932
|
+
function firstMatch(html, patterns) {
|
|
1933
|
+
for (const re of patterns) {
|
|
1934
|
+
const m = re.exec(html);
|
|
1935
|
+
if (m?.[1]) return m[1].trim();
|
|
1936
|
+
}
|
|
1937
|
+
return "";
|
|
1938
|
+
}
|
|
1939
|
+
async function resolveUrlPage(url, { timeoutMs = 15e3 } = {}) {
|
|
1940
|
+
let html = "";
|
|
1941
|
+
let error2 = "";
|
|
1942
|
+
try {
|
|
1943
|
+
const res = await httpGet(url, {
|
|
1944
|
+
timeoutMs,
|
|
1945
|
+
accept: "text/html,application/xhtml+xml,*/*;q=0.8",
|
|
1946
|
+
// Many publisher pages 403 bare requests; a browser UA keeps them open.
|
|
1947
|
+
headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36" }
|
|
1948
|
+
});
|
|
1949
|
+
html = await res.text();
|
|
1950
|
+
} catch (e) {
|
|
1951
|
+
error2 = e?.message ?? String(e);
|
|
1952
|
+
}
|
|
1953
|
+
const title = firstMatch(html, META_PATTERNS);
|
|
1954
|
+
const doi = firstMatch(html, DOI_PATTERNS);
|
|
1955
|
+
const author = firstMatch(html, AUTHOR_PATTERNS);
|
|
1956
|
+
const year = firstMatch(html, DATE_PATTERNS);
|
|
1957
|
+
const cleanedTitle = title.replace(/[|–-].*$/, "").trim();
|
|
1958
|
+
return {
|
|
1959
|
+
title: cleanedTitle || "",
|
|
1960
|
+
doi,
|
|
1961
|
+
authors: author ? [{ firstName: "", lastName: author }] : [],
|
|
1962
|
+
year: year ? Number(year) : null,
|
|
1963
|
+
url,
|
|
1964
|
+
error: error2
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
var META_PATTERNS, DOI_PATTERNS, AUTHOR_PATTERNS, DATE_PATTERNS;
|
|
1968
|
+
var init_url = __esm({
|
|
1969
|
+
"src/node/metadata/url.js"() {
|
|
1970
|
+
init_net();
|
|
1971
|
+
META_PATTERNS = [
|
|
1972
|
+
// <meta name="citation_title" content="..."> — order of attrs varies
|
|
1973
|
+
/<meta[^>]+name=["']citation_title["'][^>]+content=["']([^"']+)["']/i,
|
|
1974
|
+
/<meta[^>]+content=["']([^"']+)["'][^>]+name=["']citation_title["']/i,
|
|
1975
|
+
// Open Graph
|
|
1976
|
+
/<meta[^>]+property=["']og:title["'][^>]+content=["']([^"']+)["']/i,
|
|
1977
|
+
/<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:title["']/i,
|
|
1978
|
+
// fallback
|
|
1979
|
+
/<title[^>]*>([^<]+)<\/title>/i
|
|
1980
|
+
];
|
|
1981
|
+
DOI_PATTERNS = [
|
|
1982
|
+
/<meta[^>]+name=["']citation_doi["'][^>]+content=["']([^"']+)["']/i,
|
|
1983
|
+
/<meta[^>]+content=["']([^"']+)["'][^>]+name=["']citation_doi["']/i
|
|
1984
|
+
];
|
|
1985
|
+
AUTHOR_PATTERNS = [
|
|
1986
|
+
/<meta[^>]+name=["']citation_author["'][^>]+content=["']([^"']+)["']/i,
|
|
1987
|
+
/<meta[^>]+content=["']([^"']+)["'][^>]+name=["']citation_author["']/i
|
|
1988
|
+
];
|
|
1989
|
+
DATE_PATTERNS = [
|
|
1990
|
+
/<meta[^>]+name=["']citation_publication_date["'][^>]+content=["'](\d{4})/i,
|
|
1991
|
+
/<meta[^>]+content=["'](\d{4})["'][^>]+name=["']citation_publication_date["']/i
|
|
1992
|
+
];
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1859
1996
|
// src/node/importer.js
|
|
1860
1997
|
var importer_exports = {};
|
|
1861
1998
|
__export(importer_exports, {
|
|
@@ -1935,13 +2072,23 @@ async function importDir(dir, { autoResolve = true } = {}) {
|
|
|
1935
2072
|
skipped.push({ file: name2, reason: "duplicate" });
|
|
1936
2073
|
continue;
|
|
1937
2074
|
}
|
|
2075
|
+
let pdf = null;
|
|
2076
|
+
try {
|
|
2077
|
+
const content = await readFile3(abs);
|
|
2078
|
+
if (content.subarray(0, 5).equals(PDF_MAGIC2)) {
|
|
2079
|
+
pdf = await savePdfBuffer(provisional.key, content);
|
|
2080
|
+
}
|
|
2081
|
+
} catch (e) {
|
|
2082
|
+
warn(`copy pdf failed for ${name2}:`, e.message);
|
|
2083
|
+
}
|
|
1938
2084
|
item = await putItem({
|
|
1939
2085
|
...provisional,
|
|
1940
2086
|
kind: hit.kind,
|
|
1941
2087
|
rawValue: hit.value,
|
|
1942
2088
|
display: hit.value,
|
|
1943
2089
|
title: provisional.title || hit.value,
|
|
1944
|
-
state: "discovered",
|
|
2090
|
+
state: pdf ? "fetched" : "discovered",
|
|
2091
|
+
pdf,
|
|
1945
2092
|
sourceFile: abs,
|
|
1946
2093
|
createdAt: Date.now()
|
|
1947
2094
|
});
|
|
@@ -2083,6 +2230,7 @@ var init_importer = __esm({
|
|
|
2083
2230
|
// src/node/pipeline.js
|
|
2084
2231
|
var pipeline_exports = {};
|
|
2085
2232
|
__export(pipeline_exports, {
|
|
2233
|
+
addCandidate: () => addCandidate,
|
|
2086
2234
|
arxivBase: () => arxivBase,
|
|
2087
2235
|
discardItem: () => discardItem,
|
|
2088
2236
|
fetchItemPdf: () => fetchItemPdf,
|
|
@@ -2095,6 +2243,7 @@ __export(pipeline_exports, {
|
|
|
2095
2243
|
retryItem: () => retryItem,
|
|
2096
2244
|
saveItem: () => saveItem,
|
|
2097
2245
|
scanText: () => scanText,
|
|
2246
|
+
searchCandidates: () => searchCandidates2,
|
|
2098
2247
|
startTask: () => startTask
|
|
2099
2248
|
});
|
|
2100
2249
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -2141,14 +2290,21 @@ async function scanText(text) {
|
|
|
2141
2290
|
const existing = await listItems();
|
|
2142
2291
|
const created = [];
|
|
2143
2292
|
for (const hit of found) {
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2293
|
+
let provisional;
|
|
2294
|
+
if (hit.kind === "url") {
|
|
2295
|
+
provisional = buildItem({ title: hit.value });
|
|
2296
|
+
if (!provisional.key) continue;
|
|
2297
|
+
provisional = { ...provisional, title: hit.value };
|
|
2298
|
+
} else {
|
|
2299
|
+
provisional = buildItem({
|
|
2300
|
+
doi: hit.kind === "doi" ? hit.value : "",
|
|
2301
|
+
arxiv: hit.kind === "arxiv" ? hit.value : "",
|
|
2302
|
+
isbn: hit.kind === "isbn" ? hit.value : "",
|
|
2303
|
+
pmid: hit.kind === "pmid" ? hit.value : "",
|
|
2304
|
+
title: hit.kind === "title" ? hit.value : ""
|
|
2305
|
+
});
|
|
2306
|
+
if (!provisional.key) continue;
|
|
2307
|
+
}
|
|
2152
2308
|
const clash = existing.find((e) => sameWork(e, provisional)) ?? await getItem(provisional.key);
|
|
2153
2309
|
if (clash) continue;
|
|
2154
2310
|
const item = await putItem({
|
|
@@ -2166,6 +2322,41 @@ async function scanText(text) {
|
|
|
2166
2322
|
}
|
|
2167
2323
|
return created;
|
|
2168
2324
|
}
|
|
2325
|
+
async function searchCandidates2(query, rows) {
|
|
2326
|
+
const { searchCandidates: search } = await Promise.resolve().then(() => (init_search(), search_exports));
|
|
2327
|
+
return search(query, { rows });
|
|
2328
|
+
}
|
|
2329
|
+
async function addCandidate(candidate) {
|
|
2330
|
+
const rec = {
|
|
2331
|
+
itemType: candidate.itemType ?? "journalArticle",
|
|
2332
|
+
title: candidate.title ?? "",
|
|
2333
|
+
authors: candidate.authors ?? [],
|
|
2334
|
+
year: candidate.year ?? null,
|
|
2335
|
+
container: candidate.container ?? "",
|
|
2336
|
+
volume: candidate.volume ?? "",
|
|
2337
|
+
issue: candidate.issue ?? "",
|
|
2338
|
+
pages: candidate.pages ?? "",
|
|
2339
|
+
doi: candidate.doi ?? ""
|
|
2340
|
+
};
|
|
2341
|
+
const provisional = buildItem(rec);
|
|
2342
|
+
if (!provisional.key) throw failure("no_metadata", "\u5019\u9009\u7F3A\u5C11\u53EF\u8BC6\u522B\u4FE1\u606F");
|
|
2343
|
+
const clash = await getItem(provisional.key);
|
|
2344
|
+
if (clash) {
|
|
2345
|
+
emitItem(clash);
|
|
2346
|
+
return clash;
|
|
2347
|
+
}
|
|
2348
|
+
const item = await putItem({
|
|
2349
|
+
...provisional,
|
|
2350
|
+
kind: candidate.doi ? "doi" : "title",
|
|
2351
|
+
rawValue: candidate.doi || rec.title,
|
|
2352
|
+
display: rec.title,
|
|
2353
|
+
record: rec,
|
|
2354
|
+
state: "resolved",
|
|
2355
|
+
createdAt: Date.now()
|
|
2356
|
+
});
|
|
2357
|
+
emitItem(item);
|
|
2358
|
+
return item;
|
|
2359
|
+
}
|
|
2169
2360
|
async function resolveItem(key) {
|
|
2170
2361
|
const item = await getItem(key);
|
|
2171
2362
|
if (!item) throw failure("not_found", "\u6761\u76EE\u4E0D\u5B58\u5728");
|
|
@@ -2174,6 +2365,52 @@ async function resolveItem(key) {
|
|
|
2174
2365
|
await update(key, { state: "resolving", error: null });
|
|
2175
2366
|
const config = await loadConfig();
|
|
2176
2367
|
try {
|
|
2368
|
+
if (item.kind === "url") {
|
|
2369
|
+
const { resolveUrlPage: resolveUrlPage2 } = await Promise.resolve().then(() => (init_url(), url_exports));
|
|
2370
|
+
const page = await resolveUrlPage2(item.rawValue || item.title, { timeoutMs: 2e4 });
|
|
2371
|
+
if (page.doi) {
|
|
2372
|
+
const record2 = await resolveIdentifier({ kind: "doi", value: page.doi }, { timeoutMs: 2e4, unpaywallEmail: config.unpaywallEmail });
|
|
2373
|
+
if (record2) {
|
|
2374
|
+
const merged2 = buildItem({ ...item, ...record2 });
|
|
2375
|
+
const updated2 = await patchItem(key, {
|
|
2376
|
+
...merged2,
|
|
2377
|
+
key,
|
|
2378
|
+
state: "resolved",
|
|
2379
|
+
record: record2,
|
|
2380
|
+
url: item.rawValue || page.url,
|
|
2381
|
+
error: null,
|
|
2382
|
+
updatedAt: Date.now()
|
|
2383
|
+
});
|
|
2384
|
+
emitItem(updated2);
|
|
2385
|
+
await finishTask(task, "done", "\u5143\u6570\u636E\u89E3\u6790\u5B8C\u6210");
|
|
2386
|
+
return updated2;
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
if (page.title) {
|
|
2390
|
+
const record2 = {
|
|
2391
|
+
itemType: "webpage",
|
|
2392
|
+
title: page.title,
|
|
2393
|
+
authors: page.authors ?? [],
|
|
2394
|
+
year: page.year ?? null,
|
|
2395
|
+
url: item.rawValue || page.url,
|
|
2396
|
+
doi: page.doi
|
|
2397
|
+
};
|
|
2398
|
+
const updated2 = await patchItem(key, {
|
|
2399
|
+
key,
|
|
2400
|
+
state: "resolved",
|
|
2401
|
+
record: record2,
|
|
2402
|
+
title: page.title,
|
|
2403
|
+
error: null,
|
|
2404
|
+
updatedAt: Date.now()
|
|
2405
|
+
});
|
|
2406
|
+
emitItem(updated2);
|
|
2407
|
+
await finishTask(task, "done", "\u5DF2\u4ECE\u9875\u9762\u89E3\u6790\u5143\u6570\u636E");
|
|
2408
|
+
return updated2;
|
|
2409
|
+
}
|
|
2410
|
+
await update(key, { state: "resolve_failed", error: failure("no_metadata", page.error ? `\u65E0\u6CD5\u8BBF\u95EE\u9875\u9762\uFF1A${page.error}` : void 0) });
|
|
2411
|
+
await finishTask(task, "failed", "\u65E0\u6CD5\u4ECE\u9875\u9762\u89E3\u6790\u5143\u6570\u636E");
|
|
2412
|
+
return getItem(key);
|
|
2413
|
+
}
|
|
2177
2414
|
const record = await withRetry(
|
|
2178
2415
|
() => resolveIdentifier(
|
|
2179
2416
|
{ kind: item.kind, value: item.rawValue || item.doi || item.arxiv || item.isbn || item.pmid || item.title },
|
|
@@ -2702,7 +2939,12 @@ async function handleEvents(req, res) {
|
|
|
2702
2939
|
}
|
|
2703
2940
|
}
|
|
2704
2941
|
async function servePdf(req, res, key) {
|
|
2705
|
-
|
|
2942
|
+
let decoded = key;
|
|
2943
|
+
try {
|
|
2944
|
+
decoded = decodeURIComponent(key);
|
|
2945
|
+
} catch {
|
|
2946
|
+
}
|
|
2947
|
+
const path = safePdfPath(decoded);
|
|
2706
2948
|
if (!path) {
|
|
2707
2949
|
writeJson(res, 400, { error: "invalid key" });
|
|
2708
2950
|
return;
|
|
@@ -2878,6 +3120,18 @@ async function handler(req, res) {
|
|
|
2878
3120
|
writeJson(res, 200, result);
|
|
2879
3121
|
return;
|
|
2880
3122
|
}
|
|
3123
|
+
if (head === "search" && methodOk(req, "POST")) {
|
|
3124
|
+
const body = await readJsonBody2(req);
|
|
3125
|
+
const candidates2 = await searchCandidates2(String(body?.q ?? ""), Number(body?.rows ?? 8));
|
|
3126
|
+
writeJson(res, 200, { candidates: candidates2 });
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
if (head === "add-candidate" && methodOk(req, "POST")) {
|
|
3130
|
+
const body = await readJsonBody2(req);
|
|
3131
|
+
const item = await addCandidate(body?.candidate ?? {});
|
|
3132
|
+
writeJson(res, 200, { item });
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
2881
3135
|
if (head === "drop" && methodOk(req, "POST")) {
|
|
2882
3136
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
2883
3137
|
const filename = url.searchParams.get("filename") || "dropped.pdf";
|
package/package.json
CHANGED