dsh-plugin-litsearch 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -9
- package/client/client.js +1096 -438
- package/lib/host/events.d.ts +3 -0
- package/lib/host/events.js +25 -1
- package/lib/host/paths.d.ts +43 -0
- package/lib/host/paths.js +128 -0
- package/lib/host/router.js +12 -0
- package/lib/host/tasks/filter.d.ts +2 -0
- package/lib/host/tasks/filter.js +4 -0
- package/lib/host/tasks/service.d.ts +22 -9
- package/lib/host/tasks/service.js +101 -30
- package/lib/host/tasks/store.d.ts +23 -3
- package/lib/host/tasks/store.js +63 -5
- package/lib/host/tools.d.ts +13 -6
- package/lib/host/tools.js +56 -22
- package/lib/search/config.d.ts +0 -7
- package/lib/search/config.js +0 -7
- package/lib/search/types.d.ts +10 -1
- package/package.json +1 -1
package/lib/host/tasks/store.js
CHANGED
|
@@ -1,33 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 任务持久化:`<root>/tasks/<id>.json` + `tasks/index.json`。
|
|
3
3
|
*
|
|
4
|
+
* - 数据根目录为 `$DSH_HOME/storages/dsh-plugin-litsearch`(见 `host/paths.ts`),
|
|
5
|
+
* 加载时若新目录为空而旧目录有任务,会一次性复制过来;显式传入 root 时不迁移;
|
|
4
6
|
* - 写盘全部串行执行,先写临时文件再 rename,避免半截 JSON;
|
|
5
7
|
* - 内存里只保留摘要,任务正文按需从磁盘读;
|
|
6
8
|
* - 落盘时摘要截到 {@link MAX_ABSTRACT_CHARS} 字符,任务数按保留策略裁剪。
|
|
7
9
|
*/
|
|
8
10
|
import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
9
|
-
import { homedir } from 'node:os';
|
|
10
11
|
import { dirname, join } from 'node:path';
|
|
11
12
|
import { MAX_ABSTRACT_CHARS } from '../../search/config.js';
|
|
13
|
+
import { resolveStorageRoot, storageRoot } from '../paths.js';
|
|
12
14
|
import { cloneRound, roundsOf, toSummary, trimRounds } from './summary.js';
|
|
13
15
|
const INDEX_VERSION = 1;
|
|
14
16
|
export class TaskStore {
|
|
15
17
|
root;
|
|
16
18
|
tasksDir;
|
|
17
19
|
indexFile;
|
|
20
|
+
currentFile;
|
|
18
21
|
summaries = new Map();
|
|
19
22
|
cache = new Map();
|
|
20
23
|
chain = Promise.resolve();
|
|
21
24
|
retention = { retentionDays: 90, retentionCount: 120 };
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
/** 只有使用默认(平台统一)目录时才做旧数据迁移;显式 root 多用于测试。 */
|
|
26
|
+
migrateOnLoad;
|
|
27
|
+
/** 迁移过来的任务文件必须重建索引,避免沿用旧目录里那份空 index.json。 */
|
|
28
|
+
forceRebuildIndex = false;
|
|
29
|
+
constructor(root) {
|
|
30
|
+
this.migrateOnLoad = root === undefined;
|
|
31
|
+
this.root = root ?? storageRoot();
|
|
32
|
+
this.tasksDir = join(this.root, 'tasks');
|
|
25
33
|
this.indexFile = join(this.tasksDir, 'index.json');
|
|
34
|
+
this.currentFile = join(this.root, 'current.json');
|
|
26
35
|
}
|
|
27
36
|
async load(retention) {
|
|
28
37
|
this.retention = retention;
|
|
38
|
+
if (this.migrateOnLoad) {
|
|
39
|
+
try {
|
|
40
|
+
const resolution = await resolveStorageRoot();
|
|
41
|
+
this.rebind(resolution.root);
|
|
42
|
+
// 复制过来的 index.json 可能不含新任务,强制按任务文件重建一次。
|
|
43
|
+
this.forceRebuildIndex = resolution.migratedFrom !== undefined;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
// 目录解析失败不应阻断任务系统启动,继续用构造时的默认目录。
|
|
47
|
+
console.warn('[dsh-plugin-litsearch] 数据目录解析失败,沿用默认目录:', error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
29
50
|
await mkdir(this.tasksDir, { recursive: true });
|
|
30
|
-
const index = await this.readIndex();
|
|
51
|
+
const index = this.forceRebuildIndex ? null : await this.readIndex();
|
|
31
52
|
if (index === null) {
|
|
32
53
|
await this.rebuildIndex();
|
|
33
54
|
}
|
|
@@ -75,6 +96,9 @@ export class TaskStore {
|
|
|
75
96
|
this.summaries.set(task.id, toSummary(task));
|
|
76
97
|
}
|
|
77
98
|
async save(task) {
|
|
99
|
+
// 运行中的任务可能被用户删除:删除后的迟到落盘会重建文件与索引,这里直接丢弃。
|
|
100
|
+
if (!this.summaries.has(task.id))
|
|
101
|
+
return;
|
|
78
102
|
this.put(task);
|
|
79
103
|
const file = this.taskFile(task.id);
|
|
80
104
|
const body = JSON.stringify({ version: INDEX_VERSION, task: truncateForDisk(task) });
|
|
@@ -94,6 +118,23 @@ export class TaskStore {
|
|
|
94
118
|
async flush() {
|
|
95
119
|
await this.chain;
|
|
96
120
|
}
|
|
121
|
+
/** 读取「当前任务」快照;文件缺失或损坏时返回 null(由调用方自举)。 */
|
|
122
|
+
async loadCurrent() {
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(await readFile(this.currentFile, 'utf8'));
|
|
125
|
+
const current = readBinding(parsed.current);
|
|
126
|
+
return { version: INDEX_VERSION, ...(current === undefined ? {} : { current }) };
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async saveCurrent(snapshot) {
|
|
133
|
+
const body = JSON.stringify(snapshot);
|
|
134
|
+
await this.enqueue(async () => {
|
|
135
|
+
await atomicWrite(this.currentFile, body);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
97
138
|
/** 手动清理:删除所有未置顶任务,返回条数与释放的字节数。 */
|
|
98
139
|
async cleanupUnpinned() {
|
|
99
140
|
const targets = this.list().filter((summary) => !summary.pinned);
|
|
@@ -109,6 +150,15 @@ export class TaskStore {
|
|
|
109
150
|
}
|
|
110
151
|
return { removed: targets.length, freedBytes };
|
|
111
152
|
}
|
|
153
|
+
/** 迁移回退时切回旧目录读取,所有派生路径(含设置页展示)同步更新。 */
|
|
154
|
+
rebind(root) {
|
|
155
|
+
if (root === this.root)
|
|
156
|
+
return;
|
|
157
|
+
this.root = root;
|
|
158
|
+
this.tasksDir = join(root, 'tasks');
|
|
159
|
+
this.indexFile = join(this.tasksDir, 'index.json');
|
|
160
|
+
this.currentFile = join(root, 'current.json');
|
|
161
|
+
}
|
|
112
162
|
taskFile(id) {
|
|
113
163
|
return join(this.tasksDir, `${id}.json`);
|
|
114
164
|
}
|
|
@@ -185,6 +235,14 @@ async function atomicWrite(file, body) {
|
|
|
185
235
|
await writeFile(temp, body, 'utf8');
|
|
186
236
|
await rename(temp, file);
|
|
187
237
|
}
|
|
238
|
+
function readBinding(value) {
|
|
239
|
+
if (value === null || typeof value !== 'object')
|
|
240
|
+
return undefined;
|
|
241
|
+
const record = value;
|
|
242
|
+
if (typeof record.taskId !== 'string' || record.taskId === '')
|
|
243
|
+
return undefined;
|
|
244
|
+
return { taskId: record.taskId, at: typeof record.at === 'number' ? record.at : 0 };
|
|
245
|
+
}
|
|
188
246
|
function truncateForDisk(task) {
|
|
189
247
|
return {
|
|
190
248
|
...task,
|
package/lib/host/tools.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 模型侧工具:直接注册进 dsh 工具表(不经过外部 MCP)。
|
|
3
3
|
*
|
|
4
|
-
* - `literature_search`:一个研究主题 = 一个任务 = 一个 tab
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* - `literature_search`:一个研究主题 = 一个任务 = 一个 tab。默认复用「当前
|
|
5
|
+
* 任务」并在其中开新一轮(当前任务是持久化的最近使用任务,跨对话、宿主重启
|
|
6
|
+
* 都有效);只有用户明确要求新任务时才新建。等待最多 25 秒后把任务头部与前
|
|
7
|
+
* 若干条结果交给模型,并明确要求本轮到此为止、只给完善建议、不发起下一轮。
|
|
7
8
|
* - `literature_sources`:列出已接入的数据源与能力,便于模型决定后续策略。
|
|
8
9
|
* - `literature_task`:读取/修改已保存任务(含轮次历史),写入筛读、分组与备注,
|
|
9
10
|
* 做新老对比与统计。
|
|
@@ -16,6 +17,14 @@ import type { TaskService } from './tasks/service.js';
|
|
|
16
17
|
export interface ToolsService {
|
|
17
18
|
register(definition: unknown): () => void;
|
|
18
19
|
}
|
|
20
|
+
/** 执行时附带的上下文:取消信号 + 发起调用的会话。 */
|
|
21
|
+
interface ToolRunExec {
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
/** 当前会话;用于把聚焦请求送回发起检索的那个对话。 */
|
|
24
|
+
agent?: {
|
|
25
|
+
id: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
19
28
|
interface ToolDefinition {
|
|
20
29
|
name: string;
|
|
21
30
|
description: string;
|
|
@@ -29,9 +38,7 @@ interface ToolDefinition {
|
|
|
29
38
|
text: string;
|
|
30
39
|
}>;
|
|
31
40
|
};
|
|
32
|
-
execute: (args: unknown, exec: {
|
|
33
|
-
signal?: AbortSignal;
|
|
34
|
-
}) => Promise<{
|
|
41
|
+
execute: (args: unknown, exec: ToolRunExec) => Promise<{
|
|
35
42
|
text: string;
|
|
36
43
|
}>;
|
|
37
44
|
}
|
package/lib/host/tools.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 模型侧工具:直接注册进 dsh 工具表(不经过外部 MCP)。
|
|
3
3
|
*
|
|
4
|
-
* - `literature_search`:一个研究主题 = 一个任务 = 一个 tab
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* - `literature_search`:一个研究主题 = 一个任务 = 一个 tab。默认复用「当前
|
|
5
|
+
* 任务」并在其中开新一轮(当前任务是持久化的最近使用任务,跨对话、宿主重启
|
|
6
|
+
* 都有效);只有用户明确要求新任务时才新建。等待最多 25 秒后把任务头部与前
|
|
7
|
+
* 若干条结果交给模型,并明确要求本轮到此为止、只给完善建议、不发起下一轮。
|
|
7
8
|
* - `literature_sources`:列出已接入的数据源与能力,便于模型决定后续策略。
|
|
8
9
|
* - `literature_task`:读取/修改已保存任务(含轮次历史),写入筛读、分组与备注,
|
|
9
10
|
* 做新老对比与统计。
|
|
@@ -27,6 +28,7 @@ const LANGUAGES = ['any', 'eng', 'chi'];
|
|
|
27
28
|
const SCREENINGS = ['unread', 'read', 'included', 'excluded'];
|
|
28
29
|
const EXPORT_FORMATS = ['csv', 'ris', 'bibtex', 'md'];
|
|
29
30
|
const TASK_ACTIONS = [
|
|
31
|
+
'current',
|
|
30
32
|
'list',
|
|
31
33
|
'get',
|
|
32
34
|
'update',
|
|
@@ -64,11 +66,11 @@ const SEARCH_PARAMETERS = {
|
|
|
64
66
|
},
|
|
65
67
|
taskId: {
|
|
66
68
|
type: 'string',
|
|
67
|
-
description: '要复用的已有任务 id
|
|
69
|
+
description: '要复用的已有任务 id。同一主题的后续检索必须复用同一个任务;通常省略即可复用「当前任务」(最近一次使用或打开的任务,跨对话持续有效),也可用 literature_task list 查到 id 后显式指定。指定后不会新建任务、不会新开 tab。',
|
|
68
70
|
},
|
|
69
71
|
newTask: {
|
|
70
72
|
type: 'boolean',
|
|
71
|
-
description: '
|
|
73
|
+
description: '仅当用户明确要求「新建一个查询任务」,或研究主题与当前任务明显不同时才设为 true(此时请同时给 title)。同一主题改检索式(如加宽年份、换同义词)时绝对不要设 true。',
|
|
72
74
|
},
|
|
73
75
|
query: {
|
|
74
76
|
type: 'string',
|
|
@@ -136,9 +138,9 @@ const TASK_PARAMETERS = {
|
|
|
136
138
|
action: {
|
|
137
139
|
type: 'string',
|
|
138
140
|
enum: [...TASK_ACTIONS],
|
|
139
|
-
description: 'list 列任务;get 读条件与结果;update 改条件/标题/笔记/分组;run 重跑(仅在用户明确要求进入下一轮时使用);rounds 读该任务的检索轮次历史;annotate 写筛读/备注;compare 与旧任务对比;stats 读语料统计。',
|
|
141
|
+
description: 'current 读「当前任务」(推荐在执行检索/任务操作前先调用,确认这个对话正在用哪个任务,无需 id);list 列任务;get 读条件与结果;update 改条件/标题/笔记/分组;run 重跑(仅在用户明确要求进入下一轮时使用);rounds 读该任务的检索轮次历史;annotate 写筛读/备注;compare 与旧任务对比;stats 读语料统计。',
|
|
140
142
|
},
|
|
141
|
-
id: { type: 'string', description: '任务 id;除 list 外必填。' },
|
|
143
|
+
id: { type: 'string', description: '任务 id;除 current / list 外必填。' },
|
|
142
144
|
withTaskId: { type: 'string', description: 'compare 时的基准任务 id。' },
|
|
143
145
|
limit: {
|
|
144
146
|
type: 'integer',
|
|
@@ -245,7 +247,7 @@ export function literatureTools(service) {
|
|
|
245
247
|
{
|
|
246
248
|
name: 'literature_search',
|
|
247
249
|
description: '检索免费开放学术文献。并发查询 PubMed、Europe PMC、OpenAlex、Crossref、Semantic Scholar、arXiv、PubTator,跨源与跨检索式去重合并成一篇文献一条,返回标题、作者、年份、出处、DOI、被引次数、开放获取状态与摘要,支持一次提交多条检索式(queries)。' +
|
|
248
|
-
'【任务模型】一个研究主题 = 一个任务 = 一个右侧栏 tab
|
|
250
|
+
'【任务模型】一个研究主题 = 一个任务 = 一个右侧栏 tab;一个对话只保留一个任务 tab,切换任务由用户在任务列表里完成。若不确定这个对话当前用哪个任务,先调用 literature_task(action=current)查清再执行。默认复用「当前任务」并在同一任务内开新一轮:任务 id 不变、tab 不变,结果替换为最新一轮,已有的已读/纳入/排除/备注按文献身份保留;绝不会新建任务或新开 tab。当前任务是持久化的「最近一次使用或打开的任务」,跨对话、宿主重启都有效;用户从右侧栏点开别的任务会切换当前任务。只有用户明确要求新建查询任务,或研究主题与当前任务明显不同时,才传 newTask=true 并用 title 给新任务起一个概括主题的名称;也可用 taskId 显式指定要复用的任务(先用 literature_task current/list 查 id,或让用户点右侧栏的「带入对话」把任务 id 带进对话)。每次调用请提交【完整】的新检索条件,缺省项回落到插件默认设置而非沿用旧条件。' +
|
|
249
251
|
'【轮次约束】本轮检索结束后,只给用户一份「如何完善检索」的建议,禁止自行发起下一轮、禁止重复调用本工具;必须等用户查看结果后,在右侧栏调整条件、或在对话里明确要求调整,才可在同一任务里再搜一次。' +
|
|
250
252
|
'超过 25 秒仍在进行时会先返回当前进度,任务会继续在右侧栏完成。适合找论文、查某主题的代表作、补充参考文献。',
|
|
251
253
|
parameters: SEARCH_PARAMETERS,
|
|
@@ -261,18 +263,14 @@ export function literatureTools(service) {
|
|
|
261
263
|
reused = true;
|
|
262
264
|
}
|
|
263
265
|
else if (newTask !== true) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
task = current;
|
|
267
|
-
reused = true;
|
|
268
|
-
}
|
|
266
|
+
task = await service.currentTask();
|
|
267
|
+
reused = task !== undefined;
|
|
269
268
|
}
|
|
270
269
|
if (task === undefined) {
|
|
271
270
|
task = await service.create({
|
|
272
271
|
...createInput,
|
|
273
272
|
...(title === undefined ? {} : { title }),
|
|
274
273
|
});
|
|
275
|
-
service.setCurrentTask(task.id);
|
|
276
274
|
}
|
|
277
275
|
else {
|
|
278
276
|
// 上一轮还在跑时先等,绝不「换一个任务重搜」——那正是多开 tab 的根因。
|
|
@@ -287,6 +285,8 @@ export function literatureTools(service) {
|
|
|
287
285
|
};
|
|
288
286
|
}
|
|
289
287
|
}
|
|
288
|
+
// 让客户端把该任务 tab 打开/切到前台(右侧面板关着时重新打开)。
|
|
289
|
+
service.focusTask(task.id, task.title, exec.agent?.id);
|
|
290
290
|
// 复用时不带 title:命名只属于新建任务,避免误改已有任务名。
|
|
291
291
|
const running = reused
|
|
292
292
|
? service.rerun(task.id, createInput, 'model')
|
|
@@ -324,12 +324,14 @@ export function literatureTools(service) {
|
|
|
324
324
|
},
|
|
325
325
|
{
|
|
326
326
|
name: 'literature_task',
|
|
327
|
-
description: '读取与推进已保存的检索任务。list 列出任务;get 读条件、分组、笔记与结果(可按筛读/分组/检索式/关键词过滤);update 改标题、条件、任务笔记与分组;rounds 读该任务的检索轮次历史(每轮时间、条件、发起方、命中数与状态);annotate 批量写入筛读状态、分组、必读顺序、备注与标签;compare 与旧任务按论文身份对比新增/缺失;stats 读语料统计(年份、来源、期刊、作者、类型、语言)。' +
|
|
328
|
-
'
|
|
327
|
+
description: '读取与推进已保存的检索任务。current 读「当前任务」(推荐在执行任何检索/任务操作前先调用,确认这个对话正在用哪个任务;无需 id);list 列出任务;get 读条件、分组、笔记与结果(可按筛读/分组/检索式/关键词过滤);update 改标题、条件、任务笔记与分组;rounds 读该任务的检索轮次历史(每轮时间、条件、发起方、命中数与状态);annotate 批量写入筛读状态、分组、必读顺序、备注与标签;compare 与旧任务按论文身份对比新增/缺失;stats 读语料统计(年份、来源、期刊、作者、类型、语言)。' +
|
|
328
|
+
'注意:一个对话只保留一个任务 tab,切换任务由用户在任务列表里完成;run 会在指定任务内开新一轮检索(并把它设为当前任务),与 literature_search 一样受「用户查看结果并明确要求后才能进入下一轮」约束,不能用它绕过该约束自行加搜;也不要用 update 改完条件就立刻 run。适合在检索之后做筛选、整理与汇报。',
|
|
329
329
|
parameters: TASK_PARAMETERS,
|
|
330
330
|
output: TEXT_OUTPUT,
|
|
331
331
|
async execute(args, exec) {
|
|
332
|
-
return {
|
|
332
|
+
return {
|
|
333
|
+
text: await runTaskAction(service, readTaskArgs(args), exec.signal, exec.agent?.id),
|
|
334
|
+
};
|
|
333
335
|
},
|
|
334
336
|
},
|
|
335
337
|
{
|
|
@@ -519,7 +521,7 @@ function readTaskArgs(args) {
|
|
|
519
521
|
const id = readText(record['id'], 'id');
|
|
520
522
|
if (id !== undefined)
|
|
521
523
|
input.id = id;
|
|
522
|
-
if (input.action !== 'list' && input.id === undefined) {
|
|
524
|
+
if (input.action !== 'list' && input.action !== 'current' && input.id === undefined) {
|
|
523
525
|
throw new Error(`action=${input.action} 需要 id`);
|
|
524
526
|
}
|
|
525
527
|
const withTaskId = readText(record['withTaskId'], 'withTaskId');
|
|
@@ -739,8 +741,12 @@ function readExportArgs(args) {
|
|
|
739
741
|
input.queryId = queryId;
|
|
740
742
|
return input;
|
|
741
743
|
}
|
|
742
|
-
async function runTaskAction(service, args, signal) {
|
|
744
|
+
async function runTaskAction(service, args, signal, sessionId) {
|
|
743
745
|
switch (args.action) {
|
|
746
|
+
case 'current': {
|
|
747
|
+
const task = await service.currentTask();
|
|
748
|
+
return formatCurrentTask(task);
|
|
749
|
+
}
|
|
744
750
|
case 'list': {
|
|
745
751
|
const tasks = await service.list();
|
|
746
752
|
return formatTaskList(tasks, args.limit ?? 30);
|
|
@@ -762,10 +768,12 @@ async function runTaskAction(service, args, signal) {
|
|
|
762
768
|
}
|
|
763
769
|
case 'run': {
|
|
764
770
|
const id = requireId(args);
|
|
765
|
-
await service.get(id);
|
|
771
|
+
const task = await service.get(id);
|
|
772
|
+
// 用户明确要求推进这个任务:它成为当前任务,并让客户端打开/聚焦对应 tab。
|
|
766
773
|
service.setCurrentTask(id);
|
|
774
|
+
service.focusTask(task.id, task.title, sessionId);
|
|
767
775
|
const outcome = await runWithin(service, id, signal, service.run(id, 'new', 'model'));
|
|
768
|
-
const
|
|
776
|
+
const latest = await service.get(id);
|
|
769
777
|
const detail = await service.getDetail(id);
|
|
770
778
|
const page = await service.results(id, {
|
|
771
779
|
...readFilterArgs(args),
|
|
@@ -773,7 +781,7 @@ async function runTaskAction(service, args, signal) {
|
|
|
773
781
|
offset: args.offset ?? 0,
|
|
774
782
|
});
|
|
775
783
|
// 这条旁路同样附上「不自动下一轮」约束,避免模型绕过 literature_search。
|
|
776
|
-
return `${formatTaskDetail(detail, page,
|
|
784
|
+
return `${formatTaskDetail(detail, page, latest.states, latest.groups, outcome)}\n\n${NO_AUTO_NEXT_ROUND}`;
|
|
777
785
|
}
|
|
778
786
|
case 'rounds': {
|
|
779
787
|
const id = requireId(args);
|
|
@@ -840,6 +848,32 @@ function readText(value, name, allowEmpty = false) {
|
|
|
840
848
|
return undefined;
|
|
841
849
|
return allowEmpty ? value : value.trim();
|
|
842
850
|
}
|
|
851
|
+
/** 「当前任务」的轻量快照:模型在执行检索/任务操作前先确认这个对话正在用哪个任务。 */
|
|
852
|
+
function formatCurrentTask(task) {
|
|
853
|
+
if (task === undefined) {
|
|
854
|
+
return [
|
|
855
|
+
'当前没有任务:这个对话还没有任何检索任务。',
|
|
856
|
+
'若用户要求检索,请直接调用 literature_search(不要传 newTask);若用户要继续某个已有任务,请让用户从右侧栏任务列表打开它,或点任务页的「带入对话」把任务 id 带进对话。',
|
|
857
|
+
].join('\n');
|
|
858
|
+
}
|
|
859
|
+
const conditions = task.conditions;
|
|
860
|
+
const queries = conditions.queries.map((query) => `${query.name}=「${query.text}」`).join(';');
|
|
861
|
+
const last = task.rounds[task.rounds.length - 1];
|
|
862
|
+
const screening = task.stats.screening;
|
|
863
|
+
const lines = [
|
|
864
|
+
`当前任务:${task.title}(id: ${task.id})`,
|
|
865
|
+
`状态:${statusLabel(task.status)}${task.error === undefined ? '' : `(${task.error})`}`,
|
|
866
|
+
`检索轮次:共 ${task.rounds.length} 轮${last === undefined
|
|
867
|
+
? ''
|
|
868
|
+
: `|最近一轮 ${last.createdAt.slice(0, 19).replace('T', ' ')}(${last.origin === 'model' ? '模型' : '面板'})`}`,
|
|
869
|
+
`检索式:${queries === '' ? '(无)' : queries}`,
|
|
870
|
+
`条件:数据源 ${conditions.sources.join('/')}|每源 ${conditions.pageSize} 条|年份 ${conditions.yearFrom ?? '不限'}-${conditions.yearTo ?? '不限'}|${conditions.openAccessOnly ? '仅开放获取' : '不限获取方式'}|语言 ${conditions.language}|排序 ${conditions.sort}`,
|
|
871
|
+
`漏斗:原始 ${task.stats.rawHits}|去重 ${task.stats.deduped}|已加载 ${task.result.papers.length}|纳入 ${screening.included}、排除 ${screening.excluded}、待读 ${screening.unread}、已读 ${screening.read}`,
|
|
872
|
+
'',
|
|
873
|
+
'用法:同一主题继续检索时省略 taskId 即复用该任务;需要调整条件时直接在 literature_search 里提交【完整】的新条件;只有用户明确要求新建查询任务时才传 newTask=true。',
|
|
874
|
+
];
|
|
875
|
+
return lines.join('\n');
|
|
876
|
+
}
|
|
843
877
|
function formatTaskList(tasks, limit) {
|
|
844
878
|
if (tasks.length === 0)
|
|
845
879
|
return '还没有保存的检索任务。可先用 literature_search 检索。';
|
package/lib/search/config.d.ts
CHANGED
|
@@ -33,13 +33,6 @@ export declare const DEFAULT_RETENTION_COUNT = 120;
|
|
|
33
33
|
export declare const DEFAULT_TASK_MAX_PAPERS = 1500;
|
|
34
34
|
/** 单任务保留的检索轮数上限;只记元数据,超出后淘汰最旧的轮次。 */
|
|
35
35
|
export declare const DEFAULT_TASK_MAX_ROUNDS = 30;
|
|
36
|
-
/**
|
|
37
|
-
* Host 侧「当前任务」记忆有效期(毫秒)。
|
|
38
|
-
*
|
|
39
|
-
* 工具执行上下文拿不到会话 id,只能用进程内最近使用的任务做降级;
|
|
40
|
-
* 超过该时长没有再使用就自动失效,避免隔了很久的新主题被误并进旧任务。
|
|
41
|
-
*/
|
|
42
|
-
export declare const CURRENT_TASK_TTL_MS: number;
|
|
43
36
|
/** 各源默认最小请求间隔(毫秒);PubMed/S2/arXiv 可由设置覆盖。 */
|
|
44
37
|
export declare const DEFAULT_PUBMED_INTERVAL_MS = 350;
|
|
45
38
|
export declare const DEFAULT_S2_INTERVAL_MS = 3000;
|
package/lib/search/config.js
CHANGED
|
@@ -33,13 +33,6 @@ export const DEFAULT_RETENTION_COUNT = 120;
|
|
|
33
33
|
export const DEFAULT_TASK_MAX_PAPERS = 1500;
|
|
34
34
|
/** 单任务保留的检索轮数上限;只记元数据,超出后淘汰最旧的轮次。 */
|
|
35
35
|
export const DEFAULT_TASK_MAX_ROUNDS = 30;
|
|
36
|
-
/**
|
|
37
|
-
* Host 侧「当前任务」记忆有效期(毫秒)。
|
|
38
|
-
*
|
|
39
|
-
* 工具执行上下文拿不到会话 id,只能用进程内最近使用的任务做降级;
|
|
40
|
-
* 超过该时长没有再使用就自动失效,避免隔了很久的新主题被误并进旧任务。
|
|
41
|
-
*/
|
|
42
|
-
export const CURRENT_TASK_TTL_MS = 15 * 60_000;
|
|
43
36
|
/** 各源默认最小请求间隔(毫秒);PubMed/S2/arXiv 可由设置覆盖。 */
|
|
44
37
|
export const DEFAULT_PUBMED_INTERVAL_MS = 350;
|
|
45
38
|
export const DEFAULT_S2_INTERVAL_MS = 3000;
|
package/lib/search/types.d.ts
CHANGED
|
@@ -222,7 +222,6 @@ export interface SearchTask {
|
|
|
222
222
|
conditions: TaskConditions;
|
|
223
223
|
status: TaskStatus;
|
|
224
224
|
origin: TaskOrigin;
|
|
225
|
-
sessionId?: string;
|
|
226
225
|
fromTaskId?: string;
|
|
227
226
|
/** 每次状态/条件变化自增。 */
|
|
228
227
|
rev: number;
|
|
@@ -512,11 +511,21 @@ export interface ProbeResult {
|
|
|
512
511
|
error?: string;
|
|
513
512
|
cooldownUntil?: string;
|
|
514
513
|
}
|
|
514
|
+
/** 客户端需要打开/聚焦的任务(模型刚在当前任务里发起了一轮检索)。 */
|
|
515
|
+
export interface TaskFocus {
|
|
516
|
+
taskId: string;
|
|
517
|
+
title: string;
|
|
518
|
+
/** 请求时间(epoch ms);客户端据此丢弃过期的聚焦请求。 */
|
|
519
|
+
at: number;
|
|
520
|
+
/** 发起检索的会话;客户端只在该会话可见时打开,避免抢占其他对话的面板。 */
|
|
521
|
+
sessionId?: string;
|
|
522
|
+
}
|
|
515
523
|
/** `/events` 的响应;`reset` 表示客户端的 sinceRev 比服务端新(宿主重启)。 */
|
|
516
524
|
export interface EventsPage {
|
|
517
525
|
rev: number;
|
|
518
526
|
reset: boolean;
|
|
519
527
|
tasks: TaskSummary[];
|
|
528
|
+
focus?: TaskFocus;
|
|
520
529
|
}
|
|
521
530
|
/** `/health` 的响应:数据源能力 + 客户端需要的默认设置(不含密钥)。 */
|
|
522
531
|
export interface HealthInfo {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-litsearch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Search free scholarly literature sources from DSH. The model can search OpenAlex, Crossref and more with native tools, while you can run the same searches in a sidebar panel and send any paper into the conversation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|