dsh-zotero-skill 0.3.0 → 0.5.1
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 +68 -7
- package/cordis.patch.yml +21 -21
- package/docs/reader-market-choices.md +34 -0
- package/docs/reading-acceptance.md +47 -0
- package/lib/index.js +165 -165
- package/lib/reader-runner.js +18 -0
- package/package.json +60 -57
- package/zotero/SKILL.md +214 -163
- package/zotero/bridge/bootstrap.js +46 -0
- package/zotero/bridge/manifest.json +13 -0
- package/zotero/references/Noto-OFL.txt +93 -0
- package/zotero/references/chinese-sources.md +48 -0
- package/zotero/references/focused-reading.md +81 -0
- package/zotero/references/reading-workflow.md +75 -0
- package/zotero/references/review-workflow.md +29 -0
- package/zotero/scripts/paper_reader/__init__.py +3 -0
- package/zotero/scripts/paper_reader/cli.py +225 -0
- package/zotero/scripts/paper_reader/discovery.py +59 -0
- package/zotero/scripts/paper_reader/evidence.py +201 -0
- package/zotero/scripts/paper_reader/jobs.py +524 -0
- package/zotero/scripts/paper_reader/matrix.py +93 -0
- package/zotero/scripts/paper_reader/native.py +136 -0
- package/zotero/scripts/paper_reader/pdf.py +340 -0
- package/zotero/scripts/paper_reader/quality.py +71 -0
- package/zotero/scripts/paper_reader/review.py +103 -0
- package/zotero/scripts/paper_reader/storage.py +113 -0
- package/zotero/scripts/reader-runner.mjs +268 -0
- package/zotero/scripts/zotero.py +911 -24
package/README.md
CHANGED
|
@@ -1,22 +1,81 @@
|
|
|
1
1
|
# Zotero Skill for DeepSeek Harness
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/dsh-zotero-skill)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-zotero-skill)
|
|
4
5
|

|
|
5
6
|

|
|
6
7
|

|
|
7
8
|
|
|
8
9
|
让 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 把文献工作串成一条流水线:**找文献 → 追前沿 → 筛核心 → 管理归集 → 批量阅读 → 分类整理 → 提炼脉络 → 发现空白**,再加上导出 BibTeX 与给 LaTeX / Markdown 草稿插入引用。
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
基础文献管理由 Python 标准库实现,**不需要 MCP 服务或 Zotero 云端 API Key**。Zotero 操作默认连接本机 `127.0.0.1:23119`;论文发现访问 OpenAlex。全文阅读工作流的本地 PDF/OCR 组件首次按需下载,后续复用;分析与翻译使用 DSH 当前模型配置。
|
|
12
|
+
|
|
13
|
+
## 0.5:全文阅读与可复用的综述证据
|
|
14
|
+
|
|
15
|
+
在 DSH 中说「精读 Zotero 当前选中的论文」或「比较这个分类中的论文」。插件先展示本次输出、快速/深入模式、讲解深度、PDF 版式、优先内容和执行方式。可保存常用设置和多个研究课题档案;通用插件不预设研究领域。
|
|
16
|
+
|
|
17
|
+
- **译文与精读**:同一份已接受结果用于 Markdown、Zotero 中文笔记、标注 PDF 和双语 PDF。完整翻译覆盖摘要、正文和实质性附录。精读协议要求说明章节逻辑、方法与公式、实验和消融、贡献、局限及实践步骤。
|
|
18
|
+
- **原文依据**:每条分析带块 ID、页码和可匹配的原文片段,分别标记原文报告、分析解释和待验证建议。校验定位不等于自动证明科学结论正确。
|
|
19
|
+
- **可选版式**:左右、上下、原文页后跟译页;简洁/详细高亮和中文批注。保留原始页面,较长译文增加续页;识别到的栅格图、表格和公式复制到译文页。复杂矢量图以保留的原文页为准。
|
|
20
|
+
- **可恢复与复用**:按源文件指纹缓存 PDF 解析;分批接受译文和分析,显示缺少的块与分析项。完整导出会检查覆盖与源文件变化;显式部分导出只生成带未完成标记的 Markdown。新版导出不覆盖手工修改的旧文件。
|
|
21
|
+
- **综述与推荐**:复用单篇证据进行方法、实验协议、结果、局限和实现条件的比较,列出已有材料不足,自动检索带推荐理由的补充候选。候选仅有题录/摘要时明确标明;研究空白保持为待验证假设。
|
|
22
|
+
- **省用量默认**:当前会话顺序处理;用户可选 DSH 内置、继承当前模型配置的受限并行。没有子任务能力时回到顺序处理,不配置额外模型 API。
|
|
23
|
+
|
|
24
|
+
### 首次准备与当前选中条目
|
|
25
|
+
|
|
26
|
+
Node 20+ 是 DSH 插件的运行前提。包内启动器准备私有 Python 3.12、PDF、OCR 和可嵌入的 Noto 中文字体,不改变系统 Python:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node lib/reader-runner.js reader-config
|
|
30
|
+
node lib/reader-runner.js bridge --out dsh-reader.xpi
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
「当前选中论文」需要将生成的配套扩展在 Zotero 的 **工具 → 插件 → 从文件安装**中安装一次,然后运行已有 `authorize` 流程。扩展要求 Zotero 10 的本地 API 鉴权能力,继承 Zotero 的 API key/server-id 检查,不新增服务或凭据。分类、标签、关键词与明确 item key 无需此扩展。
|
|
34
|
+
|
|
35
|
+
首次组件下载需要网络,输出进度,后续缓存复用。uv 启动器与字体锁定下载地址和 SHA256;Python 组件安装到私有环境。Noto 字体许可证见 `zotero/references/Noto-OFL.txt`;新模块选用非 AGPL 的 PDF 依赖。Windows 为主要使用环境,macOS/Linux 同样提供运行时映射,仍需各平台实际验证。
|
|
36
|
+
|
|
37
|
+
### 阅读命令
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# 实际使用由 DSH 根据用户选择运行,无需用户自己编写分析 JSON
|
|
41
|
+
node lib/reader-runner.js read prepare --selected --outputs notes translation annotated-pdf bilingual-pdf zotero-note --mode deep --layout side-by-side
|
|
42
|
+
node lib/reader-runner.js read prepare --collection COLLECTIONKEY --outputs notes --depth beginner
|
|
43
|
+
node lib/reader-runner.js read status read-JOBID
|
|
44
|
+
# 当前 DSH 模型读取任务、写出结构化结果后:
|
|
45
|
+
node lib/reader-runner.js read accept read-JOBID --input result.json
|
|
46
|
+
node lib/reader-runner.js read export read-JOBID --out reading
|
|
47
|
+
node lib/reader-runner.js read publish read-JOBID --yes
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
使用 `reader-config --input settings.json` 保存配置,`research-profile NAME --input profile.json` 保存课题档案,`read list` 列出可复用任务;`review prepare/render/search` 用于跨论文比较与自动补充检索。协议与 JSON 格式见 [reading-workflow.md](zotero/references/reading-workflow.md) 和 [review-workflow.md](zotero/references/review-workflow.md)。
|
|
51
|
+
|
|
52
|
+
**验证边界**:已提供离线 PDF、引用、覆盖、缓存、布局及接口契约测试;未在使用者的 Windows DSH/Zotero 实例完成端到端验证。10–15 页文字型论文首分钟有实质内容、3–5 分钟完成是待测性能目标,模型速度、所选输出、OCR 与首次下载会影响耗时。现有 `digest` 仍是元数据阅读包,不是本流程的模型精读结果。
|
|
11
53
|
|
|
12
54
|
## 特性
|
|
13
55
|
|
|
14
|
-
|
|
56
|
+
### 轻量阅读增强
|
|
57
|
+
|
|
58
|
+
参考 [11 个代表性文献工具的官方资料](docs/reader-market-choices.md),本轮加入以下按需能力,继续使用 DSH 对话与 Zotero 原界面:
|
|
59
|
+
|
|
60
|
+
- **聚焦问答**:在选定论文中本地检索少量片段,复用当前模型解释方法、公式或差异;答案带原文定位,证据不足与相互冲突明确区分。使用标准库 SQLite FTS5,无需嵌入模型或向量数据库。
|
|
61
|
+
- **原生批注**:把有原文依据的高亮和中文评论写入 Zotero annotation 条目,笔记可点击回到该高亮;已有评论有变化时保留并报告。也可读取用户现有批注作为后续阅读疑问。
|
|
62
|
+
- **证据对比表**:自定义提取列,每格区分原文报告、分析解释、未报告和不可比;来源必须属于该行论文。输出 Markdown/CSV 和证据 JSON,不新增表格服务。
|
|
63
|
+
- **术语与数字复核**:研究档案可保存术语表;检查译文数字、百分比、遗漏术语与疑似未翻译长段,支持可选严格接受。确定性复核不代替科学正确性判断。
|
|
64
|
+
- **逐段对照与导航**:新增 `bilingual-markdown`,按原文段落紧接中文译文,附原页链接;PDF 书签或明确标记的章节线索帮助定位,适合窄屏阅读。
|
|
65
|
+
- **纠错与复用**:带当前修订号和理由更正已有译文/解释,保留快照;旧问答、对比表或综述在资料修正后失效,避免继续复用过时结论。
|
|
66
|
+
- **引用追踪**:从选定论文出发获取有限的一跳参考/被引候选,显示关联来源、截断范围和 OpenAlex 撤稿标记;关系本身不代表支持或质量,也不自动导入为可信证据。
|
|
67
|
+
|
|
68
|
+
这些能力没有新增第三方依赖、独立前端或常驻服务。执行协议见 [focused-reading.md](zotero/references/focused-reading.md);普通使用者仍可用自然语言提出任务,不需要手写 JSON。
|
|
69
|
+
|
|
70
|
+
### 既有文献工作流
|
|
71
|
+
|
|
72
|
+
- **找文献**:从 OpenAlex 按主题检索,按期刊影响力、引用速度、相关度、新鲜度排序,并与本地库去重;`--source crossref` 可换用 Crossref 元数据。
|
|
73
|
+
- **中文文献**:知网没有公开检索 API,所以技能生成检索链接、解析你从知网导出的 RefWorks / EndNote 文件再入库,中文条目保留卷期页、关键词与 `language=zh`,BibTeX 键仍是纯 ASCII。
|
|
15
74
|
- **追前沿**:把常读方向存成 watch,每次只拉「上次运行以来」的新论文,不重复推送。
|
|
16
75
|
- **筛核心论文**:把候选或库内条目分成 核心 / 重要 / 参考 三档,输出可复制的短名单。
|
|
17
76
|
- **管理文献**:批量打标签、归集到分类、按 DOI / 标题查重并清理副本。
|
|
18
77
|
- **批量阅读**:一次为几十篇生成阅读包(元数据 + 摘要 + 章节大纲 + 笔记骨架),不必把原文全塞进上下文。
|
|
19
|
-
-
|
|
78
|
+
- **分类整理**:按你想要的维度(论文类型 / 方法 / 数据集 / 年份 / 期刊 / 主题 / 项目)在 Zotero 里直接建文件夹并归入条目,也可以只生成 `auto:method:*` / `auto:dataset:*` 标签计划,预览后写入。
|
|
20
79
|
- **提炼脉络**:统计年份、期刊、方法、数据集及共现,直接产出综述可用的表格。
|
|
21
80
|
- **发现空白**:按项目分组找「方法 × 数据集」从未交叉的组合、停更标签与标注缺失。
|
|
22
81
|
- **检索与引用**:按关键词搜索条目,返回题名、作者、年份、item key 与 BibTeX key;在草稿唯一标记处插入 `\cite{key}` 或 `[@key]` 并同步 `.bib`。
|
|
@@ -182,12 +241,14 @@ python zotero/scripts/zotero.py daily-schedule --remove
|
|
|
182
241
|
| --- | --- |
|
|
183
242
|
| `status` | 检查本地 API、Connector、版本 |
|
|
184
243
|
| `authorize` | 取得本地 API 写权限(Zotero 弹窗点「Always Allow」) |
|
|
185
|
-
| `find <query>` | OpenAlex 检索并按分数排序,`--oa-only` / `--json` / `--out` |
|
|
244
|
+
| `find <query>` | OpenAlex 检索并按分数排序,`--oa-only` / `--json` / `--out` / `--source crossref` |
|
|
245
|
+
| `cnki-search <query...>` | 生成知网检索链接(知网无公开 API,不抓取),`--field` / `--open` / `--json` |
|
|
246
|
+
| `cnki-import --file` | 解析知网导出的 RefWorks / EndNote 文件并入 Zotero,`--dry-run` / `--bib` / `--yes` |
|
|
186
247
|
| `frontier add\|run\|list\|remove` | 保存研究方向,只拉上次之后的新论文 |
|
|
187
248
|
| `screen` | 把候选或库内条目分成 核心 / 重要 / 参考 |
|
|
188
249
|
| `tag` | 批量加 / 删 / 替换标签(`--dry-run`) |
|
|
189
250
|
| `collect <name>` | 归集到分类,不存在则新建 |
|
|
190
|
-
| `classify` |
|
|
251
|
+
| `classify` | 建文件夹:`方向 / 维度 / 取值`,方向取 `project:` 标签并可 `--rename` 改名;也可生成标签计划;`--apply` 写入 |
|
|
191
252
|
| `dupes` | 按 DOI 或标题查重,`--trash` 清理副本 |
|
|
192
253
|
| `digest` | 批量生成阅读包(每篇一个 Markdown + index.md) |
|
|
193
254
|
| `landscape` | 年份 / 期刊 / 方法 / 数据集统计与共现 |
|
|
@@ -222,7 +283,7 @@ python zotero/scripts/zotero.py daily-schedule --remove
|
|
|
222
283
|
│ ├── references/
|
|
223
284
|
│ │ ├── deepseek-harness.md # 安装、连接排查、环境变量
|
|
224
285
|
│ │ └── local-api-routes.md # 本地 API 与 Connector 路由
|
|
225
|
-
│ └── scripts/zotero.py
|
|
286
|
+
│ └── scripts/ # zotero.py 基础 CLI;paper_reader/ 全文工作流
|
|
226
287
|
├── test/provider.test.mjs # provider 的 Node 测试
|
|
227
288
|
├── tests/test_zotero.py # 模拟 API 的 Python 单元测试
|
|
228
289
|
├── tests/test_literature.py # 术语抽取、分类计划、脉络、查重、空组合
|
|
@@ -276,6 +337,6 @@ node --test test/*.test.mjs # 插件 provider
|
|
|
276
337
|
|
|
277
338
|
## English
|
|
278
339
|
|
|
279
|
-
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) skill that runs the whole literature loop against your local Zotero library: find papers on OpenAlex, track research fronts with saved watches, screen candidates into core / important / reference tiers, manage items (batch tags, collections, DOI and title de-duplication), generate batch reading packs, classify items by method
|
|
340
|
+
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) skill that runs the whole literature loop against your local Zotero library: find papers on OpenAlex, track research fronts with saved watches, screen candidates into core / important / reference tiers, manage items (batch tags, collections, DOI and title de-duplication), generate batch reading packs, classify items into Zotero folders by type / method / dataset / year / venue / topic / project, chart the landscape of years / venues / methods / datasets, and surface candidate research gaps. It also exports BibTeX, inserts citations into LaTeX/Markdown drafts, and can run a ranked daily digest of new papers.
|
|
280
341
|
|
|
281
342
|
Install with `dsh plugin --profile <name> add dsh-zotero-skill` (or copy `zotero/` to `~/.dsh/skills/zotero`, or run `python install.py`), enable Zotero's local API, and run `zotero.py authorize` once if you want write operations. Adapted from OpenAI's Zotero plugin 0.1.2 under the MIT license.
|
package/cordis.patch.yml
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
# dsh-zotero-skill bundle patch
|
|
2
|
-
#
|
|
3
|
-
# This row mounts one host plugin (lib/index.js). On apply() it registers a
|
|
4
|
-
# skill provider on ctx.skills that serves zotero/SKILL.md from inside this
|
|
5
|
-
# package, so the skill appears in the agent catalog as soon as the plugin is
|
|
6
|
-
# installed — no copy into ~/.dsh/skills or ~/.agents/skills.
|
|
7
|
-
#
|
|
8
|
-
# Install:
|
|
9
|
-
# dsh plugin --profile <name> add dsh-zotero-skill
|
|
10
|
-
# dsh plugin --profile <name> add github:Eason-0712/dsh-zotero-skill
|
|
11
|
-
#
|
|
12
|
-
# Local development (link to a checkout):
|
|
13
|
-
# dsh plugin --profile <name> add "link:D:\code\dsh-zotero-skill"
|
|
14
|
-
#
|
|
15
|
-
# Remove:
|
|
16
|
-
# dsh plugin --profile <name> remove dsh-zotero-skill
|
|
17
|
-
#
|
|
18
|
-
# The inserted `name` must match the installed package name exactly.
|
|
19
|
-
- insert:
|
|
20
|
-
- id: dsh-zotero-skill
|
|
21
|
-
name: dsh-zotero-skill
|
|
1
|
+
# dsh-zotero-skill bundle patch
|
|
2
|
+
#
|
|
3
|
+
# This row mounts one host plugin (lib/index.js). On apply() it registers a
|
|
4
|
+
# skill provider on ctx.skills that serves zotero/SKILL.md from inside this
|
|
5
|
+
# package, so the skill appears in the agent catalog as soon as the plugin is
|
|
6
|
+
# installed — no copy into ~/.dsh/skills or ~/.agents/skills.
|
|
7
|
+
#
|
|
8
|
+
# Install:
|
|
9
|
+
# dsh plugin --profile <name> add dsh-zotero-skill
|
|
10
|
+
# dsh plugin --profile <name> add github:Eason-0712/dsh-zotero-skill
|
|
11
|
+
#
|
|
12
|
+
# Local development (link to a checkout):
|
|
13
|
+
# dsh plugin --profile <name> add "link:D:\code\dsh-zotero-skill"
|
|
14
|
+
#
|
|
15
|
+
# Remove:
|
|
16
|
+
# dsh plugin --profile <name> remove dsh-zotero-skill
|
|
17
|
+
#
|
|
18
|
+
# The inserted `name` must match the installed package name exactly.
|
|
19
|
+
- insert:
|
|
20
|
+
- id: dsh-zotero-skill
|
|
21
|
+
name: dsh-zotero-skill
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# 文献阅读工具调研与轻量化取舍
|
|
2
|
+
|
|
3
|
+
核对日期:2026-11-01。范围为 11 个代表性产品/平台的公开官方功能说明,覆盖翻译阅读、文献管理、证据提取和关联发现;不是对全部在售产品的穷尽调查,也不是付费版本的速度或质量实测。只吸收交互与工作流思路,没有复制其代码、界面或专有算法。
|
|
4
|
+
|
|
5
|
+
| 产品与官方资料 | 值得吸收的思路 | 在本插件的处理 |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| [Scholaread](https://www.scholaread.com/help/guides/getting-started/scholaread-integrated-pdf-reading-academic-translation-literature-management-and-note-taking) | 逐段对照、目录导航、选段理解和学术术语 | 逐段双语 Markdown、可回跳目录、聚焦问答和课题术语表;PDF 仍由现有可选导出流程负责。没有重造其移动客户端或专有重排算法。 |
|
|
8
|
+
| [SciSpace](https://scispace.com/chat-pdf) | 从具体段落出发解释和追问,答案回到文中 | 针对显式选定的阅读任务检索有限原文,由当前 DSH 模型回答,再检查引文和上下文版本。 |
|
|
9
|
+
| [ReadCube](https://about.readcube.com/help-center/readcube-ai-chat-with-pdf/) | 围绕论文上下文问答,并提示核对具体段落 | 复用同一套来源定位、问答和“不足以回答”状态,避免每个问题重读全部 PDF。 |
|
|
10
|
+
| [Scholarcy](https://www.scholarcy.com/) | 一致的结构化阅读卡、可调深度与可导出成果 | 第一版已有结构化精读分类与深度选项;本轮加强局部复用、纠错历史和对比表,保持 Markdown 可编辑。 |
|
|
11
|
+
| [Elicit](https://support.elicit.com/en/articles/14759154-systematic-reviews-in-elicit) | 研究问题驱动的自定义提取列,单元格有依据,缺失信息可检查 | 自定义论文证据矩阵;单元格必须区分原文、解释、缺失和不可比,并引用该论文已接受的证据。输出 Markdown/CSV 与证据 JSON。 |
|
|
12
|
+
| [scite](https://scite.ai/features) | 引用方式和证据质量需要区分,撤稿信息应显著呈现 | 参考/被引关系明确标注;OpenAlex 返回的撤稿标记会提示。没有接入 scite 数据,也不把普通引用自动判断为支持/反驳。 |
|
|
13
|
+
| [ResearchRabbit](https://www.researchrabbit.ai/features) | 从已知论文出发探索相关研究和线索 | 有限的一跳参考/被引候选,解释来自哪篇种子论文;保持列表输出,不建立新的图谱应用。 |
|
|
14
|
+
| [Litmaps](https://www.litmaps.com/features) | 通过参考和引用联系发现遗漏,持续关注新工作 | 新增有请求上限的一跳检索,与既有 frontier/daily 流程衔接;不新增账户、邮件后台或另一套订阅系统。 |
|
|
15
|
+
| [Connected Papers](https://www.connectedpapers.com/about) | 共被引/共同参考所体现的相关性,与直接引用有所区别 | 仅借鉴种子扩展思路。本插件提供的是直接引用关系,不宣称实现其相似度图或大规模图算法。 |
|
|
16
|
+
| [Paperpile](https://paperpile.com/features/pdf-annotator/) | 原文批注、颜色区分和开放的笔记/PDF 导出 | 写回 Zotero 原生高亮与中文评论,保留用户修改;Markdown 和标准 PDF 继续使用通用格式。 |
|
|
17
|
+
| [Zotero](https://www.zotero.org/support/pdf_reader) | 原生批注、笔记、回到原文和现成引用工作流 | 直接利用本地 API 和内置阅读器;笔记链接可精确指向已生成的原生批注。文献库、阅读器界面、引用管理与同步继续交给 Zotero。 |
|
|
18
|
+
|
|
19
|
+
## 这轮实施
|
|
20
|
+
|
|
21
|
+
1. 小范围证据问答:标准库 SQLite FTS5 检索;不可用时采用局部词项检索,不下载嵌入模型、不接入向量服务。中文问题可由宿主提供英文检索词,或复用已有中文分析定位原文。
|
|
22
|
+
2. 原生批注:默认预览,需要明确的写入选项才调用 Zotero 的既有鉴权 API;稳定标识避免重复创建,原有评论有变化时保留并报告差异。
|
|
23
|
+
3. 文献矩阵:用户自定义列,所有论文都须出现;缺失/不可比显式保留,不用空白掩盖筛选。单元格引用本行论文证据,CSV 对公式前缀作转义。
|
|
24
|
+
4. 学术翻译检查:课题术语表、数字/百分比等值正规化检查、未翻译长段检测。它们是确定性的复核线索,不是自动证明翻译正确;可显式使用严格接受模式。
|
|
25
|
+
5. 可修订复用:以当前修订号为前提进行纠错,保留不可变快照和理由;内容修正后,旧的问答、矩阵或综述上下文会失效。
|
|
26
|
+
6. 有界发现:每次至多 5 篇种子、每种子至多 30 条参考;标注截断、歧义、索引不足与撤稿状态。候选仍需全文判断,不自动纳入为可信证据。
|
|
27
|
+
|
|
28
|
+
## 体积与交互约束
|
|
29
|
+
|
|
30
|
+
新增部分只用 Python 标准库和上一版已提供的 PDF 依赖,没有新增第三方推理、向量、Web 或前端依赖。所有功能按请求运行,不启用后台守护进程。日常使用继续通过 DSH 对话与 Zotero 原界面,只有当前任务需要的选项才展示;CLI 和 JSON 协议是宿主执行接口,不要求普通使用者自己维护。
|
|
31
|
+
|
|
32
|
+
本轮 npm 打包预览:压缩包从约 83 KB 增至约 100 KB,增加约 17 KB;包内不包含论文、缓存、数据库或模型。首次按需下载的 Python/PDF/OCR/字体组件另算,这轮没有增加它们的第三方依赖清单。
|
|
33
|
+
|
|
34
|
+
大规模图谱可视化、云同步、多人账户、独立文献库、邮件服务和完整写作排版均不在本轮实现范围内。验证边界、Windows 联调与模型耗时目标继续见 [reading-acceptance.md](reading-acceptance.md)。
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Reading workflow: scope and acceptance
|
|
2
|
+
|
|
3
|
+
## Agreed product behavior
|
|
4
|
+
|
|
5
|
+
This is a general-purpose open-source research tool. Research profiles belong to the user; the motor-imagery EEG example is a validation scenario, not a default taxonomy or scientific assumption.
|
|
6
|
+
|
|
7
|
+
| Requirement | Implementation / acceptance |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Zotero entry points | Exact current selection via a separately installed, bundled Zotero companion; existing personal-library collection/tag/query/key selectors remain available. Multiple PDFs require an explicit attachment choice. |
|
|
10
|
+
| Per-task choices | The skill asks for outputs, quick/deep mode, explanation depth, layout, annotation detail, priority and execution mode; validated preferences can be saved. |
|
|
11
|
+
| Current DSH model | Python prepares and validates data. The host model produces translation and analysis. No new model endpoint or credential is configured. Parallel work is an opt-in instruction to use available DSH internal children inheriting current configuration; economy is default. |
|
|
12
|
+
| Local setup | A pinned, checksum-verified uv archive prepares a private Python environment; PDF, OCR and embedded Noto font components are fetched on demand with progress. No install-time script runs on npm installation. |
|
|
13
|
+
| Full translation | Translation coverage is checked for text blocks in abstract, main text and substantive appendices. References, captions, tables and detected equations retain their source representation unless separately translated. Unreadable pages block complete export. |
|
|
14
|
+
| Reading quality | Claims must identify their category and whether they are reported, interpreted or proposed; exact substantial quotes must resolve to source blocks and geometry. Scientific correctness still requires model and human review. |
|
|
15
|
+
| PDF output | Annotated copy with Chinese comments; side-by-side, stacked or alternating bilingual editions. Original pages remain intact; text overflow continues on additional pages. Raster figures, detected tables and equations are copied to translated pages. Original pages are authoritative for complex vector diagrams. |
|
|
16
|
+
| Editable documents | Markdown notes, translations and reviews. Zotero note publication is explicit and idempotent for an unchanged accepted analysis. Outputs get new revision directories and do not overwrite edited files. |
|
|
17
|
+
| Multi-paper reuse | Synthesis consumes accepted single-paper claims and source links. Missing evidence produces targeted queries and automatically retrieved, reasoned candidates with metadata/abstract evidence labels. Candidates are not silently imported or treated as full-text findings. |
|
|
18
|
+
| Recovery | PDF extraction is content-addressed. Accepted chunks are immutable and serialized by a job lock; status identifies untranslated blocks, unexamined categories and unreadable pages. |
|
|
19
|
+
|
|
20
|
+
## Verified in the development container
|
|
21
|
+
|
|
22
|
+
- 90 Python tests: 44 original, 17 initial workflow tests, and 29 lightweight enhancement tests. They exercise evidence rejection, coverage, parse/model-result reuse, correction history and stale-context rejection, scoped retrieval and FTS fallback, matrix evidence/CSV handling, native annotation geometry/idempotence/user edits, terminology/numeric review, HTTP header casing, plus the original PDF/cache/selection contracts.
|
|
23
|
+
- 7 Node tests: existing skill provider plus component allowlisting, pinned runtime selection, checksum failure cleanup and companion endpoint lifecycle/authentication inheritance.
|
|
24
|
+
- A new private runtime downloaded and prepared PDF packages, an embedded regular-weight Noto font, and OCR packages. A synthetic raster PDF was actually OCRed and its expected text checked. Unit tests remain offline; OCR is mocked there.
|
|
25
|
+
- Three bilingual layouts and a highlighted PDF were generated; the side-by-side edition and highlight position were rendered and visually inspected. Chinese text, original figures and a table were readable. Fixtures are synthetic, not demonstrations of model translation quality.
|
|
26
|
+
- npm package preview contains the runner, reader modules, companion sources, skill references and font license, without caches, PDFs, model files, virtual environments or test artifacts.
|
|
27
|
+
- Bounded citation-discovery request shapes were checked against actual OpenAlex DOI resolution, batched references and citing-work queries. This is an API smoke test, not evidence of complete literature coverage.
|
|
28
|
+
|
|
29
|
+
## Lightweight enhancement boundary
|
|
30
|
+
|
|
31
|
+
The market comparison and inclusion decisions are recorded in [reader-market-choices.md](reader-market-choices.md). The second round adds no third-party dependencies or always-on processes. SQLite retrieval is lexical; absent FTS5 it falls back to a scoped standard-library scan. Source quotes, result revision and scope are validated before rendering a focused answer, matrix or synthesis. These checks do not prove scientific correctness.
|
|
32
|
+
|
|
33
|
+
Zotero-native annotations use the documented item JSON fields and existing local-API authorization. Offline tests cover rotated-page coordinates, collisions, object-level write failures and preservation of edited comments. Actual Zotero creation and reader navigation still need the on-device integration checks below. Unusual cropped/nonzero-origin PDF geometry is explicitly rejected for native writes; annotated-copy export remains available.
|
|
34
|
+
|
|
35
|
+
## Required before marking the PR production-ready
|
|
36
|
+
|
|
37
|
+
1. On a clean Windows machine, install the branch into DSH and test first-use downloads under ordinary user permissions, including paths containing Chinese characters and spaces.
|
|
38
|
+
2. Install the generated XPI in the actual supported Zotero version. Verify selection, parent attachment resolution, rejection without authorization, permission renewal, and removal on extension disable. The test double proves inheritance and lifecycle, not compatibility with every Zotero build.
|
|
39
|
+
3. Run real DSH model tasks in economy and supported internal-parallel modes. Verify the configured model is inherited, concurrency stays bounded, task choices work in the actual client, and a cancelled/interrupted task can resume.
|
|
40
|
+
4. Use at least one 10-15 page selectable-text paper, one 20-50 page review and one complex/scanned PDF. Check reading order, equations, diagram interpretation, quote locations and all requested output formats. Add representative failure cases to the offline test suite.
|
|
41
|
+
5. Evaluate translation completeness, terminology, formula explanations, experimental protocols and implementation guidance against the source. Quote matching only verifies anchoring, not reasoning validity. Verify reviews distinguish corpus limitations and hypotheses from demonstrated findings.
|
|
42
|
+
|
|
43
|
+
## Timing protocol
|
|
44
|
+
|
|
45
|
+
Record first-use component setup separately from warm-run work. Start timing after the user has selected outputs and the host begins processing. `T_first` ends at the first substantive, source-grounded card, note section or translation; a progress message does not count. `T_full` ends only when every chosen output passes coverage and export checks.
|
|
46
|
+
|
|
47
|
+
The 10-15 page target is `T_first <= 60s` and `T_full` around 180-300s. This has not been established end to end. Log model/version, mode, explanation depth, selected outputs, page/character counts, OCR pages, cache hits and actual elapsed time. Long/scanned papers require a separately measured estimate; never silently omit content to meet a deadline.
|
package/lib/index.js
CHANGED
|
@@ -1,165 +1,165 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-zotero-skill — mount the bundled Zotero skill into the agent catalog.
|
|
3
|
-
*
|
|
4
|
-
* Mounting this plugin registers a skill provider on `ctx.skills` that serves
|
|
5
|
-
* the `zotero/` directory shipped inside the package. Installing the plugin is
|
|
6
|
-
* therefore enough: the skill appears in the catalog with no copy step into
|
|
7
|
-
* `~/.dsh/skills`. The Python scripts stay next to `SKILL.md` and keep working
|
|
8
|
-
* through the host's command tool.
|
|
9
|
-
*
|
|
10
|
-
* The provider interface is a plain object, so this package declares no runtime
|
|
11
|
-
* dependencies and cannot fail on a peer range at install time.
|
|
12
|
-
*
|
|
13
|
-
* @module dsh-zotero-skill
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { readFileSync } from 'node:fs';
|
|
17
|
-
import { dirname, join } from 'node:path';
|
|
18
|
-
import { fileURLToPath } from 'node:url';
|
|
19
|
-
|
|
20
|
-
/** Precedence rank for packaged skill providers and bundled roots. */
|
|
21
|
-
const BUNDLED_SKILL_RANK = 600;
|
|
22
|
-
|
|
23
|
-
/** Provider name registered on `ctx.skills`. */
|
|
24
|
-
export const PROVIDER_NAME = 'dsh-zotero-skill';
|
|
25
|
-
|
|
26
|
-
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
-
const SKILL_DIR = join(PACKAGE_ROOT, 'zotero');
|
|
28
|
-
const SKILL_FILE = join(SKILL_DIR, 'SKILL.md');
|
|
29
|
-
|
|
30
|
-
/** Public skill-name grammar: lowercase kebab-case. */
|
|
31
|
-
const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Parse the minimal YAML frontmatter used by `SKILL.md` (scalar `key: value`).
|
|
35
|
-
* @param {string} text - raw file contents.
|
|
36
|
-
* @returns {{ attrs: Record<string, string>, body: string }} parsed parts.
|
|
37
|
-
*/
|
|
38
|
-
export function parseFrontmatter(text) {
|
|
39
|
-
const clean = String(text).replace(/^\uFEFF/, '');
|
|
40
|
-
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(clean);
|
|
41
|
-
if (!match) return { attrs: {}, body: clean };
|
|
42
|
-
|
|
43
|
-
/** @type {Record<string, string>} */
|
|
44
|
-
const attrs = {};
|
|
45
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
46
|
-
const pair = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line);
|
|
47
|
-
if (!pair) continue;
|
|
48
|
-
let value = pair[2].trim();
|
|
49
|
-
const quoted =
|
|
50
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
51
|
-
(value.startsWith("'") && value.endsWith("'"));
|
|
52
|
-
if (quoted && value.length >= 2) value = value.slice(1, -1);
|
|
53
|
-
attrs[pair[1]] = value;
|
|
54
|
-
}
|
|
55
|
-
return { attrs, body: clean.slice(match[0].length) };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Read the bundled skill into a provider candidate.
|
|
60
|
-
* @param {(message: string) => void} warn - warning sink.
|
|
61
|
-
* @returns {object | undefined} candidate, or `undefined` when unusable.
|
|
62
|
-
*/
|
|
63
|
-
function readCandidate(warn) {
|
|
64
|
-
let parsed;
|
|
65
|
-
try {
|
|
66
|
-
parsed = parseFrontmatter(readFileSync(SKILL_FILE, 'utf8'));
|
|
67
|
-
} catch (error) {
|
|
68
|
-
warn(`cannot read ${SKILL_FILE}: ${error.message}`);
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const name = String(parsed.attrs.name || '').trim();
|
|
73
|
-
if (!SKILL_NAME_RE.test(name)) {
|
|
74
|
-
warn(`skipping bundled skill: name must be kebab-case, got ${JSON.stringify(parsed.attrs.name)}`);
|
|
75
|
-
return undefined;
|
|
76
|
-
}
|
|
77
|
-
const description = String(parsed.attrs.description || '').trim();
|
|
78
|
-
if (!description) {
|
|
79
|
-
warn(`skipping ${name}: description is required`);
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
name,
|
|
85
|
-
description,
|
|
86
|
-
invocation: { modelInvocable: true, userInvocable: true },
|
|
87
|
-
source: 'bundled',
|
|
88
|
-
provider: PROVIDER_NAME,
|
|
89
|
-
path: SKILL_FILE,
|
|
90
|
-
resourceBase: { kind: 'directory', path: SKILL_DIR },
|
|
91
|
-
rank: BUNDLED_SKILL_RANK,
|
|
92
|
-
locator: SKILL_FILE,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Create the bundled skill provider registered on `ctx.skills`.
|
|
98
|
-
* The file is re-read on every lookup so edits show up without a restart.
|
|
99
|
-
* @param {(message: string) => void} [warn] - warning sink.
|
|
100
|
-
* @returns {{ name: string, list: Function, get: Function }} the provider.
|
|
101
|
-
*/
|
|
102
|
-
export function createProvider(warn = () => {}) {
|
|
103
|
-
return {
|
|
104
|
-
name: PROVIDER_NAME,
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* List the bundled skill.
|
|
108
|
-
* @param {{ signal?: AbortSignal }} [lookup] - lookup options.
|
|
109
|
-
* @returns {Promise<object[]>} candidates.
|
|
110
|
-
*/
|
|
111
|
-
async list(lookup) {
|
|
112
|
-
if (lookup?.signal?.aborted) return [];
|
|
113
|
-
const candidate = readCandidate(warn);
|
|
114
|
-
return candidate ? [candidate] : [];
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Load the bundled skill body.
|
|
119
|
-
* @param {object} candidate - candidate from `list`.
|
|
120
|
-
* @param {{ signal?: AbortSignal }} [lookup] - lookup options.
|
|
121
|
-
* @returns {Promise<object | undefined>} full definition, or `undefined`.
|
|
122
|
-
*/
|
|
123
|
-
async get(candidate, lookup) {
|
|
124
|
-
if (lookup?.signal?.aborted) return undefined;
|
|
125
|
-
let parsed;
|
|
126
|
-
try {
|
|
127
|
-
parsed = parseFrontmatter(readFileSync(candidate.locator, 'utf8'));
|
|
128
|
-
} catch (error) {
|
|
129
|
-
warn(`cannot load ${candidate.name}: ${error.message}`);
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
return {
|
|
133
|
-
...candidate,
|
|
134
|
-
content: parsed.body.replace(/^\s*\n/, ''),
|
|
135
|
-
};
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Register the bundled skill provider.
|
|
142
|
-
* @param {object} ctx - Cordis context.
|
|
143
|
-
*/
|
|
144
|
-
export function apply(ctx) {
|
|
145
|
-
const warn = (message) => {
|
|
146
|
-
try {
|
|
147
|
-
const logger = typeof ctx?.logger === 'function' ? ctx.logger(PROVIDER_NAME) : ctx?.logger;
|
|
148
|
-
if (logger && typeof logger.warn === 'function') {
|
|
149
|
-
logger.warn(message);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
} catch {
|
|
153
|
-
/* fall through to the console */
|
|
154
|
-
}
|
|
155
|
-
try {
|
|
156
|
-
console.warn(`[${PROVIDER_NAME}] ${message}`);
|
|
157
|
-
} catch {
|
|
158
|
-
/* nothing left to report to */
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
ctx.inject(['skills'], (skillCtx) => {
|
|
163
|
-
skillCtx.skills.registerProvider(() => createProvider(warn));
|
|
164
|
-
});
|
|
165
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-zotero-skill — mount the bundled Zotero skill into the agent catalog.
|
|
3
|
+
*
|
|
4
|
+
* Mounting this plugin registers a skill provider on `ctx.skills` that serves
|
|
5
|
+
* the `zotero/` directory shipped inside the package. Installing the plugin is
|
|
6
|
+
* therefore enough: the skill appears in the catalog with no copy step into
|
|
7
|
+
* `~/.dsh/skills`. The Python scripts stay next to `SKILL.md` and keep working
|
|
8
|
+
* through the host's command tool.
|
|
9
|
+
*
|
|
10
|
+
* The provider interface is a plain object, so this package declares no runtime
|
|
11
|
+
* dependencies and cannot fail on a peer range at install time.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-zotero-skill
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
/** Precedence rank for packaged skill providers and bundled roots. */
|
|
21
|
+
const BUNDLED_SKILL_RANK = 600;
|
|
22
|
+
|
|
23
|
+
/** Provider name registered on `ctx.skills`. */
|
|
24
|
+
export const PROVIDER_NAME = 'dsh-zotero-skill';
|
|
25
|
+
|
|
26
|
+
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
+
const SKILL_DIR = join(PACKAGE_ROOT, 'zotero');
|
|
28
|
+
const SKILL_FILE = join(SKILL_DIR, 'SKILL.md');
|
|
29
|
+
|
|
30
|
+
/** Public skill-name grammar: lowercase kebab-case. */
|
|
31
|
+
const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse the minimal YAML frontmatter used by `SKILL.md` (scalar `key: value`).
|
|
35
|
+
* @param {string} text - raw file contents.
|
|
36
|
+
* @returns {{ attrs: Record<string, string>, body: string }} parsed parts.
|
|
37
|
+
*/
|
|
38
|
+
export function parseFrontmatter(text) {
|
|
39
|
+
const clean = String(text).replace(/^\uFEFF/, '');
|
|
40
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(clean);
|
|
41
|
+
if (!match) return { attrs: {}, body: clean };
|
|
42
|
+
|
|
43
|
+
/** @type {Record<string, string>} */
|
|
44
|
+
const attrs = {};
|
|
45
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
46
|
+
const pair = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line);
|
|
47
|
+
if (!pair) continue;
|
|
48
|
+
let value = pair[2].trim();
|
|
49
|
+
const quoted =
|
|
50
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
51
|
+
(value.startsWith("'") && value.endsWith("'"));
|
|
52
|
+
if (quoted && value.length >= 2) value = value.slice(1, -1);
|
|
53
|
+
attrs[pair[1]] = value;
|
|
54
|
+
}
|
|
55
|
+
return { attrs, body: clean.slice(match[0].length) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Read the bundled skill into a provider candidate.
|
|
60
|
+
* @param {(message: string) => void} warn - warning sink.
|
|
61
|
+
* @returns {object | undefined} candidate, or `undefined` when unusable.
|
|
62
|
+
*/
|
|
63
|
+
function readCandidate(warn) {
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = parseFrontmatter(readFileSync(SKILL_FILE, 'utf8'));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
warn(`cannot read ${SKILL_FILE}: ${error.message}`);
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const name = String(parsed.attrs.name || '').trim();
|
|
73
|
+
if (!SKILL_NAME_RE.test(name)) {
|
|
74
|
+
warn(`skipping bundled skill: name must be kebab-case, got ${JSON.stringify(parsed.attrs.name)}`);
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const description = String(parsed.attrs.description || '').trim();
|
|
78
|
+
if (!description) {
|
|
79
|
+
warn(`skipping ${name}: description is required`);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
name,
|
|
85
|
+
description,
|
|
86
|
+
invocation: { modelInvocable: true, userInvocable: true },
|
|
87
|
+
source: 'bundled',
|
|
88
|
+
provider: PROVIDER_NAME,
|
|
89
|
+
path: SKILL_FILE,
|
|
90
|
+
resourceBase: { kind: 'directory', path: SKILL_DIR },
|
|
91
|
+
rank: BUNDLED_SKILL_RANK,
|
|
92
|
+
locator: SKILL_FILE,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create the bundled skill provider registered on `ctx.skills`.
|
|
98
|
+
* The file is re-read on every lookup so edits show up without a restart.
|
|
99
|
+
* @param {(message: string) => void} [warn] - warning sink.
|
|
100
|
+
* @returns {{ name: string, list: Function, get: Function }} the provider.
|
|
101
|
+
*/
|
|
102
|
+
export function createProvider(warn = () => {}) {
|
|
103
|
+
return {
|
|
104
|
+
name: PROVIDER_NAME,
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* List the bundled skill.
|
|
108
|
+
* @param {{ signal?: AbortSignal }} [lookup] - lookup options.
|
|
109
|
+
* @returns {Promise<object[]>} candidates.
|
|
110
|
+
*/
|
|
111
|
+
async list(lookup) {
|
|
112
|
+
if (lookup?.signal?.aborted) return [];
|
|
113
|
+
const candidate = readCandidate(warn);
|
|
114
|
+
return candidate ? [candidate] : [];
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Load the bundled skill body.
|
|
119
|
+
* @param {object} candidate - candidate from `list`.
|
|
120
|
+
* @param {{ signal?: AbortSignal }} [lookup] - lookup options.
|
|
121
|
+
* @returns {Promise<object | undefined>} full definition, or `undefined`.
|
|
122
|
+
*/
|
|
123
|
+
async get(candidate, lookup) {
|
|
124
|
+
if (lookup?.signal?.aborted) return undefined;
|
|
125
|
+
let parsed;
|
|
126
|
+
try {
|
|
127
|
+
parsed = parseFrontmatter(readFileSync(candidate.locator, 'utf8'));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
warn(`cannot load ${candidate.name}: ${error.message}`);
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
...candidate,
|
|
134
|
+
content: parsed.body.replace(/^\s*\n/, ''),
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Register the bundled skill provider.
|
|
142
|
+
* @param {object} ctx - Cordis context.
|
|
143
|
+
*/
|
|
144
|
+
export function apply(ctx) {
|
|
145
|
+
const warn = (message) => {
|
|
146
|
+
try {
|
|
147
|
+
const logger = typeof ctx?.logger === 'function' ? ctx.logger(PROVIDER_NAME) : ctx?.logger;
|
|
148
|
+
if (logger && typeof logger.warn === 'function') {
|
|
149
|
+
logger.warn(message);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
/* fall through to the console */
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
console.warn(`[${PROVIDER_NAME}] ${message}`);
|
|
157
|
+
} catch {
|
|
158
|
+
/* nothing left to report to */
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
ctx.inject(['skills'], (skillCtx) => {
|
|
163
|
+
skillCtx.skills.registerProvider(() => createProvider(warn));
|
|
164
|
+
});
|
|
165
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Package entry point; the implementation also ships inside standalone skills. */
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { main } from '../zotero/scripts/reader-runner.mjs';
|
|
6
|
+
export { componentPlan, uvAsset, downloadVerified, pipIndexes, pythonCandidates, findSystemPython, main } from '../zotero/scripts/reader-runner.mjs';
|
|
7
|
+
|
|
8
|
+
function sameFile(left, right) {
|
|
9
|
+
// Linked installs resolve to a different real path than argv[1].
|
|
10
|
+
try { return realpathSync(resolve(left)) === realpathSync(resolve(right)); } catch { return false; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (process.argv[1] && sameFile(process.argv[1], fileURLToPath(import.meta.url))) {
|
|
14
|
+
main().then(code => { process.exitCode = code; }).catch(error => {
|
|
15
|
+
console.error(`Reader setup: ${error.message}`);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
});
|
|
18
|
+
}
|