dsh-cite 0.1.0 → 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 +4 -3
- package/lib/config.d.ts +3 -1
- package/lib/config.js +11 -3
- package/lib/crossref.d.ts +1 -0
- package/lib/crossref.js +1 -0
- package/lib/format.js +6 -1
- package/lib/tools.js +26 -5
- package/package.json +4 -3
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
|
|
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 # 构建 +
|
|
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
|
|
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.
|
|
8
|
-
export
|
|
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
|
-
|
|
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
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
|
-
|
|
200
|
+
return { doi, ok: true, title: work.title, url: work.url };
|
|
180
201
|
}
|
|
181
202
|
catch (error) {
|
|
182
|
-
|
|
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.
|
|
4
|
-
"description": "DSH 参考文献工具插件:cite_lookup/cite_format/cite_bibtex/cite_check 四工具,Crossref
|
|
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",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"gb-t-7714",
|
|
41
41
|
"apa",
|
|
42
42
|
"mla",
|
|
43
|
-
"chicago"
|
|
43
|
+
"chicago",
|
|
44
|
+
"dsh-plugin"
|
|
44
45
|
],
|
|
45
46
|
"license": "MIT",
|
|
46
47
|
"engines": {
|