dsh-plugin-litsearch 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +157 -2
- package/client/client.js +9036 -0
- package/cordis.patch.yml +10 -0
- package/lib/host/events.d.ts +19 -0
- package/lib/host/events.js +58 -0
- package/lib/host/export/bibtex.d.ts +3 -0
- package/lib/host/export/bibtex.js +42 -0
- package/lib/host/export/common.d.ts +18 -0
- package/lib/host/export/common.js +25 -0
- package/lib/host/export/csv.d.ts +8 -0
- package/lib/host/export/csv.js +68 -0
- package/lib/host/export/index.d.ts +5 -0
- package/lib/host/export/index.js +25 -0
- package/lib/host/export/markdown.d.ts +6 -0
- package/lib/host/export/markdown.js +120 -0
- package/lib/host/export/ris.d.ts +3 -0
- package/lib/host/export/ris.js +35 -0
- package/lib/host/paths.d.ts +43 -0
- package/lib/host/paths.js +128 -0
- package/lib/host/pdf/store.d.ts +57 -0
- package/lib/host/pdf/store.js +291 -0
- package/lib/host/router.d.ts +20 -0
- package/lib/host/router.js +852 -0
- package/lib/host/tasks/compare.d.ts +8 -0
- package/lib/host/tasks/compare.js +49 -0
- package/lib/host/tasks/filter.d.ts +42 -0
- package/lib/host/tasks/filter.js +100 -0
- package/lib/host/tasks/rounds.d.ts +28 -0
- package/lib/host/tasks/rounds.js +74 -0
- package/lib/host/tasks/runner.d.ts +29 -0
- package/lib/host/tasks/runner.js +208 -0
- package/lib/host/tasks/service.d.ts +171 -0
- package/lib/host/tasks/service.js +976 -0
- package/lib/host/tasks/states.d.ts +28 -0
- package/lib/host/tasks/states.js +223 -0
- package/lib/host/tasks/stats.d.ts +13 -0
- package/lib/host/tasks/stats.js +233 -0
- package/lib/host/tasks/store.d.ts +64 -0
- package/lib/host/tasks/store.js +280 -0
- package/lib/host/tasks/summary.d.ts +9 -0
- package/lib/host/tasks/summary.js +54 -0
- package/lib/host/tools.d.ts +47 -0
- package/lib/host/tools.js +1138 -0
- package/lib/host/zotero/connector.d.ts +46 -0
- package/lib/host/zotero/connector.js +263 -0
- package/lib/index.d.ts +107 -0
- package/lib/index.js +153 -0
- package/lib/search/aggregate.d.ts +17 -0
- package/lib/search/aggregate.js +60 -0
- package/lib/search/config.d.ts +96 -0
- package/lib/search/config.js +96 -0
- package/lib/search/errors.d.ts +22 -0
- package/lib/search/errors.js +24 -0
- package/lib/search/health.d.ts +13 -0
- package/lib/search/health.js +53 -0
- package/lib/search/http.d.ts +13 -0
- package/lib/search/http.js +81 -0
- package/lib/search/identity.d.ts +27 -0
- package/lib/search/identity.js +154 -0
- package/lib/search/normalize.d.ts +17 -0
- package/lib/search/normalize.js +47 -0
- package/lib/search/oa.d.ts +21 -0
- package/lib/search/oa.js +149 -0
- package/lib/search/providers/arxiv.d.ts +9 -0
- package/lib/search/providers/arxiv.js +99 -0
- package/lib/search/providers/crossref.d.ts +8 -0
- package/lib/search/providers/crossref.js +107 -0
- package/lib/search/providers/europepmc.d.ts +8 -0
- package/lib/search/providers/europepmc.js +105 -0
- package/lib/search/providers/index.d.ts +13 -0
- package/lib/search/providers/index.js +34 -0
- package/lib/search/providers/openalex.d.ts +8 -0
- package/lib/search/providers/openalex.js +109 -0
- package/lib/search/providers/pubmed.d.ts +10 -0
- package/lib/search/providers/pubmed.js +143 -0
- package/lib/search/providers/pubtator.d.ts +9 -0
- package/lib/search/providers/pubtator.js +79 -0
- package/lib/search/providers/semanticscholar.d.ts +9 -0
- package/lib/search/providers/semanticscholar.js +97 -0
- package/lib/search/pubtator-entities.d.ts +9 -0
- package/lib/search/pubtator-entities.js +19 -0
- package/lib/search/pubtator-parse.d.ts +3 -0
- package/lib/search/pubtator-parse.js +42 -0
- package/lib/search/query-adapter.d.ts +15 -0
- package/lib/search/query-adapter.js +69 -0
- package/lib/search/rate-limit.d.ts +31 -0
- package/lib/search/rate-limit.js +219 -0
- package/lib/search/types.d.ts +577 -0
- package/lib/search/types.js +15 -0
- package/lib/search/xml.d.ts +20 -0
- package/lib/search/xml.js +63 -0
- package/package.json +75 -4
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 检索层公共类型:数据源无关的论文记录、任务模型与请求响应信封。
|
|
3
|
+
*
|
|
4
|
+
* 本文件同时被 Host 与 Client 引用,因此不得引入任何 Node 内置模块。
|
|
5
|
+
*/
|
|
6
|
+
/** 已接入的免费数据源 id;顺序即聚合展示顺序。 */
|
|
7
|
+
export declare const SOURCE_IDS: readonly ["pubmed", "europepmc", "openalex", "crossref", "semanticscholar", "arxiv", "pubtator"];
|
|
8
|
+
export type SourceId = (typeof SOURCE_IDS)[number];
|
|
9
|
+
/** 排序方式。 */
|
|
10
|
+
export type SortMode = 'relevance' | 'citations' | 'year';
|
|
11
|
+
/** 语言筛选。 */
|
|
12
|
+
export type TaskLanguage = 'any' | 'eng' | 'chi';
|
|
13
|
+
/** 论文的持久标识集合(裸值,不带 URL 前缀)。 */
|
|
14
|
+
export interface PaperIds {
|
|
15
|
+
doi: string;
|
|
16
|
+
pmid: string;
|
|
17
|
+
pmcid: string;
|
|
18
|
+
arxivId: string;
|
|
19
|
+
}
|
|
20
|
+
/** provider 归一化后的论文记录(尚无跨源身份)。 */
|
|
21
|
+
export interface PaperRecord {
|
|
22
|
+
source: SourceId;
|
|
23
|
+
/** 数据源内部 id(如 OpenAlex Work id、Crossref DOI)。 */
|
|
24
|
+
sourceId: string;
|
|
25
|
+
ids: PaperIds;
|
|
26
|
+
title: string;
|
|
27
|
+
authors: string[];
|
|
28
|
+
year: number | null;
|
|
29
|
+
venue: string;
|
|
30
|
+
/** 裸 DOI(不带 https://doi.org/ 前缀),无则为空串。 */
|
|
31
|
+
doi: string;
|
|
32
|
+
url: string;
|
|
33
|
+
abstract: string;
|
|
34
|
+
/** 上游给出的语言标记(如 eng / chi),未知时缺省。 */
|
|
35
|
+
language?: string;
|
|
36
|
+
citedBy: number | null;
|
|
37
|
+
openAccess: boolean;
|
|
38
|
+
/** 开放获取全文/PDF 直链,无则为空串。 */
|
|
39
|
+
pdfUrl: string;
|
|
40
|
+
type: string;
|
|
41
|
+
}
|
|
42
|
+
/** 一次命中:某个数据源在某个检索式里的排位。 */
|
|
43
|
+
export interface PaperHit {
|
|
44
|
+
source: SourceId;
|
|
45
|
+
/** 该源内的 1-based 排位。 */
|
|
46
|
+
rank: number;
|
|
47
|
+
}
|
|
48
|
+
/** 合并后的论文条目(任务结果的最小单位为它)。 */
|
|
49
|
+
export interface Paper extends PaperRecord {
|
|
50
|
+
/** 稳定身份键的散列,跨任务/跨检索一致。 */
|
|
51
|
+
paperId: string;
|
|
52
|
+
hitSources: PaperHit[];
|
|
53
|
+
/** 命中该论文的检索式 id。 */
|
|
54
|
+
matchedQueries: string[];
|
|
55
|
+
}
|
|
56
|
+
export interface ProviderCapabilities {
|
|
57
|
+
abstract: boolean;
|
|
58
|
+
citationCount: boolean;
|
|
59
|
+
openAccess: boolean;
|
|
60
|
+
pdf: boolean;
|
|
61
|
+
}
|
|
62
|
+
export interface ProviderInfo {
|
|
63
|
+
id: SourceId;
|
|
64
|
+
label: string;
|
|
65
|
+
homepage: string;
|
|
66
|
+
capabilities: ProviderCapabilities;
|
|
67
|
+
/** 面向用户/模型的限流说明。 */
|
|
68
|
+
rateLimit: string;
|
|
69
|
+
/** 检索式语法特点。 */
|
|
70
|
+
syntax: string;
|
|
71
|
+
/** 是否需要(可选)密钥才能获得更高额度。 */
|
|
72
|
+
optionalKey: boolean;
|
|
73
|
+
}
|
|
74
|
+
/** 单次检索的请求参数(任务执行器使用)。 */
|
|
75
|
+
export interface SearchOptions {
|
|
76
|
+
query: string;
|
|
77
|
+
/** 单源语法覆写(来自 QueryBlock.perSource)。 */
|
|
78
|
+
perSource?: Partial<Record<SourceId, string>>;
|
|
79
|
+
sources: SourceId[];
|
|
80
|
+
limit: number;
|
|
81
|
+
yearFrom?: number | undefined;
|
|
82
|
+
yearTo?: number | undefined;
|
|
83
|
+
openAccessOnly: boolean;
|
|
84
|
+
contactEmail: string;
|
|
85
|
+
timeoutMs: number;
|
|
86
|
+
signal?: AbortSignal | undefined;
|
|
87
|
+
/** 诊断测试用:忽略限流冷却,仍走队列与最小间隔。 */
|
|
88
|
+
bypassCooldown?: boolean;
|
|
89
|
+
}
|
|
90
|
+
/** provider 实现拿到的已解析参数。 */
|
|
91
|
+
export interface ProviderSearchOptions {
|
|
92
|
+
limit: number;
|
|
93
|
+
yearFrom?: number | undefined;
|
|
94
|
+
yearTo?: number | undefined;
|
|
95
|
+
openAccessOnly: boolean;
|
|
96
|
+
contactEmail: string;
|
|
97
|
+
timeoutMs: number;
|
|
98
|
+
signal?: AbortSignal | undefined;
|
|
99
|
+
/** 诊断测试用:忽略限流冷却。 */
|
|
100
|
+
bypassCooldown?: boolean;
|
|
101
|
+
/** 可选密钥;provider 只用于请求参数/请求头,不得写日志。 */
|
|
102
|
+
ncbiApiKey?: string;
|
|
103
|
+
s2ApiKey?: string;
|
|
104
|
+
}
|
|
105
|
+
export interface Provider {
|
|
106
|
+
info: ProviderInfo;
|
|
107
|
+
search(query: string, options: ProviderSearchOptions): Promise<PaperRecord[]>;
|
|
108
|
+
}
|
|
109
|
+
/** 单个数据源在一次检索中的结果状态。 */
|
|
110
|
+
export interface SourceStatus {
|
|
111
|
+
id: SourceId;
|
|
112
|
+
/** 是否成功参与检索;因限流跳过的源不算失败,也为 true。 */
|
|
113
|
+
ok: boolean;
|
|
114
|
+
count: number;
|
|
115
|
+
error?: string;
|
|
116
|
+
/** 因遵守上游限流而跳过,不计入失败 / partial,面板只给中性提示。 */
|
|
117
|
+
skipped?: boolean;
|
|
118
|
+
/** 跳过原因(中性文案)。 */
|
|
119
|
+
note?: string;
|
|
120
|
+
/** 该源不支持下推、结果被后置过滤过。 */
|
|
121
|
+
filtered?: boolean;
|
|
122
|
+
}
|
|
123
|
+
export type TaskStatus = 'draft' | 'queued' | 'running' | 'done' | 'partial' | 'error' | 'canceled';
|
|
124
|
+
export type TaskOrigin = 'model' | 'panel' | 'history' | 'api';
|
|
125
|
+
export type Screening = 'unread' | 'read' | 'included' | 'excluded';
|
|
126
|
+
/** 一条具名检索式;单检索式任务即长度 1。 */
|
|
127
|
+
export interface QueryBlock {
|
|
128
|
+
id: string;
|
|
129
|
+
name: string;
|
|
130
|
+
text: string;
|
|
131
|
+
/** 单源语法覆写;缺省时按源降级适配。 */
|
|
132
|
+
perSource?: Partial<Record<SourceId, string>>;
|
|
133
|
+
enabled: boolean;
|
|
134
|
+
}
|
|
135
|
+
export interface TaskConditions {
|
|
136
|
+
queries: QueryBlock[];
|
|
137
|
+
sources: SourceId[];
|
|
138
|
+
yearFrom?: number | undefined;
|
|
139
|
+
yearTo?: number | undefined;
|
|
140
|
+
openAccessOnly: boolean;
|
|
141
|
+
language: TaskLanguage;
|
|
142
|
+
types?: string[];
|
|
143
|
+
sort: SortMode;
|
|
144
|
+
/** 每次加载条数。 */
|
|
145
|
+
pageSize: number;
|
|
146
|
+
}
|
|
147
|
+
/** 全文获取状态;`unknown` 表示尚未尝试。 */
|
|
148
|
+
export type PdfStatus = 'unknown' | 'available' | 'downloaded' | 'html-only' | 'paywalled' | 'blocked' | 'failed';
|
|
149
|
+
/** 论文级人工筛选状态。 */
|
|
150
|
+
export interface PaperState {
|
|
151
|
+
screening: Screening;
|
|
152
|
+
groupId?: string;
|
|
153
|
+
shortlistOrder?: number;
|
|
154
|
+
note?: string;
|
|
155
|
+
tags?: string[];
|
|
156
|
+
pdfStatus?: PdfStatus;
|
|
157
|
+
/** 失败原因(面向用户的一句话)。 */
|
|
158
|
+
pdfError?: string;
|
|
159
|
+
/** 已下载到用户目录的绝对路径。 */
|
|
160
|
+
pdfFile?: string;
|
|
161
|
+
}
|
|
162
|
+
export interface TaskGroup {
|
|
163
|
+
id: string;
|
|
164
|
+
name: string;
|
|
165
|
+
description?: string;
|
|
166
|
+
order: number;
|
|
167
|
+
}
|
|
168
|
+
export interface TaskProgress {
|
|
169
|
+
queryIndex: number;
|
|
170
|
+
queryTotal: number;
|
|
171
|
+
source?: SourceId;
|
|
172
|
+
}
|
|
173
|
+
export interface TaskStats {
|
|
174
|
+
/** queryId → 该检索式原始命中数。 */
|
|
175
|
+
perQuery: Record<string, number>;
|
|
176
|
+
rawHits: number;
|
|
177
|
+
deduped: number;
|
|
178
|
+
screening: Record<Screening, number>;
|
|
179
|
+
}
|
|
180
|
+
export interface TaskResult {
|
|
181
|
+
papers: Paper[];
|
|
182
|
+
sources: SourceStatus[];
|
|
183
|
+
fetchedAt?: string;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* 任务内一次检索(一轮)的元数据快照。
|
|
187
|
+
*
|
|
188
|
+
* 只留「这一轮用什么条件、什么时候跑、跑出多少、结果如何」,**不保存文献列表**:
|
|
189
|
+
* 当前结果永远是最新一轮的 `result.papers`,人工标注按 paperId 跨轮保留。
|
|
190
|
+
* 历史轮次用于时间轴回看与「回填条件重跑」。
|
|
191
|
+
*/
|
|
192
|
+
export interface SearchRound {
|
|
193
|
+
/** 任务内稳定 id,形如 `r3`。 */
|
|
194
|
+
id: string;
|
|
195
|
+
/** 从 1 开始递增的序号(轮次被裁剪后仍保持递增,不复用旧号)。 */
|
|
196
|
+
index: number;
|
|
197
|
+
createdAt: string;
|
|
198
|
+
finishedAt?: string;
|
|
199
|
+
/** 本轮发起方:模型工具 / 右侧栏面板。 */
|
|
200
|
+
origin: TaskOrigin;
|
|
201
|
+
/** 本轮检索条件快照,深拷贝,与后续轮次的修改完全隔离。 */
|
|
202
|
+
conditions: TaskConditions;
|
|
203
|
+
/** 运行中为空,结束后回填任务状态。 */
|
|
204
|
+
status?: TaskStatus;
|
|
205
|
+
/** 本轮命中统计快照。 */
|
|
206
|
+
stats?: {
|
|
207
|
+
rawHits: number;
|
|
208
|
+
deduped: number;
|
|
209
|
+
loaded: number;
|
|
210
|
+
perQuery: Record<string, number>;
|
|
211
|
+
};
|
|
212
|
+
/** 因遵守上游限流而跳过的数据源数量。 */
|
|
213
|
+
skippedSources?: number;
|
|
214
|
+
/** 真实失败的数据源数量。 */
|
|
215
|
+
failedSources?: number;
|
|
216
|
+
/** 取消 / 异常原因。 */
|
|
217
|
+
error?: string;
|
|
218
|
+
}
|
|
219
|
+
export interface SearchTask {
|
|
220
|
+
id: string;
|
|
221
|
+
title: string;
|
|
222
|
+
conditions: TaskConditions;
|
|
223
|
+
status: TaskStatus;
|
|
224
|
+
origin: TaskOrigin;
|
|
225
|
+
fromTaskId?: string;
|
|
226
|
+
/** 每次状态/条件变化自增。 */
|
|
227
|
+
rev: number;
|
|
228
|
+
createdAt: string;
|
|
229
|
+
updatedAt: string;
|
|
230
|
+
pinned: boolean;
|
|
231
|
+
/** 任务级失败原因(数据源级失败在 `result.sources[].error`)。 */
|
|
232
|
+
error?: string;
|
|
233
|
+
/** 任务级研究笔记。 */
|
|
234
|
+
notes: string;
|
|
235
|
+
groups: TaskGroup[];
|
|
236
|
+
progress?: TaskProgress;
|
|
237
|
+
stats: TaskStats;
|
|
238
|
+
result: TaskResult;
|
|
239
|
+
/** paperId → 筛选状态。 */
|
|
240
|
+
states: Record<string, PaperState>;
|
|
241
|
+
/**
|
|
242
|
+
* 任务内检索轮次(按时间升序,只保留最近若干轮)。
|
|
243
|
+
* 旧版本落盘的任务没有该字段,读取时统一按 `[]` 兼容。
|
|
244
|
+
*/
|
|
245
|
+
rounds: SearchRound[];
|
|
246
|
+
}
|
|
247
|
+
/** 历史列表与事件推送用的任务摘要。 */
|
|
248
|
+
export interface TaskSummary {
|
|
249
|
+
id: string;
|
|
250
|
+
title: string;
|
|
251
|
+
status: TaskStatus;
|
|
252
|
+
origin: TaskOrigin;
|
|
253
|
+
createdAt: string;
|
|
254
|
+
updatedAt: string;
|
|
255
|
+
rev: number;
|
|
256
|
+
pinned: boolean;
|
|
257
|
+
error?: string;
|
|
258
|
+
stats: TaskStats;
|
|
259
|
+
progress?: TaskProgress;
|
|
260
|
+
/** 已完成 + 进行中的轮次数(旧数据兼容为 0)。 */
|
|
261
|
+
roundCount: number;
|
|
262
|
+
}
|
|
263
|
+
/** 任务详情(不含结果正文,结果走分页端点)。 */
|
|
264
|
+
export interface TaskDetail extends TaskSummary {
|
|
265
|
+
conditions: TaskConditions;
|
|
266
|
+
notes: string;
|
|
267
|
+
groups: TaskGroup[];
|
|
268
|
+
/** 轮次元数据,供时间轴展示与条件回填;按时间升序。 */
|
|
269
|
+
rounds: SearchRound[];
|
|
270
|
+
/** paperId → 筛读状态;只含有过人工操作或继承的条目。 */
|
|
271
|
+
states: Record<string, PaperState>;
|
|
272
|
+
/** 建立任务时继承筛读状态的来源任务。 */
|
|
273
|
+
fromTaskId?: string;
|
|
274
|
+
result: {
|
|
275
|
+
totalLoaded: number;
|
|
276
|
+
sources: SourceStatus[];
|
|
277
|
+
fetchedAt?: string;
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/** `/tasks/:id/results` 的分页响应。 */
|
|
281
|
+
export interface ResultsPage {
|
|
282
|
+
taskId: string;
|
|
283
|
+
rev: number;
|
|
284
|
+
total: number;
|
|
285
|
+
offset: number;
|
|
286
|
+
limit: number;
|
|
287
|
+
papers: Paper[];
|
|
288
|
+
}
|
|
289
|
+
/** 概览统计的计数项。 */
|
|
290
|
+
export interface CountEntry {
|
|
291
|
+
key: string;
|
|
292
|
+
count: number;
|
|
293
|
+
included: number;
|
|
294
|
+
}
|
|
295
|
+
export interface SourceStat {
|
|
296
|
+
id: SourceId;
|
|
297
|
+
hits: number;
|
|
298
|
+
uniqueOnly: number;
|
|
299
|
+
oa: number;
|
|
300
|
+
withPdf: number;
|
|
301
|
+
}
|
|
302
|
+
export interface QueryContribution {
|
|
303
|
+
queryId: string;
|
|
304
|
+
name: string;
|
|
305
|
+
hits: number;
|
|
306
|
+
uniqueOnly: number;
|
|
307
|
+
/** 与其它检索式的两两重叠计数(key = queryId)。 */
|
|
308
|
+
overlap: Record<string, number>;
|
|
309
|
+
}
|
|
310
|
+
export interface YearStat {
|
|
311
|
+
year: number;
|
|
312
|
+
count: number;
|
|
313
|
+
included: number;
|
|
314
|
+
}
|
|
315
|
+
/** `/tasks/:id/stats` 的统计口径与结果。 */
|
|
316
|
+
export interface TaskStatsReport {
|
|
317
|
+
taskId: string;
|
|
318
|
+
rev: number;
|
|
319
|
+
scope: 'task' | 'filtered';
|
|
320
|
+
/** 参与统计的已加载篇数。 */
|
|
321
|
+
loaded: number;
|
|
322
|
+
/** 任务已加载总数(未筛选)。 */
|
|
323
|
+
totalHint: number;
|
|
324
|
+
funnel: {
|
|
325
|
+
rawHits: number;
|
|
326
|
+
deduped: number;
|
|
327
|
+
filtered: number;
|
|
328
|
+
included: number;
|
|
329
|
+
excluded: number;
|
|
330
|
+
unread: number;
|
|
331
|
+
read: number;
|
|
332
|
+
};
|
|
333
|
+
bySource: SourceStat[];
|
|
334
|
+
byQuery: QueryContribution[];
|
|
335
|
+
byYear: YearStat[];
|
|
336
|
+
byType: CountEntry[];
|
|
337
|
+
byLanguage: CountEntry[];
|
|
338
|
+
topVenues: CountEntry[];
|
|
339
|
+
topAuthors: CountEntry[];
|
|
340
|
+
metrics: {
|
|
341
|
+
oaRate: number;
|
|
342
|
+
pdfRate: number;
|
|
343
|
+
htmlOnlyRate: number;
|
|
344
|
+
medianCitations: number;
|
|
345
|
+
topCited: string[];
|
|
346
|
+
latest: string[];
|
|
347
|
+
/** 相对 `fromTaskId` 的新增篇数;无基准任务时缺省。 */
|
|
348
|
+
newVsBase?: number;
|
|
349
|
+
};
|
|
350
|
+
screeningProgress: {
|
|
351
|
+
included: number;
|
|
352
|
+
excluded: number;
|
|
353
|
+
pending: number;
|
|
354
|
+
};
|
|
355
|
+
unavailableFullText: {
|
|
356
|
+
paywalled: number;
|
|
357
|
+
blocked: number;
|
|
358
|
+
failed: number;
|
|
359
|
+
};
|
|
360
|
+
/** 全文覆盖:与台账同一口径。 */
|
|
361
|
+
fullText: FullTextCoverage;
|
|
362
|
+
}
|
|
363
|
+
/** 全文获取覆盖计数。 */
|
|
364
|
+
export interface FullTextCoverage {
|
|
365
|
+
unknown: number;
|
|
366
|
+
available: number;
|
|
367
|
+
downloaded: number;
|
|
368
|
+
htmlOnly: number;
|
|
369
|
+
paywalled: number;
|
|
370
|
+
blocked: number;
|
|
371
|
+
failed: number;
|
|
372
|
+
}
|
|
373
|
+
/** 单篇论文的批量注解输入(`POST /tasks/:id/annotate`)。 */
|
|
374
|
+
export interface AnnotationInput {
|
|
375
|
+
paperId: string;
|
|
376
|
+
screening?: Screening;
|
|
377
|
+
/** `null` 表示移出分组。 */
|
|
378
|
+
groupId?: string | null;
|
|
379
|
+
/** `null` 表示清除必读顺序。 */
|
|
380
|
+
shortlistOrder?: number | null;
|
|
381
|
+
note?: string;
|
|
382
|
+
tags?: string[];
|
|
383
|
+
/** 全文状态由 Host 下载/同步流程写入,面板也可显示与筛选。 */
|
|
384
|
+
pdfStatus?: PdfStatus;
|
|
385
|
+
pdfError?: string;
|
|
386
|
+
pdfFile?: string;
|
|
387
|
+
}
|
|
388
|
+
/** 单篇 PDF 下载结果。 */
|
|
389
|
+
export interface PdfDownloadResult {
|
|
390
|
+
paperId: string;
|
|
391
|
+
status: PdfStatus;
|
|
392
|
+
bytes: number;
|
|
393
|
+
/** 用户目录中的可读文件名。 */
|
|
394
|
+
filename?: string;
|
|
395
|
+
path?: string;
|
|
396
|
+
url?: string;
|
|
397
|
+
error?: string;
|
|
398
|
+
}
|
|
399
|
+
/** Zotero connector 探测结果。 */
|
|
400
|
+
export interface ZoteroProbeResult {
|
|
401
|
+
ok: boolean;
|
|
402
|
+
version?: string;
|
|
403
|
+
collection?: {
|
|
404
|
+
id: number | string;
|
|
405
|
+
name: string;
|
|
406
|
+
libraryID?: number;
|
|
407
|
+
filesEditable?: boolean;
|
|
408
|
+
};
|
|
409
|
+
error?: string;
|
|
410
|
+
}
|
|
411
|
+
/** Zotero 批量同步结果。 */
|
|
412
|
+
export interface ZoteroSyncResult {
|
|
413
|
+
ok: boolean;
|
|
414
|
+
saved: number;
|
|
415
|
+
attached: number;
|
|
416
|
+
failed: number;
|
|
417
|
+
messages: string[];
|
|
418
|
+
}
|
|
419
|
+
/** 下载目录可写性测试结果。 */
|
|
420
|
+
export interface StorageProbeResult {
|
|
421
|
+
ok: boolean;
|
|
422
|
+
root: string;
|
|
423
|
+
writable: boolean;
|
|
424
|
+
error?: string;
|
|
425
|
+
}
|
|
426
|
+
export interface AnnotationResult {
|
|
427
|
+
taskId: string;
|
|
428
|
+
rev: number;
|
|
429
|
+
updated: number;
|
|
430
|
+
}
|
|
431
|
+
/** 单篇论文详情(独立论文页使用)。 */
|
|
432
|
+
export interface PaperDetail {
|
|
433
|
+
taskId: string;
|
|
434
|
+
taskTitle: string;
|
|
435
|
+
rev: number;
|
|
436
|
+
paper: Paper;
|
|
437
|
+
state?: PaperState;
|
|
438
|
+
groups: TaskGroup[];
|
|
439
|
+
}
|
|
440
|
+
/** PubTator 标注的单个实体(按标识符与原文去重)。 */
|
|
441
|
+
export interface PaperEntity {
|
|
442
|
+
text: string;
|
|
443
|
+
type: string;
|
|
444
|
+
identifier: string;
|
|
445
|
+
database: string;
|
|
446
|
+
name: string;
|
|
447
|
+
/** 同一实体在标题/摘要/全文中出现的次数。 */
|
|
448
|
+
count: number;
|
|
449
|
+
}
|
|
450
|
+
/** 单篇论文的 PubTator 实体结果。 */
|
|
451
|
+
export interface PaperEntities {
|
|
452
|
+
paperId: string;
|
|
453
|
+
pmid: string;
|
|
454
|
+
entities: PaperEntity[];
|
|
455
|
+
fetchedAt: string;
|
|
456
|
+
}
|
|
457
|
+
/** 对比视图里的轻量论文投影。 */
|
|
458
|
+
export interface ComparePaper {
|
|
459
|
+
paperId: string;
|
|
460
|
+
title: string;
|
|
461
|
+
authors: string[];
|
|
462
|
+
year: number | null;
|
|
463
|
+
venue: string;
|
|
464
|
+
doi: string;
|
|
465
|
+
openAccess: boolean;
|
|
466
|
+
}
|
|
467
|
+
export interface CompareResult {
|
|
468
|
+
taskId: string;
|
|
469
|
+
rev: number;
|
|
470
|
+
withTaskId: string;
|
|
471
|
+
withTitle: string;
|
|
472
|
+
counts: {
|
|
473
|
+
added: number;
|
|
474
|
+
common: number;
|
|
475
|
+
missing: number;
|
|
476
|
+
total: number;
|
|
477
|
+
baseTotal: number;
|
|
478
|
+
};
|
|
479
|
+
/** 当前任务有、基准任务没有的论文。 */
|
|
480
|
+
added: ComparePaper[];
|
|
481
|
+
/** 基准任务有、当前任务没有的论文。 */
|
|
482
|
+
missing: ComparePaper[];
|
|
483
|
+
}
|
|
484
|
+
export type ExportFormat = 'csv' | 'ris' | 'bibtex' | 'md';
|
|
485
|
+
export interface ExportFile {
|
|
486
|
+
filename: string;
|
|
487
|
+
mime: string;
|
|
488
|
+
content: string;
|
|
489
|
+
}
|
|
490
|
+
export type CleanupScope = 'tasks' | 'pdf' | 'all';
|
|
491
|
+
export interface CleanupResult {
|
|
492
|
+
scope: CleanupScope;
|
|
493
|
+
removed: number;
|
|
494
|
+
freedBytes: number;
|
|
495
|
+
remaining: number;
|
|
496
|
+
}
|
|
497
|
+
/** 单个数据源的运行时健康状态(/health 与设置页诊断使用)。 */
|
|
498
|
+
export interface SourceHealth {
|
|
499
|
+
id: SourceId;
|
|
500
|
+
lastOkAt?: string;
|
|
501
|
+
lastError?: string;
|
|
502
|
+
lastLatencyMs?: number;
|
|
503
|
+
cooldownUntil?: string;
|
|
504
|
+
}
|
|
505
|
+
/** 单源诊断结果。 */
|
|
506
|
+
export interface ProbeResult {
|
|
507
|
+
target: SourceId;
|
|
508
|
+
ok: boolean;
|
|
509
|
+
latencyMs: number;
|
|
510
|
+
count: number;
|
|
511
|
+
error?: string;
|
|
512
|
+
cooldownUntil?: string;
|
|
513
|
+
}
|
|
514
|
+
/** 客户端需要打开/聚焦的任务(模型刚在当前任务里发起了一轮检索)。 */
|
|
515
|
+
export interface TaskFocus {
|
|
516
|
+
taskId: string;
|
|
517
|
+
title: string;
|
|
518
|
+
/** 请求时间(epoch ms);客户端据此丢弃过期的聚焦请求。 */
|
|
519
|
+
at: number;
|
|
520
|
+
/** 发起检索的会话;客户端只在该会话可见时打开,避免抢占其他对话的面板。 */
|
|
521
|
+
sessionId?: string;
|
|
522
|
+
}
|
|
523
|
+
/** `/events` 的响应;`reset` 表示客户端的 sinceRev 比服务端新(宿主重启)。 */
|
|
524
|
+
export interface EventsPage {
|
|
525
|
+
rev: number;
|
|
526
|
+
reset: boolean;
|
|
527
|
+
tasks: TaskSummary[];
|
|
528
|
+
focus?: TaskFocus;
|
|
529
|
+
}
|
|
530
|
+
/** `/health` 的响应:数据源能力 + 客户端需要的默认设置(不含密钥)。 */
|
|
531
|
+
export interface HealthInfo {
|
|
532
|
+
ok: boolean;
|
|
533
|
+
globalRev: number;
|
|
534
|
+
sources: ProviderInfo[];
|
|
535
|
+
sourceHealth: SourceHealth[];
|
|
536
|
+
/** 密钥是否已配置(只给布尔值,绝不返回密钥)。 */
|
|
537
|
+
keys: {
|
|
538
|
+
ncbi: boolean;
|
|
539
|
+
s2: boolean;
|
|
540
|
+
};
|
|
541
|
+
settings: {
|
|
542
|
+
defaultSources: SourceId[];
|
|
543
|
+
defaultPageSize: number;
|
|
544
|
+
defaultSort: SortMode;
|
|
545
|
+
defaultLanguage: TaskLanguage;
|
|
546
|
+
autoOpenTaskTab: boolean;
|
|
547
|
+
maxTaskTabs: number;
|
|
548
|
+
};
|
|
549
|
+
fullText: {
|
|
550
|
+
pdfRoot: string;
|
|
551
|
+
pdfMaxMb: number;
|
|
552
|
+
autoDownload: boolean;
|
|
553
|
+
epmcFulltext: boolean;
|
|
554
|
+
};
|
|
555
|
+
zotero: {
|
|
556
|
+
enabled: boolean;
|
|
557
|
+
baseUrl: string;
|
|
558
|
+
attachPdf: boolean;
|
|
559
|
+
tag: string;
|
|
560
|
+
};
|
|
561
|
+
/** 任务文件存放目录(设置页只读展示)。 */
|
|
562
|
+
storageDir: string;
|
|
563
|
+
version: string;
|
|
564
|
+
}
|
|
565
|
+
/** Host HTTP 端点统一信封。 */
|
|
566
|
+
export type ApiEnvelope<T> = {
|
|
567
|
+
ok: true;
|
|
568
|
+
data: T;
|
|
569
|
+
} | {
|
|
570
|
+
ok: false;
|
|
571
|
+
error: ApiError;
|
|
572
|
+
};
|
|
573
|
+
export type ApiErrorCode = 'bad_request' | 'not_found' | 'conflict' | 'network_error' | 'upstream_error' | 'timeout' | 'internal_error';
|
|
574
|
+
export interface ApiError {
|
|
575
|
+
code: ApiErrorCode;
|
|
576
|
+
message: string;
|
|
577
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 检索层公共类型:数据源无关的论文记录、任务模型与请求响应信封。
|
|
3
|
+
*
|
|
4
|
+
* 本文件同时被 Host 与 Client 引用,因此不得引入任何 Node 内置模块。
|
|
5
|
+
*/
|
|
6
|
+
/** 已接入的免费数据源 id;顺序即聚合展示顺序。 */
|
|
7
|
+
export const SOURCE_IDS = [
|
|
8
|
+
'pubmed',
|
|
9
|
+
'europepmc',
|
|
10
|
+
'openalex',
|
|
11
|
+
'crossref',
|
|
12
|
+
'semanticscholar',
|
|
13
|
+
'arxiv',
|
|
14
|
+
'pubtator',
|
|
15
|
+
];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 极简 XML 取值助手。
|
|
3
|
+
*
|
|
4
|
+
* arXiv Atom 与 PubMed EFetch 都是结构稳定的服务端 XML,没有引入解析依赖的
|
|
5
|
+
* 必要;这里只做「按标签取首段 / 取全部片段」与实体解码,够 provider 映射用。
|
|
6
|
+
*/
|
|
7
|
+
/** 取第一个 `<tag ...>…</tag>` 的内部文本(已去标签与解码实体)。 */
|
|
8
|
+
export declare function xmlText(xml: string, tag: string): string;
|
|
9
|
+
/** 取全部同名标签的内部文本。 */
|
|
10
|
+
export declare function xmlTexts(xml: string, tag: string): string[];
|
|
11
|
+
/** 取全部同名标签的原始片段(保留内部标签,供再解析)。 */
|
|
12
|
+
export declare function xmlBlocks(xml: string, tag: string): string[];
|
|
13
|
+
/** 取某个起始标签里的属性值;传入完整标签文本。 */
|
|
14
|
+
export declare function xmlAttr(tagText: string, name: string): string;
|
|
15
|
+
/** 找到第一个带指定属性的自闭合/普通标签文本。 */
|
|
16
|
+
export declare function xmlTag(xml: string, tag: string): string;
|
|
17
|
+
/** 去掉 XML 标签并把空白压成单行。 */
|
|
18
|
+
export declare function stripXmlTags(value: string): string;
|
|
19
|
+
/** 解码常见命名实体与数字实体。 */
|
|
20
|
+
export declare function decodeEntities(value: string): string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 极简 XML 取值助手。
|
|
3
|
+
*
|
|
4
|
+
* arXiv Atom 与 PubMed EFetch 都是结构稳定的服务端 XML,没有引入解析依赖的
|
|
5
|
+
* 必要;这里只做「按标签取首段 / 取全部片段」与实体解码,够 provider 映射用。
|
|
6
|
+
*/
|
|
7
|
+
/** 取第一个 `<tag ...>…</tag>` 的内部文本(已去标签与解码实体)。 */
|
|
8
|
+
export function xmlText(xml, tag) {
|
|
9
|
+
const match = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)</${tag}>`, 'u').exec(xml);
|
|
10
|
+
return match === null ? '' : decodeEntities(stripXmlTags(match[1]));
|
|
11
|
+
}
|
|
12
|
+
/** 取全部同名标签的内部文本。 */
|
|
13
|
+
export function xmlTexts(xml, tag) {
|
|
14
|
+
const pattern = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)</${tag}>`, 'gu');
|
|
15
|
+
const values = [];
|
|
16
|
+
for (const match of xml.matchAll(pattern))
|
|
17
|
+
values.push(decodeEntities(stripXmlTags(match[1])));
|
|
18
|
+
return values;
|
|
19
|
+
}
|
|
20
|
+
/** 取全部同名标签的原始片段(保留内部标签,供再解析)。 */
|
|
21
|
+
export function xmlBlocks(xml, tag) {
|
|
22
|
+
const pattern = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)</${tag}>`, 'gu');
|
|
23
|
+
const blocks = [];
|
|
24
|
+
for (const match of xml.matchAll(pattern))
|
|
25
|
+
blocks.push(match[1]);
|
|
26
|
+
return blocks;
|
|
27
|
+
}
|
|
28
|
+
/** 取某个起始标签里的属性值;传入完整标签文本。 */
|
|
29
|
+
export function xmlAttr(tagText, name) {
|
|
30
|
+
const match = new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`, 'iu').exec(tagText);
|
|
31
|
+
return match === null ? '' : decodeEntities(match[1]);
|
|
32
|
+
}
|
|
33
|
+
/** 找到第一个带指定属性的自闭合/普通标签文本。 */
|
|
34
|
+
export function xmlTag(xml, tag) {
|
|
35
|
+
const match = new RegExp(`<${tag}\\b[^>]*>`, 'u').exec(xml);
|
|
36
|
+
return match === null ? '' : match[0];
|
|
37
|
+
}
|
|
38
|
+
/** 去掉 XML 标签并把空白压成单行。 */
|
|
39
|
+
export function stripXmlTags(value) {
|
|
40
|
+
return value.replace(/<[^>]*>/gu, ' ').replace(/\s+/gu, ' ').trim();
|
|
41
|
+
}
|
|
42
|
+
/** 解码常见命名实体与数字实体。 */
|
|
43
|
+
export function decodeEntities(value) {
|
|
44
|
+
return value
|
|
45
|
+
.replace(/&#x([0-9a-f]+);/giu, (_, hex) => codePoint(Number.parseInt(hex, 16)))
|
|
46
|
+
.replace(/&#(\d+);/gu, (_, dec) => codePoint(Number.parseInt(dec, 10)))
|
|
47
|
+
.replace(/</gu, '<')
|
|
48
|
+
.replace(/>/gu, '>')
|
|
49
|
+
.replace(/"/gu, '"')
|
|
50
|
+
.replace(/'/gu, "'")
|
|
51
|
+
.replace(/ /gu, ' ')
|
|
52
|
+
.replace(/&/gu, '&');
|
|
53
|
+
}
|
|
54
|
+
function codePoint(value) {
|
|
55
|
+
if (!Number.isFinite(value) || value < 0 || value > 0x10ffff)
|
|
56
|
+
return '';
|
|
57
|
+
try {
|
|
58
|
+
return String.fromCodePoint(value);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
}
|