harveyz-skill 0.23.0 → 0.24.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/CHANGELOG.md +16 -0
- package/package.json +4 -1
- package/skills/coding/explain-pm/SKILL.md +25 -0
- package/skills/coding/question-me/SKILL.md +1 -1
- package/skills/coding/rephrase/SKILL.md +25 -0
- package/skills/research/extract-url/SKILL.md +21 -21
- package/skills/research/extract-url/references/__pycache__/article_utils.cpython-314.pyc +0 -0
- package/skills/research/extract-url/references/article_utils.py +35 -25
- package/skills/research/extract-url/references/file-format.md +9 -4
- package/skills/research/extract-url/references/subagent1-fetch-prompt.md +10 -1
- package/skills/research/extract-url/references/subagent2-tag-translate-prompt.md +14 -10
- package/skills/research/extract-url/scripts/__pycache__/config.cpython-314.pyc +0 -0
- package/skills/research/extract-url/scripts/__pycache__/migrate_to_folder_structure.cpython-314.pyc +0 -0
- package/skills/research/extract-url/scripts/config.py +31 -1
- package/skills/research/extract-url/scripts/dedup_check.py +15 -33
- package/skills/research/extract-url/scripts/migrate_to_folder_structure.py +459 -0
- package/skills/research/extract-url/scripts/playwright_web.py +16 -20
- package/skills/research/extract-url/scripts/playwright_web_arxiv.py +284 -0
- package/skills/research/extract-url/scripts/playwright_xcom.py +16 -20
- package/skills/research/extract-url/scripts/validate_article.py +9 -10
- package/skills/research/extract-url/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_article_utils_meta.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_article_utils_tags.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_config.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_count_article_stats.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_dedup_check.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_migrate_to_folder_structure.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_playwright_web.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_playwright_web_arxiv.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_playwright_xcom.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_subagent1_prompt.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_subagent2_prompt.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_validate_article.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/conftest.py +15 -20
- package/skills/research/extract-url/tests/test_article_utils_meta.py +84 -0
- package/skills/research/extract-url/tests/test_config.py +47 -0
- package/skills/research/extract-url/tests/test_dedup_check.py +34 -28
- package/skills/research/extract-url/tests/test_migrate_to_folder_structure.py +492 -0
- package/skills/research/extract-url/tests/test_playwright_web.py +55 -18
- package/skills/research/extract-url/tests/test_playwright_web_arxiv.py +157 -0
- package/skills/research/extract-url/tests/test_playwright_xcom.py +8 -0
- package/skills/research/extract-url/tests/test_subagent1_prompt.py +15 -0
- package/skills/research/extract-url/tests/test_subagent2_prompt.py +22 -0
- package/skills/research/extract-url/tests/test_validate_article.py +95 -12
- package/skills/research/pdf-math-translate/SKILL.md +139 -0
- package/skills-index.json +28 -8
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.24.0] - 2026-07-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `extract-url`:用 per-article `meta.json` 索引替代 SQLite 索引,`article_utils`/`dedup_check`/`playwright_web(_arxiv)`/`playwright_xcom`/`validate_article` 全部切换到 meta.json 读写;提供旧数据迁移脚本(写 meta.json + 清理遗留文件)
|
|
14
|
+
- `pdf-math-translate`:从其他项目贡献到 `research` bundle 的 PDF 数学翻译 skill
|
|
15
|
+
- `explain-pm`:新增 skill,注册到 `coding` bundle
|
|
16
|
+
- `rephrase`:新增 skill,单次改写用户陈述以提升精确度
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- `extract-url`:放宽翻译文章的 author/publish_date 校验规则
|
|
20
|
+
- `question-me`:补充决策树格式中 label 字段的示例与一致性规则(v3.0.1)
|
|
21
|
+
- `publish-skill` 审计:修正 `learn-skill`/`survey-skillrepo`/`init-skill` 三个 skill 历史遗留的 contentHash 记录错误(内容本身未变更)
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- `extract-url`:SKILL.md 与 subagent prompt 更新以适配 meta.json 索引;skills-index.json 同步更新 contentHash
|
|
25
|
+
|
|
10
26
|
## [0.23.0] - 2026-07-09
|
|
11
27
|
|
|
12
28
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harveyz-skill",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Skill manager for Claude Code, Cursor, and Codex",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"skills/research/learn-paper/",
|
|
25
25
|
"skills/research/extract-cognition/",
|
|
26
26
|
"skills/research/probe-session/",
|
|
27
|
+
"skills/research/pdf-math-translate/",
|
|
27
28
|
"skills/creative/capture-todo/",
|
|
28
29
|
"skills/creative/capture-insight/",
|
|
29
30
|
"skills/coding/init-workflow/",
|
|
@@ -31,6 +32,8 @@
|
|
|
31
32
|
"skills/coding/init-goal/",
|
|
32
33
|
"skills/coding/question-me/",
|
|
33
34
|
"skills/coding/capture-vocab/",
|
|
35
|
+
"skills/coding/rephrase/",
|
|
36
|
+
"skills/coding/explain-pm/",
|
|
34
37
|
"skills/writing/forge-doc/",
|
|
35
38
|
"skills/writing/draw-diagram/",
|
|
36
39
|
"skills/writing/manage-dir/",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explain-pm
|
|
3
|
+
description: "Restate a recent technical explanation in product-manager language: translate jargon into user/business impact, 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'."
|
|
4
|
+
user_invocable: true
|
|
5
|
+
version: "1.0.0"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# explain-pm — PM 视角复述
|
|
9
|
+
|
|
10
|
+
把一段技术性表述从 PM 视角复述:先转译成产品/业务语言,再视情况指出值得从产品角度重新考虑的地方。
|
|
11
|
+
|
|
12
|
+
## 触发
|
|
13
|
+
|
|
14
|
+
仅手动调用:`/explain-pm` 或 `/explain-pm <主题或文件路径>`。不自动检测触发,不主动插话。
|
|
15
|
+
|
|
16
|
+
## 执行
|
|
17
|
+
|
|
18
|
+
1. **取材料**:带参数用参数指定的主题或文件;无参数则取调用前的最后一条 assistant 消息。
|
|
19
|
+
2. **转译**:把技术语言换成产品/业务语言——这段话讲给 PM 听,PM 听到的是什么(用户能得到什么、解决了什么问题、有什么代价)。
|
|
20
|
+
3. **提出关注点(视情况)**:判断原表述从 PM 角度是否有值得重新审视的地方,比如是否偏离用户实际需求、范围是否合理、是否过度工程化、优先级是否搞错了。如果原表述本身已经很贴近产品目标,挑不出问题,就只做转译,不硬造关注点。
|
|
21
|
+
4. **输出**:在对话中直接回复,简短(几句话量级)。默认不写文件;仅当用户明确要求存档时,才把这段评论写入用户指定的文件。
|
|
22
|
+
|
|
23
|
+
## 不做
|
|
24
|
+
|
|
25
|
+
多轮追问式澄清、自动检测技术语言并主动触发、默认生成或保存文档、固定的输出小标题模板。
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: question-me
|
|
3
3
|
description: "Pre-task clarification skill — clarifies ambiguous or complex tasks before execution through structured Q&A with a live decision tree. One question at a time, each with at least 3 options + custom, in decision-dependency order. Triggers: '/question-me', 'help me clarify this', 'question me before starting', 'let's define this first'. Claude auto-triggers when detecting ambiguous or complex requests (multiple conflicting goals, vague keywords like 'optimize/refactor/clean up', missing success criteria, unstated context assumptions)."
|
|
4
4
|
user_invocable: true
|
|
5
|
-
version: "3.0.
|
|
5
|
+
version: "3.0.1"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# question-me — 执行前指令澄清
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rephrase
|
|
3
|
+
description: "Single-pass rephrasing of a user statement into a more precise, less ambiguous, more actionable version, then either auto-proceeds or asks for confirmation based on Claude's own reliability judgment. Triggers: '/rephrase', '/rephrase <statement>', 'rephrase this', 'help me restate this more precisely'."
|
|
4
|
+
user_invocable: true
|
|
5
|
+
version: "1.0.1"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# rephrase — 单轮改写澄清
|
|
9
|
+
|
|
10
|
+
对用户的一句话表述做单轮改写,让它更精确、更少歧义、更可执行。不预设"合格表述"的标准或清单,由 Claude 依据当前语境自行判断。
|
|
11
|
+
|
|
12
|
+
## 触发
|
|
13
|
+
|
|
14
|
+
仅手动调用:`/rephrase` 或 `/rephrase <表述>`。不自动检测、不主动建议。多轮追问式澄清是 `question-me` 的职责,不是本 skill。
|
|
15
|
+
|
|
16
|
+
## 执行
|
|
17
|
+
|
|
18
|
+
1. **取待改写内容**:带参数用参数;无参数用用户上一条消息;两者都没有就直接问用户要表述。
|
|
19
|
+
2. **改写**:消歧义、补全隐含主语/宾语、明确动作对象,视原文缺什么而定——原文已经清楚就不用大改,也不要顺手加原文没提的验收标准/范围。
|
|
20
|
+
3. **判断可靠性**:不是看"改写里有没有任何未明说的假设"(几乎总有),而是看**猜错的代价**——如果涉及在多个同权重候选(哪个文件、哪个服务)里武断选一个,选错会做错事,判不可靠;如果只是无关紧要的实现细节、执行风险低、改错了也好回退,判可靠。
|
|
21
|
+
4. **分支**:可靠 → 展示改写结果,直接执行,不等确认。不可靠 → 展示改写结果和存疑点,等用户确认要不要改。
|
|
22
|
+
|
|
23
|
+
## 不做
|
|
24
|
+
|
|
25
|
+
多轮追问式澄清、强制补充验收标准/范围边界、自动触发。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: extract-url
|
|
3
|
-
version: "2.
|
|
3
|
+
version: "2.6.0"
|
|
4
4
|
description: "Use when a user provides a URL and wants to save, archive, fetch, or translate content to the local Obsidian Vault — even with vague phrasing like 'save this article', 'translate and save', 'put this in obsidian', 'archive this'. Skip when user only wants a summary, pastes raw text without a URL, asks about a site's tech stack, or wants to extract/list URLs from a page without saving an article."
|
|
5
5
|
user_invocable: true
|
|
6
6
|
---
|
|
@@ -73,30 +73,30 @@ ls ~/.hskill/url-extract/config.json 2>/dev/null && echo "EXISTS" || echo "NOT_F
|
|
|
73
73
|
## 路径变量(脚本自读 config.json,无需 Agent 传参)
|
|
74
74
|
|
|
75
75
|
```
|
|
76
|
-
Config:
|
|
77
|
-
Base:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
Config: ~/.hskill/url-extract/config.json
|
|
77
|
+
Base: VAULT_PATH (脚本从 config.json 读取)
|
|
78
|
+
ArticleDir: VAULT_PATH/<hash8> (hash8 = md5(source_url)[:8],由 scripts/config.py 的 get_article_paths() 统一计算)
|
|
79
|
+
Origin: ArticleDir/Origin
|
|
80
|
+
Translation: ArticleDir/Translation
|
|
81
|
+
Image: ArticleDir/Image
|
|
82
|
+
Meta: ArticleDir/meta.json
|
|
83
|
+
SkillDir: 平台固定值(见平台补丁)
|
|
82
84
|
```
|
|
83
85
|
|
|
84
86
|
---
|
|
85
87
|
|
|
86
|
-
## URL 去重索引(
|
|
88
|
+
## URL 去重索引(meta.json)
|
|
87
89
|
|
|
88
|
-
|
|
90
|
+
**索引路径:** `VAULT_PATH/<hash8>/meta.json`(`hash8` 由 URL 派生,去重时直接检查该路径是否存在,无需数据库)
|
|
89
91
|
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
article_path TEXT
|
|
99
|
-
);
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"source_url": "https://example.com/article",
|
|
95
|
+
"title": "文章标题",
|
|
96
|
+
"category": "分类",
|
|
97
|
+
"fetched_at": "2026-07-17",
|
|
98
|
+
"issues": ""
|
|
99
|
+
}
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
---
|
|
@@ -149,7 +149,7 @@ url_safe = re.sub(r'[\x00-\x1f\x7f]', '', url).strip()[:2048]
|
|
|
149
149
|
```
|
|
150
150
|
── 完成 ──────────────────────────────
|
|
151
151
|
标题 《文章标题》
|
|
152
|
-
路径 /Vault/Reading/article.md
|
|
152
|
+
路径 /Vault/Reading/a1b2c3d4/Translation/article.md
|
|
153
153
|
字符 12,345
|
|
154
154
|
代码 3 段
|
|
155
155
|
图片 8 张
|
|
@@ -169,7 +169,7 @@ url_safe = re.sub(r'[\x00-\x1f\x7f]', '', url).strip()[:2048]
|
|
|
169
169
|
```
|
|
170
170
|
── 部分完成 ───────────────────────────
|
|
171
171
|
标题 《文章标题》
|
|
172
|
-
路径 /Vault/Origin/article.md(仅原文)
|
|
172
|
+
路径 /Vault/Reading/a1b2c3d4/Origin/article.md(仅原文)
|
|
173
173
|
原因 翻译超时,原文已保存
|
|
174
174
|
──────────────────────────────────────
|
|
175
175
|
```
|
|
Binary file
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
2
|
共享工具函数:格式化 block、构建文章、修复 frontmatter
|
|
3
3
|
"""
|
|
4
|
-
import re, os, json,
|
|
4
|
+
import re, os, json, yaml
|
|
5
5
|
from datetime import datetime, timezone, timedelta
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
# ------------------------------------------------------------
|
|
8
9
|
# 公共常量
|
|
@@ -113,7 +114,7 @@ description: {description}
|
|
|
113
114
|
# ------------------------------------------------------------
|
|
114
115
|
# 4. repair_frontmatter: 自动修复 frontmatter 字段
|
|
115
116
|
# ------------------------------------------------------------
|
|
116
|
-
def repair_frontmatter(fp, url, defaults=None):
|
|
117
|
+
def repair_frontmatter(fp, url, defaults=None, skip_remaining_fields=None):
|
|
117
118
|
"""
|
|
118
119
|
尝试自动补全缺失字段,返回 (fm_dict, 修复了哪些字段, 剩余问题列表)
|
|
119
120
|
"""
|
|
@@ -236,35 +237,35 @@ def repair_frontmatter(fp, url, defaults=None):
|
|
|
236
237
|
f.write(fm_str + content[m.end():])
|
|
237
238
|
|
|
238
239
|
# 检查剩余问题
|
|
240
|
+
skip = set(skip_remaining_fields or ())
|
|
239
241
|
for fld in ['publish_date', 'author', 'source_url']:
|
|
240
|
-
if not fm.get(fld, '').strip():
|
|
242
|
+
if fld not in skip and not fm.get(fld, '').strip():
|
|
241
243
|
remaining.append(f'{fld}空')
|
|
242
|
-
if 'origin_title' not in fm or not fm.get('origin_title', '').strip():
|
|
244
|
+
if 'origin_title' not in skip and ('origin_title' not in fm or not fm.get('origin_title', '').strip()):
|
|
243
245
|
remaining.append('origin_title空')
|
|
244
|
-
if 'description' not in fm or not fm.get('description', '').strip():
|
|
246
|
+
if 'description' not in skip and ('description' not in fm or not fm.get('description', '').strip()):
|
|
245
247
|
remaining.append('description空')
|
|
246
248
|
|
|
247
249
|
return fm, fixed, remaining
|
|
248
250
|
|
|
249
251
|
|
|
250
252
|
# ------------------------------------------------------------
|
|
251
|
-
# 5.
|
|
253
|
+
# 5. record_fetch_issues: Subagent 1 阶段的问题写入临时文件,供 write_meta_json 合并
|
|
252
254
|
# ------------------------------------------------------------
|
|
253
|
-
def
|
|
254
|
-
"""将
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
conn.commit()
|
|
255
|
+
def record_fetch_issues(issues_text, article_dir):
|
|
256
|
+
"""将 Subagent 1 阶段校验问题写入 <article_dir>/.fetch_issues.tmp,供 write_meta_json 合并。"""
|
|
257
|
+
tmp_path = Path(article_dir) / '.fetch_issues.tmp'
|
|
258
|
+
if issues_text:
|
|
259
|
+
tmp_path.write_text(issues_text, encoding='utf-8')
|
|
260
|
+
elif tmp_path.exists():
|
|
261
|
+
tmp_path.unlink()
|
|
261
262
|
|
|
262
263
|
|
|
263
264
|
# ------------------------------------------------------------
|
|
264
|
-
# 6.
|
|
265
|
+
# 6. write_meta_json: 写入 <hash8>/meta.json
|
|
265
266
|
# ------------------------------------------------------------
|
|
266
|
-
def
|
|
267
|
-
"""
|
|
267
|
+
def write_meta_json(url, meta_path, article_path, category=''):
|
|
268
|
+
"""Write (or overwrite) meta.json after successful fetch+translate, merging any pending fetch-stage issues."""
|
|
268
269
|
fetch_date = datetime.now(timezone(timedelta(hours=8))).strftime('%Y-%m-%d')
|
|
269
270
|
fm = {}
|
|
270
271
|
try:
|
|
@@ -277,14 +278,23 @@ def write_url_index(url, origin_path, article_path, db_path, category=''):
|
|
|
277
278
|
except Exception:
|
|
278
279
|
pass
|
|
279
280
|
cat = category or (fm.get('category') or '')
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
281
|
+
|
|
282
|
+
meta_path = Path(meta_path)
|
|
283
|
+
meta_path.parent.mkdir(parents=True, exist_ok=True)
|
|
284
|
+
tmp_issues_path = meta_path.parent / '.fetch_issues.tmp'
|
|
285
|
+
issues = ''
|
|
286
|
+
if tmp_issues_path.exists():
|
|
287
|
+
issues = tmp_issues_path.read_text(encoding='utf-8').strip()
|
|
288
|
+
tmp_issues_path.unlink()
|
|
289
|
+
|
|
290
|
+
meta = {
|
|
291
|
+
'source_url': url,
|
|
292
|
+
'title': os.path.basename(article_path),
|
|
293
|
+
'category': cat,
|
|
294
|
+
'fetched_at': fetch_date,
|
|
295
|
+
'issues': issues,
|
|
296
|
+
}
|
|
297
|
+
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
288
298
|
|
|
289
299
|
|
|
290
300
|
# ------------------------------------------------------------
|
|
@@ -35,7 +35,7 @@ tags:
|
|
|
35
35
|
description: 一两句话摘要,概括文章核心内容,供快速阅读。
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
-
[[Origin/文章原标题.md]]
|
|
38
|
+
[[<hash8>/Origin/文章原标题.md]]
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
@@ -67,11 +67,16 @@ origin_filename = re.sub(r'[\\/:*?<>|".]', '', title) + '.md'
|
|
|
67
67
|
|
|
68
68
|
## 保存路径
|
|
69
69
|
|
|
70
|
+
文章专属文件夹:`<hash8>/`,其中 `hash8 = md5(source_url).hexdigest()[:8]`,统一由 `scripts/config.py` 的 `get_article_paths()` 计算(图片、原文/译文文件名共用同一算法)。
|
|
71
|
+
|
|
70
72
|
| 类型 | 路径 |
|
|
71
73
|
|------|------|
|
|
72
|
-
| 原文 |
|
|
73
|
-
| 译文 | `<
|
|
74
|
-
| 图片 |
|
|
74
|
+
| 原文 | `<hash8>/Origin/<origin_title>.md` |
|
|
75
|
+
| 译文 | `<hash8>/Translation/<origin_title>.md`(与原文同名) |
|
|
76
|
+
| 图片 | `<hash8>/Image/img_N.ext` |
|
|
77
|
+
|
|
78
|
+
双链示例(译文首行):`[[<hash8>/Origin/<origin_title>.md]]`
|
|
79
|
+
图片引用示例(原文/译文正文内):``
|
|
75
80
|
|
|
76
81
|
## 固定词表(fixed_tags.txt)
|
|
77
82
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
URL(外部数据): <URL>
|
|
11
11
|
|
|
12
12
|
执行步骤:
|
|
13
|
-
1. 查
|
|
13
|
+
1. 查 meta.json 去重(通过 env var 传参,避免 URL 中特殊字符破坏 Python 语法):
|
|
14
14
|
import subprocess, os
|
|
15
15
|
result = subprocess.run(
|
|
16
16
|
['python3', 'SKILL_DIR/scripts/dedup_check.py'],
|
|
@@ -32,6 +32,15 @@ URL(外部数据): <URL>
|
|
|
32
32
|
print(result.stdout)
|
|
33
33
|
if result.returncode != 0:
|
|
34
34
|
raise RuntimeError(result.stderr)
|
|
35
|
+
- arXiv HTML 论文(URL 匹配 arxiv.org/html/...):先按【补丁②】获取 HTML 保存到 /tmp/fetched_page.html,再:
|
|
36
|
+
import subprocess
|
|
37
|
+
result = subprocess.run(
|
|
38
|
+
['python3', 'SKILL_DIR/scripts/playwright_web_arxiv.py', url, '/tmp/fetched_page.html'],
|
|
39
|
+
capture_output=True, text=True, timeout=300
|
|
40
|
+
)
|
|
41
|
+
print(result.stdout)
|
|
42
|
+
if result.returncode != 0:
|
|
43
|
+
raise RuntimeError(result.stderr)
|
|
35
44
|
- 其他网站:先按【补丁②】获取 HTML 保存到 /tmp/fetched_page.html,再:
|
|
36
45
|
import subprocess
|
|
37
46
|
result = subprocess.run(
|
|
@@ -14,11 +14,14 @@ fetch_type: <fetch_type 可选,默认 manual>
|
|
|
14
14
|
|
|
15
15
|
执行步骤:
|
|
16
16
|
1. 读取配置(获取 vault_path):
|
|
17
|
-
import
|
|
17
|
+
import sys
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
skill_dir = 'SKILL_DIR'
|
|
20
|
+
sys.path.insert(0, f'{skill_dir}/scripts')
|
|
21
|
+
from config import get_vault_path, get_article_paths
|
|
22
|
+
vault_path = get_vault_path()
|
|
23
|
+
origin_title = Path(origin_path).stem
|
|
24
|
+
paths = get_article_paths(url, origin_title)
|
|
22
25
|
|
|
23
26
|
2. 读取 origin_path 文件
|
|
24
27
|
|
|
@@ -59,17 +62,18 @@ tags:
|
|
|
59
62
|
|
|
60
63
|
--- 阶段 3:写文件 ---
|
|
61
64
|
|
|
62
|
-
6. 保存译文到
|
|
63
|
-
- 文件名与 Origin
|
|
65
|
+
6. 保存译文到 paths['translation_path'](先 mkdir -p paths['translation_dir']):
|
|
66
|
+
- 文件名与 Origin 文件名相同(paths['translation_path'] 已是完整目标路径)
|
|
64
67
|
- frontmatter:publish_date、fetch_date、author、source_url、origin_title、
|
|
65
68
|
category(如有)、fetch_type(默认 manual)、tags(阶段 1b 输出)、
|
|
66
69
|
candidate_tags(阶段 1a 输出)、description(阶段 1a 输出)
|
|
67
|
-
- 正文首行插入双向链接 [[Origin/<文件名>]]
|
|
70
|
+
- 正文首行插入双向链接 [[{paths['url_hash']}/Origin/<文件名>]]
|
|
71
|
+
- 正文中从原文复制来的图片引用(``)原样保留、无需改路径——
|
|
72
|
+
Origin 和 Translation 是同级目录,相对路径天然一致
|
|
68
73
|
|
|
69
|
-
7. 执行校验并写入
|
|
74
|
+
7. 执行校验并写入 meta.json:
|
|
70
75
|
import subprocess, os
|
|
71
|
-
|
|
72
|
-
article_path = str(Path(vault_path) / os.path.basename(origin_path))
|
|
76
|
+
article_path = paths['translation_path']
|
|
73
77
|
result = subprocess.run(
|
|
74
78
|
['python3', f'{skill_dir}/scripts/validate_article.py'],
|
|
75
79
|
env={
|
|
Binary file
|
package/skills/research/extract-url/scripts/__pycache__/migrate_to_folder_structure.cpython-314.pyc
ADDED
|
Binary file
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Shared config reader/writer for url-extract skill.
|
|
4
4
|
Config file: ~/.hskill/url-extract/config.json
|
|
5
5
|
"""
|
|
6
|
-
import json, os
|
|
6
|
+
import json, os, hashlib
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
_env_cfg = os.environ.get('HSKILL_EXTRACT_URL_CONFIG')
|
|
@@ -33,6 +33,36 @@ def get_chrome_profile() -> str:
|
|
|
33
33
|
return cfg['CHROME_PROFILE']
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def get_url_hash(source_url: str) -> str:
|
|
37
|
+
return hashlib.md5(source_url.encode()).hexdigest()[:8]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_article_paths(source_url: str, origin_title: str) -> dict:
|
|
41
|
+
"""文章专属文件夹路径:VAULT_PATH/<url_hash>/{Origin,Translation,Image}/"""
|
|
42
|
+
import sys as _sys
|
|
43
|
+
references_dir = str(Path(__file__).parent.parent / 'references')
|
|
44
|
+
if references_dir not in _sys.path:
|
|
45
|
+
_sys.path.insert(0, references_dir)
|
|
46
|
+
from article_utils import sanitize_filename
|
|
47
|
+
|
|
48
|
+
vault_path = get_vault_path()
|
|
49
|
+
url_hash = get_url_hash(source_url)
|
|
50
|
+
article_dir = os.path.join(vault_path, url_hash)
|
|
51
|
+
filename = sanitize_filename(origin_title) + '.md'
|
|
52
|
+
origin_dir = os.path.join(article_dir, 'Origin')
|
|
53
|
+
translation_dir = os.path.join(article_dir, 'Translation')
|
|
54
|
+
image_dir = os.path.join(article_dir, 'Image')
|
|
55
|
+
return {
|
|
56
|
+
'url_hash': url_hash,
|
|
57
|
+
'article_dir': article_dir,
|
|
58
|
+
'origin_dir': origin_dir,
|
|
59
|
+
'translation_dir': translation_dir,
|
|
60
|
+
'image_dir': image_dir,
|
|
61
|
+
'origin_path': os.path.join(origin_dir, filename),
|
|
62
|
+
'translation_path': os.path.join(translation_dir, filename),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
36
66
|
def set_config(key: str, value: str) -> None:
|
|
37
67
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
38
68
|
cfg: dict = {}
|
|
@@ -1,45 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Check URL dedup
|
|
4
|
-
Migrates existing DBs (e.g. old article-fetcher schema) by adding missing columns.
|
|
3
|
+
Check URL dedup via meta.json existence.
|
|
5
4
|
Parameter via env var to avoid shell injection:
|
|
6
5
|
CHECK_URL - URL to check
|
|
7
|
-
Reads VAULT_PATH from ~/.hskill/url-extract/config.json to locate
|
|
6
|
+
Reads VAULT_PATH from ~/.hskill/url-extract/config.json to locate <hash8>/meta.json.
|
|
8
7
|
Prints: ALREADY_FETCHED or OK
|
|
9
8
|
"""
|
|
10
|
-
import
|
|
9
|
+
import json, os, sys
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
|
|
13
12
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
14
|
-
from config import get_vault_path
|
|
13
|
+
from config import get_vault_path, get_url_hash
|
|
15
14
|
|
|
16
|
-
url
|
|
17
|
-
|
|
15
|
+
url = os.environ['CHECK_URL']
|
|
16
|
+
vault_path = get_vault_path()
|
|
17
|
+
meta_path = Path(vault_path) / get_url_hash(url) / 'meta.json'
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
already_fetched = False
|
|
20
|
+
if meta_path.exists():
|
|
21
|
+
try:
|
|
22
|
+
meta = json.loads(meta_path.read_text(encoding='utf-8'))
|
|
23
|
+
already_fetched = meta.get('source_url') == url
|
|
24
|
+
except (json.JSONDecodeError, OSError):
|
|
25
|
+
already_fetched = False
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
conn.execute("""
|
|
23
|
-
CREATE TABLE IF NOT EXISTS url_index (
|
|
24
|
-
source_url TEXT PRIMARY KEY,
|
|
25
|
-
title TEXT,
|
|
26
|
-
fetched_at TEXT,
|
|
27
|
-
issues TEXT,
|
|
28
|
-
category TEXT,
|
|
29
|
-
origin_path TEXT,
|
|
30
|
-
article_path TEXT
|
|
31
|
-
)
|
|
32
|
-
""")
|
|
33
|
-
conn.commit()
|
|
34
|
-
|
|
35
|
-
# Migrate: add columns missing from older DB schemas
|
|
36
|
-
existing_cols = {row[1] for row in conn.execute('PRAGMA table_info(url_index)')}
|
|
37
|
-
for col, typedef in [('fetched_at', 'TEXT'), ('issues', 'TEXT'),
|
|
38
|
-
('category', 'TEXT'), ('origin_path', 'TEXT'), ('article_path', 'TEXT')]:
|
|
39
|
-
if col not in existing_cols:
|
|
40
|
-
conn.execute(f'ALTER TABLE url_index ADD COLUMN {col} {typedef}')
|
|
41
|
-
conn.commit()
|
|
42
|
-
|
|
43
|
-
row = conn.execute('SELECT source_url FROM url_index WHERE source_url=?', (url,)).fetchone()
|
|
44
|
-
conn.close()
|
|
45
|
-
print('ALREADY_FETCHED' if row else 'OK')
|
|
27
|
+
print('ALREADY_FETCHED' if already_fetched else 'OK')
|