dsh-cite 0.3.1 → 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/lib/config.d.ts +1 -1
- package/lib/config.js +1 -1
- package/lib/crossref.d.ts +2 -2
- package/lib/crossref.js +10 -4
- package/lib/index.d.ts +1 -3
- package/lib/index.js +1 -1
- package/lib/tools.js +33 -17
- package/package.json +1 -1
package/lib/config.d.ts
CHANGED
|
@@ -11,7 +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.
|
|
14
|
+
export declare const DEFAULT_USER_AGENT = "dsh-cite/0.3.2 (DeepSeek Harness citation plugin; mailto:STARDUSTLC666@users.noreply.github.com)";
|
|
15
15
|
export declare const CITE_TIMEOUT_ENV = "DSH_CITE_TIMEOUT_MS";
|
|
16
16
|
export declare const CITE_USER_AGENT_ENV = "DSH_CITE_USER_AGENT";
|
|
17
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
|
-
export const DEFAULT_USER_AGENT = 'dsh-cite/0.3.
|
|
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
|
@@ -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/index.d.ts
CHANGED
|
@@ -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
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cite",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
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",
|