dsh-plugin-litsearch 0.3.0 → 0.4.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.
Files changed (39) hide show
  1. package/README.md +158 -41
  2. package/client/client.js +2632 -926
  3. package/lib/host/events.d.ts +9 -1
  4. package/lib/host/events.js +50 -3
  5. package/lib/host/export/bibtex.js +1 -4
  6. package/lib/host/export/common.d.ts +0 -2
  7. package/lib/host/export/common.js +0 -7
  8. package/lib/host/export/csv.js +1 -5
  9. package/lib/host/export/markdown.d.ts +1 -1
  10. package/lib/host/export/markdown.js +9 -26
  11. package/lib/host/export/ris.js +1 -4
  12. package/lib/host/paths.d.ts +1 -1
  13. package/lib/host/paths.js +1 -1
  14. package/lib/host/pdf/store.d.ts +5 -3
  15. package/lib/host/pdf/store.js +50 -17
  16. package/lib/host/router.js +146 -79
  17. package/lib/host/tasks/download.d.ts +104 -0
  18. package/lib/host/tasks/download.js +274 -0
  19. package/lib/host/tasks/filter.d.ts +8 -1
  20. package/lib/host/tasks/filter.js +14 -0
  21. package/lib/host/tasks/service.d.ts +44 -11
  22. package/lib/host/tasks/service.js +216 -48
  23. package/lib/host/tasks/states.d.ts +1 -3
  24. package/lib/host/tasks/states.js +2 -66
  25. package/lib/host/tasks/stats.js +3 -0
  26. package/lib/host/tasks/summary.js +1 -1
  27. package/lib/host/tools.d.ts +5 -1
  28. package/lib/host/tools.js +813 -37
  29. package/lib/host/zotero/connector.d.ts +19 -10
  30. package/lib/host/zotero/connector.js +142 -39
  31. package/lib/host/zotero/match.d.ts +27 -0
  32. package/lib/host/zotero/match.js +59 -0
  33. package/lib/search/config.d.ts +0 -2
  34. package/lib/search/config.js +0 -2
  35. package/lib/search/oa.d.ts +8 -0
  36. package/lib/search/oa.js +53 -6
  37. package/lib/search/types.d.ts +154 -10
  38. package/lib/search/types.js +11 -0
  39. package/package.json +1 -1
@@ -0,0 +1,274 @@
1
+ /**
2
+ * 模型下载指令的范围解析(纯函数,便于单测)。
3
+ *
4
+ * 模型跑在 Host 侧,看不到右侧栏翻到第几页、用了什么筛选。范围统一由视图
5
+ * 快照 + 服务端 `filterPapers` 算准,并逐个与任务结果取交集,最终只把一份
6
+ * 「已校验的 paperId 清单」交给 Client —— 浏览器不再解析任何条件,也不会
7
+ * 出现「模型理解的筛选」与「界面筛选」两套口径。
8
+ */
9
+ /**
10
+ * 每页篇数的兜底值,与 `types.ts` 的 `RESULT_PAGE_SIZE`(面板分页粒度)一致。
11
+ *
12
+ * 本文件要能被 `node --test` 直接加载,因此不做任何运行时模块导入(`.js`
13
+ * 后缀在同环境里无法回落到 `.ts`);真正的每页条数总由调用方通过视图快照
14
+ * 传入,这里只兜底。`test/download-scope.test.ts` 会断言它仍是 20。
15
+ */
16
+ const FALLBACK_PAGE_SIZE = 20;
17
+ /**
18
+ * 与 `types.ts` 的 `TASK_LANGUAGES` 一致。这里刻意只做类型导入、不产生运行时
19
+ * 模块依赖:`src` 源码可被 `node --test` 直接加载,Host 构建产物仍走 `.js`。
20
+ */
21
+ const LANGUAGE_VALUES = [
22
+ 'any',
23
+ 'eng',
24
+ 'chi',
25
+ 'jpn',
26
+ 'kor',
27
+ 'fre',
28
+ 'ger',
29
+ 'spa',
30
+ 'rus',
31
+ 'ita',
32
+ 'por',
33
+ ];
34
+ const SCREENING_VALUES = ['unread', 'read', 'included', 'excluded'];
35
+ const PDF_STATUS_VALUES = [
36
+ 'unknown',
37
+ 'available',
38
+ 'downloaded',
39
+ 'missing',
40
+ 'html-only',
41
+ 'paywalled',
42
+ 'blocked',
43
+ 'failed',
44
+ ];
45
+ /**
46
+ * 把视图快照的扁平筛选投影收窄回 `PaperFilter`。
47
+ *
48
+ * 快照来自 Client 的 `ResultsQuery`,键名与筛选口径一致;这里只按白名单取,
49
+ * 未知键与非法枚举值一律忽略,避免模型/前端写入任意字段影响后台过滤。
50
+ */
51
+ export function toPaperFilter(raw) {
52
+ const text = (key) => {
53
+ const value = raw[key];
54
+ return typeof value === 'string' && value !== '' ? value : undefined;
55
+ };
56
+ const number = (key) => {
57
+ const value = raw[key];
58
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
59
+ };
60
+ const flag = (key) => (raw[key] === true ? true : undefined);
61
+ const oneOf = (key, values) => {
62
+ const value = raw[key];
63
+ return typeof value === 'string' ? values.find((item) => item === value) : undefined;
64
+ };
65
+ return {
66
+ source: text('source'),
67
+ q: text('q'),
68
+ queryId: text('queryId'),
69
+ type: text('type'),
70
+ venue: text('venue'),
71
+ author: text('author'),
72
+ groupId: text('groupId'),
73
+ yearFrom: number('yearFrom'),
74
+ yearTo: number('yearTo'),
75
+ openAccessOnly: flag('openAccessOnly'),
76
+ hasPdf: flag('hasPdf'),
77
+ missingFullText: flag('missingFullText'),
78
+ newOnly: flag('newOnly'),
79
+ screening: oneOf('screening', SCREENING_VALUES),
80
+ language: oneOf('language', LANGUAGE_VALUES),
81
+ pdfStatus: oneOf('pdfStatus', PDF_STATUS_VALUES),
82
+ };
83
+ }
84
+ /** 视图筛选打底,工具显式字段覆盖同名项;显式值为 undefined 的不覆盖。 */
85
+ export function mergeDownloadFilter(base, explicit) {
86
+ const merged = { ...base };
87
+ for (const [key, value] of Object.entries(explicit)) {
88
+ if (value === undefined)
89
+ continue;
90
+ Object.assign(merged, { [key]: value });
91
+ }
92
+ return merged;
93
+ }
94
+ /**
95
+ * 把「第 4–5 页」解析成 paperId 清单。
96
+ *
97
+ * 页码语义固定在「用户屏幕上那一页」:每页 `pageSize` 条(面板恒为 20)。
98
+ * **前置条件**:`matched` / `viewMatched` 必须已按界面排序排好(调用方用
99
+ * `filter.sortPapers`),否则切出来的页与面板不一致。
100
+ *
101
+ * `pageBase='view'` 时先切页、再与附加条件取交集,这样「第二页里有 PDF 的
102
+ * 文献」不会因为加了 `hasPdf` 而把页面整体前移。
103
+ */
104
+ export function resolvePageWindow(options) {
105
+ const pageSize = Math.max(1, Math.floor(options.pageSize ?? FALLBACK_PAGE_SIZE));
106
+ const base = options.pageBase === 'view' ? options.viewMatched : options.matched;
107
+ const pageCount = Math.max(1, Math.ceil(base.length / pageSize));
108
+ const outOfRange = [];
109
+ const seen = new Set();
110
+ const paperIds = [];
111
+ for (const page of [...options.pages].sort((a, b) => a - b)) {
112
+ const index = Math.floor(page) - 1;
113
+ if (index < 0 || page > pageCount) {
114
+ outOfRange.push(page);
115
+ continue;
116
+ }
117
+ const start = index * pageSize;
118
+ for (const paper of base.slice(start, start + pageSize)) {
119
+ if (seen.has(paper.paperId))
120
+ continue;
121
+ seen.add(paper.paperId);
122
+ paperIds.push(paper.paperId);
123
+ }
124
+ }
125
+ return { paperIds, pageSize, pageCount, outOfRange };
126
+ }
127
+ /**
128
+ * 解析范围 → 已校验的 paperId 清单。
129
+ *
130
+ * `ids` 按模型给定顺序、`currentPage` 按用户当前页顺序,其余按任务结果原序;
131
+ * 每种范围都会再与 `filterPapers` 的结果取交集,因此叠加 `hasPdf`、年份等
132
+ * 条件时与界面口径完全一致。全量直接用纯函数过滤,不受 `/results` 的 200
133
+ * 条分页上限约束。
134
+ */
135
+ export function resolveDownloadScope(task, options) {
136
+ const byId = new Map(task.result.papers.map((paper) => [paper.paperId, paper]));
137
+ const eligible = new Set(options.matched.map((paper) => paper.paperId));
138
+ let ordered;
139
+ let pageCount;
140
+ let outOfRange;
141
+ switch (options.scope) {
142
+ case 'ids': {
143
+ const seen = new Set();
144
+ ordered = [];
145
+ for (const raw of options.explicitIds ?? []) {
146
+ const id = raw.trim();
147
+ if (id === '' || seen.has(id))
148
+ continue;
149
+ seen.add(id);
150
+ ordered.push(id);
151
+ }
152
+ break;
153
+ }
154
+ case 'currentPage': {
155
+ ordered = [...(options.pagePaperIds ?? options.snapshot?.pagePaperIds ?? [])];
156
+ break;
157
+ }
158
+ case 'pages': {
159
+ const window = resolvePageWindow({
160
+ pages: options.pages ?? [],
161
+ pageBase: options.pageBase ?? 'view',
162
+ matched: options.matched,
163
+ viewMatched: options.viewMatched ?? options.matched,
164
+ ...(options.pageSize === undefined ? {} : { pageSize: options.pageSize }),
165
+ });
166
+ ordered = window.paperIds;
167
+ pageCount = window.pageCount;
168
+ outOfRange = window.outOfRange;
169
+ break;
170
+ }
171
+ default: {
172
+ ordered = options.matched.map((paper) => paper.paperId);
173
+ break;
174
+ }
175
+ }
176
+ const paperIds = [];
177
+ const items = [];
178
+ for (const id of ordered) {
179
+ if (!eligible.has(id))
180
+ continue;
181
+ const paper = byId.get(id);
182
+ if (paper === undefined)
183
+ continue;
184
+ paperIds.push(id);
185
+ const meta = [paper.venue, paper.year === null ? '' : String(paper.year)]
186
+ .filter((part) => part !== '')
187
+ .join(' · ');
188
+ items.push({
189
+ paperId: id,
190
+ title: paper.title,
191
+ source: paper.hitSources[0]?.source ?? paper.source,
192
+ ...(meta === '' ? {} : { meta }),
193
+ });
194
+ }
195
+ return {
196
+ paperIds,
197
+ items,
198
+ ignored: ordered.length - paperIds.length,
199
+ scopeLabel: buildScopeLabel(options.scope, paperIds.length, options),
200
+ ...(pageCount === undefined ? {} : { pageCount }),
201
+ ...(outOfRange === undefined ? {} : { outOfRange }),
202
+ };
203
+ }
204
+ /** 生成给弹窗展示的中文范围说明(含篇数);Client 只显示、不翻译、不再解析。 */
205
+ export function buildScopeLabel(scope, count, options) {
206
+ const parts = [];
207
+ switch (scope) {
208
+ case 'ids':
209
+ parts.push('指定文献');
210
+ break;
211
+ case 'currentPage':
212
+ parts.push(`当前页(第 ${(options.snapshot?.pageIndex ?? 0) + 1} 页)`);
213
+ break;
214
+ case 'pages':
215
+ parts.push(`第 ${describePages(options.pages ?? [])} 页`);
216
+ break;
217
+ case 'currentView':
218
+ parts.push('当前筛选全部');
219
+ break;
220
+ case 'all':
221
+ parts.push('整个任务全部文献');
222
+ break;
223
+ case 'group':
224
+ parts.push(`分组「${options.groupName ?? options.filter.groupId ?? ''}」`);
225
+ break;
226
+ case 'filters':
227
+ parts.push('按条件筛选');
228
+ break;
229
+ }
230
+ const filter = options.filter;
231
+ if (filter.hasPdf === true)
232
+ parts.push('仅含 PDF 直链或已下载');
233
+ if (filter.openAccessOnly === true)
234
+ parts.push('仅开放获取');
235
+ if (filter.q !== undefined && filter.q !== '')
236
+ parts.push(`关键词「${filter.q}」`);
237
+ if (filter.groupId !== undefined && scope !== 'group')
238
+ parts.push(`分组 ${filter.groupId}`);
239
+ if (filter.yearFrom !== undefined || filter.yearTo !== undefined) {
240
+ parts.push(`${filter.yearFrom ?? '不限'}–${filter.yearTo ?? '不限'} 年`);
241
+ }
242
+ if (filter.source !== undefined)
243
+ parts.push(`来源 ${filter.source}`);
244
+ if (filter.screening !== undefined)
245
+ parts.push(`筛读 ${filter.screening}`);
246
+ if (filter.pdfStatus !== undefined)
247
+ parts.push(`全文状态 ${filter.pdfStatus}`);
248
+ if (filter.venue !== undefined && filter.venue !== '')
249
+ parts.push(`出处 ${filter.venue}`);
250
+ if (filter.author !== undefined && filter.author !== '')
251
+ parts.push(`作者 ${filter.author}`);
252
+ return `${parts.join(' · ')} · 共 ${count} 篇`;
253
+ }
254
+ /** 页码列表 → 可读文案:`[2]` → `2`,`[4,5,6]` → `4–6`,`[2,5]` → `2、5`。 */
255
+ export function describePages(pages) {
256
+ const sorted = [...new Set(pages)].sort((a, b) => a - b);
257
+ if (sorted.length === 0)
258
+ return '';
259
+ const chunks = [];
260
+ let start = sorted[0];
261
+ let prev = sorted[0];
262
+ for (let index = 1; index < sorted.length; index += 1) {
263
+ const page = sorted[index];
264
+ if (page === prev + 1) {
265
+ prev = page;
266
+ continue;
267
+ }
268
+ chunks.push(start === prev ? String(start) : `${start}–${prev}`);
269
+ start = page;
270
+ prev = page;
271
+ }
272
+ chunks.push(start === prev ? String(start) : `${start}–${prev}`);
273
+ return chunks.join('、');
274
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 任何新筛选条件都必须先加到这里,避免列表与统计出现两套过滤逻辑。
5
5
  */
6
- import type { Paper, PdfStatus, Screening, SearchTask, TaskLanguage } from '../../search/types.js';
6
+ import type { Paper, PdfStatus, Screening, SearchTask, SortMode, TaskLanguage } from '../../search/types.js';
7
7
  export interface PaperFilter {
8
8
  source?: string;
9
9
  yearFrom?: number;
@@ -35,6 +35,13 @@ export interface PaperFilter {
35
35
  }
36
36
  export declare function filterPapers(task: SearchTask, filter?: PaperFilter): Paper[];
37
37
  export declare function screeningOf(task: SearchTask, paper: Paper): Screening;
38
+ /**
39
+ * 结果排序:`/results`、`/stats` 与模型侧的按页取数共用这一份口径。
40
+ *
41
+ * `relevance` 保持上游合并后的原序(不做二次排序);被引/年份缺失的排在最后,
42
+ * 与面板看到的顺序一致。原地排序,调用方传入副本。
43
+ */
44
+ export declare function sortPapers(papers: Paper[], sort: SortMode | undefined): void;
38
45
  /** 有效全文状态:没有人工/下载记录时,上游给的 PDF 直链视为可用。 */
39
46
  export declare function pdfStatusOf(task: SearchTask, paper: Paper): PdfStatus;
40
47
  export declare function matchesLanguage(paper: {
@@ -62,6 +62,20 @@ export function filterPapers(task, filter = {}) {
62
62
  export function screeningOf(task, paper) {
63
63
  return task.states[paper.paperId]?.screening ?? 'unread';
64
64
  }
65
+ /**
66
+ * 结果排序:`/results`、`/stats` 与模型侧的按页取数共用这一份口径。
67
+ *
68
+ * `relevance` 保持上游合并后的原序(不做二次排序);被引/年份缺失的排在最后,
69
+ * 与面板看到的顺序一致。原地排序,调用方传入副本。
70
+ */
71
+ export function sortPapers(papers, sort) {
72
+ if (sort === 'citations') {
73
+ papers.sort((a, b) => (b.citedBy ?? -1) - (a.citedBy ?? -1));
74
+ }
75
+ else if (sort === 'year') {
76
+ papers.sort((a, b) => (b.year ?? 0) - (a.year ?? 0));
77
+ }
78
+ }
65
79
  /** 有效全文状态:没有人工/下载记录时,上游给的 PDF 直链视为可用。 */
66
80
  export function pdfStatusOf(task, paper) {
67
81
  const recorded = task.states[paper.paperId]?.pdfStatus;
@@ -4,7 +4,7 @@
4
4
  * 负责校验/规范化任务条件、维护内存与磁盘状态、串行化执行并广播 rev。
5
5
  * 模型工具与面板 HTTP 路由都只调用这里,保证两边操作同一份状态。
6
6
  */
7
- import type { AnnotationInput, AnnotationResult, CleanupResult, CleanupScope, CompareResult, EventsPage, ExportFile, ExportFormat, HealthInfo, PaperDetail, PaperEntities, PdfDownloadResult, ProbeResult, QueryBlock, ResultsPage, SearchRound, SearchTask, SortMode, SourceId, StorageProbeResult, TaskDetail, TaskGroup, TaskLanguage, TaskOrigin, TaskStatsReport, TaskSummary, ZoteroProbeResult, ZoteroSyncResult } from '../../search/types.js';
7
+ import type { AnnotationInput, AnnotationResult, CleanupResult, CleanupScope, CompareResult, DownloadCommand, EventsPage, ExportFile, ExportFormat, HealthInfo, PaperDetail, PaperEntities, PdfDownloadResult, ProbeResult, QueryBlock, ResultsPage, SearchRound, SearchTask, SortMode, SourceId, StorageProbeResult, TaskDetail, TaskGroup, TaskLanguage, TaskOrigin, TaskStatsReport, TaskSummary, ViewSnapshot, ZoteroCommand, ZoteroProbeResult, ZoteroSyncResult, ZoteroTargetsResult } from '../../search/types.js';
8
8
  import { type PaperFilter } from './filter.js';
9
9
  export interface QueryInput {
10
10
  name?: string;
@@ -62,6 +62,13 @@ export declare class TaskService {
62
62
  private readonly store;
63
63
  private readonly events;
64
64
  private readonly runner;
65
+ /**
66
+ * 每个会话最近一次的右侧栏视图快照(内存、不落盘)。
67
+ *
68
+ * 模型工具跑在 Host 侧看不到界面,靠它把「当前页」「当前筛选全部」算准;
69
+ * 宿主重启即失效,与事件日志 rev 的生命周期一致。
70
+ */
71
+ private readonly views;
65
72
  /**
66
73
  * 全局「当前任务」:最后一次使用或打开的任务。
67
74
  *
@@ -89,7 +96,7 @@ export declare class TaskService {
89
96
  eventsSince(sinceRev: number): Promise<EventsPage>;
90
97
  list(filter?: TaskListFilter): Promise<TaskSummary[]>;
91
98
  get(id: string): Promise<SearchTask>;
92
- getDetail(id: string): Promise<TaskDetail>;
99
+ getDetail(id: string, workspace?: string): Promise<TaskDetail>;
93
100
  /** 任务内轮次历史(时间升序、深拷贝)。 */
94
101
  getRounds(id: string): Promise<SearchRound[]>;
95
102
  /**
@@ -103,6 +110,17 @@ export declare class TaskService {
103
110
  setCurrentTask(id: string): void;
104
111
  /** 请求客户端打开/聚焦某任务 tab(面板被关掉时重新打开)。 */
105
112
  focusTask(id: string, title: string, sessionId?: string): void;
113
+ /** 下发一条模型批量下载指令;右侧栏轮询 /events 拿增量后自行执行。 */
114
+ publishDownloadCommand(command: DownloadCommand): void;
115
+ /** 下发一条模型批量存入 Zotero 指令;与下载指令走独立的事件日志。 */
116
+ publishZoteroCommand(command: ZoteroCommand): void;
117
+ /**
118
+ * 面板上报当前视图。会话 id 以请求头为准覆盖 body,防止伪造别的会话的视图;
119
+ * 时间也由 Host 重置,避免客户端时钟偏差影响过期判断。
120
+ */
121
+ saveViewSnapshot(sessionId: string, snapshot: ViewSnapshot): void;
122
+ /** 取该会话在该任务上的最近视图;过期、跨任务或 rev 超前都视为不可信。 */
123
+ getViewSnapshot(sessionId: string | undefined, taskId: string): Promise<ViewSnapshot | undefined>;
106
124
  private persistCurrent;
107
125
  /** 用户从任务列表点开某任务 = 进入该任务,之后的模型检索默认落在这里。 */
108
126
  activate(id: string, workspace?: string): Promise<SearchTask>;
@@ -129,7 +147,7 @@ export declare class TaskService {
129
147
  cancel(id: string): Promise<SearchTask>;
130
148
  results(id: string, filter?: ResultsFilter): Promise<ResultsPage>;
131
149
  stats(id: string, filter?: PaperFilter, scope?: 'task' | 'filtered'): Promise<TaskStatsReport>;
132
- /** 写入筛读 / 分组 / 必读 / 备注;返回新的 rev 与命中条数。 */
150
+ /** 写入筛读 / 分组;返回新的 rev 与命中条数。 */
133
151
  annotate(id: string, inputs: readonly AnnotationInput[], expectedRev?: number): Promise<AnnotationResult>;
134
152
  compare(id: string, withTaskId: string): Promise<CompareResult>;
135
153
  /** 生成导出内容;`paperIds` 为空表示按当前筛选导出全部。 */
@@ -142,23 +160,33 @@ export declare class TaskService {
142
160
  }>;
143
161
  /** 设置页清理:任务文件与 PDF 缓存。 */
144
162
  cleanup(scope: CleanupScope): Promise<CleanupResult>;
163
+ /**
164
+ * 「查看已下载」前的存在性检查:页面加载后文件可能被删除或移走。
165
+ * 缺失时把状态改为 `missing` 并广播,所有面板随即显示「下载文件已删除」。
166
+ */
167
+ recheckPdf(id: string, paperId: string): Promise<{
168
+ exists: boolean;
169
+ }>;
145
170
  /** 下载单篇 PDF;失败时写入失败状态,若有 EPMC 全文则降级为 html-only。 */
146
171
  downloadPdf(id: string, paperId: string, workspace?: string): Promise<PdfDownloadResult>;
147
- /** PDF 预览:优先用已落地文件;没有则临时抓取一次但不落盘。 */
148
- previewPdf(id: string, paperId: string, workspace?: string): Promise<{
149
- buffer: Buffer;
150
- filename: string;
151
- }>;
152
172
  /** Europe PMC 全文(JATS → 只含安全标签的 HTML)。 */
153
173
  fullText(id: string, paperId: string): Promise<string>;
154
174
  /** 单篇论文详情:独立论文页使用,含任务标题与分组。 */
155
- paperDetail(id: string, paperId: string): Promise<PaperDetail>;
175
+ paperDetail(id: string, paperId: string, workspace?: string): Promise<PaperDetail>;
156
176
  /** PubTator3 实体标注;无 PMID 时明确告知不可标注。 */
157
177
  entities(id: string, paperId: string, signal?: AbortSignal): Promise<PaperEntities>;
158
178
  /** 测试 Zotero connector 是否可达,并返回当前选中的分类。 */
159
179
  zoteroProbe(): Promise<ZoteroProbeResult>;
160
- /** 把选中论文同步到 Zotero(含本地 PDF 附件)。 */
161
- zoteroSync(id: string, paperIds?: readonly string[], workspace?: string): Promise<ZoteroSyncResult>;
180
+ /** 可写入的 Zotero 库与分类,供同步弹窗选择目标目录。 */
181
+ zoteroTargets(): Promise<ZoteroTargetsResult>;
182
+ /** 把选中论文同步到 Zotero(含本地 PDF 附件),并记录每篇的同步状态。 */
183
+ zoteroSync(id: string, paperIds?: readonly string[], workspace?: string, target?: {
184
+ id: string;
185
+ name?: string;
186
+ filesEditable?: boolean;
187
+ }): Promise<ZoteroSyncResult>;
188
+ /** 记录单篇文献的 Zotero 同步状态;与 PDF 状态一样写回任务并广播。 */
189
+ private setZoteroState;
162
190
  /** 下载目录可写性测试(设置页)。 */
163
191
  storageProbe(workspace?: string): Promise<StorageProbeResult>;
164
192
  private ensureLocalPdf;
@@ -167,6 +195,11 @@ export declare class TaskService {
167
195
  probe(source: SourceId): Promise<ProbeResult>;
168
196
  private tryGet;
169
197
  private baseInfo;
198
+ /**
199
+ * 供模型下载工具复用:`newOnly` 需要基准任务的论文集合才能与界面口径一致。
200
+ * 直接暴露给工具层,避免下载范围解析和 `/results` 出现两套过滤逻辑。
201
+ */
202
+ resolveDownloadFilter(id: string, filter: PaperFilter): Promise<PaperFilter>;
170
203
  /** `newOnly` 需要基准任务的论文集合;基准已删除时忽略该条件。 */
171
204
  private withBaseFilter;
172
205
  dispose(): void;