@a9i5k4/dsh-literature 0.2.3 → 0.2.4
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 +248 -9
- 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, {
|
|
@@ -2083,6 +2220,7 @@ var init_importer = __esm({
|
|
|
2083
2220
|
// src/node/pipeline.js
|
|
2084
2221
|
var pipeline_exports = {};
|
|
2085
2222
|
__export(pipeline_exports, {
|
|
2223
|
+
addCandidate: () => addCandidate,
|
|
2086
2224
|
arxivBase: () => arxivBase,
|
|
2087
2225
|
discardItem: () => discardItem,
|
|
2088
2226
|
fetchItemPdf: () => fetchItemPdf,
|
|
@@ -2095,6 +2233,7 @@ __export(pipeline_exports, {
|
|
|
2095
2233
|
retryItem: () => retryItem,
|
|
2096
2234
|
saveItem: () => saveItem,
|
|
2097
2235
|
scanText: () => scanText,
|
|
2236
|
+
searchCandidates: () => searchCandidates2,
|
|
2098
2237
|
startTask: () => startTask
|
|
2099
2238
|
});
|
|
2100
2239
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -2141,14 +2280,21 @@ async function scanText(text) {
|
|
|
2141
2280
|
const existing = await listItems();
|
|
2142
2281
|
const created = [];
|
|
2143
2282
|
for (const hit of found) {
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2283
|
+
let provisional;
|
|
2284
|
+
if (hit.kind === "url") {
|
|
2285
|
+
provisional = buildItem({ title: hit.value });
|
|
2286
|
+
if (!provisional.key) continue;
|
|
2287
|
+
provisional = { ...provisional, title: hit.value };
|
|
2288
|
+
} else {
|
|
2289
|
+
provisional = buildItem({
|
|
2290
|
+
doi: hit.kind === "doi" ? hit.value : "",
|
|
2291
|
+
arxiv: hit.kind === "arxiv" ? hit.value : "",
|
|
2292
|
+
isbn: hit.kind === "isbn" ? hit.value : "",
|
|
2293
|
+
pmid: hit.kind === "pmid" ? hit.value : "",
|
|
2294
|
+
title: hit.kind === "title" ? hit.value : ""
|
|
2295
|
+
});
|
|
2296
|
+
if (!provisional.key) continue;
|
|
2297
|
+
}
|
|
2152
2298
|
const clash = existing.find((e) => sameWork(e, provisional)) ?? await getItem(provisional.key);
|
|
2153
2299
|
if (clash) continue;
|
|
2154
2300
|
const item = await putItem({
|
|
@@ -2166,6 +2312,41 @@ async function scanText(text) {
|
|
|
2166
2312
|
}
|
|
2167
2313
|
return created;
|
|
2168
2314
|
}
|
|
2315
|
+
async function searchCandidates2(query, rows) {
|
|
2316
|
+
const { searchCandidates: search } = await Promise.resolve().then(() => (init_search(), search_exports));
|
|
2317
|
+
return search(query, { rows });
|
|
2318
|
+
}
|
|
2319
|
+
async function addCandidate(candidate) {
|
|
2320
|
+
const rec = {
|
|
2321
|
+
itemType: candidate.itemType ?? "journalArticle",
|
|
2322
|
+
title: candidate.title ?? "",
|
|
2323
|
+
authors: candidate.authors ?? [],
|
|
2324
|
+
year: candidate.year ?? null,
|
|
2325
|
+
container: candidate.container ?? "",
|
|
2326
|
+
volume: candidate.volume ?? "",
|
|
2327
|
+
issue: candidate.issue ?? "",
|
|
2328
|
+
pages: candidate.pages ?? "",
|
|
2329
|
+
doi: candidate.doi ?? ""
|
|
2330
|
+
};
|
|
2331
|
+
const provisional = buildItem(rec);
|
|
2332
|
+
if (!provisional.key) throw failure("no_metadata", "\u5019\u9009\u7F3A\u5C11\u53EF\u8BC6\u522B\u4FE1\u606F");
|
|
2333
|
+
const clash = await getItem(provisional.key);
|
|
2334
|
+
if (clash) {
|
|
2335
|
+
emitItem(clash);
|
|
2336
|
+
return clash;
|
|
2337
|
+
}
|
|
2338
|
+
const item = await putItem({
|
|
2339
|
+
...provisional,
|
|
2340
|
+
kind: candidate.doi ? "doi" : "title",
|
|
2341
|
+
rawValue: candidate.doi || rec.title,
|
|
2342
|
+
display: rec.title,
|
|
2343
|
+
record: rec,
|
|
2344
|
+
state: "resolved",
|
|
2345
|
+
createdAt: Date.now()
|
|
2346
|
+
});
|
|
2347
|
+
emitItem(item);
|
|
2348
|
+
return item;
|
|
2349
|
+
}
|
|
2169
2350
|
async function resolveItem(key) {
|
|
2170
2351
|
const item = await getItem(key);
|
|
2171
2352
|
if (!item) throw failure("not_found", "\u6761\u76EE\u4E0D\u5B58\u5728");
|
|
@@ -2174,6 +2355,52 @@ async function resolveItem(key) {
|
|
|
2174
2355
|
await update(key, { state: "resolving", error: null });
|
|
2175
2356
|
const config = await loadConfig();
|
|
2176
2357
|
try {
|
|
2358
|
+
if (item.kind === "url") {
|
|
2359
|
+
const { resolveUrlPage: resolveUrlPage2 } = await Promise.resolve().then(() => (init_url(), url_exports));
|
|
2360
|
+
const page = await resolveUrlPage2(item.rawValue || item.title, { timeoutMs: 2e4 });
|
|
2361
|
+
if (page.doi) {
|
|
2362
|
+
const record2 = await resolveIdentifier({ kind: "doi", value: page.doi }, { timeoutMs: 2e4, unpaywallEmail: config.unpaywallEmail });
|
|
2363
|
+
if (record2) {
|
|
2364
|
+
const merged2 = buildItem({ ...item, ...record2 });
|
|
2365
|
+
const updated2 = await patchItem(key, {
|
|
2366
|
+
...merged2,
|
|
2367
|
+
key,
|
|
2368
|
+
state: "resolved",
|
|
2369
|
+
record: record2,
|
|
2370
|
+
url: item.rawValue || page.url,
|
|
2371
|
+
error: null,
|
|
2372
|
+
updatedAt: Date.now()
|
|
2373
|
+
});
|
|
2374
|
+
emitItem(updated2);
|
|
2375
|
+
await finishTask(task, "done", "\u5143\u6570\u636E\u89E3\u6790\u5B8C\u6210");
|
|
2376
|
+
return updated2;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
if (page.title) {
|
|
2380
|
+
const record2 = {
|
|
2381
|
+
itemType: "webpage",
|
|
2382
|
+
title: page.title,
|
|
2383
|
+
authors: page.authors ?? [],
|
|
2384
|
+
year: page.year ?? null,
|
|
2385
|
+
url: item.rawValue || page.url,
|
|
2386
|
+
doi: page.doi
|
|
2387
|
+
};
|
|
2388
|
+
const updated2 = await patchItem(key, {
|
|
2389
|
+
key,
|
|
2390
|
+
state: "resolved",
|
|
2391
|
+
record: record2,
|
|
2392
|
+
title: page.title,
|
|
2393
|
+
error: null,
|
|
2394
|
+
updatedAt: Date.now()
|
|
2395
|
+
});
|
|
2396
|
+
emitItem(updated2);
|
|
2397
|
+
await finishTask(task, "done", "\u5DF2\u4ECE\u9875\u9762\u89E3\u6790\u5143\u6570\u636E");
|
|
2398
|
+
return updated2;
|
|
2399
|
+
}
|
|
2400
|
+
await update(key, { state: "resolve_failed", error: failure("no_metadata", page.error ? `\u65E0\u6CD5\u8BBF\u95EE\u9875\u9762\uFF1A${page.error}` : void 0) });
|
|
2401
|
+
await finishTask(task, "failed", "\u65E0\u6CD5\u4ECE\u9875\u9762\u89E3\u6790\u5143\u6570\u636E");
|
|
2402
|
+
return getItem(key);
|
|
2403
|
+
}
|
|
2177
2404
|
const record = await withRetry(
|
|
2178
2405
|
() => resolveIdentifier(
|
|
2179
2406
|
{ kind: item.kind, value: item.rawValue || item.doi || item.arxiv || item.isbn || item.pmid || item.title },
|
|
@@ -2878,6 +3105,18 @@ async function handler(req, res) {
|
|
|
2878
3105
|
writeJson(res, 200, result);
|
|
2879
3106
|
return;
|
|
2880
3107
|
}
|
|
3108
|
+
if (head === "search" && methodOk(req, "POST")) {
|
|
3109
|
+
const body = await readJsonBody2(req);
|
|
3110
|
+
const candidates2 = await searchCandidates2(String(body?.q ?? ""), Number(body?.rows ?? 8));
|
|
3111
|
+
writeJson(res, 200, { candidates: candidates2 });
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
if (head === "add-candidate" && methodOk(req, "POST")) {
|
|
3115
|
+
const body = await readJsonBody2(req);
|
|
3116
|
+
const item = await addCandidate(body?.candidate ?? {});
|
|
3117
|
+
writeJson(res, 200, { item });
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
2881
3120
|
if (head === "drop" && methodOk(req, "POST")) {
|
|
2882
3121
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
2883
3122
|
const filename = url.searchParams.get("filename") || "dropped.pdf";
|
package/package.json
CHANGED