dsh-cite 0.1.1 → 0.2.0

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/README.md CHANGED
@@ -11,7 +11,7 @@ DeepSeek Harness 参考文献工具插件:通过 Crossref API 查询文献元
11
11
  | `cite_lookup` | 查文献元数据(DOI 精确查询 / 题录检索) | `doi` 或 `query` 至少一个;`limit` 1-10 默认 5 |
12
12
  | `cite_format` | 生成规范引文 | `doi` 必填;`style`:gb-t-7714 / apa / mla / chicago |
13
13
  | `cite_bibtex` | 生成 BibTeX 条目 | `doi` 必填;`key` 可选 |
14
- | `cite_check` | 从文本提取 DOI 并校验是否存在 | `text` 必填;`maxChecks` 1-50 默认 10 |
14
+ | `cite_check` | 从文本提取 DOI 并并发校验是否存在(并发 3,保持输入顺序) | `text` 必填;`maxChecks` 1-50 默认 10 |
15
15
 
16
16
  ## 安装
17
17
 
@@ -38,7 +38,8 @@ Agent:
38
38
  - id: cite
39
39
  name: 'dsh-cite'
40
40
  config:
41
- timeoutMs: 15000 # Crossref 请求超时
41
+ timeoutMs: 15000 # Crossref 请求超时(也可用 DSH_CITE_TIMEOUT_MS)
42
+ # userAgent: ... # 自定义 User-Agent(也可用 DSH_CITE_USER_AGENT)
42
43
  userAgent: '' # 自定义 UA(建议带上可联系邮箱)
43
44
  ```
44
45
 
@@ -51,7 +52,7 @@ Agent:
51
52
  ## 开发
52
53
 
53
54
  ```bash
54
- pnpm test # 构建 + 14 个测试
55
+ pnpm test # 构建 + 18 个测试
55
56
  ```
56
57
 
57
58
  MIT
package/lib/config.d.ts CHANGED
@@ -11,7 +11,9 @@ export interface ResolvedCiteConfig {
11
11
  timeoutMs: number;
12
12
  userAgent: string;
13
13
  }
14
- export declare function resolveConfig(config: CiteConfig | undefined | null): ResolvedCiteConfig;
14
+ export declare const CITE_TIMEOUT_ENV = "DSH_CITE_TIMEOUT_MS";
15
+ export declare const CITE_USER_AGENT_ENV = "DSH_CITE_USER_AGENT";
16
+ export declare function resolveConfig(config: CiteConfig | undefined | null, env?: NodeJS.ProcessEnv): ResolvedCiteConfig;
15
17
  export declare function optionalString(args: Record<string, unknown>, key: string): string | undefined;
16
18
  export declare function requiredString(args: Record<string, unknown>, key: string, label: string): string;
17
19
  export declare function optionalInteger(args: Record<string, unknown>, key: string, label: string, lo: number, hi: number, fallback: number): number;
package/lib/config.js CHANGED
@@ -4,8 +4,10 @@
4
4
  * @module dsh-cite/config
5
5
  */
6
6
  const DEFAULT_TIMEOUT_MS = 15000;
7
- const DEFAULT_USER_AGENT = 'dsh-cite/0.1.0 (DeepSeek Harness citation plugin; mailto:STARDUSTLC666@users.noreply.github.com)';
8
- export function resolveConfig(config) {
7
+ const DEFAULT_USER_AGENT = 'dsh-cite/0.2.0 (DeepSeek Harness citation plugin; mailto:STARDUSTLC666@users.noreply.github.com)';
8
+ export const CITE_TIMEOUT_ENV = 'DSH_CITE_TIMEOUT_MS';
9
+ export const CITE_USER_AGENT_ENV = 'DSH_CITE_USER_AGENT';
10
+ export function resolveConfig(config, env = process.env) {
9
11
  const cfg = config ?? {};
10
12
  let timeoutMs = DEFAULT_TIMEOUT_MS;
11
13
  if (cfg.timeoutMs !== undefined) {
@@ -14,7 +16,13 @@ export function resolveConfig(config) {
14
16
  }
15
17
  timeoutMs = Math.min(120000, Math.max(2000, Math.round(cfg.timeoutMs)));
16
18
  }
17
- const userAgent = typeof cfg.userAgent === 'string' && cfg.userAgent.trim() !== '' ? cfg.userAgent.trim() : DEFAULT_USER_AGENT;
19
+ else if (env[CITE_TIMEOUT_ENV]?.trim()) {
20
+ const parsed = Number(env[CITE_TIMEOUT_ENV].trim());
21
+ if (!Number.isFinite(parsed) || parsed <= 0)
22
+ throw new Error(CITE_TIMEOUT_ENV + ' 必须是大于 0 的数字(毫秒)。');
23
+ timeoutMs = Math.min(120000, Math.max(2000, Math.round(parsed)));
24
+ }
25
+ const userAgent = typeof cfg.userAgent === 'string' && cfg.userAgent.trim() !== '' ? cfg.userAgent.trim() : (env[CITE_USER_AGENT_ENV]?.trim() || DEFAULT_USER_AGENT);
18
26
  return { timeoutMs, userAgent };
19
27
  }
20
28
  export function optionalString(args, key) {
package/lib/crossref.d.ts CHANGED
@@ -17,6 +17,7 @@ export interface Work {
17
17
  containerTitle: string;
18
18
  publisher: string;
19
19
  year: number;
20
+ language: string;
20
21
  volume: string;
21
22
  issue: string;
22
23
  page: string;
package/lib/crossref.js CHANGED
@@ -52,6 +52,7 @@ function normalizeWork(raw) {
52
52
  containerTitle: firstText(raw['container-title']),
53
53
  publisher: typeof raw.publisher === 'string' ? raw.publisher.trim() : '',
54
54
  year: year || 0,
55
+ language: typeof raw.language === 'string' ? raw.language.trim().toLowerCase() : '',
55
56
  volume: typeof raw.volume === 'string' ? raw.volume.trim() : '',
56
57
  issue: typeof raw.issue === 'string' ? raw.issue.trim() : '',
57
58
  page: typeof raw.page === 'string' ? raw.page.trim() : '',
package/lib/format.js CHANGED
@@ -203,6 +203,11 @@ export function buildCitation(work, style, lang = 'zh') {
203
203
  function escapeLatex(value) {
204
204
  return value.replace(/([{}])/g, '\\$1').replace(/&/g, '\\&').replace(/%/g, '\\%').replace(/\$/g, '\\$');
205
205
  }
206
+ /** BibTeX 引用键只允许安全字符,空格/逗号/花括号等替换为下划线。 */
207
+ function sanitizeBibtexKey(value) {
208
+ const cleaned = value.replace(/[^A-Za-z0-9_.:\/-]+/g, '_').replace(/^_+|_+$/g, '');
209
+ return cleaned === '' ? 'work' : cleaned;
210
+ }
206
211
  function bibtexType(work) {
207
212
  switch (work.type) {
208
213
  case 'journal-article': return 'article';
@@ -220,7 +225,7 @@ function bibtexType(work) {
220
225
  export function buildBibtex(work, key) {
221
226
  const first = work.authors[0];
222
227
  const firstWord = titleOf(work).split(/[^\p{L}\p{N}]+/u).find((part) => part !== '') ?? 'work';
223
- const baseKey = key !== undefined && key.trim() !== '' ? key.trim() : ((first?.family ?? first?.name ?? 'author') + yearText(work) + firstWord).toLowerCase();
228
+ const baseKey = key !== undefined && key.trim() !== '' ? sanitizeBibtexKey(key.trim()) : ((first?.family ?? first?.name ?? 'author') + yearText(work) + firstWord).toLowerCase();
224
229
  const type = bibtexType(work);
225
230
  const fields = [];
226
231
  const authors = work.authors.map((author) => fullName(author)).join(' and ');
package/lib/tools.js CHANGED
@@ -39,6 +39,7 @@ const workSchema = {
39
39
  containerTitle: { type: 'string' },
40
40
  publisher: { type: 'string' },
41
41
  year: { type: 'integer' },
42
+ language: { type: 'string' },
42
43
  volume: { type: 'string' },
43
44
  issue: { type: 'string' },
44
45
  page: { type: 'string' },
@@ -72,6 +73,22 @@ const checkSchema = {
72
73
  properties: { count: { type: 'integer' }, results: { type: 'array', items: checkItemSchema } },
73
74
  additionalProperties: true,
74
75
  };
76
+ /** 以固定并发度执行异步任务并保持原顺序。 */
77
+ async function mapWithConcurrency(items, concurrency, fn) {
78
+ const results = new Array(items.length);
79
+ let next = 0;
80
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
81
+ while (true) {
82
+ const index = next;
83
+ next += 1;
84
+ if (index >= items.length)
85
+ return;
86
+ results[index] = await fn(items[index], index);
87
+ }
88
+ });
89
+ await Promise.all(workers);
90
+ return results;
91
+ }
75
92
  function workLabel(work) {
76
93
  const title = work.title !== '' ? work.title : '(无标题)';
77
94
  return title + '(' + (work.year > 0 ? work.year + ', ' : '') + (work.containerTitle !== '' ? work.containerTitle : work.publisher) + ')';
@@ -100,6 +117,11 @@ export function buildCiteTools(cfg, fetchImpl) {
100
117
  const work = await lookupDoi(doi, cfg, fetchImpl);
101
118
  return { count: 1, works: [work] };
102
119
  }
120
+ const queryDoi = extractDois(query)[0];
121
+ if (queryDoi !== undefined) {
122
+ const work = await lookupDoi(queryDoi, cfg, fetchImpl);
123
+ return { count: 1, works: [work], matchedDoi: queryDoi };
124
+ }
103
125
  const limit = optionalInteger(args, 'limit', '返回条数', 1, 10, 5);
104
126
  const works = await searchWorks(query, limit, cfg, fetchImpl);
105
127
  if (works.length === 0)
@@ -172,16 +194,15 @@ export function buildCiteTools(cfg, fetchImpl) {
172
194
  const text = requiredString(args, 'text', '待检查文本');
173
195
  const maxChecks = optionalInteger(args, 'maxChecks', '校验数量', 1, 50, 10);
174
196
  const dois = extractDois(text).slice(0, maxChecks);
175
- const results = [];
176
- for (const doi of dois) {
197
+ const results = await mapWithConcurrency(dois, 3, async (doi) => {
177
198
  try {
178
199
  const work = await lookupDoi(doi, cfg, fetchImpl);
179
- results.push({ doi, ok: true, title: work.title, url: work.url });
200
+ return { doi, ok: true, title: work.title, url: work.url };
180
201
  }
181
202
  catch (error) {
182
- results.push({ doi, ok: false, title: '', url: '', error: error instanceof Error ? error.message : String(error) });
203
+ return { doi, ok: false, title: '', url: '', error: error instanceof Error ? error.message : String(error) };
183
204
  }
184
- }
205
+ });
185
206
  return { count: results.length, results };
186
207
  },
187
208
  timeoutMs: cfg.timeoutMs * 3 + 5000,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-cite",
3
- "version": "0.1.1",
4
- "description": "DSH 参考文献工具插件:cite_lookup/cite_format/cite_bibtex/cite_check 四工具,Crossref 元数据查询,GB/T 7714 / APA / MLA / Chicago 格式化,零运行时依赖。",
3
+ "version": "0.2.0",
4
+ "description": "DSH 参考文献工具插件:cite_lookup/cite_format/cite_bibtex/cite_check 四工具,Crossref 元数据查询(含 DOI 精确解析与并发校验),GB/T 7714 / APA / MLA / Chicago 格式化,零运行时依赖。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",