dsh-cite 0.1.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.en.md ADDED
@@ -0,0 +1,22 @@
1
+ # dsh-cite
2
+
3
+ > Give it a DOI, get a proper citation — GB/T 7714 / APA / MLA / Chicago / BibTeX.
4
+
5
+ A DeepSeek Harness plugin for bibliographic references: query Crossref and format citations. Four tools, zero runtime dependencies.
6
+
7
+ ## Tools
8
+
9
+ | Tool | Purpose | Key parameters |
10
+ | :-- | :-- | :-- |
11
+ | `cite_lookup` | Look up metadata by DOI or bibliographic query | `doi` or `query`; `limit` 1-10, default 5 |
12
+ | `cite_format` | Generate a formatted citation | `doi`; `style`: gb-t-7714 / apa / mla / chicago |
13
+ | `cite_bibtex` | Generate a BibTeX entry | `doi`; `key` optional |
14
+ | `cite_check` | Extract DOIs from text and validate them | `text`; `maxChecks` 1-50, default 10 |
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ dsh plugin --profile web add dsh-cite
20
+ ```
21
+
22
+ MIT
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # dsh-cite
2
+
3
+ > 给一个 DOI,还你规范参考文献——GB/T 7714 / APA / MLA / Chicago / BibTeX。
4
+
5
+ DeepSeek Harness 参考文献工具插件:通过 Crossref API 查询文献元数据并格式化引用。四个工具、零运行时依赖、全平台通用。
6
+
7
+ ## 工具
8
+
9
+ | 工具 | 作用 | 关键参数 |
10
+ | :-- | :-- | :-- |
11
+ | `cite_lookup` | 查文献元数据(DOI 精确查询 / 题录检索) | `doi` 或 `query` 至少一个;`limit` 1-10 默认 5 |
12
+ | `cite_format` | 生成规范引文 | `doi` 必填;`style`:gb-t-7714 / apa / mla / chicago |
13
+ | `cite_bibtex` | 生成 BibTeX 条目 | `doi` 必填;`key` 可选 |
14
+ | `cite_check` | 从文本提取 DOI 并校验是否存在 | `text` 必填;`maxChecks` 1-50 默认 10 |
15
+
16
+ ## 安装
17
+
18
+ ```bash
19
+ dsh plugin --profile web add dsh-cite
20
+ ```
21
+
22
+ ## 示例
23
+
24
+ ```text
25
+ 用户:给我 10.1038/nature12345 的 GB/T 7714 引用
26
+ Agent:
27
+ cite_format { doi: "10.1038/nature12345", style: "gb-t-7714" }
28
+ → LeCun Y, Bengio Y, Hinton G. Deep learning[J]. Nature, 2015, 521(7553): 436-444.
29
+
30
+ 用户:检查这段参考文献的 DOI 是否有效
31
+ Agent:
32
+ cite_check { text: "..." }
33
+ ```
34
+
35
+ ## 配置
36
+
37
+ ```yaml
38
+ - id: cite
39
+ name: 'dsh-cite'
40
+ config:
41
+ timeoutMs: 15000 # Crossref 请求超时
42
+ userAgent: '' # 自定义 UA(建议带上可联系邮箱)
43
+ ```
44
+
45
+ ## 说明
46
+
47
+ - 数据源:Crossref REST API,无需 API key
48
+ - 引文为纯文本输出;格式按常见模板生成,正式投稿前请核对目标期刊的细节要求
49
+ - 不采集、不上传任何本地文献数据
50
+
51
+ ## 开发
52
+
53
+ ```bash
54
+ pnpm test # 构建 + 14 个测试
55
+ ```
56
+
57
+ MIT
@@ -0,0 +1,14 @@
1
+ # dsh-cite 组合包补丁:把参考文献工具插件行插入 profile。
2
+ #
3
+ # 示例(用户 profile 的 cordis.patch.yml):
4
+ # - id: cite
5
+ # name: 'dsh-cite'
6
+ # config:
7
+ # timeoutMs: 15000 # Crossref 请求超时(默认 15 秒)
8
+ # userAgent: '' # 自定义 User-Agent(默认 dsh-cite/0.1.0)
9
+ #
10
+ - insert:
11
+ - id: cite
12
+ name: 'dsh-cite'
13
+ config:
14
+ timeoutMs: 15000
@@ -0,0 +1,17 @@
1
+ /**
2
+ * dsh-cite 配置解析:Crossref 请求超时与 User-Agent。
3
+ *
4
+ * @module dsh-cite/config
5
+ */
6
+ export interface CiteConfig {
7
+ timeoutMs?: number;
8
+ userAgent?: string;
9
+ }
10
+ export interface ResolvedCiteConfig {
11
+ timeoutMs: number;
12
+ userAgent: string;
13
+ }
14
+ export declare function resolveConfig(config: CiteConfig | undefined | null): ResolvedCiteConfig;
15
+ export declare function optionalString(args: Record<string, unknown>, key: string): string | undefined;
16
+ export declare function requiredString(args: Record<string, unknown>, key: string, label: string): string;
17
+ export declare function optionalInteger(args: Record<string, unknown>, key: string, label: string, lo: number, hi: number, fallback: number): number;
package/lib/config.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * dsh-cite 配置解析:Crossref 请求超时与 User-Agent。
3
+ *
4
+ * @module dsh-cite/config
5
+ */
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) {
9
+ const cfg = config ?? {};
10
+ let timeoutMs = DEFAULT_TIMEOUT_MS;
11
+ if (cfg.timeoutMs !== undefined) {
12
+ if (typeof cfg.timeoutMs !== 'number' || !Number.isFinite(cfg.timeoutMs) || cfg.timeoutMs <= 0) {
13
+ throw new Error('timeoutMs 必须是大于 0 的数字(毫秒),例如 15000。');
14
+ }
15
+ timeoutMs = Math.min(120000, Math.max(2000, Math.round(cfg.timeoutMs)));
16
+ }
17
+ const userAgent = typeof cfg.userAgent === 'string' && cfg.userAgent.trim() !== '' ? cfg.userAgent.trim() : DEFAULT_USER_AGENT;
18
+ return { timeoutMs, userAgent };
19
+ }
20
+ export function optionalString(args, key) {
21
+ const value = args[key];
22
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
23
+ }
24
+ export function requiredString(args, key, label) {
25
+ const value = optionalString(args, key);
26
+ if (value === undefined)
27
+ throw new Error(label + '(参数 ' + key + ')为必填,请提供非空字符串。');
28
+ return value;
29
+ }
30
+ export function optionalInteger(args, key, label, lo, hi, fallback) {
31
+ const value = args[key];
32
+ if (value === undefined || value === null)
33
+ return fallback;
34
+ if (typeof value !== 'number' || !Number.isFinite(value))
35
+ throw new Error(label + '(参数 ' + key + ')必须是数字。');
36
+ const rounded = Math.round(value);
37
+ if (rounded < lo || rounded > hi)
38
+ throw new Error(label + '(参数 ' + key + ')必须在 ' + lo + '-' + hi + ' 之间。');
39
+ return rounded;
40
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Crossref REST API 适配:按 DOI 查询 / 题录检索,统一归一化为 Work。
3
+ *
4
+ * @module dsh-cite/crossref
5
+ */
6
+ import { type ResolvedCiteConfig } from './config.js';
7
+ export interface CiteAuthor {
8
+ given: string;
9
+ family: string;
10
+ name: string;
11
+ }
12
+ export interface Work {
13
+ doi: string;
14
+ type: string;
15
+ title: string;
16
+ authors: CiteAuthor[];
17
+ containerTitle: string;
18
+ publisher: string;
19
+ year: number;
20
+ volume: string;
21
+ issue: string;
22
+ page: string;
23
+ url: string;
24
+ isbn: string;
25
+ }
26
+ export type FetchLike = (url: string, init?: {
27
+ headers?: Record<string, string>;
28
+ signal?: AbortSignal;
29
+ }) => Promise<Response>;
30
+ export declare function assertDoi(value: string): string;
31
+ /** 按 DOI 查询 Crossref 并归一化。 */
32
+ export declare function lookupDoi(doi: string, cfg: ResolvedCiteConfig, fetchImpl?: FetchLike): Promise<Work>;
33
+ /** 按题录文本检索 Crossref,返回归一化结果。 */
34
+ export declare function searchWorks(query: string, limit: number, cfg: ResolvedCiteConfig, fetchImpl?: FetchLike): Promise<Work[]>;
35
+ /** 从任意文本里提取 DOI。 */
36
+ export declare function extractDois(text: string): string[];
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Crossref REST API 适配:按 DOI 查询 / 题录检索,统一归一化为 Work。
3
+ *
4
+ * @module dsh-cite/crossref
5
+ */
6
+ const CROSSREF_API = 'https://api.crossref.org/works/';
7
+ const DOI_PATTERN = /^10\.\d{4,9}\/[-._;()/:A-Za-z0-9]+$/i;
8
+ export function assertDoi(value) {
9
+ const doi = value.trim().replace(/^https?:\/\/doi\.org\//i, '').replace(/^doi:\s*/i, '');
10
+ if (!DOI_PATTERN.test(doi))
11
+ throw new Error('DOI 格式不正确:' + value + '(示例:10.1038/nature12345)。');
12
+ return doi;
13
+ }
14
+ function firstText(value) {
15
+ if (typeof value === 'string')
16
+ return value.trim();
17
+ if (Array.isArray(value))
18
+ return firstText(value[0]);
19
+ return '';
20
+ }
21
+ function firstNumber(value) {
22
+ if (typeof value === 'number' && Number.isFinite(value))
23
+ return value;
24
+ if (Array.isArray(value))
25
+ return firstNumber(value[0]);
26
+ return 0;
27
+ }
28
+ function record(value) {
29
+ return typeof value === 'object' && value !== null ? value : {};
30
+ }
31
+ function normalizeAuthor(raw) {
32
+ const rec = record(raw);
33
+ const family = typeof rec.family === 'string' ? rec.family.trim() : '';
34
+ const given = typeof rec.given === 'string' ? rec.given.trim() : '';
35
+ const name = typeof rec.name === 'string' ? rec.name.trim() : '';
36
+ if (family === '' && given === '' && name === '')
37
+ return null;
38
+ return { given, family, name };
39
+ }
40
+ function normalizeWork(raw) {
41
+ const authorsRaw = Array.isArray(raw.author) ? raw.author : [];
42
+ const authors = authorsRaw.map(normalizeAuthor).filter((item) => item !== null);
43
+ const issued = record(raw.issued);
44
+ const dateParts = Array.isArray(issued['date-parts']) ? issued['date-parts'] : [];
45
+ const year = firstNumber(Array.isArray(dateParts[0]) ? dateParts[0][0] : raw.year);
46
+ const doi = typeof raw.DOI === 'string' ? raw.DOI.trim() : '';
47
+ return {
48
+ doi,
49
+ type: typeof raw.type === 'string' ? raw.type : 'other',
50
+ title: firstText(raw.title),
51
+ authors,
52
+ containerTitle: firstText(raw['container-title']),
53
+ publisher: typeof raw.publisher === 'string' ? raw.publisher.trim() : '',
54
+ year: year || 0,
55
+ volume: typeof raw.volume === 'string' ? raw.volume.trim() : '',
56
+ issue: typeof raw.issue === 'string' ? raw.issue.trim() : '',
57
+ page: typeof raw.page === 'string' ? raw.page.trim() : '',
58
+ url: typeof raw.URL === 'string' ? raw.URL.trim() : (doi !== '' ? 'https://doi.org/' + doi : ''),
59
+ isbn: firstText(raw.ISBN),
60
+ };
61
+ }
62
+ function makeHeaders(cfg) {
63
+ return { 'user-agent': cfg.userAgent, accept: 'application/json' };
64
+ }
65
+ /** 按 DOI 查询 Crossref 并归一化。 */
66
+ export async function lookupDoi(doi, cfg, fetchImpl) {
67
+ const clean = assertDoi(doi);
68
+ const fetcher = fetchImpl ?? globalThis.fetch;
69
+ let response;
70
+ try {
71
+ response = await fetcher(CROSSREF_API + encodeURIComponent(clean), {
72
+ headers: makeHeaders(cfg),
73
+ signal: AbortSignal.timeout(cfg.timeoutMs),
74
+ });
75
+ }
76
+ catch (error) {
77
+ throw new Error('Crossref 请求失败:' + (error instanceof Error ? error.message : String(error)));
78
+ }
79
+ if (!response.ok) {
80
+ if (response.status === 404)
81
+ throw new Error('DOI 在 Crossref 中不存在:' + clean);
82
+ throw new Error('Crossref 返回 HTTP ' + response.status + ',无法完成查询。');
83
+ }
84
+ let payload;
85
+ try {
86
+ payload = await response.json();
87
+ }
88
+ catch {
89
+ throw new Error('Crossref 响应不是合法 JSON。');
90
+ }
91
+ const message = record(payload).message;
92
+ if (typeof message !== 'object' || message === null)
93
+ throw new Error('Crossref 响应缺少 message 字段。');
94
+ const work = normalizeWork(message);
95
+ if (work.doi === '')
96
+ work.doi = clean;
97
+ return work;
98
+ }
99
+ /** 按题录文本检索 Crossref,返回归一化结果。 */
100
+ export async function searchWorks(query, limit, cfg, fetchImpl) {
101
+ const q = query.trim();
102
+ if (q === '')
103
+ throw new Error('检索词不能为空。');
104
+ const url = CROSSREF_API + '?query.bibliographic=' + encodeURIComponent(q) + '&rows=' + String(limit);
105
+ const fetcher = fetchImpl ?? globalThis.fetch;
106
+ let response;
107
+ try {
108
+ response = await fetcher(url, {
109
+ headers: makeHeaders(cfg),
110
+ signal: AbortSignal.timeout(cfg.timeoutMs),
111
+ });
112
+ }
113
+ catch (error) {
114
+ throw new Error('Crossref 检索失败:' + (error instanceof Error ? error.message : String(error)));
115
+ }
116
+ if (!response.ok)
117
+ throw new Error('Crossref 返回 HTTP ' + response.status + ',无法完成检索。');
118
+ let payload;
119
+ try {
120
+ payload = await response.json();
121
+ }
122
+ catch {
123
+ throw new Error('Crossref 响应不是合法 JSON。');
124
+ }
125
+ const items = record(payload).message;
126
+ const list = Array.isArray(items) ? items : (typeof items === 'object' && items !== null ? record(items).items : []);
127
+ if (!Array.isArray(list))
128
+ return [];
129
+ return list.map((item) => normalizeWork(record(item)));
130
+ }
131
+ /** 从任意文本里提取 DOI。 */
132
+ export function extractDois(text) {
133
+ const pattern = /\b10\.\d{4,9}\/[-._;()/:A-Za-z0-9]+/gi;
134
+ const seen = new Set();
135
+ const result = [];
136
+ for (const match of text.match(pattern) ?? []) {
137
+ const clean = match.replace(/[.,;]+$/, '').trim();
138
+ if (seen.has(clean.toLowerCase()))
139
+ continue;
140
+ seen.add(clean.toLowerCase());
141
+ result.push(clean);
142
+ }
143
+ return result;
144
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 参考文献格式化:GB/T 7714-2015 / APA 7 / MLA 9 / Chicago note,以及 BibTeX。
3
+ *
4
+ * @module dsh-cite/format
5
+ */
6
+ import type { Work } from './crossref.js';
7
+ export type CiteStyle = 'gb-t-7714' | 'apa' | 'mla' | 'chicago';
8
+ export declare function readStyle(raw: unknown): CiteStyle;
9
+ /** 按样式生成引文。 */
10
+ export declare function buildCitation(work: Work, style: CiteStyle, lang?: string): string;
11
+ /** 生成 BibTeX 条目。 */
12
+ export declare function buildBibtex(work: Work, key?: string): string;
package/lib/format.js ADDED
@@ -0,0 +1,247 @@
1
+ /**
2
+ * 参考文献格式化:GB/T 7714-2015 / APA 7 / MLA 9 / Chicago note,以及 BibTeX。
3
+ *
4
+ * @module dsh-cite/format
5
+ */
6
+ export function readStyle(raw) {
7
+ if (raw === undefined || raw === null || raw === '')
8
+ return 'gb-t-7714';
9
+ if (raw === 'gb-t-7714' || raw === 'apa' || raw === 'mla' || raw === 'chicago')
10
+ return raw;
11
+ throw new Error('style 只支持 gb-t-7714 / apa / mla / chicago,收到:' + String(raw));
12
+ }
13
+ function fullName(author) {
14
+ if (author.name !== '')
15
+ return author.name;
16
+ if (author.given !== '' && author.family !== '')
17
+ return author.family + ' ' + author.given;
18
+ return author.family !== '' ? author.family : author.given;
19
+ }
20
+ function initials(given) {
21
+ return given
22
+ .split(/[\s.-]+/)
23
+ .filter((part) => part !== '')
24
+ .map((part) => part[0].toUpperCase() + '.')
25
+ .join(' ');
26
+ }
27
+ function apaName(author) {
28
+ const family = author.family !== '' ? author.family : (author.name !== '' ? author.name : author.given);
29
+ const initialsText = author.given !== '' ? initials(author.given) : (author.name !== '' ? initials(author.name.split(/[\s-]+/)[0] ?? '') : '');
30
+ return initialsText !== '' ? family + ', ' + initialsText : family;
31
+ }
32
+ function mlaName(author, first) {
33
+ const name = fullName(author);
34
+ const parts = name.trim().split(/\s+/);
35
+ if (parts.length < 2)
36
+ return name;
37
+ if (first)
38
+ return parts[0] + ', ' + parts.slice(1).join(' ');
39
+ return parts.slice(1).join(' ') + ' ' + parts[0];
40
+ }
41
+ function gbtAuthors(authors) {
42
+ if (authors.length === 0)
43
+ return '佚名';
44
+ const names = authors.map((author) => fullName(author));
45
+ return names.length <= 3 ? names.join(', ') : names.slice(0, 3).join(', ') + ', 等';
46
+ }
47
+ function apaAuthors(authors) {
48
+ if (authors.length === 0)
49
+ return 'Anonymous';
50
+ const names = authors.map(apaName);
51
+ if (names.length === 1)
52
+ return names[0];
53
+ if (names.length === 2)
54
+ return names[0] + ', & ' + names[1];
55
+ return names.slice(0, 19).join(', ') + ', ...';
56
+ }
57
+ function mlaAuthors(authors) {
58
+ if (authors.length === 0)
59
+ return 'Anonymous';
60
+ if (authors.length === 1)
61
+ return mlaName(authors[0], true);
62
+ if (authors.length === 2)
63
+ return mlaName(authors[0], true) + ', and ' + mlaName(authors[1], false);
64
+ return mlaName(authors[0], true) + ', et al.';
65
+ }
66
+ function chicagoAuthors(authors) {
67
+ if (authors.length === 0)
68
+ return 'Anonymous';
69
+ if (authors.length <= 3)
70
+ return authors.map((author) => fullName(author)).join(', ');
71
+ return fullName(authors[0]) + ', et al.';
72
+ }
73
+ function gbtTypeLabel(type, work) {
74
+ switch (type) {
75
+ case 'journal-article': return '[J]';
76
+ case 'book':
77
+ case 'monograph':
78
+ case 'edited-book': return '[M]';
79
+ case 'proceedings-article':
80
+ case 'proceedings': return '[C]';
81
+ case 'dissertation': return '[D]';
82
+ case 'report': return '[R]';
83
+ case 'standard': return '[S]';
84
+ case 'patent': return '[P]';
85
+ case 'posted-content':
86
+ case 'dataset':
87
+ case 'other': return work.url !== '' ? '[EB/OL]' : '[Z]';
88
+ default: return work.url !== '' ? '[EB/OL]' : '[Z]';
89
+ }
90
+ }
91
+ function yearText(work) {
92
+ return work.year > 0 ? String(work.year) : 'n.d.';
93
+ }
94
+ function volumeIssuePages(work) {
95
+ let text = work.volume;
96
+ if (work.issue !== '')
97
+ text += '(' + work.issue + ')';
98
+ if (work.page !== '')
99
+ text += (text !== '' ? ': ' : '') + work.page;
100
+ return text;
101
+ }
102
+ function cleanupTitle(title) {
103
+ return title.replace(/\s+/g, ' ').trim();
104
+ }
105
+ function titleOf(work) {
106
+ return cleanupTitle(work.title !== '' ? work.title : '(无标题)');
107
+ }
108
+ /** 生成 GB/T 7714-2015 参考文献条目。 */
109
+ function buildGbt(work, lang) {
110
+ const authors = gbtAuthors(work.authors);
111
+ const title = titleOf(work);
112
+ const label = gbtTypeLabel(work.type, work);
113
+ if (work.type === 'journal-article' && work.containerTitle !== '') {
114
+ let tail = work.containerTitle + ', ' + yearText(work);
115
+ const vip = volumeIssuePages(work);
116
+ if (vip !== '')
117
+ tail += ', ' + vip;
118
+ return authors + '. ' + title + label + '. ' + tail + '.';
119
+ }
120
+ if (work.type === 'book' || work.type === 'monograph' || work.type === 'edited-book') {
121
+ let tail = work.publisher !== '' ? work.publisher + ', ' : '';
122
+ tail += yearText(work);
123
+ if (work.isbn !== '')
124
+ tail += '. ISBN ' + work.isbn;
125
+ return authors + '. ' + title + '[M]. ' + tail + '.';
126
+ }
127
+ if (work.containerTitle !== '' && work.type === 'proceedings-article') {
128
+ return authors + '. ' + title + '[C]//' + work.containerTitle + '. ' + yearText(work) + '.';
129
+ }
130
+ const urlPart = work.url !== '' ? ' ' + work.url + '.' : '';
131
+ const accessed = new Date().toISOString().slice(0, 10);
132
+ return authors + '. ' + title + label + '. (' + yearText(work) + ')[' + (lang === 'zh' ? '引用日期 ' : 'cited ') + accessed + '].' + urlPart;
133
+ }
134
+ /** 生成 APA 7 参考文献条目。 */
135
+ function buildApa(work) {
136
+ const authors = apaAuthors(work.authors);
137
+ const title = titleOf(work);
138
+ const year = yearText(work);
139
+ if (work.type === 'journal-article' && work.containerTitle !== '') {
140
+ let text = authors + ' (' + year + '). ' + title + '. ' + work.containerTitle;
141
+ if (work.volume !== '')
142
+ text += ', ' + work.volume;
143
+ if (work.issue !== '')
144
+ text += '(' + work.issue + ')';
145
+ if (work.page !== '')
146
+ text += ', ' + work.page;
147
+ text += '. ';
148
+ text += work.doi !== '' ? 'https://doi.org/' + work.doi : work.url;
149
+ return text;
150
+ }
151
+ if (work.type === 'book' || work.type === 'monograph' || work.type === 'edited-book') {
152
+ return authors + ' (' + year + '). ' + title + '. ' + (work.publisher !== '' ? work.publisher + '.' : '');
153
+ }
154
+ return authors + ' (' + year + '). ' + title + '. ' + (work.url !== '' ? work.url : '');
155
+ }
156
+ /** 生成 MLA 9 参考文献条目。 */
157
+ function buildMla(work) {
158
+ const authors = mlaAuthors(work.authors);
159
+ const title = titleOf(work);
160
+ let text = authors + '. "' + title + '." ';
161
+ if (work.containerTitle !== '')
162
+ text += work.containerTitle + ', ';
163
+ if (work.volume !== '')
164
+ text += 'vol. ' + work.volume + ', ';
165
+ if (work.issue !== '')
166
+ text += 'no. ' + work.issue + ', ';
167
+ text += yearText(work);
168
+ if (work.page !== '')
169
+ text += ', pp. ' + work.page;
170
+ text += '.';
171
+ if (work.doi !== '')
172
+ text += ' https://doi.org/' + work.doi;
173
+ return text;
174
+ }
175
+ /** 生成 Chicago(note 格式)引用。 */
176
+ function buildChicago(work) {
177
+ const authors = chicagoAuthors(work.authors);
178
+ const title = titleOf(work);
179
+ let text = authors + ', "' + title + '"';
180
+ if (work.containerTitle !== '')
181
+ text += ', ' + work.containerTitle;
182
+ if (work.volume !== '')
183
+ text += ' ' + work.volume;
184
+ if (work.issue !== '')
185
+ text += ', no. ' + work.issue;
186
+ text += ' (' + yearText(work) + ')';
187
+ if (work.page !== '')
188
+ text += ': ' + work.page;
189
+ text += '.';
190
+ if (work.doi !== '')
191
+ text += ' https://doi.org/' + work.doi;
192
+ return text;
193
+ }
194
+ /** 按样式生成引文。 */
195
+ export function buildCitation(work, style, lang = 'zh') {
196
+ switch (style) {
197
+ case 'gb-t-7714': return buildGbt(work, lang);
198
+ case 'apa': return buildApa(work);
199
+ case 'mla': return buildMla(work);
200
+ case 'chicago': return buildChicago(work);
201
+ }
202
+ }
203
+ function escapeLatex(value) {
204
+ return value.replace(/([{}])/g, '\\$1').replace(/&/g, '\\&').replace(/%/g, '\\%').replace(/\$/g, '\\$');
205
+ }
206
+ function bibtexType(work) {
207
+ switch (work.type) {
208
+ case 'journal-article': return 'article';
209
+ case 'book':
210
+ case 'monograph':
211
+ case 'edited-book': return 'book';
212
+ case 'proceedings-article':
213
+ case 'proceedings': return 'inproceedings';
214
+ case 'dissertation': return 'phdthesis';
215
+ case 'report': return 'techreport';
216
+ default: return 'misc';
217
+ }
218
+ }
219
+ /** 生成 BibTeX 条目。 */
220
+ export function buildBibtex(work, key) {
221
+ const first = work.authors[0];
222
+ 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();
224
+ const type = bibtexType(work);
225
+ const fields = [];
226
+ const authors = work.authors.map((author) => fullName(author)).join(' and ');
227
+ if (authors !== '')
228
+ fields.push(' author={' + escapeLatex(authors) + '}');
229
+ fields.push(' title={' + escapeLatex(titleOf(work)) + '}');
230
+ if (work.containerTitle !== '')
231
+ fields.push(type === 'article' ? ' journal={' + escapeLatex(work.containerTitle) + '}' : ' booktitle={' + escapeLatex(work.containerTitle) + '}');
232
+ if (work.year > 0)
233
+ fields.push(' year={' + work.year + '}');
234
+ if (work.volume !== '')
235
+ fields.push(' volume={' + work.volume + '}');
236
+ if (work.issue !== '')
237
+ fields.push(' number={' + work.issue + '}');
238
+ if (work.page !== '')
239
+ fields.push(' pages={' + work.page + '}');
240
+ if (work.publisher !== '')
241
+ fields.push(' publisher={' + escapeLatex(work.publisher) + '}');
242
+ if (work.doi !== '')
243
+ fields.push(' doi={' + work.doi + '}');
244
+ if (work.url !== '')
245
+ fields.push(' url={' + escapeLatex(work.url) + '}');
246
+ return '@' + type + '{' + baseKey + ',\n' + fields.join(',\n') + '\n}';
247
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * dsh-cite —— 参考文献工具插件(node 半身,配置走 cordis.patch.yml)。
3
+ *
4
+ * 插件导出 apply(ctx, config):注册四个面向模型的工具(cite_lookup / cite_format /
5
+ * cite_bibtex / cite_check),通过 Crossref API 查询元数据并生成 GB/T 7714 / APA /
6
+ * MLA / Chicago 引文。零运行时依赖。
7
+ *
8
+ * @module dsh-cite
9
+ */
10
+ import { type CiteConfig } from './config.js';
11
+ import { type CiteToolDefinition } from './tools.js';
12
+ /** cordis 服务注入:apply 里要用 ctx.tools。 */
13
+ export declare const inject: string[];
14
+ export interface CitePluginContext {
15
+ tools: {
16
+ register(definition: CiteToolDefinition, options?: {
17
+ prepend?: boolean;
18
+ }): () => void;
19
+ };
20
+ on?(event: string, listener: () => void): () => void;
21
+ }
22
+ export declare function apply(ctx: CitePluginContext, config?: CiteConfig | null): void;
23
+ export * from './config.js';
24
+ export * from './crossref.js';
25
+ export * from './format.js';
26
+ export * from './tools.js';
package/lib/index.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * dsh-cite —— 参考文献工具插件(node 半身,配置走 cordis.patch.yml)。
3
+ *
4
+ * 插件导出 apply(ctx, config):注册四个面向模型的工具(cite_lookup / cite_format /
5
+ * cite_bibtex / cite_check),通过 Crossref API 查询元数据并生成 GB/T 7714 / APA /
6
+ * MLA / Chicago 引文。零运行时依赖。
7
+ *
8
+ * @module dsh-cite
9
+ */
10
+ import { resolveConfig } from './config.js';
11
+ import { buildCiteTools } from './tools.js';
12
+ /** cordis 服务注入:apply 里要用 ctx.tools。 */
13
+ export const inject = ['tools'];
14
+ export function apply(ctx, config) {
15
+ let cfg;
16
+ try {
17
+ cfg = resolveConfig(config);
18
+ }
19
+ catch (error) {
20
+ console.warn('[dsh-cite] ' + (error instanceof Error ? error.message : String(error)));
21
+ cfg = resolveConfig(null);
22
+ }
23
+ const disposers = [];
24
+ for (const definition of buildCiteTools(cfg)) {
25
+ disposers.push(ctx.tools.register(definition, { prepend: true }));
26
+ }
27
+ if (typeof ctx.on === 'function') {
28
+ ctx.on('dispose', () => {
29
+ for (const dispose of disposers)
30
+ dispose();
31
+ });
32
+ }
33
+ }
34
+ export * from './config.js';
35
+ export * from './crossref.js';
36
+ export * from './format.js';
37
+ export * from './tools.js';
package/lib/tools.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 四个面向模型的参考文献工具:
3
+ * cite_lookup / cite_format / cite_bibtex / cite_check。
4
+ *
5
+ * @module dsh-cite/tools
6
+ */
7
+ import { type ResolvedCiteConfig } from './config.js';
8
+ import { type FetchLike } from './crossref.js';
9
+ export interface ContentBlock {
10
+ type: 'text';
11
+ text: string;
12
+ }
13
+ export interface CiteToolDefinition {
14
+ name: string;
15
+ description: string;
16
+ parameters: {
17
+ type: 'object';
18
+ properties: Record<string, unknown>;
19
+ required?: string[];
20
+ };
21
+ output: {
22
+ schema: Record<string, unknown>;
23
+ render(args: unknown, value: unknown): ContentBlock[];
24
+ };
25
+ execute(args: unknown, exec: unknown): Promise<unknown>;
26
+ timeoutMs?: number;
27
+ }
28
+ export declare function buildCiteTools(cfg: ResolvedCiteConfig, fetchImpl?: FetchLike): CiteToolDefinition[];
package/lib/tools.js ADDED
@@ -0,0 +1,190 @@
1
+ /**
2
+ * 四个面向模型的参考文献工具:
3
+ * cite_lookup / cite_format / cite_bibtex / cite_check。
4
+ *
5
+ * @module dsh-cite/tools
6
+ */
7
+ import { optionalInteger, optionalString, requiredString } from './config.js';
8
+ import { extractDois, lookupDoi, searchWorks } from './crossref.js';
9
+ import { buildBibtex, buildCitation, readStyle } from './format.js';
10
+ function compileParameters(spec) {
11
+ const properties = {};
12
+ const required = [];
13
+ for (const [key, prop] of Object.entries(spec)) {
14
+ if (prop?.required === true)
15
+ required.push(key);
16
+ const node = {};
17
+ if (typeof prop?.type === 'string')
18
+ node.type = prop.type;
19
+ if (typeof prop?.description === 'string')
20
+ node.description = prop.description;
21
+ if (prop?.enum !== undefined)
22
+ node.enum = prop.enum;
23
+ properties[key] = node;
24
+ }
25
+ return { type: 'object', properties, ...(required.length > 0 ? { required } : {}) };
26
+ }
27
+ const authorSchema = {
28
+ type: 'object',
29
+ properties: { given: { type: 'string' }, family: { type: 'string' }, name: { type: 'string' } },
30
+ additionalProperties: true,
31
+ };
32
+ const workSchema = {
33
+ type: 'object',
34
+ properties: {
35
+ doi: { type: 'string' },
36
+ type: { type: 'string' },
37
+ title: { type: 'string' },
38
+ authors: { type: 'array', items: authorSchema },
39
+ containerTitle: { type: 'string' },
40
+ publisher: { type: 'string' },
41
+ year: { type: 'integer' },
42
+ volume: { type: 'string' },
43
+ issue: { type: 'string' },
44
+ page: { type: 'string' },
45
+ url: { type: 'string' },
46
+ isbn: { type: 'string' },
47
+ },
48
+ additionalProperties: true,
49
+ };
50
+ const lookupSchema = {
51
+ type: 'object',
52
+ properties: { count: { type: 'integer' }, works: { type: 'array', items: workSchema } },
53
+ additionalProperties: true,
54
+ };
55
+ const formatSchema = {
56
+ type: 'object',
57
+ properties: { doi: { type: 'string' }, style: { type: 'string' }, citation: { type: 'string' }, work: workSchema },
58
+ additionalProperties: true,
59
+ };
60
+ const bibtexSchema = {
61
+ type: 'object',
62
+ properties: { doi: { type: 'string' }, key: { type: 'string' }, bibtex: { type: 'string' } },
63
+ additionalProperties: true,
64
+ };
65
+ const checkItemSchema = {
66
+ type: 'object',
67
+ properties: { doi: { type: 'string' }, ok: { type: 'boolean' }, title: { type: 'string' }, url: { type: 'string' }, error: { type: 'string' } },
68
+ additionalProperties: true,
69
+ };
70
+ const checkSchema = {
71
+ type: 'object',
72
+ properties: { count: { type: 'integer' }, results: { type: 'array', items: checkItemSchema } },
73
+ additionalProperties: true,
74
+ };
75
+ function workLabel(work) {
76
+ const title = work.title !== '' ? work.title : '(无标题)';
77
+ return title + '(' + (work.year > 0 ? work.year + ', ' : '') + (work.containerTitle !== '' ? work.containerTitle : work.publisher) + ')';
78
+ }
79
+ export function buildCiteTools(cfg, fetchImpl) {
80
+ const citeLookup = {
81
+ name: 'cite_lookup',
82
+ description: '查询文献元数据:给 DOI 精确查询,或给题录文本(标题/作者/刊名)到 Crossref 检索。返回归一化后的作者、标题、期刊、年份、卷期页、DOI 等信息。',
83
+ parameters: compileParameters({
84
+ doi: { type: 'string', description: 'DOI(可选;与 query 至少给一个,例如 10.1038/nature12345)。' },
85
+ query: { type: 'string', description: '题录检索文本(可选;与 doi 至少给一个)。' },
86
+ limit: { type: 'integer', description: 'query 检索返回条数 1-10(默认 5)。' },
87
+ }),
88
+ output: { schema: lookupSchema, render: (_args, value) => {
89
+ const rec = (value ?? {});
90
+ const works = Array.isArray(rec.works) ? rec.works : [];
91
+ return [{ type: 'text', text: works.map((work, index) => (index + 1) + '. ' + workLabel(work)).join('\n') }];
92
+ } },
93
+ async execute(rawArgs) {
94
+ const args = (rawArgs ?? {});
95
+ const doi = optionalString(args, 'doi');
96
+ const query = optionalString(args, 'query');
97
+ if (doi === undefined && query === undefined)
98
+ throw new Error('cite_lookup 需要 doi 或 query 参数(至少一个)。');
99
+ if (doi !== undefined) {
100
+ const work = await lookupDoi(doi, cfg, fetchImpl);
101
+ return { count: 1, works: [work] };
102
+ }
103
+ const limit = optionalInteger(args, 'limit', '返回条数', 1, 10, 5);
104
+ const works = await searchWorks(query, limit, cfg, fetchImpl);
105
+ if (works.length === 0)
106
+ throw new Error('Crossref 没有找到匹配文献,请尝试更精确的标题/作者。');
107
+ return { count: works.length, works };
108
+ },
109
+ timeoutMs: cfg.timeoutMs + 2000,
110
+ };
111
+ const citeFormat = {
112
+ name: 'cite_format',
113
+ description: '按 DOI 生成规范参考文献条目。style 支持 gb-t-7714(默认,中国论文标准)、apa、mla、chicago;lang 为 zh 或 en(影响电子文献引用日期措辞)。',
114
+ parameters: compileParameters({
115
+ doi: { type: 'string', required: true, description: '文献 DOI(必填)。' },
116
+ style: { type: 'string', enum: ['gb-t-7714', 'apa', 'mla', 'chicago'], description: '引用格式(默认 gb-t-7714)。' },
117
+ lang: { type: 'string', enum: ['zh', 'en'], description: '语言(默认 zh)。' },
118
+ }),
119
+ output: { schema: formatSchema, render: (_args, value) => {
120
+ const rec = (value ?? {});
121
+ return [{ type: 'text', text: typeof rec.citation === 'string' ? rec.citation : '' }];
122
+ } },
123
+ async execute(rawArgs) {
124
+ const args = (rawArgs ?? {});
125
+ const doi = requiredString(args, 'doi', 'DOI');
126
+ const style = readStyle(args.style);
127
+ const lang = args.lang === 'en' ? 'en' : 'zh';
128
+ const work = await lookupDoi(doi, cfg, fetchImpl);
129
+ const citation = buildCitation(work, style, lang);
130
+ return { doi: work.doi || doi, style, citation, work };
131
+ },
132
+ timeoutMs: cfg.timeoutMs + 2000,
133
+ };
134
+ const citeBibtex = {
135
+ name: 'cite_bibtex',
136
+ description: '按 DOI 生成 BibTeX 条目。key 可选,缺省用第一作者 + 年份 + 标题首词生成。',
137
+ parameters: compileParameters({
138
+ doi: { type: 'string', required: true, description: '文献 DOI(必填)。' },
139
+ key: { type: 'string', description: 'BibTeX 引用键(可选)。' },
140
+ }),
141
+ output: { schema: bibtexSchema, render: (_args, value) => {
142
+ const rec = (value ?? {});
143
+ return [{ type: 'text', text: typeof rec.bibtex === 'string' ? rec.bibtex : '' }];
144
+ } },
145
+ async execute(rawArgs) {
146
+ const args = (rawArgs ?? {});
147
+ const doi = requiredString(args, 'doi', 'DOI');
148
+ const key = optionalString(args, 'key');
149
+ const work = await lookupDoi(doi, cfg, fetchImpl);
150
+ const bibtex = buildBibtex(work, key);
151
+ return { doi: work.doi || doi, key, bibtex };
152
+ },
153
+ timeoutMs: cfg.timeoutMs + 2000,
154
+ };
155
+ const citeCheck = {
156
+ name: 'cite_check',
157
+ description: '从一段文本中提取所有 DOI 并逐个到 Crossref 校验是否存在,返回每个 DOI 的状态与标题。用于检查参考文献清单里的 DOI 是否有效。',
158
+ parameters: compileParameters({
159
+ text: { type: 'string', required: true, description: '要检查的文本(参考文献列表或任意含 DOI 的文本)。' },
160
+ maxChecks: { type: 'integer', description: '最多校验多少个 DOI,1-50(默认 10)。' },
161
+ }),
162
+ output: { schema: checkSchema, render: (_args, value) => {
163
+ const rec = (value ?? {});
164
+ const results = Array.isArray(rec.results) ? rec.results : [];
165
+ return [{ type: 'text', text: 'DOI 校验:' + results.map((item) => {
166
+ const row = (item ?? {});
167
+ return (row.ok === true ? '✅' : '❌') + ' ' + row.doi + (typeof row.title === 'string' && row.title !== '' ? ' — ' + row.title : '');
168
+ }).join('\n') }];
169
+ } },
170
+ async execute(rawArgs) {
171
+ const args = (rawArgs ?? {});
172
+ const text = requiredString(args, 'text', '待检查文本');
173
+ const maxChecks = optionalInteger(args, 'maxChecks', '校验数量', 1, 50, 10);
174
+ const dois = extractDois(text).slice(0, maxChecks);
175
+ const results = [];
176
+ for (const doi of dois) {
177
+ try {
178
+ const work = await lookupDoi(doi, cfg, fetchImpl);
179
+ results.push({ doi, ok: true, title: work.title, url: work.url });
180
+ }
181
+ catch (error) {
182
+ results.push({ doi, ok: false, title: '', url: '', error: error instanceof Error ? error.message : String(error) });
183
+ }
184
+ }
185
+ return { count: results.length, results };
186
+ },
187
+ timeoutMs: cfg.timeoutMs * 3 + 5000,
188
+ };
189
+ return [citeLookup, citeFormat, citeBibtex, citeCheck];
190
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "dsh-cite",
3
+ "version": "0.1.0",
4
+ "description": "DSH 参考文献工具插件:cite_lookup/cite_format/cite_bibtex/cite_check 四工具,Crossref 元数据查询,GB/T 7714 / APA / MLA / Chicago 格式化,零运行时依赖。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./cordis.patch.yml": "./cordis.patch.yml",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "cordis.patch.yml",
19
+ "README.md",
20
+ "README.en.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "prepare": "tsc -p tsconfig.json",
25
+ "typecheck": "tsc -p tsconfig.json --noEmit",
26
+ "test": "pnpm run build && node --test \"test/*.test.mjs\"",
27
+ "prepublishOnly": "pnpm run build"
28
+ },
29
+ "dsh": {
30
+ "bundle": {
31
+ "patch": "./cordis.patch.yml"
32
+ }
33
+ },
34
+ "keywords": [
35
+ "dsh",
36
+ "deepseek-harness",
37
+ "plugin",
38
+ "citation",
39
+ "crossref",
40
+ "gb-t-7714",
41
+ "apa",
42
+ "mla",
43
+ "chicago"
44
+ ],
45
+ "license": "MIT",
46
+ "engines": {
47
+ "node": ">=22.13"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^24.0.0",
51
+ "typescript": "^5.6.0"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/STARDUSTLC666/dsh-cite.git"
56
+ },
57
+ "bugs": {
58
+ "url": "https://github.com/STARDUSTLC666/dsh-cite/issues"
59
+ },
60
+ "homepage": "https://github.com/STARDUSTLC666/dsh-cite#readme",
61
+ "author": "stardustlc",
62
+ "packageManager": "pnpm@11.7.0"
63
+ }