harveyz-skill 0.27.0 → 0.28.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 (82) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +4 -2
  3. package/skills/coding/explain-pm/SKILL.md +39 -13
  4. package/skills/mint/publish-skill/SKILL.md +36 -15
  5. package/skills/research/clip-url/SKILL.md +13 -17
  6. package/skills/research/clip-url/scripts/__pycache__/article_meta.cpython-314.pyc +0 -0
  7. package/skills/research/clip-url/scripts/__pycache__/browser_fetch_mcp_locate.cpython-314.pyc +0 -0
  8. package/skills/research/clip-url/scripts/__pycache__/dedup_check.cpython-314.pyc +0 -0
  9. package/skills/research/clip-url/scripts/__pycache__/mcp_debug_client.cpython-314.pyc +0 -0
  10. package/skills/research/clip-url/scripts/__pycache__/mcp_fetch_client.cpython-314.pyc +0 -0
  11. package/skills/research/clip-url/scripts/__pycache__/vault_config.cpython-314.pyc +0 -0
  12. package/skills/research/clip-url/scripts/__pycache__/write_meta_and_separate.cpython-314.pyc +0 -0
  13. package/skills/research/clip-url/tests/__pycache__/conftest.cpython-314-pytest-9.1.1.pyc +0 -0
  14. package/skills/research/clip-url/tests/__pycache__/test_article_meta.cpython-314-pytest-9.1.1.pyc +0 -0
  15. package/skills/research/clip-url/tests/__pycache__/test_browser_fetch_mcp_locate.cpython-314-pytest-9.1.1.pyc +0 -0
  16. package/skills/research/clip-url/tests/__pycache__/test_chrome_profile_config.cpython-314-pytest-9.1.1.pyc +0 -0
  17. package/skills/research/clip-url/tests/__pycache__/test_dedup_check.cpython-314-pytest-9.1.1.pyc +0 -0
  18. package/skills/research/clip-url/tests/__pycache__/test_detect_xcom_chrome_profile.cpython-314-pytest-9.1.1.pyc +0 -0
  19. package/skills/research/clip-url/tests/__pycache__/test_mcp_debug_client.cpython-314-pytest-9.1.1.pyc +0 -0
  20. package/skills/research/clip-url/tests/__pycache__/test_mcp_fetch_client.cpython-314-pytest-9.1.1.pyc +0 -0
  21. package/skills/research/clip-url/tests/__pycache__/test_vault_config.cpython-314-pytest-9.1.1.pyc +0 -0
  22. package/skills/research/clip-url/tests/__pycache__/test_write_meta_and_separate.cpython-314-pytest-9.1.1.pyc +0 -0
  23. package/skills/research/extract-url/references/__pycache__/article_utils.cpython-314.pyc +0 -0
  24. package/skills/research/extract-url/scripts/__pycache__/config.cpython-314.pyc +0 -0
  25. package/skills/research/extract-url/scripts/__pycache__/migrate_to_folder_structure.cpython-314.pyc +0 -0
  26. package/skills/research/fetch-paper/SKILL.md +239 -0
  27. package/skills/research/learn-video/SKILL.md +58 -22
  28. package/skills/research/sync-xtimeline/SKILL.md +62 -0
  29. package/skills/research/sync-xtimeline/scripts/__pycache__/archive_tweets.cpython-314.pyc +0 -0
  30. package/skills/research/sync-xtimeline/scripts/__pycache__/browser_fetch_mcp_locate.cpython-314.pyc +0 -0
  31. package/skills/research/sync-xtimeline/scripts/__pycache__/fetch_new_tweets.cpython-314.pyc +0 -0
  32. package/skills/research/sync-xtimeline/scripts/__pycache__/mcp_timeline_client.cpython-314.pyc +0 -0
  33. package/skills/research/sync-xtimeline/scripts/__pycache__/render_digest.cpython-314.pyc +0 -0
  34. package/skills/research/sync-xtimeline/scripts/__pycache__/render_view.cpython-314.pyc +0 -0
  35. package/skills/research/sync-xtimeline/scripts/__pycache__/watchlist.cpython-314.pyc +0 -0
  36. package/skills/research/sync-xtimeline/scripts/archive_tweets.py +45 -0
  37. package/skills/research/sync-xtimeline/scripts/browser_fetch_mcp_locate.py +48 -0
  38. package/skills/research/sync-xtimeline/scripts/fetch_new_tweets.py +76 -0
  39. package/skills/research/sync-xtimeline/scripts/mcp_timeline_client.py +49 -0
  40. package/skills/research/sync-xtimeline/scripts/render_digest.py +106 -0
  41. package/skills/research/sync-xtimeline/scripts/render_view.py +269 -0
  42. package/skills/research/sync-xtimeline/scripts/watchlist.py +118 -0
  43. package/skills/research/sync-xtimeline/tests/__pycache__/conftest.cpython-314-pytest-9.1.1.pyc +0 -0
  44. package/skills/research/sync-xtimeline/tests/__pycache__/test_archive_tweets.cpython-314-pytest-9.1.1.pyc +0 -0
  45. package/skills/research/sync-xtimeline/tests/__pycache__/test_browser_fetch_mcp_locate.cpython-314-pytest-9.1.1.pyc +0 -0
  46. package/skills/research/sync-xtimeline/tests/__pycache__/test_fetch_new_tweets.cpython-314-pytest-9.1.1.pyc +0 -0
  47. package/skills/research/sync-xtimeline/tests/__pycache__/test_mcp_timeline_client.cpython-314-pytest-9.1.1.pyc +0 -0
  48. package/skills/research/sync-xtimeline/tests/__pycache__/test_render_digest.cpython-314-pytest-9.1.1.pyc +0 -0
  49. package/skills/research/sync-xtimeline/tests/__pycache__/test_render_view.cpython-314-pytest-9.1.1.pyc +0 -0
  50. package/skills/research/sync-xtimeline/tests/__pycache__/test_watchlist.cpython-314-pytest-9.1.1.pyc +0 -0
  51. package/skills/research/sync-xtimeline/tests/conftest.py +16 -0
  52. package/skills/research/sync-xtimeline/tests/test_archive_tweets.py +75 -0
  53. package/skills/research/sync-xtimeline/tests/test_browser_fetch_mcp_locate.py +12 -0
  54. package/skills/research/sync-xtimeline/tests/test_fetch_new_tweets.py +96 -0
  55. package/skills/research/sync-xtimeline/tests/test_mcp_timeline_client.py +30 -0
  56. package/skills/research/sync-xtimeline/tests/test_render_digest.py +222 -0
  57. package/skills/research/sync-xtimeline/tests/test_render_view.py +101 -0
  58. package/skills/research/sync-xtimeline/tests/test_watchlist.py +110 -0
  59. package/skills-index.json +21 -7
  60. package/tools/browser-fetch-mcp/browser_fetch_mcp/__pycache__/__init__.cpython-314.pyc +0 -0
  61. package/tools/browser-fetch-mcp/browser_fetch_mcp/__pycache__/config.cpython-314.pyc +0 -0
  62. package/tools/browser-fetch-mcp/browser_fetch_mcp/__pycache__/cookies.cpython-314.pyc +0 -0
  63. package/tools/browser-fetch-mcp/browser_fetch_mcp/__pycache__/extractors.cpython-314.pyc +0 -0
  64. package/tools/browser-fetch-mcp/browser_fetch_mcp/__pycache__/images.cpython-314.pyc +0 -0
  65. package/tools/browser-fetch-mcp/browser_fetch_mcp/__pycache__/markdown.cpython-314.pyc +0 -0
  66. package/tools/browser-fetch-mcp/browser_fetch_mcp/__pycache__/profiles.cpython-314.pyc +0 -0
  67. package/tools/browser-fetch-mcp/browser_fetch_mcp/__pycache__/server.cpython-314.pyc +0 -0
  68. package/tools/browser-fetch-mcp/browser_fetch_mcp/extractors.py +100 -0
  69. package/tools/browser-fetch-mcp/browser_fetch_mcp/server.py +153 -0
  70. package/tools/browser-fetch-mcp/tests/__pycache__/test_config.cpython-314-pytest-9.1.1.pyc +0 -0
  71. package/tools/browser-fetch-mcp/tests/__pycache__/test_cookies.cpython-314-pytest-9.1.1.pyc +0 -0
  72. package/tools/browser-fetch-mcp/tests/__pycache__/test_evaluate_js.cpython-314-pytest-9.1.1.pyc +0 -0
  73. package/tools/browser-fetch-mcp/tests/__pycache__/test_extractors.cpython-314-pytest-9.1.1.pyc +0 -0
  74. package/tools/browser-fetch-mcp/tests/__pycache__/test_extractors_timeline.cpython-314-pytest-9.1.1.pyc +0 -0
  75. package/tools/browser-fetch-mcp/tests/__pycache__/test_fetch_article.cpython-314-pytest-9.1.1.pyc +0 -0
  76. package/tools/browser-fetch-mcp/tests/__pycache__/test_fetch_user_timeline.cpython-314-pytest-9.1.1.pyc +0 -0
  77. package/tools/browser-fetch-mcp/tests/__pycache__/test_images.cpython-314-pytest-9.1.1.pyc +0 -0
  78. package/tools/browser-fetch-mcp/tests/__pycache__/test_markdown.cpython-314-pytest-9.1.1.pyc +0 -0
  79. package/tools/browser-fetch-mcp/tests/__pycache__/test_profiles.cpython-314-pytest-9.1.1.pyc +0 -0
  80. package/tools/browser-fetch-mcp/tests/__pycache__/test_server.cpython-314-pytest-9.1.1.pyc +0 -0
  81. package/tools/browser-fetch-mcp/tests/test_extractors_timeline.py +170 -0
  82. package/tools/browser-fetch-mcp/tests/test_fetch_user_timeline.py +80 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.28.0] - 2026-08-15
11
+
12
+ ### Added
13
+ - `sync-xtimeline`:新增 skill(原名 watch-x),批量追更一批固定的 X 博主推文,抓取时自动识别推文类型(post/repost/quote/reply),产出翻译摘要 + 累计静态 HTML 时间线视图(X 风格深色主题)
14
+ - `browser-fetch-mcp`:新增 `fetch_user_timeline` MCP 工具,支持抓取 X 用户时间线
15
+ - `fetch-paper`:新增 skill,贡献到 research bundle
16
+
17
+ ### Changed
18
+ - `explain-pm`:输出改为分层,界定技术语言边界,允许调研代码
19
+
20
+ ### Fixed
21
+ - `fetch-paper`:修复 PMC/OA 页面被反爬拦截无法抓取、PDF 二进制内容误用 Write 工具写盘、SKILL.md F3 违规
22
+ - `learn-video`:硬编码用户路径改用 `$HOME` + task_id 读取重试;改为后台执行+日志判定,避免阻塞调用
23
+
10
24
  ## [0.27.0] - 2026-08-13
11
25
 
12
26
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harveyz-skill",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Skill manager for Claude Code, Cursor, Codex, OpenClaw, Hermes, OpenCode, and Pi",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "prepack": "node scripts/generate-npmignore.js",
11
- "test": "bats tests/ && bash scripts/run-skill-tests.sh && node --test tests/mcp.test.mjs"
11
+ "test": "bats tests/ && bash scripts/run-skill-tests.sh && node --test tests/mcp.test.mjs tests/harness/*.test.mjs"
12
12
  },
13
13
  "files": [
14
14
  "bin/",
@@ -22,10 +22,12 @@
22
22
  "skills/research/learn-video/",
23
23
  "skills/research/survey-skillrepo/",
24
24
  "skills/research/learn-paper/",
25
+ "skills/research/fetch-paper/",
25
26
  "skills/research/extract-cognition/",
26
27
  "skills/research/probe-session/",
27
28
  "skills/research/pdf-math-translate/",
28
29
  "skills/research/clip-url/",
30
+ "skills/research/sync-xtimeline/",
29
31
  "skills/creative/capture-todo/",
30
32
  "skills/creative/capture-insight/",
31
33
  "skills/coding/init-workflow/",
@@ -1,34 +1,60 @@
1
1
  ---
2
2
  name: explain-pm
3
- description: "Restate a recent technical explanation from a product-manager's analytical lensuser experience, product philosophy, execution mechanics, and architectural healthrather than translating jargon, and flag product-perspective concerns like scope, over-engineering, priority, or drift from user need. Triggers: '/explain-pm', '/explain-pm <topic or file>', 'explain this like a PM', 'restate from a PM perspective'."
3
+ description: "Restate a technical explanation in language a product manager can act on what changes for users, which components are involved, and the call/timing dependencies between them keeping technical terms that carry architecture and dropping pure implementation mechanics. May read related code to fill in facts the original left out. Product-angle observations, if any, go in a separate optional section, never mixed into the restatement. Triggers: '/explain-pm', '/explain-pm <topic or file>', 'explain this like a PM', 'restate from a PM perspective'."
4
4
  user_invocable: true
5
- version: "1.1.1"
5
+ version: "1.2.0"
6
6
  ---
7
7
 
8
8
  # explain-pm — PM 视角复述
9
9
 
10
- 把一段技术性表述从 PM 视角复述:换成 PM 关注的分析维度,再视情况指出值得从产品角度重新考虑的地方。
10
+ 把一段技术性表述复述成 PM(产品经理)能听懂、能据以决策的版本。
11
+
12
+ 输出分两层,边界必须清晰:
13
+
14
+ 1. **核心复述**(必出)——严格不超出原材料的范围。
15
+ 2. **产品角度的延伸**(可选)——有才写,没有就整节不出现。
11
16
 
12
17
  ## 触发
13
18
 
14
19
  仅手动调用:`/explain-pm` 或 `/explain-pm <主题或文件路径>`。不自动检测触发,不主动插话。
15
20
 
16
- ## PM 视角的关注维度
21
+ ## 技术语言的保留边界
22
+
23
+ 不是把技术语言全部剥离——PM 本身熟悉基础技术语言,剥干净反而看不懂全貌。判据是:
24
+
25
+ > PM 依赖的是「什么在什么之后发生、谁依赖谁」,不依赖「这段代码是怎么写的」。
26
+
27
+ **保留**(撑起架构轮廓和调用关系的技术表述):
28
+
29
+ - 组件、服务、模块的名字
30
+ - 调用方向:谁调用谁,同步还是异步
31
+ - 依赖与时序:谁必须先就绪,哪一步是阻塞的
32
+ - 失败传播:某一环挂了,影响面到哪里为止
33
+
34
+ **剥离**(纯实现机制,对理解全貌没有帮助):
35
+
36
+ - 函数签名、参数、返回值类型
37
+ - 数据结构、字段名、序列化格式
38
+ - 算法实现、设计模式名称
39
+ - 具体代码怎么写的、用了哪个库的哪个 API
40
+
41
+ ## 核心复述的组织维度
17
42
 
18
- 不是语言转译——PM 本身熟悉基础技术语言。要转换的是关注点:
43
+ 分析时的参考,不必在回复里逐条列出,也不套固定小标题:
19
44
 
20
- - **用户体验**:这个决定最终会让用户感受到什么变化?
21
- - **产品哲学**:是否符合产品一贯的取舍和原则?
22
- - **执行机制与流程**:具体怎么落地——谁来做、分几步、影响什么协作节奏?
23
- - **架构健康度(次要)**:是否有明显的技术债或长期维护风险,只做提醒,不展开实现细节本身。
45
+ - **用户侧变化**:这件事最终让用户感受到什么?
46
+ - **参与者**:涉及哪些组件/角色,各自负责什么?
47
+ - **调用关系与时序依赖**:谁先谁后、谁依赖谁、哪一环可能卡住?
48
+ - **执行机制与影响面**:谁来做、分几步、影响什么协作节奏、边界在哪?
24
49
 
25
50
  ## 执行
26
51
 
27
52
  1. **取材料**:带参数用参数指定的主题或文件;无参数则取调用前的最后一条 assistant 消息。
28
- 2. **换视角**:对照上面的关注维度重新审视材料——不必在回复里逐条列出,只是分析时的参考。
29
- 3. **提出关注点(视情况)**:判断原表述从 PM 角度是否有值得重新审视的地方,比如是否偏离用户实际需求、范围是否合理、是否过度工程化、优先级是否搞错了。如果原表述本身已经很贴近产品目标,挑不出问题,就只做视角复述,不硬造关注点。关注点如果成立,放在回复最后,不要打断前面的视角复述。
30
- 4. **输出**:在对话中直接回复,简短(几句话量级)。默认不写文件;仅当用户明确要求存档时,才把这段评论写入用户指定的文件。
53
+ 2. **按需调研**:如果原表述没交代清楚 PM 需要知道的调用关系或依赖顺序,去读相关代码/文件把事实补齐。调研只为补核心复述的事实,不用于给延伸小节攒素材。
54
+ 3. **写核心复述**:对照上面的维度和保留边界重述,严格限定在原材料(含第 2 步补齐的事实)范围内,不做评价、不做建议。
55
+ 4. **写延伸(视情况)**:判断是否真有值得从产品角度说的——产品哲学上是否符合一贯取舍、有无明显技术债或长期维护风险、范围是否合理、优先级是否搞错、是否过度工程化。有就单独成节,与核心复述之间保持明确分隔;**挑不出就整节不写,不硬造**。
56
+ 5. **输出**:在对话中直接回复,简短(核心复述几句话量级)。默认不写文件;仅当用户明确要求存档时,才写入用户指定的文件。
31
57
 
32
58
  ## 不做
33
59
 
34
- 多轮追问式澄清、自动检测技术语言并主动触发、默认生成或保存文档、固定的输出小标题模板。
60
+ 多轮追问式澄清、自动检测技术语言并主动触发、默认生成或保存文档、在核心复述里夹带评价或改进建议、为了凑出延伸小节而硬造关注点。
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: publish-skill
3
- description: "Validate and publish a skill to the harveyz-skill repository. Checks SKILL.md format compliance (frontmatter fields, semver version, name-directory match, verb-noun naming convention) and registration in skills-index.json. Rules defined in docs/reference/skill-spec.md. Triggers: publish skill, register skill, validate skill format, check skill, add skill to index, is skill ready to publish."
3
+ description: "Validate and publish a skill to the harveyz-skill repository. Checks SKILL.md format compliance (frontmatter fields, semver version, name-directory match, verb-noun or single-verb naming convention) and registration in skills-index.json. Rules defined in docs/reference/skill-spec.md. Triggers: publish skill, register skill, validate skill format, check skill, add skill to index, is skill ready to publish."
4
4
  user_invocable: true
5
- version: "1.3.1"
5
+ version: "1.4.0"
6
6
  ---
7
7
 
8
8
  # skill-publish
@@ -59,8 +59,9 @@ cat /tmp/sv-unregistered.txt
59
59
  | F4 | `version` 字段 | 非空,格式为 `X.Y.Z`(semver) |
60
60
  | F5 | `user_invocable` 字段 | 显式声明 `true` 或 `false` |
61
61
  | F6 | 正文语言 | frontmatter 结束后的正文内容应为中文(含中文字符即视为合规) |
62
- | F7 | 目录命名规范 | skill 目录名须为 `<动词>-<名词>` 格式(2 词,连字符分隔,全小写),且动词必须在规范词表中;`archived/` 下的 skill 跳过此检查 |
62
+ | F7 | 目录命名规范 | skill 目录名须为 `<动词>-<名词>`(2 段)或单独 `<动词>`(1 段)格式,连字符分隔,全小写;第一段必须语义上是动词,名词(若存在)不得为工具/平台专有名;`archived/` 下的 skill 跳过此检查 |
63
63
  | F8 | 内容 hash | 若 `skills-index.json` 中有 `contentHash` 记录:hash 不同且 version 未变 → 报错;hash 不同且 version 已递增 → 通过,Step 7 更新;无记录 → 首次初始化,Step 7 写入 |
64
+ | F9 | 动词覆盖建议(警告,不阻断) | 目录名中的动词语义上合理但未命中规范词表时触发,建议评估是否补充进词表 |
64
65
 
65
66
  **F3 英文检测方法:** 提取 `description` 字段值,检查是否包含中文字符(`一-鿿` 范围)。有则报错。
66
67
 
@@ -69,19 +70,27 @@ cat /tmp/sv-unregistered.txt
69
70
  **F7 命名规范检测方法:**
70
71
 
71
72
  1. 取目录名(`path` 最后一段)
72
- 2. 按连字符分割,检查是否恰好 2 段
73
- 3. 检查第一段(动词)是否在规范词表中:
73
+ 2. 按连字符分割,允许恰好 1 段或 2 段
74
+ 3. 若 2 段:第一段为动词,第二段为名词;名词不得为工具/平台专有名(youtube、npm、diataxis 等)
75
+ 4. 若 1 段:整个目录名须是动词,不允许是单独名词(如 `diagram`、`config`)
76
+ 5. 动词判定(无论 1 段还是 2 段的第一段):
77
+ a. 先在规范词表中查找:
74
78
 
75
- ```
76
- extract learn forge draw manage migrate scout
77
- build sync publish archive contribute analyze clean
78
- release validate init dispatch close setup capture
79
- runby probe dedup fix survey
80
- ```
79
+ ```
80
+ fetch extract learn forge draw manage migrate
81
+ scout build sync publish archive contribute analyze
82
+ clean release validate init dispatch close setup
83
+ capture clip runby dedup
84
+ ```
85
+
86
+ 命中 → 直接合规。
87
+ b. 未命中 → 按语义判断该词是否为合理动词(而非名词/专有名词/工具名)。若是合理动词 → 合规,但记为 **F9 警告**(建议评估是否补充进词表);若明显不是动词 → **F7 违规**。
81
88
 
82
- 特殊模式:若目录名以 `runby-` 开头,直接视为合规(无需检查名词部分)。
89
+ 特殊模式:若目录名以 `runby-` 开头,直接视为合规(无需检查名词部分,也不参与动词判定)。
83
90
 
84
- 违规示例:`skill-analyzer`(动词不在词表)、`diagram`(单词,非 2 段)、`youtube-learner`(动词不在词表)
91
+ 违规示例:`skill-analyzer`(`skill` 是名词非动词)、`diagram`(单段但是名词非动词)、`youtube-learner`(`youtube` 是平台专有名非动词)
92
+
93
+ 合规但触发 F9 警告的示例:`probe-session`(`probe` 语义上是合理动词,未命中词表)、`handoff`(单段动词,语义完整,未命中词表)
85
94
 
86
95
  **F8 hash 计算方法:**
87
96
 
@@ -147,6 +156,11 @@ skill-publish 检查结果
147
156
  skills/meta/my-skill
148
157
  R1 未在 skills-index.json 中注册
149
158
 
159
+ 格式警告(不阻止通过,建议修复)
160
+ --------
161
+ skills/mint/probe-session
162
+ F9 动词 'probe' 未命中规范词表,语义合理,建议评估是否补充进词表
163
+
150
164
  注册警告(不阻止通过,建议修复)
151
165
  --------
152
166
  skills/coding/setup-debug
@@ -161,7 +175,7 @@ skill-publish 检查结果
161
175
  ...
162
176
 
163
177
  ========================
164
- 共发现 3 个问题(2 个格式,1 个注册),2 个警告
178
+ 共发现 3 个问题(2 个格式,1 个注册),3 个警告
165
179
  ```
166
180
 
167
181
  若全部通过(含警告):
@@ -169,7 +183,7 @@ skill-publish 检查结果
169
183
  ```
170
184
  skill-publish 检查结果
171
185
  ========================
172
- 所有 N 个 skill 格式与注册均符合规范。✓(2 个 R4 警告,建议填写 installScope)
186
+ 所有 N 个 skill 格式与注册均符合规范。✓(1F9 警告,1 个 R4 警告,建议评估补充词表 / 填写 installScope)
173
187
  ```
174
188
 
175
189
  若完全无问题无警告:
@@ -202,6 +216,13 @@ skill-publish 检查结果
202
216
  ```
203
217
  确认后执行 `git mv` 重命名目录,并更新 `skills-index.json` 中对应的 `path` 值,最后运行 `node scripts/generate-npmignore.js`。
204
218
 
219
+ **格式警告(F9)** — 动词未命中词表但语义合理,不阻止发布,仅询问是否顺带补充词表:
220
+ ```
221
+ F9 警告:动词 'probe' 未命中规范词表(语义合理)。
222
+ 是否将 'probe' 加入 docs/reference/skill-spec.md 的规范动词词表?(y/n/跳过)
223
+ ```
224
+ 选 `y` 则用 Edit 工具在 `docs/reference/skill-spec.md` 的词表中追加该动词及含义(需用户补充一句含义描述),并同步追加到本文件 Step 3 的词表副本;选 `n`/跳过则保留警告,不做修改。
225
+
205
226
  **格式问题(F8)** — 询问 bump 类型后自动更新版本号,随即执行 Step 7:
206
227
  ```
207
228
  修复 F8(内容 hash 不匹配):
@@ -7,15 +7,7 @@ user_invocable: true
7
7
 
8
8
  # clip-url(Stage 4,验证性构建)
9
9
 
10
- 这是 [browser-fetch-mcp](../../../tools/browser-fetch-mcp/) 的验证性消费者,跟 extract-url 的 Subagent 1/2 结构对齐,做"抓取(MCP,经 fetch_article 做站点感知抽取)→ 打标 + 翻译 → 存文件"两阶段流程。URL 去重和固定标签词表与 extract-url 共用同一份 `~/.hskill/url-extract/config.json`(`VAULT_PATH`)和 `fixed_tags.txt`,两边抓过的文章互相认得出"已抓取"。仍不是 extract-url 的完全等价替代(例如没有 `validate_article.py` 那样的 frontmatter 自动修复),只用于验证 MCP 抓取链路能否支撑一个完整的两阶段 skill 流程并逐步对齐生产行为。抓取产出的原文文件名与 extract-url 一致,按标题命名(`Origin/<标题>.md`,Translation 沿用同一文件名),两者共存于同一个 `<hash8>/` 目录下,去重判定只看 `meta.json` 的 `source_url`,不受文件名影响。
11
-
12
- **依赖**:本 skill 的 MCP 抓取脚本(`scripts/mcp_fetch_client.py` 等)依赖 `browser-fetch-mcp`。在本仓库 checkout 内直接运行时会自动找到 `tools/browser-fetch-mcp/browser-fetch-mcp.sh`;若这个 skill 是通过 `hskill install` 安装到别处运行的(`~/.claude/skills/`、`~/.pi/agent/skills/` 等),需要额外单独运行 `hskill install --tool browser-fetch-mcp` 装好 launcher(落到 `~/.local/bin/browser-fetch-mcp`)。步骤 1.5 会在流程一开始就检测这个依赖是否满足。
13
-
14
- ## 路径变量
15
-
16
- ```
17
- SkillDir: skills/research/clip-url
18
- ```
10
+ 这是 [browser-fetch-mcp](../../../tools/browser-fetch-mcp/) 的验证性消费者,做"抓取(MCP,经 fetch_article 做站点感知抽取)→ 打标 + 翻译 → 存文件"两阶段流程,跟 extract-url Subagent 1/2 结构对齐。下文脚本路径均相对本 SKILL.md 所在目录。
19
11
 
20
12
  ## 执行流程
21
13
 
@@ -39,23 +31,23 @@ url_safe = re.sub(r'[\x00-\x1f\x7f]', '', url).strip()[:2048]
39
31
 
40
32
  ### 步骤 1.5:确认 browser-fetch-mcp 可用
41
33
 
42
- 运行 `python3 SkillDir/scripts/browser_fetch_mcp_locate.py`。
34
+ 运行 `python3 scripts/browser_fetch_mcp_locate.py`。
43
35
 
44
36
  - 若输出 `FOUND: <path>`:继续步骤 2。
45
37
  - 若输出 `NOT_FOUND: <error>`(exit code 1):向用户报告"browser-fetch-mcp 未安装或未找到:{error}。若在本仓库 checkout 内运行,请确认 `tools/browser-fetch-mcp/browser-fetch-mcp.sh` 存在;若是通过 `hskill install` 安装的 skill 副本,请先运行 `hskill install --tool browser-fetch-mcp`",流程终止,不再执行后续步骤。
46
38
 
47
39
  ### 步骤 2:确认默认 chrome_profile(只在第一次使用本 skill 时问一次,之后不再询问)
48
40
 
49
- 运行 `python3 SkillDir/scripts/chrome_profile_config.py get`。
41
+ 运行 `python3 scripts/chrome_profile_config.py get`。
50
42
 
51
43
  - 若输出 `CONFIGURED: <path>`:已经配置过默认 profile,跳过下面的检测和提问,直接进入步骤 2.5。
52
- - 若输出 `NOT_CONFIGURED`:运行 `python3 SkillDir/scripts/chrome_profile_config.py prompted`。
44
+ - 若输出 `NOT_CONFIGURED`:运行 `python3 scripts/chrome_profile_config.py prompted`。
53
45
  - 若输出 `YES`(之前已经问过一次,不管当时用户是设置了值还是选择不设置):跳过检测和提问,直接进入步骤 2.5。
54
46
  - 若输出 `NO`(第一次遇到这个状态,不论当前 URL 是什么网站都会命中):
55
- 1. 运行 `python3 SkillDir/scripts/detect_xcom_chrome_profile.py`,把完整输出(对比表 + `RECOMMENDED_PROFILE:` 那行)原样展示给用户。
47
+ 1. 运行 `python3 scripts/detect_xcom_chrome_profile.py`,把完整输出(对比表 + `RECOMMENDED_PROFILE:` 那行)原样展示给用户。
56
48
  2. 向用户提问:把推荐的 profile 设为以后的默认值?或输入一个替代路径?也可以选择不设置。
57
- 3. 不论用户如何回答,运行 `python3 SkillDir/scripts/chrome_profile_config.py mark-prompted`,记录"已经问过一次"——此后所有网站的抓取都不会再触发这个设置流程(包括用户当时选择不设置的情况)。
58
- 4. 若用户提供了 profile 路径(推荐的或自己输入的):运行 `python3 SkillDir/scripts/chrome_profile_config.py set <path>` 持久化。
49
+ 3. 不论用户如何回答,运行 `python3 scripts/chrome_profile_config.py mark-prompted`,记录"已经问过一次"——此后所有网站的抓取都不会再触发这个设置流程(包括用户当时选择不设置的情况)。
50
+ 4. 若用户提供了 profile 路径(推荐的或自己输入的):运行 `python3 scripts/chrome_profile_config.py set <path>` 持久化。
59
51
  5. 若用户选择不设置:不持久化 profile 值,本次继续(x.com 的 URL 会在 Subagent 1 里因为 `fetch_article` 报错而失败——x.com 没有匿名抓取选项;非 x.com 的 URL 正常匿名抓取,不受影响)。用户之后可随时手动运行 `chrome_profile_config.py set <path>` 补配置。
60
52
 
61
53
  **不允许**:跳过展示直接把探测到的 profile 设为默认值——必须等用户明确回答,且只有用户确认后才能调用 `chrome_profile_config.py set`。
@@ -66,7 +58,7 @@ url_safe = re.sub(r'[\x00-\x1f\x7f]', '', url).strip()[:2048]
66
58
  import subprocess
67
59
  result = subprocess.run(
68
60
  ['python3', '-c',
69
- 'import sys; sys.path.insert(0, "SkillDir/scripts"); import vault_config; print(vault_config.get_vault_path())'],
61
+ 'import sys; sys.path.insert(0, "scripts"); import vault_config; print(vault_config.get_vault_path())'],
70
62
  capture_output=True, text=True
71
63
  )
72
64
  ```
@@ -87,7 +79,7 @@ result = subprocess.run(
87
79
  从报告中读取 `RESULT:` 那行。
88
80
 
89
81
  - 若 `RESULT: SKIPPED`:该 URL 已经抓取过(去重命中),提取 `META_PATH:` 那行的值记作 meta_path,向用户输出「已跳过」卡片(步骤 6 卡片格式,标题用 url_safe),流程终止,不再派发 Subagent 2 或 Subagent 3。
90
- - 若 `RESULT: OK` 且(`CONTENT_THIN: False`,或 `CONTENT_THIN: True` 但 `THIN_RETRY_USED: False`):提取 `ORIGIN_PATH:`/`TITLE:`/`CODE_BLOCK_COUNT:`/`IMAGE_COUNT:` 那几行的值,分别记作 origin_path / title / code_block_count / image_count——步骤 6 的完成卡片要用,此后各步骤间一直带着这四个值。跳到步骤 5。`CONTENT_THIN: True` 且 `THIN_RETRY_USED: False` 的情况(例如文章本来就短、或没有配置 chrome_profile 因而从未触发过认证重试)不算需要自优化——没有更多现有手段可以尝试,按正常内容处理。
82
+ - 若 `RESULT: OK` 且(`CONTENT_THIN: False`,或 `CONTENT_THIN: True` 但 `THIN_RETRY_USED: False`):提取 `ORIGIN_PATH:`/`TITLE:`/`CODE_BLOCK_COUNT:`/`IMAGE_COUNT:` 那几行的值,分别记作 origin_path / title / code_block_count / image_count——步骤 6 的完成卡片要用,此后各步骤间一直带着这四个值。跳到步骤 5。`CONTENT_THIN: True` 且 `THIN_RETRY_USED: False` 不算需要自优化——没有更多现有手段可以尝试,按正常内容处理。
91
83
  - 若 `RESULT: OK` 且 `CONTENT_THIN: True` 且 `THIN_RETRY_USED: True`,或 `RESULT: FAILED`:`RESULT: OK` 时提取 `TITLE:` 那行的值记作 title(`RESULT: FAILED` 时没有 title,title 留空,卡片渲染时用 url_safe 代替);进入步骤 4.5(自优化)。本次 URL 最多只走一次步骤 4.5——若步骤 4.5 重试后仍然满足这个条件,直接终止流程,向用户输出「失败」卡片,不再第二次派发自优化 subagent。
92
84
 
93
85
  ### 步骤 4.5:派发 Subagent 3(自优化,仅在步骤 4 判定需要时执行)
@@ -148,6 +140,10 @@ result = subprocess.run(
148
140
 
149
141
  任意状态下,若本次运行中步骤 4.5 曾经出现过 `RESULT: SOLIDIFIED`,在卡片后额外报告一行:本次抓取新增了未合并分支 `<BRANCH>`,需要用户决定后续(合并/PR/保留)。
150
142
 
143
+ ## 边界
144
+
145
+ 仍不是 extract-url 的完全等价替代,只用于验证 MCP 抓取链路能否支撑一个完整的两阶段 skill 流程并逐步对齐生产行为。跟 extract-url 共用同一份存储与去重索引:URL 去重和固定标签词表读同一份 `~/.hskill/url-extract/config.json`(`VAULT_PATH`)和 `fixed_tags.txt`;抓取产出的原文文件名与 extract-url 一致,按标题命名(`Origin/<标题>.md`,Translation 沿用同一文件名),两者共存于同一个 `<hash8>/` 目录下,去重判定只看 `meta.json` 的 `source_url`,不受文件名影响——两边抓过的文章互相认得出"已抓取"。
146
+
151
147
  ## 参考文件
152
148
 
153
149
  | 文件 | 用途 |
@@ -0,0 +1,239 @@
1
+ ---
2
+ name: fetch-paper
3
+ description: "Locate and download a single academic paper's full text via legitimate open-access channels (arXiv, Unpaywall, Semantic Scholar, PMC, publisher OA pages). Verifies the paper's bibliographic record (DOI/arXiv ID) before searching, disambiguates when multiple candidates match, and reports one of four outcomes: auto-downloaded, free-but-manual, paid/registration-required, or not found. Maintains a growing list of free sources ranked by success rate. Triggers: 'find and download this paper', 'get me the PDF of <title>', 'download this paper', 'fetch paper <title/DOI>'."
4
+ user_invocable: true
5
+ version: "0.1.0"
6
+ ---
7
+
8
+ # fetch-paper
9
+
10
+ 联网核实论文真实存在,仅通过合法开放获取渠道下载全文 PDF;找不到免费全文时,报告手动/付费入口而非代为绕过付费墙。
11
+
12
+ 底层设计说明见 `docs/superpowers/specs/2026-08-15-fetch-paper-design.md`。
13
+
14
+ ---
15
+
16
+ ## 路径变量
17
+
18
+ ```
19
+ ConfigPath: ~/.hskill/fetch-paper/config.json
20
+ SourcesPath: ~/.hskill/fetch-paper/sources.json
21
+ ```
22
+
23
+ ---
24
+
25
+ ## 执行流程
26
+
27
+ ### Step 0:初始化配置与清单
28
+
29
+ 用 Read 工具读取 `~/.hskill/fetch-paper/config.json`。
30
+
31
+ 若文件不存在,询问用户:
32
+
33
+ ```
34
+ 论文下载到哪个目录?(直接回车使用默认:~/Documents/paper-downloads)
35
+ ```
36
+
37
+ 用户回复后,用 Bash 工具写入配置(路径必须用 `$HOME` 展开,不可写字面量 `~`):
38
+
39
+ ```bash
40
+ mkdir -p "$HOME/.hskill/fetch-paper"
41
+ download_dir="${用户指定路径/#\~/$HOME}"
42
+ echo "{\"download_dir\": \"$download_dir\"}" > "$HOME/.hskill/fetch-paper/config.json"
43
+ ```
44
+
45
+ 若文件已存在,解析 JSON 取出 `download_dir` 字段,展开残留的 `~`:
46
+
47
+ ```bash
48
+ download_dir=$(python3 -c "import json,os; d=json.load(open('$HOME/.hskill/fetch-paper/config.json')); print(d['download_dir'].replace('~', os.environ['HOME'], 1))")
49
+ ```
50
+
51
+ 用 Read 工具读取 `~/.hskill/fetch-paper/sources.json`。若不存在,用 Bash 工具写入种子清单:
52
+
53
+ ```bash
54
+ cat > "$HOME/.hskill/fetch-paper/sources.json" <<'EOF'
55
+ {
56
+ "sources": [
57
+ { "name": "arXiv", "type": "api", "auto": true, "success_count": 0 },
58
+ { "name": "Unpaywall", "type": "api", "auto": true, "success_count": 0 },
59
+ { "name": "Semantic Scholar", "type": "api", "auto": true, "success_count": 0 },
60
+ { "name": "PMC", "type": "api", "auto": true, "success_count": 0 }
61
+ ]
62
+ }
63
+ EOF
64
+ ```
65
+
66
+ ---
67
+
68
+ ### Step 1:解析输入
69
+
70
+ 从用户消息中提取论文线索:标题 / DOI / arXiv ID / 完整引用字符串。
71
+
72
+ 判断输入是否已含:
73
+ - DOI(形如 `10.xxxx/...`)
74
+ - arXiv ID(形如 `2401.12345` 或 `hep-th/9901001`)
75
+
76
+ 若已含,跳过检索的模糊匹配部分,直接用于 Step 2 的精确查询。
77
+
78
+ ---
79
+
80
+ ### Step 2:检索候选
81
+
82
+ **若输入已含 DOI:**
83
+
84
+ ```
85
+ WebFetch(url="https://api.crossref.org/works/<DOI>")
86
+ ```
87
+
88
+ **若输入已含 arXiv ID:**
89
+
90
+ ```
91
+ WebFetch(url="http://export.arxiv.org/api/query?id_list=<arXiv ID>")
92
+ ```
93
+
94
+ **否则(只有标题/引用字符串):**
95
+
96
+ 同时执行:
97
+
98
+ ```
99
+ WebSearch("<标题>")
100
+ WebFetch(url="https://api.crossref.org/works?query.bibliographic=<urlencode(标题)>&rows=5")
101
+ ```
102
+
103
+ 从返回结果中,对每个候选提取:标题、作者、年份、期刊/会议、DOI、arXiv ID(若有)。
104
+
105
+ ---
106
+
107
+ ### Step 3:核验与消歧
108
+
109
+ - **唯一命中**(用 DOI/arXiv ID 精确查询命中,或标题/作者/年份高度吻合且只有一个候选)→ 直接确认,进入 Step 4。
110
+ - **多个相似候选** → 停下来,向用户列出候选表格,等待用户确认后再继续:
111
+
112
+ ```
113
+ 找到多个相似标题的论文,请确认目标:
114
+
115
+ 1. {标题A} — {作者} ({年份}),DOI: {doi或"无"}
116
+ 2. {标题B} — {作者} ({年份}),DOI: {doi或"无"}
117
+
118
+ 请回复序号,或提供更精确的信息(如 DOI)。
119
+ ```
120
+
121
+ - **零命中** → 归为「D」,跳到 Step 5 的 D 分支,不再继续后续步骤。
122
+
123
+ ---
124
+
125
+ ### Step 4:按优先级尝试免费渠道
126
+
127
+ 读取 `~/.hskill/fetch-paper/sources.json`,把 `auto: true` 的站点按 `success_count` 降序排序,依次尝试(命中一个立即停止,不再尝试后面的):
128
+
129
+ 1. **arXiv** — 若已确认 arXiv ID,PDF 直链为 `https://arxiv.org/pdf/<arXiv ID>`
130
+ 2. **Unpaywall** — 若已确认 DOI,`WebFetch(url="https://api.unpaywall.org/v2/<DOI>?email=fetch-paper@localhost")`,取响应中 `best_oa_location.url_for_pdf`
131
+ 3. **Semantic Scholar** — `WebFetch(url="https://api.semanticscholar.org/graph/v1/paper/DOI:<DOI>?fields=openAccessPdf")`,取 `openAccessPdf.url`
132
+ 4. **PMC** — 若论文属生物医学领域,`WebFetch(url="https://www.ncbi.nlm.nih.gov/pmc/utils/idconv/v1.0/?ids=<DOI>&format=json")` 换取 PMC ID(该 URL 会 301 到 `pmc.ncbi.nlm.nih.gov` 新域名,WebFetch 会提示重新请求一次;返回 JSON 里 PMC ID 在 `records[0].pmcid`)。**注意**:PDF 直链 `https://www.ncbi.nlm.nih.gov/pmc/articles/<PMC ID>/pdf/` 实测已失效——该链接最终落地到一个 200 状态的"Preparing to download..."HTML 中间页,需要浏览器执行 JS 解一个 Proof-of-Work 挑战(cloudpmc-viewer)并写入 cookie 才能拿到真正的 PDF,WebFetch/纯 HTTP 请求无法完成,判定为必然失败,直接记入「B 手动候选」,不必重试。另外 Unpaywall/Semantic Scholar 对 PMC 收录的论文通常也会把同一个 PMC 落地页当作 `best_oa_location`/`openAccessPdf`(但 `url_for_pdf` 常为 null),所以此渠道确实会被触达,只是触达后大概率下载失败
133
+ 5. **期刊官网 OA 页面** — 若 Crossref 元数据里 `license` 字段显示开放许可(如 CC-BY),尝试该页面上的 PDF 链接。**注意**:实测 Unpaywall/Semantic Scholar 判定为 OA 但 `url_for_pdf` 为 null 的情况很常见(渠道 2/3 未必能给出可下载直链),此时确实会落到这一步;但 ScienceDirect、MDPI 等主流出版商的文章落地页对自动化请求(含 WebFetch、curl 加常规浏览器 UA)普遍返回 403,即便该文确为 CC-BY 开放许可也一样——这是发布方的反爬拦截,不是个别失败。遇到 403 或非 PDF 响应直接判定该渠道失败,记入「B 手动候选」,不要反复重试同一出版商
134
+ 6. **作者/机构仓库自存版** — `WebSearch("\"<标题>\" filetype:pdf")`,人工判断结果是否为作者自存版(个人主页、机构仓库域名,而非镜像站)
135
+
136
+ 对每个候选 URL:
137
+ - 用 WebFetch 请求。注意:WebFetch 遇到二进制/PDF 响应时不会把原始字节当文本返回,而是自动把内容存到一个临时文件,并在返回文本里报告该临时文件路径(形如"Binary content ... also saved to /path/to/file.pdf")——**记下这个临时文件路径**,Step 5 A 写盘时要用它
138
+ - **校验返回内容确为 PDF**:检查响应 `Content-Type` 是否为 `application/pdf`,或内容开头是否为 `%PDF-`。若是登录页/验证码页/HTML 错误页伪装的 200 响应,判定该渠道失败——记入「B 手动候选」列表,继续尝试下一个渠道,不重试同一渠道
139
+ - 若该渠道明确需要人工点击/验证码/交互式操作才能拿到文件本身(例如落地页只有"Request full text"按钮)→ 直接记入「B 手动候选」列表,不算失败也不算成功,继续尝试下一个渠道
140
+
141
+ 若全部 auto 渠道都试完仍未拿到文件:
142
+ - 若「B 手动候选」列表非空 → 进入 Step 5 的 B 分支
143
+ - 若「B 手动候选」列表为空,但通过 Step 2/Step 3 已知官方页面(期刊官网、ResearchGate 请求作者、机构登录等)→ 进入 Step 5 的 C 分支
144
+ - 若连官方页面都没有 → 进入 Step 5 的 D 分支
145
+
146
+ ---
147
+
148
+ ### Step 5:产出结果
149
+
150
+ 先用 Bash 生成 paper slug(标题转小写、空格与特殊字符替换为 `-`、汉字保留):
151
+
152
+ ```bash
153
+ slug=$(echo "<论文标题>" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9一-鿿]/-/g' | sed -E 's/-+/-/g' | sed 's/^-//;s/-$//')
154
+ ```
155
+
156
+ **A. 自动下载成功**(Step 4 拿到了合法 PDF):
157
+
158
+ ```bash
159
+ mkdir -p "<download_dir>/$slug"
160
+ ```
161
+
162
+ 用 Bash 把 Step 4 中 WebFetch 报告的临时文件路径复制到目标位置(PDF 是二进制内容,不能用 Write 工具写入——Write 只接受文本,二进制字节会被破坏):
163
+
164
+ ```bash
165
+ cp "<WebFetch 报告的临时文件路径>" "<download_dir>/$slug/paper.pdf"
166
+ ```
167
+
168
+ 用 Write 工具写入 `<download_dir>/$slug/metadata.md`:
169
+
170
+ ```markdown
171
+ # {论文标题}
172
+
173
+ **作者**: {作者}
174
+ **年份**: {年份}
175
+ **期刊/会议**: {期刊}
176
+ **DOI**: {DOI或"无"}
177
+ **arXiv ID**: {arXiv ID或"无"}
178
+ **核验日期**: {YYYY-MM-DD}
179
+ **结果**: A 自动下载成功
180
+
181
+ ## 获取渠道
182
+
183
+ - [自动] {站点名}:{URL}
184
+ ```
185
+
186
+ 用 Bash 更新 `~/.hskill/fetch-paper/sources.json`,命中站点的 `success_count` 加 1(用 python3 读写 JSON,保留其余字段不变)。
187
+
188
+ **B. 免费全文存在,但需手动下载**(有手动候选,但没有 auto 渠道成功):
189
+
190
+ 不写 PDF。用 Write 工具写入 `<download_dir>/$slug/metadata.md`(同上结构),获取渠道行改为:
191
+
192
+ ```markdown
193
+ - [手动下载] {站点名}:{URL} —— {具体操作,如"打开链接,点击 Download PDF 按钮"}
194
+ ```
195
+
196
+ **C. 无免费全文,但有收费/需注册渠道**:
197
+
198
+ 用 Write 工具写入 `<download_dir>/$slug/metadata.md`,获取渠道行改为:
199
+
200
+ ```markdown
201
+ - [付费/需注册] {站点名}:{URL} —— {购买 / 向作者请求全文 / 机构登录}
202
+ ```
203
+
204
+ **D. 完全找不到任何获取渠道**:不创建目录、不写任何文件。直接在对话中回复:
205
+
206
+ ```
207
+ 未找到「{用户输入}」对应的可靠论文记录(或该论文没有任何已知获取渠道)。
208
+ {若 Step 2/3 已核验到部分元数据,在此列出}
209
+ ```
210
+
211
+ ---
212
+
213
+ ### Step 6:汇报结果
214
+
215
+ 无论哪一态,最后都用明确标签总结,不能含糊表述"已尝试下载":
216
+
217
+ ```
218
+ 结果:[A 自动下载成功 / B 免费需手动 / C 付费或需注册 / D 完全找不到]
219
+
220
+ {论文标题}
221
+ {对应 metadata.md 完整路径,若有产出}
222
+ ```
223
+
224
+ ---
225
+
226
+ ## 边界
227
+
228
+ - **不做**:不集成、不自动访问任何绕过付费墙的镜像站/影子图书馆(如 Sci-Hub 一类)
229
+ - **不做**:不代下载付费论文,不代填注册表单
230
+ - **不做**:不批量处理多篇论文(每次只处理一个)
231
+ - **不做**:下载后不自动调用 `learn-paper` 精读(用户需另行触发)
232
+ - **不做**:不深度校验全文内容与标题/摘要的一致性(核验只到"元数据记录真实存在"这一层)
233
+
234
+ ---
235
+
236
+ ## 依赖
237
+
238
+ - WebSearch(检索候选)
239
+ - WebFetch(调用 Crossref / Unpaywall / Semantic Scholar / arXiv / PMC 等公开 API,下载 PDF)