@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.
Files changed (3) hide show
  1. package/lib/client.js +35 -13
  2. package/lib/index.js +248 -9
  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, {
@@ -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
- 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;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a9i5k4/dsh-literature",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "DSH Literature 文献侧窗:在 DeepSeek Harness 侧边栏识别 DOI/arXiv/标题、抓取元数据与全文、写入本地文献库或导出目录,并提供内置 PDF 阅读器(缩放/翻页/目录/搜索/高亮/笔记)。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",