dsh-cite 0.3.0 → 0.3.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/README.en.md +5 -4
- package/README.md +5 -2
- package/lib/config.d.ts +1 -0
- package/lib/config.js +1 -1
- package/lib/crossref.d.ts +2 -2
- package/lib/crossref.js +11 -5
- package/lib/format.js +7 -6
- package/lib/index.d.ts +4 -6
- package/lib/index.js +4 -4
- package/lib/tools.js +33 -17
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# dsh-cite
|
|
2
2
|
|
|
3
|
+
   
|
|
4
|
+
|
|
3
5
|
> Give it a DOI, get a proper citation — GB/T 7714 / APA / MLA / Chicago / BibTeX.
|
|
4
6
|
|
|
5
|
-
A DeepSeek Harness plugin for bibliographic references: query Crossref and format citations.
|
|
7
|
+
A DeepSeek Harness plugin for bibliographic references: query Crossref and format citations. Five tools (including the `cite_health` self-check), zero runtime dependencies.
|
|
6
8
|
|
|
7
9
|
## Tools
|
|
8
10
|
|
|
@@ -12,10 +14,11 @@ A DeepSeek Harness plugin for bibliographic references: query Crossref and forma
|
|
|
12
14
|
| `cite_format` | Generate a formatted citation | `doi`; `style`: gb-t-7714 / apa / mla / chicago |
|
|
13
15
|
| `cite_bibtex` | Generate a BibTeX entry | `doi`; `key` optional |
|
|
14
16
|
| `cite_check` | Extract DOIs from text and validate them | `text`; `maxChecks` 1-50, default 10 |
|
|
17
|
+
| `cite_health` | Self-check: probe Crossref connectivity and report latency | none |
|
|
15
18
|
|
|
16
19
|
## Compatibility
|
|
17
20
|
|
|
18
|
-
Verified against `@deepseek-ai/dsh@0.1.
|
|
21
|
+
Verified against `@deepseek-ai/dsh@0.1.2-alpha.2` on 2026-08-31. Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
|
|
19
22
|
|
|
20
23
|
## Install
|
|
21
24
|
|
|
@@ -23,8 +26,6 @@ Verified against `@deepseek-ai/dsh@0.1.1-rc.2` on 2026-08-26. Built for the cord
|
|
|
23
26
|
dsh plugin --profile web add dsh-cite
|
|
24
27
|
```
|
|
25
28
|
|
|
26
|
-
MIT
|
|
27
|
-
|
|
28
29
|
## Uninstall
|
|
29
30
|
|
|
30
31
|
```bash
|
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# dsh-cite
|
|
2
2
|
|
|
3
|
+
   
|
|
4
|
+
|
|
3
5
|
> 给一个 DOI,还你规范参考文献——GB/T 7714 / APA / MLA / Chicago / BibTeX。
|
|
4
6
|
|
|
5
|
-
DeepSeek Harness 参考文献工具插件:通过 Crossref API
|
|
7
|
+
DeepSeek Harness 参考文献工具插件:通过 Crossref API 查询文献元数据并格式化引用。五个工具(含 `cite_health` 自检)、零运行时依赖、全平台通用。
|
|
6
8
|
|
|
7
9
|
## 工具
|
|
8
10
|
|
|
@@ -12,10 +14,11 @@ DeepSeek Harness 参考文献工具插件:通过 Crossref API 查询文献元
|
|
|
12
14
|
| `cite_format` | 生成规范引文 | `doi` 必填;`style`:gb-t-7714 / apa / mla / chicago |
|
|
13
15
|
| `cite_bibtex` | 生成 BibTeX 条目 | `doi` 必填;`key` 可选 |
|
|
14
16
|
| `cite_check` | 从文本提取 DOI 并并发校验是否存在(并发 3,保持输入顺序) | `text` 必填;`maxChecks` 1-50 默认 10 |
|
|
17
|
+
| `cite_health` | 自检:探测 Crossref 连通性并报告延迟 | 无 |
|
|
15
18
|
|
|
16
19
|
## 兼容性
|
|
17
20
|
|
|
18
|
-
在 `@deepseek-ai/dsh@0.1.
|
|
21
|
+
在 `@deepseek-ai/dsh@0.1.2-alpha.2` 上验证(2026-08-31)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
|
|
19
22
|
|
|
20
23
|
## 安装
|
|
21
24
|
|
package/lib/config.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface ResolvedCiteConfig {
|
|
|
11
11
|
timeoutMs: number;
|
|
12
12
|
userAgent: string;
|
|
13
13
|
}
|
|
14
|
+
export declare const DEFAULT_USER_AGENT = "dsh-cite/0.3.2 (DeepSeek Harness citation plugin; mailto:STARDUSTLC666@users.noreply.github.com)";
|
|
14
15
|
export declare const CITE_TIMEOUT_ENV = "DSH_CITE_TIMEOUT_MS";
|
|
15
16
|
export declare const CITE_USER_AGENT_ENV = "DSH_CITE_USER_AGENT";
|
|
16
17
|
export declare function resolveConfig(config: CiteConfig | undefined | null, env?: NodeJS.ProcessEnv): ResolvedCiteConfig;
|
package/lib/config.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module dsh-cite/config
|
|
5
5
|
*/
|
|
6
6
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
7
|
-
const DEFAULT_USER_AGENT = 'dsh-cite/0.2
|
|
7
|
+
export const DEFAULT_USER_AGENT = 'dsh-cite/0.3.2 (DeepSeek Harness citation plugin; mailto:STARDUSTLC666@users.noreply.github.com)';
|
|
8
8
|
export const CITE_TIMEOUT_ENV = 'DSH_CITE_TIMEOUT_MS';
|
|
9
9
|
export const CITE_USER_AGENT_ENV = 'DSH_CITE_USER_AGENT';
|
|
10
10
|
export function resolveConfig(config, env = process.env) {
|
package/lib/crossref.d.ts
CHANGED
|
@@ -30,8 +30,8 @@ export type FetchLike = (url: string, init?: {
|
|
|
30
30
|
}) => Promise<Response>;
|
|
31
31
|
export declare function assertDoi(value: string): string;
|
|
32
32
|
/** 按 DOI 查询 Crossref 并归一化。 */
|
|
33
|
-
export declare function lookupDoi(doi: string, cfg: ResolvedCiteConfig, fetchImpl?: FetchLike): Promise<Work>;
|
|
33
|
+
export declare function lookupDoi(doi: string, cfg: ResolvedCiteConfig, fetchImpl?: FetchLike, signal?: AbortSignal): Promise<Work>;
|
|
34
34
|
/** 按题录文本检索 Crossref,返回归一化结果。 */
|
|
35
|
-
export declare function searchWorks(query: string, limit: number, cfg: ResolvedCiteConfig, fetchImpl?: FetchLike): Promise<Work[]>;
|
|
35
|
+
export declare function searchWorks(query: string, limit: number, cfg: ResolvedCiteConfig, fetchImpl?: FetchLike, signal?: AbortSignal): Promise<Work[]>;
|
|
36
36
|
/** 从任意文本里提取 DOI。 */
|
|
37
37
|
export declare function extractDois(text: string): string[];
|
package/lib/crossref.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
const CROSSREF_API = 'https://api.crossref.org/works/';
|
|
7
7
|
const DOI_PATTERN = /^10\.\d{4,9}\/[-._;()/:A-Za-z0-9]+$/i;
|
|
8
8
|
export function assertDoi(value) {
|
|
9
|
-
const doi = value.trim().replace(/^https?:\/\/doi\.org\//i, '').replace(/^doi:\s*/i, '');
|
|
9
|
+
const doi = value.trim().replace(/^https?:\/\/(dx\.)?doi\.org\//i, '').replace(/^doi:\s*/i, '');
|
|
10
10
|
if (!DOI_PATTERN.test(doi))
|
|
11
11
|
throw new Error('DOI 格式不正确:' + value + '(示例:10.1038/nature12345)。');
|
|
12
12
|
return doi;
|
|
@@ -63,18 +63,23 @@ function normalizeWork(raw) {
|
|
|
63
63
|
function makeHeaders(cfg) {
|
|
64
64
|
return { 'user-agent': cfg.userAgent, accept: 'application/json' };
|
|
65
65
|
}
|
|
66
|
+
function requestSignal(timeoutMs, signal) {
|
|
67
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
68
|
+
return signal === undefined ? timeoutSignal : AbortSignal.any([signal, timeoutSignal]);
|
|
69
|
+
}
|
|
66
70
|
/** 按 DOI 查询 Crossref 并归一化。 */
|
|
67
|
-
export async function lookupDoi(doi, cfg, fetchImpl) {
|
|
71
|
+
export async function lookupDoi(doi, cfg, fetchImpl, signal) {
|
|
68
72
|
const clean = assertDoi(doi);
|
|
69
73
|
const fetcher = fetchImpl ?? globalThis.fetch;
|
|
70
74
|
let response;
|
|
71
75
|
try {
|
|
72
76
|
response = await fetcher(CROSSREF_API + encodeURIComponent(clean), {
|
|
73
77
|
headers: makeHeaders(cfg),
|
|
74
|
-
signal:
|
|
78
|
+
signal: requestSignal(cfg.timeoutMs, signal),
|
|
75
79
|
});
|
|
76
80
|
}
|
|
77
81
|
catch (error) {
|
|
82
|
+
signal?.throwIfAborted();
|
|
78
83
|
throw new Error('Crossref 请求失败:' + (error instanceof Error ? error.message : String(error)));
|
|
79
84
|
}
|
|
80
85
|
if (!response.ok) {
|
|
@@ -98,7 +103,7 @@ export async function lookupDoi(doi, cfg, fetchImpl) {
|
|
|
98
103
|
return work;
|
|
99
104
|
}
|
|
100
105
|
/** 按题录文本检索 Crossref,返回归一化结果。 */
|
|
101
|
-
export async function searchWorks(query, limit, cfg, fetchImpl) {
|
|
106
|
+
export async function searchWorks(query, limit, cfg, fetchImpl, signal) {
|
|
102
107
|
const q = query.trim();
|
|
103
108
|
if (q === '')
|
|
104
109
|
throw new Error('检索词不能为空。');
|
|
@@ -108,10 +113,11 @@ export async function searchWorks(query, limit, cfg, fetchImpl) {
|
|
|
108
113
|
try {
|
|
109
114
|
response = await fetcher(url, {
|
|
110
115
|
headers: makeHeaders(cfg),
|
|
111
|
-
signal:
|
|
116
|
+
signal: requestSignal(cfg.timeoutMs, signal),
|
|
112
117
|
});
|
|
113
118
|
}
|
|
114
119
|
catch (error) {
|
|
120
|
+
signal?.throwIfAborted();
|
|
115
121
|
throw new Error('Crossref 检索失败:' + (error instanceof Error ? error.message : String(error)));
|
|
116
122
|
}
|
|
117
123
|
if (!response.ok)
|
package/lib/format.js
CHANGED
|
@@ -146,14 +146,15 @@ function buildApa(work) {
|
|
|
146
146
|
text += '(' + work.issue + ')';
|
|
147
147
|
if (work.page !== '')
|
|
148
148
|
text += ', ' + work.page;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
const tail = work.doi !== '' ? 'https://doi.org/' + work.doi : work.url;
|
|
150
|
+
if (tail !== '')
|
|
151
|
+
return text + '. ' + tail;
|
|
152
|
+
return text + '.';
|
|
152
153
|
}
|
|
153
154
|
if (work.type === 'book' || work.type === 'monograph' || work.type === 'edited-book') {
|
|
154
|
-
return authors + ' (' + year + '). ' + title + '.
|
|
155
|
+
return authors + ' (' + year + '). ' + title + '.' + (work.publisher !== '' ? ' ' + work.publisher + '.' : '');
|
|
155
156
|
}
|
|
156
|
-
return authors + ' (' + year + '). ' + title + '.
|
|
157
|
+
return authors + ' (' + year + '). ' + title + '.' + (work.url !== '' ? ' ' + work.url : '');
|
|
157
158
|
}
|
|
158
159
|
/** 生成 MLA 9 参考文献条目。 */
|
|
159
160
|
function buildMla(work) {
|
|
@@ -241,7 +242,7 @@ function bibtexType(work) {
|
|
|
241
242
|
export function buildBibtex(work, key) {
|
|
242
243
|
const first = work.authors[0];
|
|
243
244
|
const firstWord = titleOf(work).split(/[^\p{L}\p{N}]+/u).find((part) => part !== '') ?? 'work';
|
|
244
|
-
const baseKey = key !== undefined && key.trim() !== '' ? sanitizeBibtexKey(key.trim()) : ((first?.family
|
|
245
|
+
const baseKey = key !== undefined && key.trim() !== '' ? sanitizeBibtexKey(key.trim()) : sanitizeBibtexKey(((first?.family || first?.name || 'author') + yearText(work) + firstWord).toLowerCase());
|
|
245
246
|
const type = bibtexType(work);
|
|
246
247
|
const fields = [];
|
|
247
248
|
const authors = work.authors.map((author) => fullName(author)).join(' and ');
|
package/lib/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-cite —— 参考文献工具插件(node 半身,配置走 cordis.patch.yml)。
|
|
3
3
|
*
|
|
4
|
-
* 插件导出 apply(ctx, config)
|
|
5
|
-
* cite_bibtex / cite_check),通过 Crossref API 查询元数据并生成
|
|
6
|
-
* MLA / Chicago 引文。零运行时依赖。
|
|
4
|
+
* 插件导出 apply(ctx, config):注册五个面向模型的工具(cite_lookup / cite_format /
|
|
5
|
+
* cite_bibtex / cite_check / cite_health),通过 Crossref API 查询元数据并生成
|
|
6
|
+
* GB/T 7714 / APA / MLA / Chicago 引文。零运行时依赖。
|
|
7
7
|
*
|
|
8
8
|
* @module dsh-cite
|
|
9
9
|
*/
|
|
@@ -14,9 +14,7 @@ export declare const name = "cite";
|
|
|
14
14
|
export declare const inject: string[];
|
|
15
15
|
export interface CitePluginContext {
|
|
16
16
|
tools: {
|
|
17
|
-
register(definition: CiteToolDefinition
|
|
18
|
-
prepend?: boolean;
|
|
19
|
-
}): () => void;
|
|
17
|
+
register(definition: CiteToolDefinition): () => void;
|
|
20
18
|
};
|
|
21
19
|
on?(event: string, listener: () => void): () => void;
|
|
22
20
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-cite —— 参考文献工具插件(node 半身,配置走 cordis.patch.yml)。
|
|
3
3
|
*
|
|
4
|
-
* 插件导出 apply(ctx, config)
|
|
5
|
-
* cite_bibtex / cite_check),通过 Crossref API 查询元数据并生成
|
|
6
|
-
* MLA / Chicago 引文。零运行时依赖。
|
|
4
|
+
* 插件导出 apply(ctx, config):注册五个面向模型的工具(cite_lookup / cite_format /
|
|
5
|
+
* cite_bibtex / cite_check / cite_health),通过 Crossref API 查询元数据并生成
|
|
6
|
+
* GB/T 7714 / APA / MLA / Chicago 引文。零运行时依赖。
|
|
7
7
|
*
|
|
8
8
|
* @module dsh-cite
|
|
9
9
|
*/
|
|
@@ -23,7 +23,7 @@ export function apply(ctx, config) {
|
|
|
23
23
|
}
|
|
24
24
|
const disposers = [];
|
|
25
25
|
for (const definition of buildCiteTools(cfg)) {
|
|
26
|
-
disposers.push(ctx.tools.register(definition
|
|
26
|
+
disposers.push(ctx.tools.register(definition));
|
|
27
27
|
}
|
|
28
28
|
if (typeof ctx.on === 'function') {
|
|
29
29
|
ctx.on('dispose', () => {
|
package/lib/tools.js
CHANGED
|
@@ -74,11 +74,12 @@ const checkSchema = {
|
|
|
74
74
|
additionalProperties: true,
|
|
75
75
|
};
|
|
76
76
|
/** 以固定并发度执行异步任务并保持原顺序。 */
|
|
77
|
-
async function mapWithConcurrency(items, concurrency, fn) {
|
|
77
|
+
async function mapWithConcurrency(items, concurrency, fn, signal) {
|
|
78
78
|
const results = new Array(items.length);
|
|
79
79
|
let next = 0;
|
|
80
80
|
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
81
81
|
while (true) {
|
|
82
|
+
signal?.throwIfAborted();
|
|
82
83
|
const index = next;
|
|
83
84
|
next += 1;
|
|
84
85
|
if (index >= items.length)
|
|
@@ -89,6 +90,14 @@ async function mapWithConcurrency(items, concurrency, fn) {
|
|
|
89
90
|
await Promise.all(workers);
|
|
90
91
|
return results;
|
|
91
92
|
}
|
|
93
|
+
function executionSignal(exec) {
|
|
94
|
+
if (typeof exec !== 'object' || exec === null)
|
|
95
|
+
return undefined;
|
|
96
|
+
const signal = exec.signal;
|
|
97
|
+
return signal instanceof AbortSignal ? signal : undefined;
|
|
98
|
+
}
|
|
99
|
+
const CHECK_CONCURRENCY = 3;
|
|
100
|
+
const MAX_CHECKS = 50;
|
|
92
101
|
function workLabel(work) {
|
|
93
102
|
const title = work.title !== '' ? work.title : '(无标题)';
|
|
94
103
|
return title + '(' + (work.year > 0 ? work.year + ', ' : '') + (work.containerTitle !== '' ? work.containerTitle : work.publisher) + ')';
|
|
@@ -107,23 +116,23 @@ export function buildCiteTools(cfg, fetchImpl) {
|
|
|
107
116
|
const works = Array.isArray(rec.works) ? rec.works : [];
|
|
108
117
|
return [{ type: 'text', text: works.map((work, index) => (index + 1) + '. ' + workLabel(work)).join('\n') }];
|
|
109
118
|
} },
|
|
110
|
-
async execute(rawArgs) {
|
|
119
|
+
async execute(rawArgs, exec) {
|
|
111
120
|
const args = (rawArgs ?? {});
|
|
112
121
|
const doi = optionalString(args, 'doi');
|
|
113
122
|
const query = optionalString(args, 'query');
|
|
114
123
|
if (doi === undefined && query === undefined)
|
|
115
124
|
throw new Error('cite_lookup 需要 doi 或 query 参数(至少一个)。');
|
|
116
125
|
if (doi !== undefined) {
|
|
117
|
-
const work = await lookupDoi(doi, cfg, fetchImpl);
|
|
126
|
+
const work = await lookupDoi(doi, cfg, fetchImpl, executionSignal(exec));
|
|
118
127
|
return { count: 1, works: [work] };
|
|
119
128
|
}
|
|
120
129
|
const queryDoi = extractDois(query)[0];
|
|
121
130
|
if (queryDoi !== undefined) {
|
|
122
|
-
const work = await lookupDoi(queryDoi, cfg, fetchImpl);
|
|
131
|
+
const work = await lookupDoi(queryDoi, cfg, fetchImpl, executionSignal(exec));
|
|
123
132
|
return { count: 1, works: [work], matchedDoi: queryDoi };
|
|
124
133
|
}
|
|
125
134
|
const limit = optionalInteger(args, 'limit', '返回条数', 1, 10, 5);
|
|
126
|
-
const works = await searchWorks(query, limit, cfg, fetchImpl);
|
|
135
|
+
const works = await searchWorks(query, limit, cfg, fetchImpl, executionSignal(exec));
|
|
127
136
|
if (works.length === 0)
|
|
128
137
|
throw new Error('Crossref 没有找到匹配文献,请尝试更精确的标题/作者。');
|
|
129
138
|
return { count: works.length, works };
|
|
@@ -142,12 +151,12 @@ export function buildCiteTools(cfg, fetchImpl) {
|
|
|
142
151
|
const rec = (value ?? {});
|
|
143
152
|
return [{ type: 'text', text: typeof rec.citation === 'string' ? rec.citation : '' }];
|
|
144
153
|
} },
|
|
145
|
-
async execute(rawArgs) {
|
|
154
|
+
async execute(rawArgs, exec) {
|
|
146
155
|
const args = (rawArgs ?? {});
|
|
147
156
|
const doi = requiredString(args, 'doi', 'DOI');
|
|
148
157
|
const style = readStyle(args.style);
|
|
149
158
|
const lang = args.lang === 'en' ? 'en' : 'zh';
|
|
150
|
-
const work = await lookupDoi(doi, cfg, fetchImpl);
|
|
159
|
+
const work = await lookupDoi(doi, cfg, fetchImpl, executionSignal(exec));
|
|
151
160
|
const citation = buildCitation(work, style, lang);
|
|
152
161
|
return { doi: work.doi || doi, style, citation, work };
|
|
153
162
|
},
|
|
@@ -164,13 +173,13 @@ export function buildCiteTools(cfg, fetchImpl) {
|
|
|
164
173
|
const rec = (value ?? {});
|
|
165
174
|
return [{ type: 'text', text: typeof rec.bibtex === 'string' ? rec.bibtex : '' }];
|
|
166
175
|
} },
|
|
167
|
-
async execute(rawArgs) {
|
|
176
|
+
async execute(rawArgs, exec) {
|
|
168
177
|
const args = (rawArgs ?? {});
|
|
169
178
|
const doi = requiredString(args, 'doi', 'DOI');
|
|
170
179
|
const key = optionalString(args, 'key');
|
|
171
|
-
const work = await lookupDoi(doi, cfg, fetchImpl);
|
|
180
|
+
const work = await lookupDoi(doi, cfg, fetchImpl, executionSignal(exec));
|
|
172
181
|
const bibtex = buildBibtex(work, key);
|
|
173
|
-
return { doi: work.doi || doi, key, bibtex };
|
|
182
|
+
return { doi: work.doi || doi, ...(key !== undefined ? { key } : {}), bibtex };
|
|
174
183
|
},
|
|
175
184
|
timeoutMs: cfg.timeoutMs + 2000,
|
|
176
185
|
};
|
|
@@ -189,23 +198,25 @@ export function buildCiteTools(cfg, fetchImpl) {
|
|
|
189
198
|
return (row.ok === true ? '✅' : '❌') + ' ' + row.doi + (typeof row.title === 'string' && row.title !== '' ? ' — ' + row.title : '');
|
|
190
199
|
}).join('\n') }];
|
|
191
200
|
} },
|
|
192
|
-
async execute(rawArgs) {
|
|
201
|
+
async execute(rawArgs, exec) {
|
|
193
202
|
const args = (rawArgs ?? {});
|
|
194
203
|
const text = requiredString(args, 'text', '待检查文本');
|
|
195
204
|
const maxChecks = optionalInteger(args, 'maxChecks', '校验数量', 1, 50, 10);
|
|
196
205
|
const dois = extractDois(text).slice(0, maxChecks);
|
|
197
|
-
const
|
|
206
|
+
const signal = executionSignal(exec);
|
|
207
|
+
const results = await mapWithConcurrency(dois, CHECK_CONCURRENCY, async (doi) => {
|
|
198
208
|
try {
|
|
199
|
-
const work = await lookupDoi(doi, cfg, fetchImpl);
|
|
209
|
+
const work = await lookupDoi(doi, cfg, fetchImpl, signal);
|
|
200
210
|
return { doi, ok: true, title: work.title, url: work.url };
|
|
201
211
|
}
|
|
202
212
|
catch (error) {
|
|
213
|
+
signal?.throwIfAborted();
|
|
203
214
|
return { doi, ok: false, title: '', url: '', error: error instanceof Error ? error.message : String(error) };
|
|
204
215
|
}
|
|
205
|
-
});
|
|
216
|
+
}, signal);
|
|
206
217
|
return { count: results.length, results };
|
|
207
218
|
},
|
|
208
|
-
timeoutMs: cfg.timeoutMs *
|
|
219
|
+
timeoutMs: cfg.timeoutMs * Math.ceil(MAX_CHECKS / CHECK_CONCURRENCY) + 5000,
|
|
209
220
|
};
|
|
210
221
|
const citeHealth = {
|
|
211
222
|
name: 'cite_health',
|
|
@@ -224,16 +235,20 @@ export function buildCiteTools(cfg, fetchImpl) {
|
|
|
224
235
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
225
236
|
},
|
|
226
237
|
},
|
|
227
|
-
async execute() {
|
|
238
|
+
async execute(_rawArgs, exec) {
|
|
228
239
|
const checks = [];
|
|
229
240
|
let ok = true;
|
|
230
241
|
const startedAt = Date.now();
|
|
242
|
+
const signal = executionSignal(exec);
|
|
231
243
|
try {
|
|
232
244
|
const fetcher = fetchImpl ?? globalThis.fetch;
|
|
233
245
|
const response = await fetcher('https://api.crossref.org/works?rows=1&select=DOI', {
|
|
234
246
|
headers: { 'user-agent': cfg.userAgent },
|
|
235
|
-
signal:
|
|
247
|
+
signal: signal === undefined
|
|
248
|
+
? AbortSignal.timeout(Math.min(cfg.timeoutMs, 15000))
|
|
249
|
+
: AbortSignal.any([signal, AbortSignal.timeout(Math.min(cfg.timeoutMs, 15000))]),
|
|
236
250
|
});
|
|
251
|
+
signal?.throwIfAborted();
|
|
237
252
|
if (response.ok) {
|
|
238
253
|
checks.push({ name: 'Crossref API', ok: true, detail: 'HTTP ' + response.status + ',' + (Date.now() - startedAt) + 'ms' });
|
|
239
254
|
}
|
|
@@ -243,6 +258,7 @@ export function buildCiteTools(cfg, fetchImpl) {
|
|
|
243
258
|
}
|
|
244
259
|
}
|
|
245
260
|
catch (error) {
|
|
261
|
+
signal?.throwIfAborted();
|
|
246
262
|
ok = false;
|
|
247
263
|
checks.push({ name: 'Crossref API', ok: false, detail: (error instanceof Error ? error.message : String(error)) + '(网络需要特殊代理时请配置系统代理后重启)' });
|
|
248
264
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cite",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "DSH 参考文献工具插件:cite_lookup/cite_format/cite_bibtex/cite_check
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "DSH 参考文献工具插件:cite_lookup/cite_format/cite_bibtex/cite_check/cite_health 五工具(含自检),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",
|