@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.
Files changed (3) hide show
  1. package/lib/client.js +35 -13
  2. package/lib/index.js +265 -11
  3. 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
- const provisional = buildItem({
2145
- doi: hit.kind === "doi" ? hit.value : "",
2146
- arxiv: hit.kind === "arxiv" ? hit.value : "",
2147
- isbn: hit.kind === "isbn" ? hit.value : "",
2148
- pmid: hit.kind === "pmid" ? hit.value : "",
2149
- title: hit.kind === "title" ? hit.value : ""
2150
- });
2151
- if (!provisional.key) continue;
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
- const path = safePdfPath(key);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a9i5k4/dsh-literature",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "DSH Literature 文献侧窗:在 DeepSeek Harness 侧边栏识别 DOI/arXiv/标题、抓取元数据与全文、写入本地文献库或导出目录,并提供内置 PDF 阅读器(缩放/翻页/目录/搜索/高亮/笔记)。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",