dsh-cite 0.4.0 → 0.4.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@
4
4
 
5
5
  [历史英文记录](docs/CHANGELOG.en.md)
6
6
 
7
+ ## 0.4.2 (2026-10-05)
8
+
9
+ - 保留 DOI 内合法的成对括号,正确剥除正文外层标点;Crossref 响应读取也受取消与超时控制。
10
+
11
+ ## 0.4.1 (2026-10-03)
12
+
13
+ - 修复含多个出版社名称的 BibTeX 导出后再导入失败,保留出版社名称及原始来源。41 项 Windows 测试与真实下载文件的再导入通过。
14
+
7
15
  ## 0.4.0 (2026-10-03)
8
16
 
9
17
  - 新增中文 / English 文献工作台和 `cite_batch`:DOI / BibTeX 批量导入、本地预览、选择保存、搜索及格式切换。原有五个工具保留。
package/README.en.md CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  [中文](README.md)
4
4
 
5
+ ![dsh-cite whale girl plugin cover](https://raw.githubusercontent.com/STARDUSTLC666/dsh-cite/master/assets/cover-whale-girl.png)
6
+
5
7
  Organize DOI lists or BibTeX into a reviewable local library, then export references and BibTeX.
6
8
 
7
- [![npm](https://img.shields.io/npm/v/dsh-cite)](https://www.npmjs.com/package/dsh-cite) [![downloads](https://img.shields.io/npm/dm/dsh-cite)](https://www.npmjs.com/package/dsh-cite)
9
+ [![npm](https://img.shields.io/npm/v/dsh-cite)](https://www.npmjs.com/package/dsh-cite) [![downloads](https://raw.githubusercontent.com/STARDUSTLC666/dsh-suite/npm-downloads/assets/dsh-cite-downloads.svg)](https://www.npmjs.com/package/dsh-cite)
8
10
 
9
11
  ## What it does
10
12
 
package/README.md CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  [English](README.en.md)
4
4
 
5
+ ![dsh-cite 鲸鱼娘插件封面](https://raw.githubusercontent.com/STARDUSTLC666/dsh-cite/master/assets/cover-whale-girl.png)
6
+
5
7
  把 DOI 列表或 BibTeX 整理成可核对的本地文献库,再导出参考文献与 BibTeX。
6
8
 
7
- [![npm](https://img.shields.io/npm/v/dsh-cite)](https://www.npmjs.com/package/dsh-cite) [![downloads](https://img.shields.io/npm/dm/dsh-cite)](https://www.npmjs.com/package/dsh-cite)
9
+ [![npm](https://img.shields.io/npm/v/dsh-cite)](https://www.npmjs.com/package/dsh-cite) [![downloads](https://raw.githubusercontent.com/STARDUSTLC666/dsh-suite/npm-downloads/assets/dsh-cite-downloads.svg)](https://www.npmjs.com/package/dsh-cite)
8
10
 
9
11
  ## 功能
10
12
 
@@ -4,6 +4,14 @@
4
4
 
5
5
  These English notes preserve the earlier translations. The main changelog contains the consolidated version history.
6
6
 
7
+ ## 0.4.2 (2026-10-05)
8
+
9
+ - Preserve balanced parentheses inside DOIs while removing surrounding prose punctuation. Crossref response parsing respects cancellation and timeouts.
10
+
11
+ ## 0.4.1 (2026-10-03)
12
+
13
+ Fixes re-importing exported BibTeX containing publisher lists. Publisher names and original sources remain intact. All 41 Windows tests and re-importing the actual browser download pass.
14
+
7
15
  ## 0.4.0 (2026-10-03)
8
16
 
9
17
  Adds a Chinese / English citation workbench and `cite_batch` for offline DOI / BibTeX preview, selective import, duplicate review, search and basic citation exports. Explicit Crossref enrichment fills empty fields, retains local differences and supports progress, cancellation and retry. Original input is preserved in full backups; revision checks protect concurrent changes. The existing five tools remain available.
package/docs/USAGE.en.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  [Overview](../README.en.md) · [Changelog](../CHANGELOG.md) · [Validation](VALIDATION.md)
4
4
 
5
+ ## Current improvements
6
+
7
+ DOIs with balanced parentheses can be queried directly. Transport or parsing timeouts are explicit, and cancellation stops waiting. Offline library import and external Crossref lookup are verified separately.
8
+
5
9
  ## Citation workbench
6
10
 
7
11
  1. Open **Settings → Cite**. Paste one DOI per line, paste BibTeX, or upload a `.bib` file.
package/docs/USAGE.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  [返回简介](../README.md) · [更新记录](../CHANGELOG.md) · [验证记录](VALIDATION.md)
4
4
 
5
+ ## 本次改进
6
+
7
+ 可直接查询带成对括号的 DOI。网络或响应读取超时会给出明确错误;取消后不继续等待。文献库离线导入与外部 Crossref 检索分别验收。
8
+
5
9
  ## 文献工作台
6
10
 
7
11
  1. 打开「设置 → Cite」,选择 DOI 列表或 BibTeX。DOI 每行一条,也可上传 `.bib` 文件。
@@ -2,9 +2,9 @@
2
2
 
3
3
  本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
4
4
 
5
- ## 0.4.0(2026-10-03)
5
+ ## 0.4.1(含 0.4.0 功能,2026-10-03)
6
6
 
7
- - Windows:40 项自动测试全部通过,无跳过。覆盖嵌套 BibTeX、宏、机构作者、复合姓氏、畸形输入、容量边界、精确与候选重复、并发修订冲突、部分失败重试、取消不写入、元数据差异保留和英文错误信息。
7
+ - Windows:41 项自动测试全部通过,无跳过。覆盖嵌套 BibTeX、宏、机构作者、复合姓氏、出版社列表的导出再导入、畸形输入、容量边界、精确与候选重复、并发修订冲突、部分失败重试、取消不写入、元数据差异保留和英文错误信息。
8
8
  - 宿主契约:官方源码构建的 RC2(639ed015)和公开 npm 包 `0.2.1-alpha.1` 均在 E 盘隔离环境测试;18 插件共同注册 110 工具、35 技能,19 输出样例通过 schema 检查。未配置真实账号的健康检查可返回未配置,这不表示真实服务连接通过。
9
9
  - 可见浏览器操作:官方 alpha 的设置页中实际上传 `.bib`、本地预览、选择保存、关闭 / 重开设置保留输入、搜索、引文预览及三种下载。DOI 大小写与链接形式的重复被跳过,错误行不可保存。中英文切换使用同一份两条题录的文献库,修订与原始资料未改变;390px 布局另作可见检查。
10
10
  - 真实 Crossref:显式查询 `10.1038/nature12345`,作者、年份等空字段已补全;本地测试标题被保留,实际提供者标题作为差异显示。曾出现网络请求失败,重试后成功;不把网络波动当作已定位或修复的代理问题。没有 DOI 的条目显示单独失败。
@@ -1,5 +1,7 @@
1
1
  /** English UI messages; bibliographic content and original source text stay unchanged. */
2
2
  export function englishCitationError(raw) {
3
+ if (/^Crossref.*超时/.test(raw))
4
+ return 'Crossref timed out. Retry later; your local entries were preserved.';
3
5
  if (/^Crossref 请求失败:/.test(raw))
4
6
  return raw.replace(/^Crossref 请求失败:/, 'Crossref request failed: ');
5
7
  if (/^DOI 在 Crossref 中不存在:/.test(raw))
@@ -2,13 +2,16 @@ import { parentPort, workerData } from 'node:worker_threads';
2
2
  import { Cite } from '@citation-js/core';
3
3
  import '@citation-js/plugin-bibtex';
4
4
  import { assertDoi } from './crossref.js';
5
- import { MAX_BATCH, normalizeWork, workWarnings } from './citation-model.js';
5
+ import { MAX_BATCH, normalizeWork, plainText, workWarnings } from './citation-model.js';
6
6
  const types = { 'article-journal': 'journal-article', book: 'book', chapter: 'book-chapter', 'paper-conference': 'proceedings-article', thesis: 'dissertation', report: 'report', dataset: 'dataset', patent: 'patent', standard: 'standard' };
7
+ // Citation.js treats BibTeX's publisher "and" separator as a list. The local
8
+ // Work model is plain text; preserve these names and retain the original source.
9
+ const cslText = (value) => Array.isArray(value) ? value.map(plainText).join(' and ') : plainText(value);
7
10
  function workOf(csl) {
8
11
  return normalizeWork({
9
12
  doi: csl.DOI, type: types[csl.type] || 'other', title: csl.title,
10
13
  authors: (csl.author || []).map(a => { const family = [a['non-dropping-particle'], a.family, a.suffix].filter(Boolean).join(' '); const given = [a.given, a['dropping-particle']].filter(Boolean).join(' '); return a.literal || !given ? { name: a.literal || family, family: '', given: '' } : { name: '', family, given }; }),
11
- containerTitle: csl['container-title'], publisher: csl.publisher, year: Number(csl.issued?.['date-parts']?.[0]?.[0] || 0), language: csl.language, volume: csl.volume, issue: csl.issue, page: csl.page, url: csl.URL, isbn: csl.ISBN,
14
+ containerTitle: cslText(csl['container-title']), publisher: cslText(csl.publisher), year: Number(csl.issued?.['date-parts']?.[0]?.[0] || 0), language: csl.language, volume: csl.volume, issue: csl.issue, page: csl.page, url: csl.URL, isbn: csl.ISBN,
12
15
  });
13
16
  }
14
17
  try {
package/lib/crossref.js CHANGED
@@ -67,19 +67,32 @@ function requestSignal(timeoutMs, signal) {
67
67
  const timeoutSignal = AbortSignal.timeout(timeoutMs);
68
68
  return signal === undefined ? timeoutSignal : AbortSignal.any([signal, timeoutSignal]);
69
69
  }
70
+ function withAbort(value, signal) {
71
+ return new Promise((resolve, reject) => {
72
+ const abort = () => reject(signal.reason);
73
+ signal.addEventListener('abort', abort, { once: true });
74
+ value.then(result => { signal.removeEventListener('abort', abort); resolve(result); }, error => { signal.removeEventListener('abort', abort); reject(error); });
75
+ if (signal.aborted)
76
+ abort();
77
+ });
78
+ }
70
79
  /** 按 DOI 查询 Crossref 并归一化。 */
71
80
  export async function lookupDoi(doi, cfg, fetchImpl, signal) {
72
81
  const clean = assertDoi(doi);
82
+ signal?.throwIfAborted();
83
+ const deadline = requestSignal(cfg.timeoutMs, signal);
73
84
  const fetcher = fetchImpl ?? globalThis.fetch;
74
85
  let response;
75
86
  try {
76
- response = await fetcher(CROSSREF_API + encodeURIComponent(clean), {
87
+ response = await withAbort(fetcher(CROSSREF_API + encodeURIComponent(clean), {
77
88
  headers: makeHeaders(cfg),
78
- signal: requestSignal(cfg.timeoutMs, signal),
79
- });
89
+ signal: deadline,
90
+ }), deadline);
80
91
  }
81
92
  catch (error) {
82
93
  signal?.throwIfAborted();
94
+ if (deadline.aborted)
95
+ throw new Error('Crossref 请求超时,请稍后重试;本地文献已保留。');
83
96
  const code = error instanceof Error && typeof error.cause?.code === 'string' && /^[A-Z0-9_]+$/.test(error.cause.code) ? ' (' + error.cause.code + ')' : '';
84
97
  throw new Error('Crossref 请求失败:' + (error instanceof Error ? error.message : String(error)) + code);
85
98
  }
@@ -90,9 +103,12 @@ export async function lookupDoi(doi, cfg, fetchImpl, signal) {
90
103
  }
91
104
  let payload;
92
105
  try {
93
- payload = await response.json();
106
+ payload = await withAbort(response.json(), deadline);
94
107
  }
95
108
  catch {
109
+ signal?.throwIfAborted();
110
+ if (deadline.aborted)
111
+ throw new Error('Crossref 响应读取超时,请稍后重试;本地文献已保留。');
96
112
  throw new Error('Crossref 响应不是合法 JSON。');
97
113
  }
98
114
  const message = record(payload).message;
@@ -106,28 +122,35 @@ export async function lookupDoi(doi, cfg, fetchImpl, signal) {
106
122
  /** 按题录文本检索 Crossref,返回归一化结果。 */
107
123
  export async function searchWorks(query, limit, cfg, fetchImpl, signal) {
108
124
  const q = query.trim();
125
+ signal?.throwIfAborted();
126
+ const deadline = requestSignal(cfg.timeoutMs, signal);
109
127
  if (q === '')
110
128
  throw new Error('检索词不能为空。');
111
129
  const url = CROSSREF_API + '?query.bibliographic=' + encodeURIComponent(q) + '&rows=' + String(limit);
112
130
  const fetcher = fetchImpl ?? globalThis.fetch;
113
131
  let response;
114
132
  try {
115
- response = await fetcher(url, {
133
+ response = await withAbort(fetcher(url, {
116
134
  headers: makeHeaders(cfg),
117
- signal: requestSignal(cfg.timeoutMs, signal),
118
- });
135
+ signal: deadline,
136
+ }), deadline);
119
137
  }
120
138
  catch (error) {
121
139
  signal?.throwIfAborted();
140
+ if (deadline.aborted)
141
+ throw new Error('Crossref 请求超时,请稍后重试;本地文献已保留。');
122
142
  throw new Error('Crossref 检索失败:' + (error instanceof Error ? error.message : String(error)));
123
143
  }
124
144
  if (!response.ok)
125
145
  throw new Error('Crossref 返回 HTTP ' + response.status + ',无法完成检索。');
126
146
  let payload;
127
147
  try {
128
- payload = await response.json();
148
+ payload = await withAbort(response.json(), deadline);
129
149
  }
130
150
  catch {
151
+ signal?.throwIfAborted();
152
+ if (deadline.aborted)
153
+ throw new Error('Crossref 响应读取超时,请稍后重试;本地文献已保留。');
131
154
  throw new Error('Crossref 响应不是合法 JSON。');
132
155
  }
133
156
  const items = record(payload).message;
@@ -142,7 +165,9 @@ export function extractDois(text) {
142
165
  const seen = new Set();
143
166
  const result = [];
144
167
  for (const match of text.match(pattern) ?? []) {
145
- const clean = match.replace(/[.,;:)\]}'">]+$/, '').trim();
168
+ let clean = match.replace(/[.,;:\]}'">]+$/, '').trim();
169
+ while (clean.endsWith(')') && (clean.match(/\)/g)?.length ?? 0) > (clean.match(/\(/g)?.length ?? 0))
170
+ clean = clean.slice(0, -1);
146
171
  if (seen.has(clean.toLowerCase()))
147
172
  continue;
148
173
  seen.add(clean.toLowerCase());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cite",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "批量导入 DOI / BibTeX,预览去重、补全文献元数据并导出引文。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -29,7 +29,12 @@
29
29
  "prepublishOnly": "pnpm run build"
30
30
  },
31
31
  "dsh": {
32
- "client": { "inject": ["@deepseek-ai/dsh-client-ui-settings"], "platform": "web" },
32
+ "client": {
33
+ "inject": [
34
+ "@deepseek-ai/dsh-client-ui-settings"
35
+ ],
36
+ "platform": "web"
37
+ },
33
38
  "bundle": {
34
39
  "patch": "./cordis.patch.yml"
35
40
  }