dsh-plugin-litsearch 0.2.0 → 0.3.5
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 +80 -38
- package/client/client.js +4750 -2342
- package/lib/host/export/bibtex.js +1 -4
- package/lib/host/export/common.d.ts +0 -2
- package/lib/host/export/common.js +0 -7
- package/lib/host/export/csv.js +1 -5
- package/lib/host/export/markdown.d.ts +1 -1
- package/lib/host/export/markdown.js +9 -26
- package/lib/host/export/ris.js +1 -4
- package/lib/host/paths.d.ts +4 -3
- package/lib/host/paths.js +4 -3
- package/lib/host/pdf/root.d.ts +11 -0
- package/lib/host/pdf/root.js +27 -0
- package/lib/host/pdf/store.d.ts +17 -7
- package/lib/host/pdf/store.js +110 -32
- package/lib/host/router.js +101 -87
- package/lib/host/tasks/filter.d.ts +3 -1
- package/lib/host/tasks/filter.js +45 -4
- package/lib/host/tasks/runner.js +1 -8
- package/lib/host/tasks/service.d.ts +21 -13
- package/lib/host/tasks/service.js +158 -47
- package/lib/host/tasks/states.d.ts +1 -3
- package/lib/host/tasks/states.js +2 -66
- package/lib/host/tasks/stats.js +9 -4
- package/lib/host/tasks/store.d.ts +1 -1
- package/lib/host/tasks/store.js +1 -1
- package/lib/host/tasks/summary.js +1 -1
- package/lib/host/tools.d.ts +1 -1
- package/lib/host/tools.js +25 -38
- package/lib/host/workspace.d.ts +20 -0
- package/lib/host/workspace.js +32 -0
- package/lib/index.d.ts +17 -8
- package/lib/index.js +33 -11
- package/lib/search/aggregate.js +1 -0
- package/lib/search/config.d.ts +17 -1
- package/lib/search/config.js +16 -1
- package/lib/search/identity.js +1 -0
- package/lib/search/oa.d.ts +8 -0
- package/lib/search/oa.js +53 -6
- package/lib/search/providers/googlescholar.d.ts +12 -0
- package/lib/search/providers/googlescholar.js +154 -0
- package/lib/search/providers/index.js +2 -0
- package/lib/search/query-adapter.js +3 -0
- package/lib/search/rate-limit.js +2 -1
- package/lib/search/types.d.ts +35 -13
- package/lib/search/types.js +25 -1
- package/package.json +1 -1
package/lib/host/tools.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 都有效);只有用户明确要求新任务时才新建。等待最多 25 秒后把任务头部与前
|
|
7
7
|
* 若干条结果交给模型,并明确要求本轮到此为止、只给完善建议、不发起下一轮。
|
|
8
8
|
* - `literature_sources`:列出已接入的数据源与能力,便于模型决定后续策略。
|
|
9
|
-
* - `literature_task
|
|
9
|
+
* - `literature_task`:读取/修改已保存任务(含轮次历史),写入筛读与分组,
|
|
10
10
|
* 做新老对比与统计。
|
|
11
11
|
* - `literature_export`:把任务(按筛选)导出为 CSV / RIS / BibTeX / Markdown 并落盘。
|
|
12
12
|
*
|
|
@@ -16,7 +16,8 @@ import { MAX_LIMIT } from '../search/config.js';
|
|
|
16
16
|
import { SearchError } from '../search/errors.js';
|
|
17
17
|
import { asRecord } from '../search/normalize.js';
|
|
18
18
|
import { PROVIDER_INFOS } from '../search/providers/index.js';
|
|
19
|
-
import { SOURCE_IDS } from '../search/types.js';
|
|
19
|
+
import { SOURCE_IDS, TASK_LANGUAGES } from '../search/types.js';
|
|
20
|
+
import { resolveSessionWorkspace } from './workspace.js';
|
|
20
21
|
/** 模型侧最多等多久;超时后任务继续跑,只返回当前进度与已有结果。 */
|
|
21
22
|
const MODEL_WAIT_MS = 25_000;
|
|
22
23
|
/** 交给模型的条数(UI 走分页端点,不受此限制)。 */
|
|
@@ -24,7 +25,7 @@ const TOOL_PAPER_LIMIT = 10;
|
|
|
24
25
|
/** 复用任务时,最多等多久让上一轮跑完;超时直接返回提示,绝不新建任务。 */
|
|
25
26
|
const WAIT_PREVIOUS_MS = 12_000;
|
|
26
27
|
const SORT_MODES = ['relevance', 'citations', 'year'];
|
|
27
|
-
const LANGUAGES =
|
|
28
|
+
const LANGUAGES = TASK_LANGUAGES;
|
|
28
29
|
const SCREENINGS = ['unread', 'read', 'included', 'excluded'];
|
|
29
30
|
const EXPORT_FORMATS = ['csv', 'ris', 'bibtex', 'md'];
|
|
30
31
|
const TASK_ACTIONS = [
|
|
@@ -105,7 +106,7 @@ const SEARCH_PARAMETERS = {
|
|
|
105
106
|
sources: {
|
|
106
107
|
type: 'array',
|
|
107
108
|
items: { type: 'string', enum: [...SOURCE_IDS] },
|
|
108
|
-
description: `要查询的数据源:${SOURCE_IDS.join(', ')}
|
|
109
|
+
description: `要查询的数据源:${SOURCE_IDS.join(', ')};省略表示默认数据源(googlescholar 需配置 SerpAPI Key,默认关闭)。`,
|
|
109
110
|
},
|
|
110
111
|
limit: {
|
|
111
112
|
type: 'integer',
|
|
@@ -138,7 +139,7 @@ const TASK_PARAMETERS = {
|
|
|
138
139
|
action: {
|
|
139
140
|
type: 'string',
|
|
140
141
|
enum: [...TASK_ACTIONS],
|
|
141
|
-
description: 'current 读「当前任务」(推荐在执行检索/任务操作前先调用,确认这个对话正在用哪个任务,无需 id);list 列任务;get 读条件与结果;update 改条件/标题/笔记/分组;run 重跑(仅在用户明确要求进入下一轮时使用);rounds 读该任务的检索轮次历史;annotate
|
|
142
|
+
description: 'current 读「当前任务」(推荐在执行检索/任务操作前先调用,确认这个对话正在用哪个任务,无需 id);list 列任务;get 读条件与结果;update 改条件/标题/笔记/分组;run 重跑(仅在用户明确要求进入下一轮时使用);rounds 读该任务的检索轮次历史;annotate 写筛读/分组;compare 与旧任务对比;stats 读语料统计。',
|
|
142
143
|
},
|
|
143
144
|
id: { type: 'string', description: '任务 id;除 current / list 外必填。' },
|
|
144
145
|
withTaskId: { type: 'string', description: 'compare 时的基准任务 id。' },
|
|
@@ -200,16 +201,13 @@ const TASK_PARAMETERS = {
|
|
|
200
201
|
papers: {
|
|
201
202
|
type: 'array',
|
|
202
203
|
maxItems: 50,
|
|
203
|
-
description: 'annotate 时的逐篇写入(筛读 /
|
|
204
|
+
description: 'annotate 时的逐篇写入(筛读 / 分组)。',
|
|
204
205
|
items: {
|
|
205
206
|
type: 'object',
|
|
206
207
|
properties: {
|
|
207
208
|
paperId: { type: 'string' },
|
|
208
209
|
screening: { type: 'string', enum: [...SCREENINGS] },
|
|
209
210
|
groupId: { type: ['string', 'null'] },
|
|
210
|
-
shortlistOrder: { type: ['integer', 'null'] },
|
|
211
|
-
note: { type: 'string' },
|
|
212
|
-
tags: { type: 'array', items: { type: 'string' } },
|
|
213
211
|
},
|
|
214
212
|
required: ['paperId'],
|
|
215
213
|
additionalProperties: false,
|
|
@@ -247,7 +245,7 @@ export function literatureTools(service) {
|
|
|
247
245
|
{
|
|
248
246
|
name: 'literature_search',
|
|
249
247
|
description: '检索免费开放学术文献。并发查询 PubMed、Europe PMC、OpenAlex、Crossref、Semantic Scholar、arXiv、PubTator,跨源与跨检索式去重合并成一篇文献一条,返回标题、作者、年份、出处、DOI、被引次数、开放获取状态与摘要,支持一次提交多条检索式(queries)。' +
|
|
250
|
-
'【任务模型】一个研究主题 = 一个任务 = 一个右侧栏 tab;一个对话只保留一个任务 tab,切换任务由用户在任务列表里完成。若不确定这个对话当前用哪个任务,先调用 literature_task(action=current)查清再执行。默认复用「当前任务」并在同一任务内开新一轮:任务 id 不变、tab 不变,结果替换为最新一轮,已有的已读/纳入/排除/备注按文献身份保留;绝不会新建任务或新开 tab。当前任务是持久化的「最近一次使用或打开的任务」,跨对话、宿主重启都有效;用户从右侧栏点开别的任务会切换当前任务。只有用户明确要求新建查询任务,或研究主题与当前任务明显不同时,才传 newTask=true 并用 title 给新任务起一个概括主题的名称;也可用 taskId 显式指定要复用的任务(先用 literature_task current/list 查 id
|
|
248
|
+
'【任务模型】一个研究主题 = 一个任务 = 一个右侧栏 tab;一个对话只保留一个任务 tab,切换任务由用户在任务列表里完成。若不确定这个对话当前用哪个任务,先调用 literature_task(action=current)查清再执行。默认复用「当前任务」并在同一任务内开新一轮:任务 id 不变、tab 不变,结果替换为最新一轮,已有的已读/纳入/排除/备注按文献身份保留;绝不会新建任务或新开 tab。当前任务是持久化的「最近一次使用或打开的任务」,跨对话、宿主重启都有效;用户从右侧栏点开别的任务会切换当前任务。只有用户明确要求新建查询任务,或研究主题与当前任务明显不同时,才传 newTask=true 并用 title 给新任务起一个概括主题的名称;也可用 taskId 显式指定要复用的任务(先用 literature_task current/list 查 id,或让用户点右侧栏的「加入对话」把任务 id 带进对话)。每次调用请提交【完整】的新检索条件,缺省项回落到插件默认设置而非沿用旧条件。' +
|
|
251
249
|
'【轮次约束】本轮检索结束后,只给用户一份「如何完善检索」的建议,禁止自行发起下一轮、禁止重复调用本工具;必须等用户查看结果后,在右侧栏调整条件、或在对话里明确要求调整,才可在同一任务里再搜一次。' +
|
|
252
250
|
'超过 25 秒仍在进行时会先返回当前进度,任务会继续在右侧栏完成。适合找论文、查某主题的代表作、补充参考文献。',
|
|
253
251
|
parameters: SEARCH_PARAMETERS,
|
|
@@ -255,7 +253,13 @@ export function literatureTools(service) {
|
|
|
255
253
|
async execute(args, exec) {
|
|
256
254
|
const input = readSearchArgs(args);
|
|
257
255
|
const { title, taskId, newTask, ...conditions } = input;
|
|
258
|
-
const
|
|
256
|
+
const workspace = await resolveSessionWorkspace(exec.agent?.id);
|
|
257
|
+
const createInput = {
|
|
258
|
+
...conditions,
|
|
259
|
+
origin: 'model',
|
|
260
|
+
runNow: false,
|
|
261
|
+
...(workspace === undefined ? {} : { workspace }),
|
|
262
|
+
};
|
|
259
263
|
let task;
|
|
260
264
|
let reused = false;
|
|
261
265
|
if (taskId !== undefined) {
|
|
@@ -315,7 +319,7 @@ export function literatureTools(service) {
|
|
|
315
319
|
},
|
|
316
320
|
{
|
|
317
321
|
name: 'literature_sources',
|
|
318
|
-
description: '
|
|
322
|
+
description: '列出本插件已接入的文献数据源:能力(摘要、被引次数、开放获取、PDF 链接)、限流额度、检索式语法与是否需要可选 API Key。默认数据源均免费;Google Scholar 需在设置页配置 SerpAPI Key(有免费额度,超出付费),未配置时不参与检索。',
|
|
319
323
|
parameters: { type: 'object', properties: {}, additionalProperties: false },
|
|
320
324
|
output: TEXT_OUTPUT,
|
|
321
325
|
async execute() {
|
|
@@ -324,7 +328,7 @@ export function literatureTools(service) {
|
|
|
324
328
|
},
|
|
325
329
|
{
|
|
326
330
|
name: 'literature_task',
|
|
327
|
-
description: '读取与推进已保存的检索任务。current 读「当前任务」(推荐在执行任何检索/任务操作前先调用,确认这个对话正在用哪个任务;无需 id);list 列出任务;get 读条件、分组、笔记与结果(可按筛读/分组/检索式/关键词过滤);update 改标题、条件、任务笔记与分组;rounds 读该任务的检索轮次历史(每轮时间、条件、发起方、命中数与状态);annotate
|
|
331
|
+
description: '读取与推进已保存的检索任务。current 读「当前任务」(推荐在执行任何检索/任务操作前先调用,确认这个对话正在用哪个任务;无需 id);list 列出任务;get 读条件、分组、笔记与结果(可按筛读/分组/检索式/关键词过滤);update 改标题、条件、任务笔记与分组;rounds 读该任务的检索轮次历史(每轮时间、条件、发起方、命中数与状态);annotate 批量写入筛读状态与分组;compare 与旧任务按论文身份对比新增/缺失;stats 读语料统计(年份、来源、期刊、作者、类型、语言)。' +
|
|
328
332
|
'注意:一个对话只保留一个任务 tab,切换任务由用户在任务列表里完成;run 会在指定任务内开新一轮检索(并把它设为当前任务),与 literature_search 一样受「用户查看结果并明确要求后才能进入下一轮」约束,不能用它绕过该约束自行加搜;也不要用 update 改完条件就立刻 run。适合在检索之后做筛选、整理与汇报。',
|
|
329
333
|
parameters: TASK_PARAMETERS,
|
|
330
334
|
output: TEXT_OUTPUT,
|
|
@@ -336,7 +340,7 @@ export function literatureTools(service) {
|
|
|
336
340
|
},
|
|
337
341
|
{
|
|
338
342
|
name: 'literature_export',
|
|
339
|
-
description: '把检索任务导出为文件并返回落盘路径。csv
|
|
343
|
+
description: '把检索任务导出为文件并返回落盘路径。csv 是全量筛读台账(含命中检索式、筛读状态、分组);ris / bibtex 供 Zotero、EndNote 等文献管理器;md 是分层检索报告(漏斗、按分组的阅读清单、开放获取与 PDF 覆盖率、对比新增)。可按筛读状态、分组、检索式或指定 paperIds 限定范围。',
|
|
340
344
|
parameters: EXPORT_PARAMETERS,
|
|
341
345
|
output: TEXT_OUTPUT,
|
|
342
346
|
async execute(args) {
|
|
@@ -687,23 +691,6 @@ function readAnnotations(value) {
|
|
|
687
691
|
}
|
|
688
692
|
input.groupId = groupId;
|
|
689
693
|
}
|
|
690
|
-
if (record['shortlistOrder'] !== undefined) {
|
|
691
|
-
const order = record['shortlistOrder'];
|
|
692
|
-
if (order !== null && (typeof order !== 'number' || !Number.isFinite(order))) {
|
|
693
|
-
throw new Error(`papers[${index}].shortlistOrder 必须是数字或 null`);
|
|
694
|
-
}
|
|
695
|
-
input.shortlistOrder = order;
|
|
696
|
-
}
|
|
697
|
-
const note = readText(record['note'], `papers[${index}].note`, true);
|
|
698
|
-
if (note !== undefined)
|
|
699
|
-
input.note = note;
|
|
700
|
-
if (record['tags'] !== undefined) {
|
|
701
|
-
const tags = record['tags'];
|
|
702
|
-
if (!Array.isArray(tags) || !tags.every((tag) => typeof tag === 'string')) {
|
|
703
|
-
throw new Error(`papers[${index}].tags 必须是字符串数组`);
|
|
704
|
-
}
|
|
705
|
-
input.tags = tags;
|
|
706
|
-
}
|
|
707
694
|
return input;
|
|
708
695
|
});
|
|
709
696
|
}
|
|
@@ -769,10 +756,11 @@ async function runTaskAction(service, args, signal, sessionId) {
|
|
|
769
756
|
case 'run': {
|
|
770
757
|
const id = requireId(args);
|
|
771
758
|
const task = await service.get(id);
|
|
759
|
+
const workspace = await resolveSessionWorkspace(sessionId);
|
|
772
760
|
// 用户明确要求推进这个任务:它成为当前任务,并让客户端打开/聚焦对应 tab。
|
|
773
761
|
service.setCurrentTask(id);
|
|
774
762
|
service.focusTask(task.id, task.title, sessionId);
|
|
775
|
-
const outcome = await runWithin(service, id, signal, service.run(id, 'new', 'model'));
|
|
763
|
+
const outcome = await runWithin(service, id, signal, service.run(id, 'new', 'model', workspace));
|
|
776
764
|
const latest = await service.get(id);
|
|
777
765
|
const detail = await service.getDetail(id);
|
|
778
766
|
const page = await service.results(id, {
|
|
@@ -790,7 +778,7 @@ async function runTaskAction(service, args, signal, sessionId) {
|
|
|
790
778
|
}
|
|
791
779
|
case 'annotate': {
|
|
792
780
|
const result = await service.annotate(requireId(args), args.papers ?? []);
|
|
793
|
-
return `已写入 ${result.updated}
|
|
781
|
+
return `已写入 ${result.updated} 篇的筛读/分组(任务 id: ${result.taskId},rev=${result.rev})。`;
|
|
794
782
|
}
|
|
795
783
|
case 'compare': {
|
|
796
784
|
const report = await service.compare(requireId(args), args.withTaskId ?? '');
|
|
@@ -853,7 +841,7 @@ function formatCurrentTask(task) {
|
|
|
853
841
|
if (task === undefined) {
|
|
854
842
|
return [
|
|
855
843
|
'当前没有任务:这个对话还没有任何检索任务。',
|
|
856
|
-
'若用户要求检索,请直接调用 literature_search(不要传 newTask
|
|
844
|
+
'若用户要求检索,请直接调用 literature_search(不要传 newTask);若用户要继续某个已有任务,请让用户从右侧栏任务列表打开它,或点任务页的「加入对话」把任务 id 带进对话。',
|
|
857
845
|
].join('\n');
|
|
858
846
|
}
|
|
859
847
|
const conditions = task.conditions;
|
|
@@ -918,7 +906,6 @@ function formatTaskDetail(detail, page, states, groups, outcome) {
|
|
|
918
906
|
const marks = [
|
|
919
907
|
state?.screening ?? 'unread',
|
|
920
908
|
state?.groupId === undefined ? '' : groupNames.get(state.groupId) ?? state.groupId,
|
|
921
|
-
state?.shortlistOrder === undefined ? '' : `必读#${state.shortlistOrder}`,
|
|
922
909
|
].filter((part) => part !== '');
|
|
923
910
|
const meta = [
|
|
924
911
|
paper.authors.slice(0, 4).join(', '),
|
|
@@ -930,8 +917,6 @@ function formatTaskDetail(detail, page, states, groups, outcome) {
|
|
|
930
917
|
lines.push(`${page.offset + index + 1}. [${marks.join('|')}] ${paper.title}`);
|
|
931
918
|
if (meta.length > 0)
|
|
932
919
|
lines.push(` ${meta.join(' | ')}`);
|
|
933
|
-
if (state?.note !== undefined && state.note !== '')
|
|
934
|
-
lines.push(` 备注:${state.note}`);
|
|
935
920
|
if (paper.abstract !== '')
|
|
936
921
|
lines.push(` 摘要:${truncate(paper.abstract, 300)}`);
|
|
937
922
|
});
|
|
@@ -1002,7 +987,9 @@ function percent(value) {
|
|
|
1002
987
|
return `${Math.round(value * 100)}%`;
|
|
1003
988
|
}
|
|
1004
989
|
function formatSources() {
|
|
1005
|
-
const lines = [
|
|
990
|
+
const lines = [
|
|
991
|
+
'已接入的文献数据源(默认均为免费;Google Scholar 需在设置页配置 SerpAPI Key,未配置时不参与检索):',
|
|
992
|
+
];
|
|
1006
993
|
for (const info of PROVIDER_INFOS) {
|
|
1007
994
|
const caps = [
|
|
1008
995
|
info.capabilities.abstract ? '摘要' : '',
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话工作目录解析。
|
|
3
|
+
*
|
|
4
|
+
* DSH 为每个会话保存一个绝对工作目录(SessionHeader.cwd)。PDF 默认落在
|
|
5
|
+
* 该目录下的 `dsh-litsearch/`,让下载物跟随项目而不是散落到 `~/Downloads`。
|
|
6
|
+
* 会话服务由宿主注入;未装配或读不到时返回 undefined,由调用方回落到旧默认。
|
|
7
|
+
*/
|
|
8
|
+
/** `ctx.sessionPersistence` 中本插件用到的最小结构。 */
|
|
9
|
+
export interface SessionStoreLike {
|
|
10
|
+
stat(id: string): Promise<{
|
|
11
|
+
header: {
|
|
12
|
+
cwd?: string;
|
|
13
|
+
};
|
|
14
|
+
} | undefined>;
|
|
15
|
+
}
|
|
16
|
+
export declare function setSessionStore(store: SessionStoreLike | undefined): void;
|
|
17
|
+
/** 读取某个会话的工作目录;只接受绝对路径。 */
|
|
18
|
+
export declare function resolveSessionWorkspace(sessionId: string | undefined): Promise<string | undefined>;
|
|
19
|
+
/** 规范化工作目录:去空白、去尾斜杠,只接受绝对路径。 */
|
|
20
|
+
export declare function normalizeWorkspace(value: string | undefined): string | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话工作目录解析。
|
|
3
|
+
*
|
|
4
|
+
* DSH 为每个会话保存一个绝对工作目录(SessionHeader.cwd)。PDF 默认落在
|
|
5
|
+
* 该目录下的 `dsh-litsearch/`,让下载物跟随项目而不是散落到 `~/Downloads`。
|
|
6
|
+
* 会话服务由宿主注入;未装配或读不到时返回 undefined,由调用方回落到旧默认。
|
|
7
|
+
*/
|
|
8
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
9
|
+
let sessionStore;
|
|
10
|
+
export function setSessionStore(store) {
|
|
11
|
+
sessionStore = store;
|
|
12
|
+
}
|
|
13
|
+
/** 读取某个会话的工作目录;只接受绝对路径。 */
|
|
14
|
+
export async function resolveSessionWorkspace(sessionId) {
|
|
15
|
+
const id = sessionId?.trim() ?? '';
|
|
16
|
+
if (id === '' || sessionStore === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
try {
|
|
19
|
+
const snapshot = await sessionStore.stat(id);
|
|
20
|
+
return normalizeWorkspace(snapshot?.header.cwd);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** 规范化工作目录:去空白、去尾斜杠,只接受绝对路径。 */
|
|
27
|
+
export function normalizeWorkspace(value) {
|
|
28
|
+
const path = value?.trim() ?? '';
|
|
29
|
+
if (path === '' || !isAbsolute(path))
|
|
30
|
+
return undefined;
|
|
31
|
+
return resolve(path);
|
|
32
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { Context } from '@deepseek-ai/cordis';
|
|
16
16
|
import Schema from '@deepseek-ai/schemastery';
|
|
17
|
+
import type { TaskLanguage } from './search/types.js';
|
|
17
18
|
export declare const name = "dsh-plugin-litsearch";
|
|
18
19
|
/**
|
|
19
20
|
* 插件配置 schema(逐字段 volatile)。
|
|
@@ -28,6 +29,8 @@ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
|
28
29
|
ncbiApiKey: Schema<string, string, "volatile-defined">;
|
|
29
30
|
/** 可选 Semantic Scholar API Key。 */
|
|
30
31
|
s2ApiKey: Schema<string, string, "volatile-defined">;
|
|
32
|
+
/** 可选 SerpAPI Key;配置后启用 Google Scholar 数据源(付费/有免费额度)。 */
|
|
33
|
+
serpApiKey: Schema<string, string, "volatile-defined">;
|
|
31
34
|
/** 各源最小请求间隔(毫秒)。 */
|
|
32
35
|
pubmedIntervalMs: Schema<number, number, "volatile-defined">;
|
|
33
36
|
s2IntervalMs: Schema<number, number, "volatile-defined">;
|
|
@@ -37,22 +40,24 @@ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
|
37
40
|
/** 单次上游请求超时(毫秒)。 */
|
|
38
41
|
requestTimeoutMs: Schema<number, number, "volatile-defined">;
|
|
39
42
|
/** 默认数据源。 */
|
|
40
|
-
defaultSources: Schema<NoInfer<("pubmed" | "europepmc" | "openalex" | "crossref" | "semanticscholar" | "arxiv" | "pubtator")[]>, NoInfer<("pubmed" | "europepmc" | "openalex" | "crossref" | "semanticscholar" | "arxiv" | "pubtator")[]>, "volatile-defined">;
|
|
43
|
+
defaultSources: Schema<NoInfer<("pubmed" | "europepmc" | "openalex" | "crossref" | "semanticscholar" | "arxiv" | "pubtator" | "googlescholar")[]>, NoInfer<("pubmed" | "europepmc" | "openalex" | "crossref" | "semanticscholar" | "arxiv" | "pubtator" | "googlescholar")[]>, "volatile-defined">;
|
|
41
44
|
defaultSort: Schema<"relevance" | "citations" | "year", "relevance" | "citations" | "year", "volatile-defined">;
|
|
42
|
-
defaultLanguage: Schema<
|
|
45
|
+
defaultLanguage: Schema<TaskLanguage, TaskLanguage, "volatile-defined">;
|
|
43
46
|
/** 模型发起检索后是否自动打开任务 tab。 */
|
|
44
47
|
autoOpenTaskTab: Schema<boolean, boolean, "volatile-defined">;
|
|
45
48
|
/** 同时保留的任务 tab 上限。 */
|
|
46
49
|
maxTaskTabs: Schema<number, number, "volatile-defined">;
|
|
47
|
-
/**
|
|
50
|
+
/** 任务保留天数与条数(已保留任务不受限)。 */
|
|
48
51
|
taskRetentionDays: Schema<number, number, "volatile-defined">;
|
|
49
52
|
taskRetentionCount: Schema<number, number, "volatile-defined">;
|
|
50
53
|
/** 单任务结果上限。 */
|
|
51
54
|
taskMaxPapers: Schema<number, number, "volatile-defined">;
|
|
52
|
-
/** PDF
|
|
55
|
+
/** PDF 下载根目录;留空时跟随会话工作目录(其下的 dsh-litsearch/)。 */
|
|
53
56
|
pdfRoot: Schema<string, string, "volatile-defined">;
|
|
54
57
|
/** 单个 PDF 最大体积(MB)。 */
|
|
55
58
|
pdfMaxMb: Schema<number, number, "volatile-defined">;
|
|
59
|
+
/** PDF 去重缓存总量上限(MB),启动时随任务清理一起修剪;0 表示关闭。 */
|
|
60
|
+
pdfCacheMaxMb: Schema<number, number, "volatile-defined">;
|
|
56
61
|
/** 检索完成后自动下载缺失 PDF。 */
|
|
57
62
|
autoDownloadPdf: Schema<boolean, boolean, "volatile-defined">;
|
|
58
63
|
/** 无 PDF 时用 Europe PMC HTML 全文兜底。 */
|
|
@@ -69,6 +74,8 @@ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
|
69
74
|
ncbiApiKey: Schema<string, string, "volatile-defined">;
|
|
70
75
|
/** 可选 Semantic Scholar API Key。 */
|
|
71
76
|
s2ApiKey: Schema<string, string, "volatile-defined">;
|
|
77
|
+
/** 可选 SerpAPI Key;配置后启用 Google Scholar 数据源(付费/有免费额度)。 */
|
|
78
|
+
serpApiKey: Schema<string, string, "volatile-defined">;
|
|
72
79
|
/** 各源最小请求间隔(毫秒)。 */
|
|
73
80
|
pubmedIntervalMs: Schema<number, number, "volatile-defined">;
|
|
74
81
|
s2IntervalMs: Schema<number, number, "volatile-defined">;
|
|
@@ -78,22 +85,24 @@ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
|
78
85
|
/** 单次上游请求超时(毫秒)。 */
|
|
79
86
|
requestTimeoutMs: Schema<number, number, "volatile-defined">;
|
|
80
87
|
/** 默认数据源。 */
|
|
81
|
-
defaultSources: Schema<NoInfer<("pubmed" | "europepmc" | "openalex" | "crossref" | "semanticscholar" | "arxiv" | "pubtator")[]>, NoInfer<("pubmed" | "europepmc" | "openalex" | "crossref" | "semanticscholar" | "arxiv" | "pubtator")[]>, "volatile-defined">;
|
|
88
|
+
defaultSources: Schema<NoInfer<("pubmed" | "europepmc" | "openalex" | "crossref" | "semanticscholar" | "arxiv" | "pubtator" | "googlescholar")[]>, NoInfer<("pubmed" | "europepmc" | "openalex" | "crossref" | "semanticscholar" | "arxiv" | "pubtator" | "googlescholar")[]>, "volatile-defined">;
|
|
82
89
|
defaultSort: Schema<"relevance" | "citations" | "year", "relevance" | "citations" | "year", "volatile-defined">;
|
|
83
|
-
defaultLanguage: Schema<
|
|
90
|
+
defaultLanguage: Schema<TaskLanguage, TaskLanguage, "volatile-defined">;
|
|
84
91
|
/** 模型发起检索后是否自动打开任务 tab。 */
|
|
85
92
|
autoOpenTaskTab: Schema<boolean, boolean, "volatile-defined">;
|
|
86
93
|
/** 同时保留的任务 tab 上限。 */
|
|
87
94
|
maxTaskTabs: Schema<number, number, "volatile-defined">;
|
|
88
|
-
/**
|
|
95
|
+
/** 任务保留天数与条数(已保留任务不受限)。 */
|
|
89
96
|
taskRetentionDays: Schema<number, number, "volatile-defined">;
|
|
90
97
|
taskRetentionCount: Schema<number, number, "volatile-defined">;
|
|
91
98
|
/** 单任务结果上限。 */
|
|
92
99
|
taskMaxPapers: Schema<number, number, "volatile-defined">;
|
|
93
|
-
/** PDF
|
|
100
|
+
/** PDF 下载根目录;留空时跟随会话工作目录(其下的 dsh-litsearch/)。 */
|
|
94
101
|
pdfRoot: Schema<string, string, "volatile-defined">;
|
|
95
102
|
/** 单个 PDF 最大体积(MB)。 */
|
|
96
103
|
pdfMaxMb: Schema<number, number, "volatile-defined">;
|
|
104
|
+
/** PDF 去重缓存总量上限(MB),启动时随任务清理一起修剪;0 表示关闭。 */
|
|
105
|
+
pdfCacheMaxMb: Schema<number, number, "volatile-defined">;
|
|
97
106
|
/** 检索完成后自动下载缺失 PDF。 */
|
|
98
107
|
autoDownloadPdf: Schema<boolean, boolean, "volatile-defined">;
|
|
99
108
|
/** 无 PDF 时用 Europe PMC HTML 全文兜底。 */
|
package/lib/index.js
CHANGED
|
@@ -18,9 +18,12 @@ import { join } from 'node:path';
|
|
|
18
18
|
import { createHandler } from './host/router.js';
|
|
19
19
|
import { literatureTools } from './host/tools.js';
|
|
20
20
|
import { TaskService } from './host/tasks/service.js';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
21
|
+
import { setSessionStore } from './host/workspace.js';
|
|
22
|
+
import { DEFAULT_ARXIV_INTERVAL_MS, DEFAULT_AUTO_DOWNLOAD_PDF, DEFAULT_AUTO_OPEN, DEFAULT_ENABLE_EPMC_FULLTEXT, DEFAULT_LANGUAGE, DEFAULT_MAX_TASK_TABS, DEFAULT_PAGE_SIZE, DEFAULT_PDF_CACHE_MAX_MB, DEFAULT_PDF_MAX_MB, DEFAULT_PUBMED_INTERVAL_MS, DEFAULT_RETENTION_COUNT, DEFAULT_RETENTION_DAYS, DEFAULT_S2_INTERVAL_MS, DEFAULT_SOURCE_IDS, DEFAULT_SORT, DEFAULT_TASK_MAX_PAPERS, DEFAULT_ZOTERO_ATTACH_PDF, DEFAULT_ZOTERO_BASE_URL, DEFAULT_ZOTERO_ENABLED, DEFAULT_ZOTERO_TAG, MAX_LIMIT, REQUEST_TIMEOUT_MS, ROUTE_MATCH, setSettingsReader, } from './search/config.js';
|
|
23
|
+
import { SOURCE_IDS, TASK_LANGUAGES } from './search/types.js';
|
|
23
24
|
export const name = 'dsh-plugin-litsearch';
|
|
25
|
+
/** 旧版默认下载目录;读取时视为「未配置」,让默认落点切换到工作目录。 */
|
|
26
|
+
const LEGACY_PDF_ROOT = join(homedir(), 'Downloads', 'dsh-litsearch');
|
|
24
27
|
/**
|
|
25
28
|
* 插件配置 schema(逐字段 volatile)。
|
|
26
29
|
*
|
|
@@ -34,6 +37,8 @@ export const Config = Schema.object({
|
|
|
34
37
|
ncbiApiKey: Schema.string().default('').role('secret').volatile(),
|
|
35
38
|
/** 可选 Semantic Scholar API Key。 */
|
|
36
39
|
s2ApiKey: Schema.string().default('').role('secret').volatile(),
|
|
40
|
+
/** 可选 SerpAPI Key;配置后启用 Google Scholar 数据源(付费/有免费额度)。 */
|
|
41
|
+
serpApiKey: Schema.string().default('').role('secret').volatile(),
|
|
37
42
|
/** 各源最小请求间隔(毫秒)。 */
|
|
38
43
|
pubmedIntervalMs: Schema.number().default(DEFAULT_PUBMED_INTERVAL_MS).volatile(),
|
|
39
44
|
s2IntervalMs: Schema.number().default(DEFAULT_S2_INTERVAL_MS).volatile(),
|
|
@@ -51,8 +56,9 @@ export const Config = Schema.object({
|
|
|
51
56
|
Schema.const('semanticscholar'),
|
|
52
57
|
Schema.const('arxiv'),
|
|
53
58
|
Schema.const('pubtator'),
|
|
59
|
+
Schema.const('googlescholar'),
|
|
54
60
|
]))
|
|
55
|
-
.default([...
|
|
61
|
+
.default([...DEFAULT_SOURCE_IDS])
|
|
56
62
|
.volatile(),
|
|
57
63
|
defaultSort: Schema.union([
|
|
58
64
|
Schema.const('relevance'),
|
|
@@ -61,22 +67,24 @@ export const Config = Schema.object({
|
|
|
61
67
|
])
|
|
62
68
|
.default(DEFAULT_SORT)
|
|
63
69
|
.volatile(),
|
|
64
|
-
defaultLanguage: Schema.union(
|
|
70
|
+
defaultLanguage: Schema.union(TASK_LANGUAGES.map((code) => Schema.const(code)))
|
|
65
71
|
.default(DEFAULT_LANGUAGE)
|
|
66
72
|
.volatile(),
|
|
67
73
|
/** 模型发起检索后是否自动打开任务 tab。 */
|
|
68
74
|
autoOpenTaskTab: Schema.boolean().default(DEFAULT_AUTO_OPEN).volatile(),
|
|
69
75
|
/** 同时保留的任务 tab 上限。 */
|
|
70
76
|
maxTaskTabs: Schema.number().default(DEFAULT_MAX_TASK_TABS).volatile(),
|
|
71
|
-
/**
|
|
77
|
+
/** 任务保留天数与条数(已保留任务不受限)。 */
|
|
72
78
|
taskRetentionDays: Schema.number().default(DEFAULT_RETENTION_DAYS).volatile(),
|
|
73
79
|
taskRetentionCount: Schema.number().default(DEFAULT_RETENTION_COUNT).volatile(),
|
|
74
80
|
/** 单任务结果上限。 */
|
|
75
81
|
taskMaxPapers: Schema.number().default(DEFAULT_TASK_MAX_PAPERS).volatile(),
|
|
76
|
-
/** PDF
|
|
77
|
-
pdfRoot: Schema.string().default(
|
|
82
|
+
/** PDF 下载根目录;留空时跟随会话工作目录(其下的 dsh-litsearch/)。 */
|
|
83
|
+
pdfRoot: Schema.string().default('').volatile(),
|
|
78
84
|
/** 单个 PDF 最大体积(MB)。 */
|
|
79
85
|
pdfMaxMb: Schema.number().default(DEFAULT_PDF_MAX_MB).volatile(),
|
|
86
|
+
/** PDF 去重缓存总量上限(MB),启动时随任务清理一起修剪;0 表示关闭。 */
|
|
87
|
+
pdfCacheMaxMb: Schema.number().default(DEFAULT_PDF_CACHE_MAX_MB).volatile(),
|
|
80
88
|
/** 检索完成后自动下载缺失 PDF。 */
|
|
81
89
|
autoDownloadPdf: Schema.boolean().default(DEFAULT_AUTO_DOWNLOAD_PDF).volatile(),
|
|
82
90
|
/** 无 PDF 时用 Europe PMC HTML 全文兜底。 */
|
|
@@ -93,12 +101,13 @@ export function apply(ctx, config) {
|
|
|
93
101
|
contactEmail: config.contactEmail.get().trim(),
|
|
94
102
|
ncbiApiKey: config.ncbiApiKey.get().trim(),
|
|
95
103
|
s2ApiKey: config.s2ApiKey.get().trim(),
|
|
104
|
+
serpApiKey: config.serpApiKey.get().trim(),
|
|
96
105
|
pubmedIntervalMs: clampInt(config.pubmedIntervalMs.get(), 50, 10_000, DEFAULT_PUBMED_INTERVAL_MS),
|
|
97
106
|
s2IntervalMs: clampInt(config.s2IntervalMs.get(), 50, 60_000, DEFAULT_S2_INTERVAL_MS),
|
|
98
107
|
arxivIntervalMs: clampInt(config.arxivIntervalMs.get(), 50, 60_000, DEFAULT_ARXIV_INTERVAL_MS),
|
|
99
108
|
defaultPageSize: clampInt(config.defaultPageSize.get(), 1, MAX_LIMIT, DEFAULT_PAGE_SIZE),
|
|
100
109
|
requestTimeoutMs: clampInt(config.requestTimeoutMs.get(), 1_000, 120_000, REQUEST_TIMEOUT_MS),
|
|
101
|
-
defaultSources: normalizeSources(config.defaultSources.get()),
|
|
110
|
+
defaultSources: normalizeSources(config.defaultSources.get()).filter((id) => id !== 'googlescholar' || config.serpApiKey.get().trim() !== ''),
|
|
102
111
|
defaultSort: normalizeSort(config.defaultSort.get()),
|
|
103
112
|
defaultLanguage: normalizeLanguage(config.defaultLanguage.get()),
|
|
104
113
|
autoOpenTaskTab: config.autoOpenTaskTab.get(),
|
|
@@ -106,8 +115,9 @@ export function apply(ctx, config) {
|
|
|
106
115
|
taskRetentionDays: clampInt(config.taskRetentionDays.get(), 1, 3_650, DEFAULT_RETENTION_DAYS),
|
|
107
116
|
taskRetentionCount: clampInt(config.taskRetentionCount.get(), 1, 10_000, DEFAULT_RETENTION_COUNT),
|
|
108
117
|
taskMaxPapers: clampInt(config.taskMaxPapers.get(), 10, 10_000, DEFAULT_TASK_MAX_PAPERS),
|
|
109
|
-
pdfRoot: config.pdfRoot.get()
|
|
118
|
+
pdfRoot: normalizePdfRoot(config.pdfRoot.get()),
|
|
110
119
|
pdfMaxMb: clampInt(config.pdfMaxMb.get(), 1, 200, DEFAULT_PDF_MAX_MB),
|
|
120
|
+
pdfCacheMaxMb: clampInt(config.pdfCacheMaxMb.get(), 0, 102_400, DEFAULT_PDF_CACHE_MAX_MB),
|
|
111
121
|
autoDownloadPdf: config.autoDownloadPdf.get(),
|
|
112
122
|
enableEpmcFulltext: config.enableEpmcFulltext.get(),
|
|
113
123
|
zoteroEnabled: config.zoteroEnabled.get(),
|
|
@@ -118,6 +128,13 @@ export function apply(ctx, config) {
|
|
|
118
128
|
// 任务状态是模型工具与 HTTP 路由的唯一事实源。
|
|
119
129
|
const service = new TaskService();
|
|
120
130
|
ctx.effect(() => () => service.dispose(), 'dsh-plugin-litsearch: task service');
|
|
131
|
+
// 会话工作目录:PDF 默认落在其中,跟随用户当前项目。
|
|
132
|
+
ctx.inject(['sessionPersistence'], (sessionCtx) => {
|
|
133
|
+
const store = sessionCtx
|
|
134
|
+
.sessionPersistence;
|
|
135
|
+
setSessionStore(store);
|
|
136
|
+
sessionCtx.effect(() => () => setSessionStore(undefined), 'dsh-plugin-litsearch: session workspace');
|
|
137
|
+
});
|
|
121
138
|
// 模型侧:原生注册,不依赖外部 MCP。
|
|
122
139
|
ctx.inject(['tools'], (toolCtx) => {
|
|
123
140
|
const toolsService = toolCtx.tools;
|
|
@@ -139,15 +156,20 @@ function clampInt(value, min, max, fallback) {
|
|
|
139
156
|
}
|
|
140
157
|
function normalizeSources(value) {
|
|
141
158
|
const valid = SOURCE_IDS.filter((id) => value.includes(id));
|
|
142
|
-
return valid.length === 0 ? [...
|
|
159
|
+
return valid.length === 0 ? [...DEFAULT_SOURCE_IDS] : valid;
|
|
143
160
|
}
|
|
144
161
|
function normalizeSort(value) {
|
|
145
162
|
return value === 'citations' || value === 'year' ? value : 'relevance';
|
|
146
163
|
}
|
|
147
164
|
function normalizeLanguage(value) {
|
|
148
|
-
return value
|
|
165
|
+
return TASK_LANGUAGES.includes(value) ? value : 'any';
|
|
149
166
|
}
|
|
150
167
|
function normalizeBaseUrl(value) {
|
|
151
168
|
const trimmed = value.trim().replace(/\/+$/u, '');
|
|
152
169
|
return /^https?:\/\//u.test(trimmed) ? trimmed : DEFAULT_ZOTERO_BASE_URL;
|
|
153
170
|
}
|
|
171
|
+
/** 旧默认值等同未配置:改由会话工作目录决定下载落点。 */
|
|
172
|
+
function normalizePdfRoot(value) {
|
|
173
|
+
const trimmed = value.trim();
|
|
174
|
+
return trimmed === LEGACY_PDF_ROOT ? '' : trimmed;
|
|
175
|
+
}
|
package/lib/search/aggregate.js
CHANGED
package/lib/search/config.d.ts
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
import type { SortMode, SourceId, TaskLanguage } from './types.js';
|
|
8
8
|
/** 插件版本(与 package.json 同步;仅用于 /health 诊断展示)。 */
|
|
9
9
|
export declare const PLUGIN_VERSION = "0.1.0";
|
|
10
|
+
/**
|
|
11
|
+
* 默认数据源。
|
|
12
|
+
*
|
|
13
|
+
* Google Scholar 依赖用户自备的 SerpAPI Key,未配置时默认关闭,
|
|
14
|
+
* 因此不进入默认勾选列表。
|
|
15
|
+
*/
|
|
16
|
+
export declare const DEFAULT_SOURCE_IDS: readonly SourceId[];
|
|
10
17
|
/** 默认每次加载条数。 */
|
|
11
18
|
export declare const DEFAULT_PAGE_SIZE = 20;
|
|
12
19
|
/**
|
|
@@ -37,8 +44,12 @@ export declare const DEFAULT_TASK_MAX_ROUNDS = 30;
|
|
|
37
44
|
export declare const DEFAULT_PUBMED_INTERVAL_MS = 350;
|
|
38
45
|
export declare const DEFAULT_S2_INTERVAL_MS = 3000;
|
|
39
46
|
export declare const DEFAULT_ARXIV_INTERVAL_MS = 3000;
|
|
47
|
+
/** SerpAPI 按次计费,保守限制为 1 次/秒。 */
|
|
48
|
+
export declare const DEFAULT_SERPAPI_INTERVAL_MS = 1000;
|
|
40
49
|
/** 全文下载默认设置。 */
|
|
41
50
|
export declare const DEFAULT_PDF_MAX_MB = 30;
|
|
51
|
+
/** PDF 去重缓存总量上限(MB);0 表示关闭自动清理。 */
|
|
52
|
+
export declare const DEFAULT_PDF_CACHE_MAX_MB = 512;
|
|
42
53
|
export declare const DEFAULT_AUTO_DOWNLOAD_PDF = false;
|
|
43
54
|
export declare const DEFAULT_ENABLE_EPMC_FULLTEXT = true;
|
|
44
55
|
/** Zotero connector 默认设置。 */
|
|
@@ -50,6 +61,8 @@ export declare const DEFAULT_ZOTERO_TAG = "dsh-litsearch";
|
|
|
50
61
|
export declare const MAX_ABSTRACT_CHARS = 800;
|
|
51
62
|
/** 写端点的自定义请求头(与 JSON content-type 一起构成非简单请求)。 */
|
|
52
63
|
export declare const WRITE_HEADER = "x-dsh-litsearch-write";
|
|
64
|
+
/** 面板请求携带的会话 id;Host 用它解析会话工作目录。 */
|
|
65
|
+
export declare const SESSION_HEADER = "x-dsh-litsearch-session";
|
|
53
66
|
/** Host 路由前缀(Client fetch 路径与 handler 的剥离基准,带尾斜杠)。 */
|
|
54
67
|
export declare const ROUTE_PREFIX = "/dsh-litsearch/api/";
|
|
55
68
|
/**
|
|
@@ -76,13 +89,16 @@ export interface LitsearchSettings {
|
|
|
76
89
|
ncbiApiKey: string;
|
|
77
90
|
/** 可选 Semantic Scholar API Key;提高配额。 */
|
|
78
91
|
s2ApiKey: string;
|
|
92
|
+
/** 可选 SerpAPI Key;配置后启用 Google Scholar 数据源。 */
|
|
93
|
+
serpApiKey: string;
|
|
79
94
|
/** 各源最小请求间隔覆盖值。 */
|
|
80
95
|
pubmedIntervalMs: number;
|
|
81
96
|
s2IntervalMs: number;
|
|
82
97
|
arxivIntervalMs: number;
|
|
83
|
-
/** PDF 下载根目录;空串由 Host
|
|
98
|
+
/** PDF 下载根目录;空串由 Host 回落到会话工作目录下的 dsh-litsearch/。 */
|
|
84
99
|
pdfRoot: string;
|
|
85
100
|
pdfMaxMb: number;
|
|
101
|
+
pdfCacheMaxMb: number;
|
|
86
102
|
autoDownloadPdf: boolean;
|
|
87
103
|
enableEpmcFulltext: boolean;
|
|
88
104
|
zoteroEnabled: boolean;
|
package/lib/search/config.js
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
import { SOURCE_IDS } from './types.js';
|
|
8
8
|
/** 插件版本(与 package.json 同步;仅用于 /health 诊断展示)。 */
|
|
9
9
|
export const PLUGIN_VERSION = '0.1.0';
|
|
10
|
+
/**
|
|
11
|
+
* 默认数据源。
|
|
12
|
+
*
|
|
13
|
+
* Google Scholar 依赖用户自备的 SerpAPI Key,未配置时默认关闭,
|
|
14
|
+
* 因此不进入默认勾选列表。
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_SOURCE_IDS = SOURCE_IDS.filter((id) => id !== 'googlescholar');
|
|
10
17
|
/** 默认每次加载条数。 */
|
|
11
18
|
export const DEFAULT_PAGE_SIZE = 20;
|
|
12
19
|
/**
|
|
@@ -37,8 +44,12 @@ export const DEFAULT_TASK_MAX_ROUNDS = 30;
|
|
|
37
44
|
export const DEFAULT_PUBMED_INTERVAL_MS = 350;
|
|
38
45
|
export const DEFAULT_S2_INTERVAL_MS = 3000;
|
|
39
46
|
export const DEFAULT_ARXIV_INTERVAL_MS = 3000;
|
|
47
|
+
/** SerpAPI 按次计费,保守限制为 1 次/秒。 */
|
|
48
|
+
export const DEFAULT_SERPAPI_INTERVAL_MS = 1000;
|
|
40
49
|
/** 全文下载默认设置。 */
|
|
41
50
|
export const DEFAULT_PDF_MAX_MB = 30;
|
|
51
|
+
/** PDF 去重缓存总量上限(MB);0 表示关闭自动清理。 */
|
|
52
|
+
export const DEFAULT_PDF_CACHE_MAX_MB = 512;
|
|
42
53
|
export const DEFAULT_AUTO_DOWNLOAD_PDF = false;
|
|
43
54
|
export const DEFAULT_ENABLE_EPMC_FULLTEXT = true;
|
|
44
55
|
/** Zotero connector 默认设置。 */
|
|
@@ -50,6 +61,8 @@ export const DEFAULT_ZOTERO_TAG = 'dsh-litsearch';
|
|
|
50
61
|
export const MAX_ABSTRACT_CHARS = 800;
|
|
51
62
|
/** 写端点的自定义请求头(与 JSON content-type 一起构成非简单请求)。 */
|
|
52
63
|
export const WRITE_HEADER = 'x-dsh-litsearch-write';
|
|
64
|
+
/** 面板请求携带的会话 id;Host 用它解析会话工作目录。 */
|
|
65
|
+
export const SESSION_HEADER = 'x-dsh-litsearch-session';
|
|
53
66
|
/** Host 路由前缀(Client fetch 路径与 handler 的剥离基准,带尾斜杠)。 */
|
|
54
67
|
export const ROUTE_PREFIX = '/dsh-litsearch/api/';
|
|
55
68
|
/**
|
|
@@ -64,7 +77,7 @@ let readSettings = () => ({
|
|
|
64
77
|
contactEmail: '',
|
|
65
78
|
defaultPageSize: DEFAULT_PAGE_SIZE,
|
|
66
79
|
requestTimeoutMs: REQUEST_TIMEOUT_MS,
|
|
67
|
-
defaultSources: [...
|
|
80
|
+
defaultSources: [...DEFAULT_SOURCE_IDS],
|
|
68
81
|
defaultSort: DEFAULT_SORT,
|
|
69
82
|
defaultLanguage: DEFAULT_LANGUAGE,
|
|
70
83
|
autoOpenTaskTab: DEFAULT_AUTO_OPEN,
|
|
@@ -74,11 +87,13 @@ let readSettings = () => ({
|
|
|
74
87
|
taskMaxPapers: DEFAULT_TASK_MAX_PAPERS,
|
|
75
88
|
ncbiApiKey: '',
|
|
76
89
|
s2ApiKey: '',
|
|
90
|
+
serpApiKey: '',
|
|
77
91
|
pubmedIntervalMs: DEFAULT_PUBMED_INTERVAL_MS,
|
|
78
92
|
s2IntervalMs: DEFAULT_S2_INTERVAL_MS,
|
|
79
93
|
arxivIntervalMs: DEFAULT_ARXIV_INTERVAL_MS,
|
|
80
94
|
pdfRoot: '',
|
|
81
95
|
pdfMaxMb: DEFAULT_PDF_MAX_MB,
|
|
96
|
+
pdfCacheMaxMb: DEFAULT_PDF_CACHE_MAX_MB,
|
|
82
97
|
autoDownloadPdf: DEFAULT_AUTO_DOWNLOAD_PDF,
|
|
83
98
|
enableEpmcFulltext: DEFAULT_ENABLE_EPMC_FULLTEXT,
|
|
84
99
|
zoteroEnabled: DEFAULT_ZOTERO_ENABLED,
|
package/lib/search/identity.js
CHANGED
package/lib/search/oa.d.ts
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
import type { Paper } from './types.js';
|
|
7
7
|
/** 去重后的 PDF 候选地址(顺序即尝试优先级)。 */
|
|
8
8
|
export declare function pdfCandidates(paper: Paper): string[];
|
|
9
|
+
/**
|
|
10
|
+
* 仓储的 PDF 渲染页:浏览器能打开,后台请求却普遍被拦截。
|
|
11
|
+
*
|
|
12
|
+
* 实测 `europepmc.org/articles/...?pdf=render` 返回 403 Cloudflare 页、
|
|
13
|
+
* `ncbi.nlm.nih.gov/pmc/articles/.../pdf/` 返回 200 但内容是 HTML 落地页。
|
|
14
|
+
* 它们仍保留为最后兜底,但必须排在所有静态直链与 Unpaywall 结果之后。
|
|
15
|
+
*/
|
|
16
|
+
export declare function isPdfRenderUrl(url: string): boolean;
|
|
9
17
|
/** Europe PMC 的全文 XML 地址;没有 PMCID 时返回 null。 */
|
|
10
18
|
export declare function epmcXmlUrl(paper: Paper): string | null;
|
|
11
19
|
/** Unpaywall 查询(需要联系邮箱,遵守其 polite pool)。 */
|