harveyz-skill 0.1.2 → 0.2.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/bin/cli.js +116 -44
- package/lib/bundles.js +97 -22
- package/lib/installer.js +126 -5
- package/lib/targets.js +1 -0
- package/lib/vars.js +34 -0
- package/package.json +7 -3
- package/skills/harness/diataxis-docs/SKILL.md +128 -0
- package/skills/harness/diataxis-docs/references/custom-categories.md +32 -0
- package/skills/harness/diataxis-docs/references/index-template.md +29 -0
- package/skills/web-fetch/article-fetcher/SKILL.md +625 -0
- package/skills/web-fetch/article-fetcher/references/article_utils.py +225 -0
- package/skills/web-fetch/article-fetcher/references/file-format.md +73 -0
- package/skills/web-fetch/article-fetcher/scripts/import_reading.py +75 -0
- package/skills/web-fetch/article-fetcher/scripts/init_db.py +33 -0
- package/skills/web-fetch/article-fetcher/scripts/url-index.db +0 -0
- package/skills/web-fetch/article-fetcher/scripts/url-index.db.bak +0 -0
- package/skills/web-fetch/article-fetcher/vars.json +12 -0
- package/skills/writing/mermaid-diagram/SKILL.md +220 -0
- package/skills/writing/mermaid-diagram/references/color-templates.md +75 -0
- package/skills/writing/mermaid-diagram/references/flowchart.md +88 -0
- package/skills/writing/mermaid-diagram/references/gantt-timeline.md +62 -0
- package/skills/writing/mermaid-diagram/references/other-charts.md +55 -0
- package/skills/writing/mermaid-diagram/references/sequence.md +143 -0
- package/skills/writing/mermaid-diagram/references/statediagram.md +54 -0
- package/skills/writing/mermaid-diagram/scripts/check_mermaid.py +84 -0
- package/skills-index.json +32 -0
- package/tools/p-launch/p-launch.sh +275 -0
- package/tools/p-launch/tests/e2e.bats +164 -0
- package/tools/p-launch/tests/unit.bats +151 -0
- package/tools/p-launch/vars.json +9 -0
- package/tools/p-launch/zshrc.snippet +5 -0
- package/bundles.json +0 -24
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
共享工具函数:格式化 block、构建文章、修复 frontmatter
|
|
3
|
+
"""
|
|
4
|
+
import re, os, json, sqlite3
|
|
5
|
+
from datetime import datetime, timezone, timedelta
|
|
6
|
+
|
|
7
|
+
# ------------------------------------------------------------
|
|
8
|
+
# 公共常量
|
|
9
|
+
# ------------------------------------------------------------
|
|
10
|
+
HEADING_PREFIX = {'h1': '# ', 'h2': '## ', 'h3': '### ', 'h4': '#### '}
|
|
11
|
+
IMAGE_BASE_URL = 'Image/'
|
|
12
|
+
|
|
13
|
+
FETCH_DATE = datetime.now(timezone(timedelta(hours=8))).strftime('%Y-%m-%d')
|
|
14
|
+
|
|
15
|
+
# ------------------------------------------------------------
|
|
16
|
+
# 1. format_block: 将单个 block 转为 Markdown 字符串
|
|
17
|
+
# ------------------------------------------------------------
|
|
18
|
+
def format_block(block):
|
|
19
|
+
"""将 {tag, content} 转为 Markdown 格式"""
|
|
20
|
+
tag = block.get('tag', 'p')
|
|
21
|
+
content = block.get('content', '')
|
|
22
|
+
if tag in HEADING_PREFIX:
|
|
23
|
+
return HEADING_PREFIX[tag] + content
|
|
24
|
+
if tag == 'pre':
|
|
25
|
+
return f'```\n{content}\n```'
|
|
26
|
+
if tag == 'li':
|
|
27
|
+
return f'- {content}'
|
|
28
|
+
# 内容以 # 开头且含 / 或 --- = SKILL.md 代码块
|
|
29
|
+
if content.startswith('# ') and ('/' in content or '---' in content):
|
|
30
|
+
return f'```\n{content}\n```'
|
|
31
|
+
return content
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ------------------------------------------------------------
|
|
35
|
+
# 2. infer_ext: 从 URL 或 Content-Type 推断图片扩展名
|
|
36
|
+
# ------------------------------------------------------------
|
|
37
|
+
def infer_ext(url, content_type=''):
|
|
38
|
+
"""从 URL 或 Content-Type 推断图片扩展名"""
|
|
39
|
+
ext_map = {'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif', 'image/webp': '.webp'}
|
|
40
|
+
if content_type:
|
|
41
|
+
return ext_map.get(content_type, '.jpg')
|
|
42
|
+
url_lower = url.lower()
|
|
43
|
+
for ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
|
|
44
|
+
if ext in url_lower:
|
|
45
|
+
return '.jpg' if ext == '.jpeg' else ext
|
|
46
|
+
return '.jpg'
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ------------------------------------------------------------
|
|
50
|
+
# 3. build_article_from_json: 从统一 JSON 构建文章
|
|
51
|
+
# ------------------------------------------------------------
|
|
52
|
+
def build_article_from_json(json_path, title, source_url, origin_filename,
|
|
53
|
+
tags, description, author='', publish_date='',
|
|
54
|
+
origin_path=None, article_path=None):
|
|
55
|
+
"""
|
|
56
|
+
从 article_combined.json 构建原文/译文文件。
|
|
57
|
+
|
|
58
|
+
JSON 结构:
|
|
59
|
+
blocks: [{tag, content, blockIndex}]
|
|
60
|
+
images: [{src, alt, afterBlock, filename}]
|
|
61
|
+
"""
|
|
62
|
+
with open(json_path, encoding='utf-8') as f:
|
|
63
|
+
data = json.load(f)
|
|
64
|
+
|
|
65
|
+
blocks = data.get('blocks', [])
|
|
66
|
+
images = data.get('images') or data.get('imageBlocks') or [] # 兼容 imageBlocks/image 两种 key
|
|
67
|
+
|
|
68
|
+
# 按 afterBlock 插入图片
|
|
69
|
+
body_units = []
|
|
70
|
+
# afterBlock == -1 的图片插入文章最开头
|
|
71
|
+
pre_imgs = [f''
|
|
72
|
+
for img in images if img.get('afterBlock') == -1]
|
|
73
|
+
if pre_imgs:
|
|
74
|
+
body_units.append('\n'.join(pre_imgs))
|
|
75
|
+
|
|
76
|
+
for i, block in enumerate(blocks):
|
|
77
|
+
unit_parts = [format_block(block)]
|
|
78
|
+
for img in images:
|
|
79
|
+
if img.get('afterBlock') == i:
|
|
80
|
+
unit_parts.append(f'')
|
|
81
|
+
body_units.append('\n'.join(unit_parts))
|
|
82
|
+
|
|
83
|
+
body = '\n\n'.join(body_units)
|
|
84
|
+
|
|
85
|
+
tags_str = ' - ' + '\n - '.join(tags)
|
|
86
|
+
|
|
87
|
+
fm = f"""---
|
|
88
|
+
publish_date: {publish_date}
|
|
89
|
+
fetch_date: {FETCH_DATE}
|
|
90
|
+
author: {author}
|
|
91
|
+
source_url: {source_url}
|
|
92
|
+
origin_title: "{origin_filename.replace('.md', '')}"
|
|
93
|
+
tags:
|
|
94
|
+
{tags_str}
|
|
95
|
+
description: {description}
|
|
96
|
+
---"""
|
|
97
|
+
|
|
98
|
+
content = f"""{fm}
|
|
99
|
+
|
|
100
|
+
[[Origin/{origin_filename}]]
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
# {title}
|
|
105
|
+
|
|
106
|
+
{body}
|
|
107
|
+
"""
|
|
108
|
+
return content
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ------------------------------------------------------------
|
|
112
|
+
# 4. repair_frontmatter: 自动修复 frontmatter 字段
|
|
113
|
+
# ------------------------------------------------------------
|
|
114
|
+
def repair_frontmatter(fp, url, defaults=None):
|
|
115
|
+
"""
|
|
116
|
+
尝试自动补全缺失字段,返回 (fm_dict, 修复了哪些字段, 剩余问题列表)
|
|
117
|
+
"""
|
|
118
|
+
defaults = defaults or {}
|
|
119
|
+
with open(fp, encoding='utf-8') as f:
|
|
120
|
+
content = f.read()
|
|
121
|
+
|
|
122
|
+
m = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
|
|
123
|
+
if not m:
|
|
124
|
+
return {}, [], ['无frontmatter']
|
|
125
|
+
|
|
126
|
+
fm = {}
|
|
127
|
+
for line in m.group(1).split('\n'):
|
|
128
|
+
if ':' in line:
|
|
129
|
+
k, v = line.split(':', 1)
|
|
130
|
+
fm[k.strip()] = v.strip()
|
|
131
|
+
|
|
132
|
+
fixed, remaining = [], []
|
|
133
|
+
|
|
134
|
+
# 自动补全 defaults
|
|
135
|
+
for field, value in defaults.items():
|
|
136
|
+
if not fm.get(field, '').strip():
|
|
137
|
+
fm[field] = value
|
|
138
|
+
fixed.append(f'{field}={value}')
|
|
139
|
+
|
|
140
|
+
# tags 格式修复:逗号分隔 → YAML 列表
|
|
141
|
+
tags_raw = fm.get('tags', '')
|
|
142
|
+
if tags_raw and ',' in tags_raw and not tags_raw.startswith('-'):
|
|
143
|
+
raw_list = [t.strip() for t in tags_raw.split(',') if t.strip()]
|
|
144
|
+
def _norm_tag(t):
|
|
145
|
+
if re.search(r'[\u4e00-\u9fff]', t):
|
|
146
|
+
return t.lower()
|
|
147
|
+
return t.lower().replace(' ', '-')
|
|
148
|
+
fm['tags'] = '\n - ' + '\n - '.join(_norm_tag(t) for t in raw_list)
|
|
149
|
+
fixed.append('tags=YAML列表')
|
|
150
|
+
|
|
151
|
+
# tags 大写修复
|
|
152
|
+
if fm.get('tags', '').startswith('-'):
|
|
153
|
+
tag_lines = [l for l in fm['tags'].strip().split('\n') if l.strip().startswith('-')]
|
|
154
|
+
new_lines = []
|
|
155
|
+
for tl in tag_lines:
|
|
156
|
+
tag_val = tl.lstrip('- ').strip()
|
|
157
|
+
if tag_val and tag_val != tag_val.lower():
|
|
158
|
+
new_lines.append(f' - {tag_val.lower()}')
|
|
159
|
+
fixed.append(f'tag-lowercase={tag_val}')
|
|
160
|
+
else:
|
|
161
|
+
new_lines.append(tl)
|
|
162
|
+
fm['tags'] = '\n'.join(new_lines)
|
|
163
|
+
|
|
164
|
+
# 写回文件
|
|
165
|
+
fm_lines = [f'{k}: {v}' for k, v in fm.items()]
|
|
166
|
+
fm_str = '---\n' + '\n'.join(fm_lines) + '\n---\n'
|
|
167
|
+
with open(fp, 'w', encoding='utf-8') as f:
|
|
168
|
+
f.write(fm_str + content[m.end():])
|
|
169
|
+
|
|
170
|
+
# 检查剩余问题
|
|
171
|
+
for fld in ['publish_date', 'author', 'source_url']:
|
|
172
|
+
if not fm.get(fld, '').strip():
|
|
173
|
+
remaining.append(f'{fld}空')
|
|
174
|
+
if 'origin_title' not in fm or not fm.get('origin_title', '').strip():
|
|
175
|
+
remaining.append('origin_title空')
|
|
176
|
+
if 'description' not in fm or not fm.get('description', '').strip():
|
|
177
|
+
remaining.append('description空')
|
|
178
|
+
|
|
179
|
+
return fm, fixed, remaining
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ------------------------------------------------------------
|
|
183
|
+
# 5. record_issues: 写入 issues 字段
|
|
184
|
+
# ------------------------------------------------------------
|
|
185
|
+
def record_issues(url, issues_text, db_path=None):
|
|
186
|
+
"""将 issues 写入 SQLite"""
|
|
187
|
+
if db_path is None:
|
|
188
|
+
db_path = os.path.expanduser(
|
|
189
|
+
'{{SKILL_DIR}}/scripts/url-index.db'
|
|
190
|
+
)
|
|
191
|
+
conn = sqlite3.connect(db_path)
|
|
192
|
+
conn.execute('UPDATE url_index SET issues=? WHERE source_url=?',
|
|
193
|
+
(issues_text, url))
|
|
194
|
+
conn.commit()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------
|
|
198
|
+
# 6. validate_and_repair: 完整校验 + 自动修复流程
|
|
199
|
+
# ------------------------------------------------------------
|
|
200
|
+
def validate_and_repair(origin_path, article_path, url, defaults=None):
|
|
201
|
+
"""
|
|
202
|
+
执行保存后校验,自动修复可修复的问题。
|
|
203
|
+
返回 (是否通过, fixed列表, remaining列表)
|
|
204
|
+
"""
|
|
205
|
+
defaults = defaults or {}
|
|
206
|
+
defaults.setdefault('fetch_date', FETCH_DATE)
|
|
207
|
+
|
|
208
|
+
fm_orig, fixed_orig, rem_orig = repair_frontmatter(origin_path, url, defaults)
|
|
209
|
+
fm_art, fixed_art, rem_art = repair_frontmatter(article_path, url, defaults)
|
|
210
|
+
|
|
211
|
+
all_fixed = fixed_orig + fixed_art
|
|
212
|
+
all_remaining = [f'[Origin] {r}' for r in rem_orig] + [f'[Article] {r}' for r in rem_art]
|
|
213
|
+
|
|
214
|
+
passed = len(all_remaining) == 0
|
|
215
|
+
return passed, all_fixed, all_remaining
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ------------------------------------------------------------
|
|
219
|
+
# 7. sanitize_filename: 文件名清理
|
|
220
|
+
# ------------------------------------------------------------
|
|
221
|
+
def sanitize_filename(name):
|
|
222
|
+
"""移除文件名中的特殊字符"""
|
|
223
|
+
for ch in ['\\', '/', '*', '?', '<', '>', '|', ':', '"']:
|
|
224
|
+
name = name.replace(ch, '')
|
|
225
|
+
return name.lstrip('.')
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 文件格式模板
|
|
2
|
+
|
|
3
|
+
## 原文文件格式
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
---
|
|
7
|
+
publish_date: YYYY-MM-DD
|
|
8
|
+
fetch_date: YYYY-MM-DD
|
|
9
|
+
author: 作者名
|
|
10
|
+
source_url: https://xxx.com/article
|
|
11
|
+
origin_title: Original Title
|
|
12
|
+
category: Category(可选,来源列表页抓取的分类标签)
|
|
13
|
+
tags:
|
|
14
|
+
- tag1
|
|
15
|
+
- tag2
|
|
16
|
+
description: 一两句话摘要
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
文章正文...
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 译文文件格式
|
|
23
|
+
|
|
24
|
+
```markdown
|
|
25
|
+
---
|
|
26
|
+
publish_date: YYYY-MM-DD
|
|
27
|
+
fetch_date: YYYY-MM-DD
|
|
28
|
+
author: 作者名
|
|
29
|
+
source_url: https://xxx.com/article
|
|
30
|
+
origin_title: Original Title
|
|
31
|
+
category: Category(可选,来源列表页抓取的分类标签)
|
|
32
|
+
tags:
|
|
33
|
+
- tag1
|
|
34
|
+
- tag2
|
|
35
|
+
description: 一两句话摘要,概括文章核心内容,供快速阅读。
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
[[Origin/文章原标题.md]]
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
翻译后的正文...
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## frontmatter 字段说明
|
|
46
|
+
|
|
47
|
+
| 字段 | 原文文件 | 译文文件 | 来源 |
|
|
48
|
+
|------|----------|----------|------|
|
|
49
|
+
| `publish_date` | ✅ 必须 | ✅ 必须 | 页面显示日期,ISO 8601 格式取前 10 位(YYYY-MM-DD) |
|
|
50
|
+
| `fetch_date` | ✅ 必须 | ✅ 必须 | 抓取时间,格式 YYYY-MM-DD(北京时间) |
|
|
51
|
+
| `author` | ✅ 必须 | ✅ 必须 | 页面显示作者,若无则留空 |
|
|
52
|
+
| `source_url` | ✅ 必须 | ✅ 必须 | 当前抓取的 URL |
|
|
53
|
+
| `origin_title` | — | ✅ 必须 | 原文标题,用于 obsidian 双向链接 |
|
|
54
|
+
| `category` | 可选 | 可选 | 来源列表页抓取的分类标签,由 cron 任务注入,article-fetcher skill 只做透传 |
|
|
55
|
+
| `tags` | — | ✅ 必须 | YAML 列表格式,非逗号分隔 |
|
|
56
|
+
| `description` | — | ✅ 必须 | 一两句话摘要,供快速阅读,基于译文内容提取 |
|
|
57
|
+
|
|
58
|
+
## 文件命名规则
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import re
|
|
62
|
+
origin_filename = re.sub(r'[\\/:*?<>|".]', '', title) + '.md'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
移除字符:`\ / : * ? < > | " .`(与 SKILL.md 保持一致)。
|
|
66
|
+
|
|
67
|
+
## 保存路径
|
|
68
|
+
|
|
69
|
+
| 类型 | 路径 |
|
|
70
|
+
|------|------|
|
|
71
|
+
| 原文 | `Origin/<origin_title>.md` |
|
|
72
|
+
| 译文 | `<title>.md`(无 Origin 子文件夹) |
|
|
73
|
+
| 图片 | `Image/<url_hash>_img_N.ext` |
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
一次性脚本:将 Reading 目录下(不含子文件夹)的 Markdown 文档
|
|
4
|
+
frontmatter 信息解析后存入 url-index.db。
|
|
5
|
+
"""
|
|
6
|
+
import sqlite3, os, re, glob
|
|
7
|
+
|
|
8
|
+
BASE_DIR = "{{VAULT_PATH}}"
|
|
9
|
+
DB_PATH = os.path.expanduser("{{SKILL_DIR}}/scripts/url-index.db")
|
|
10
|
+
|
|
11
|
+
def parse_frontmatter(content):
|
|
12
|
+
m = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
|
|
13
|
+
if not m:
|
|
14
|
+
return {}
|
|
15
|
+
fm = {}
|
|
16
|
+
for line in m.group(1).split('\n'):
|
|
17
|
+
if ':' in line:
|
|
18
|
+
key, val = line.split(':', 1)
|
|
19
|
+
fm[key.strip()] = val.strip()
|
|
20
|
+
return fm
|
|
21
|
+
|
|
22
|
+
def extract_title(content, filepath):
|
|
23
|
+
# 先从 frontmatter 读 title
|
|
24
|
+
fm = parse_frontmatter(content)
|
|
25
|
+
if fm.get('title'):
|
|
26
|
+
return fm['title']
|
|
27
|
+
# 再从文件名
|
|
28
|
+
name = os.path.splitext(os.path.basename(filepath))[0]
|
|
29
|
+
return name
|
|
30
|
+
|
|
31
|
+
conn = sqlite3.connect(DB_PATH)
|
|
32
|
+
files = glob.glob(os.path.join(BASE_DIR, "*.md"))
|
|
33
|
+
|
|
34
|
+
inserted = 0
|
|
35
|
+
skipped = 0
|
|
36
|
+
errors = []
|
|
37
|
+
|
|
38
|
+
for fp in files:
|
|
39
|
+
name = os.path.basename(fp)
|
|
40
|
+
try:
|
|
41
|
+
with open(fp, encoding='utf-8') as f:
|
|
42
|
+
content = f.read()
|
|
43
|
+
except Exception as e:
|
|
44
|
+
errors.append(f"{name}: 读取失败 {e}")
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
fm = parse_frontmatter(content)
|
|
48
|
+
|
|
49
|
+
source_url = fm.get('source_url', '')
|
|
50
|
+
publish_date = fm.get('publish_date', '')
|
|
51
|
+
author = fm.get('author', '')
|
|
52
|
+
origin_title = fm.get('origin_title', '')
|
|
53
|
+
title = extract_title(content, fp)
|
|
54
|
+
|
|
55
|
+
if not source_url:
|
|
56
|
+
# 无 source_url 视为无法去重,跳过
|
|
57
|
+
skipped += 1
|
|
58
|
+
print(f" 跳过(无 source_url): {name}")
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
conn.execute("""
|
|
63
|
+
INSERT OR IGNORE INTO url_index
|
|
64
|
+
(source_url, title, origin_title, author, publish_date)
|
|
65
|
+
VALUES (?, ?, ?, ?, ?)
|
|
66
|
+
""", (source_url, title, origin_title, author, publish_date))
|
|
67
|
+
inserted += 1
|
|
68
|
+
print(f" ✓ {name}")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
errors.append(f"{name}: 写入失败 {e}")
|
|
71
|
+
|
|
72
|
+
conn.commit()
|
|
73
|
+
print(f"\n完成:插入 {inserted} 条,跳过 {skipped} 条,错误 {len(errors)} 条")
|
|
74
|
+
for e in errors:
|
|
75
|
+
print(f" ✗ {e}")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""初始化 SQLite url_index 表"""
|
|
3
|
+
import sqlite3, os
|
|
4
|
+
|
|
5
|
+
DB_PATH = os.path.expanduser(
|
|
6
|
+
'{{SKILL_DIR}}/scripts/url-index.db'
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
# 确保目录存在
|
|
10
|
+
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
11
|
+
|
|
12
|
+
conn = sqlite3.connect(DB_PATH)
|
|
13
|
+
conn.execute("""
|
|
14
|
+
CREATE TABLE IF NOT EXISTS url_index (
|
|
15
|
+
source_url TEXT PRIMARY KEY,
|
|
16
|
+
title TEXT,
|
|
17
|
+
origin_title TEXT,
|
|
18
|
+
author TEXT,
|
|
19
|
+
publish_date TEXT,
|
|
20
|
+
fetch_date TEXT,
|
|
21
|
+
tags TEXT,
|
|
22
|
+
description TEXT,
|
|
23
|
+
issues TEXT,
|
|
24
|
+
origin_path TEXT,
|
|
25
|
+
article_path TEXT
|
|
26
|
+
)
|
|
27
|
+
""")
|
|
28
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_publish_date ON url_index(publish_date)")
|
|
29
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_author ON url_index(author)")
|
|
30
|
+
conn.commit()
|
|
31
|
+
conn.close()
|
|
32
|
+
|
|
33
|
+
print(f"✅ url_index 表初始化完成: {DB_PATH}")
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "VAULT_PATH",
|
|
4
|
+
"description": "Obsidian Reading 目录完整路径(例如 /Users/you/Vault/Product/Reading)",
|
|
5
|
+
"default": "{{HOME}}/Vault/Product/Reading"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"name": "SKILL_DIR",
|
|
9
|
+
"description": "article-fetcher skill 安装后的完整路径(用于定位辅助脚本和数据库文件)",
|
|
10
|
+
"default": "{{HOME}}/.claude/skills/article-fetcher"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mermaid-diagram
|
|
3
|
+
description: >
|
|
4
|
+
Mermaid 专业作图标准。只要用户提到"画图"、"mermaid"、"流程图"、"产业链"、
|
|
5
|
+
"板块地图"、"时序图"、"序列图"、"状态机"、"甘特图"、"象限图"、"思维导图"、
|
|
6
|
+
"接口调用"、"系统交互",或需要在 Markdown 文档中嵌入任何 Mermaid 图表,
|
|
7
|
+
立即调用本 skill。支持 flowchart / sequenceDiagram / stateDiagram / timeline /
|
|
8
|
+
gantt / quadrantChart / mindmap 全类型。
|
|
9
|
+
user_invocable: true
|
|
10
|
+
version: "1.3.0"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Mermaid 专业作图标准
|
|
14
|
+
|
|
15
|
+
> **按需读取对应文件,不要一次性全部读入:**
|
|
16
|
+
> - flowchart / 产业链 / 板块地图 → `references/flowchart.md` + `references/color-templates.md`
|
|
17
|
+
> - sequenceDiagram / 时序图 / 接口调用 → `references/sequence.md`
|
|
18
|
+
> - stateDiagram / 状态机 / 周期图 → `references/statediagram.md`
|
|
19
|
+
> - gantt / timeline → `references/gantt-timeline.md`
|
|
20
|
+
> - quadrantChart / mindmap → `references/other-charts.md`
|
|
21
|
+
> - Python 批量检测 → `scripts/check_mermaid.py`(直接运行,无需读入)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 1. 图类型选择矩阵(最关键一步)
|
|
26
|
+
|
|
27
|
+
### 按语义选型
|
|
28
|
+
|
|
29
|
+
| 要表达的语义 | 正确图类型 | 错误选择 | 理由 |
|
|
30
|
+
|------------|----------|---------|------|
|
|
31
|
+
| 产业链 / 供应链(有方向的流动) | `flowchart TD` | `mindmap` | mindmap 是放射状从属关系,无法表达方向性流动 |
|
|
32
|
+
| 层级树形结构(无流向) | `mindmap` | `flowchart` | mindmap 的分支布局更自然 |
|
|
33
|
+
| 循环状态转换 | `stateDiagram-v2` | `flowchart` | stateDiagram 语义最精确 |
|
|
34
|
+
| 时间轴事件 | `timeline` | `gantt` | timeline 按时间节点列举,无时段重叠 |
|
|
35
|
+
| 并行时间段 / 甘特 | `gantt` | `timeline` | gantt 能展示多条目时间段重叠 |
|
|
36
|
+
| 二维象限定位 | `quadrantChart` | 自制坐标 | 原生支持,配置简单 |
|
|
37
|
+
| 系统 / 服务间有时序的调用链 | `sequenceDiagram` | `flowchart` | sequenceDiagram 原生支持激活框、条件块、并行 |
|
|
38
|
+
| 资金 / 数据流(有环路) | `flowchart TD` | `sequenceDiagram` | flowchart 支持循环箭头 |
|
|
39
|
+
| 板块地图(股票/行业) | `flowchart TD` + subgraph | `mindmap` / 表格 | subgraph 支持分组+颜色+箭头 |
|
|
40
|
+
| 时间轮动 / 波浪 | `timeline` | `graph LR` | timeline 语义直接对应时序 |
|
|
41
|
+
|
|
42
|
+
### 图类型速查
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
flowchart TD → 产业链、流程、板块地图、资本流
|
|
46
|
+
flowchart LR → 仅当内容天然横向(如横向对比)才用,否则用 TD
|
|
47
|
+
sequenceDiagram → 系统/服务间交互、API 调用时序、接口联调
|
|
48
|
+
mindmap → 纯树形知识结构,无需表达流向
|
|
49
|
+
timeline → 历史事件/里程碑时间轴,每个时间点有多个条目
|
|
50
|
+
gantt → 多轨道并行时间段(如技术迭代、项目计划)
|
|
51
|
+
stateDiagram-v2 → 状态机、周期轮动(如估值周期)
|
|
52
|
+
quadrantChart → 2×2 矩阵定位(如确定性 vs 弹性)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 2. 布局原则
|
|
58
|
+
|
|
59
|
+
### 宽度控制(窄页优先)
|
|
60
|
+
|
|
61
|
+
Markdown 渲染器页面宽度通常 700–900px,图表必须适配:
|
|
62
|
+
|
|
63
|
+
| 场景 | 规则 |
|
|
64
|
+
|------|------|
|
|
65
|
+
| 主流方向 | 优先 `flowchart TD`(纵向),避免 `flowchart LR` |
|
|
66
|
+
| subgraph 内节点 | **最多 2–3 个节点并排**;超过则用 `---` 配对 |
|
|
67
|
+
| stateDiagram | 使用 `direction TB`,不用 `direction LR` |
|
|
68
|
+
| 不可用横向LR的场景 | 产业链、板块地图、状态机、节奏图 |
|
|
69
|
+
|
|
70
|
+
### 禁止的布局写法
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
❌ direction TB in flowchart subgraph # 仅 v10+ 支持,兼容性差
|
|
74
|
+
❌ 4+ 孤立节点在同一 subgraph # 全部横向排开,过宽
|
|
75
|
+
❌ flowchart LR 用于产业链 # 三列并排超出页面
|
|
76
|
+
❌ nested subgraph(嵌套子图) # Mermaid 渲染不稳定
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> 矩阵布局 `---` 配对技法、subgraph 间箭头写法见 `references/flowchart.md 或对应图类型文件`。
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 3. Roland Berger 配色体系
|
|
84
|
+
|
|
85
|
+
### 核心色板
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
主色 Deep Navy #00205B 用于最重要/最底层/上游 subgraph
|
|
89
|
+
主色 RB Blue #003E96 用于中间层/平台层 subgraph
|
|
90
|
+
中蓝 Medium Blue #1E5C9E 用于应用层/下游 subgraph
|
|
91
|
+
节点 Navy Node #0A2E7A 上游 subgraph 内节点
|
|
92
|
+
节点 Blue Node #0050B8 中游 subgraph 内节点
|
|
93
|
+
节点 Mid Node #2A6EAE 下游 subgraph 内节点
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 语义辅助色
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
预警 / 高风险 fill:#7B1010 stroke:#B52020
|
|
100
|
+
机会 / 买入 fill:#1A5E3A stroke:#2A7E50
|
|
101
|
+
等待 / 持有 fill:#003E96 stroke:#1A6AC4
|
|
102
|
+
投机 / 主题 fill:#2E0078 stroke:#5A20A0
|
|
103
|
+
价值 / 配置 fill:#004060 stroke:#1A5E80
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 配色规则
|
|
107
|
+
|
|
108
|
+
- 深色背景(fill 暗于 `#4A4A4A`)**必须**加 `color:#fff`
|
|
109
|
+
- subgraph 背景比节点背景**深 10–15%**(提供层次感)
|
|
110
|
+
- 同一图内最多使用 **3 种主色** + 语义辅助色
|
|
111
|
+
- `quadrantChart` 的 quadrant 标签不加颜色(库自动渲染)
|
|
112
|
+
- `timeline` / `gantt` / `stateDiagram` 使用库默认颜色,不强制 RB 色
|
|
113
|
+
|
|
114
|
+
> 完整配色模板(含三层产业链示例)→ `references/color-templates.md`
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 4. 语法禁区(Syntax Forbidden Zone)
|
|
119
|
+
|
|
120
|
+
### 禁用字符(Fatal Characters)
|
|
121
|
+
|
|
122
|
+
| 字符 | Unicode | 替换方案 |
|
|
123
|
+
|------|---------|---------|
|
|
124
|
+
| `·` 中点 | U+00B7 | 用 `/` 或空格 |
|
|
125
|
+
| `→` 右箭头 | U+2192 | 用 `->` 或 `to` |
|
|
126
|
+
| `—` 破折号 | U+2014 | 用 `-` |
|
|
127
|
+
| `"` `"` | U+201C/D | 用 `"` 标准双引号 |
|
|
128
|
+
| emoji | >U+1F000 | 完全删除 |
|
|
129
|
+
|
|
130
|
+
### 禁用语法
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
❌ \n 换行符在节点标签内 → 改用 <br/>
|
|
134
|
+
❌ / 在 quadrant 标签内 → 改用空格或删除
|
|
135
|
+
❌ + 在 timeline key 内 → 改用 "以后" 或文字后缀
|
|
136
|
+
❌ 中文作为 stateDiagram stateID → 用 ASCII ID + as 别名
|
|
137
|
+
❌ YYYY-Q 日期格式在 gantt → 改用 YYYY-MM-DD
|
|
138
|
+
❌ direction TB 在 flowchart subgraph → 改为全局 TD,删除子图内 direction
|
|
139
|
+
❌ 多行 edge label(|"line1\nline2"|)→ 改为单行短语
|
|
140
|
+
❌ sequenceDiagram 中文/空格参与者名不加处理 → 用 alias 或引号
|
|
141
|
+
❌ sequenceDiagram activate 未配对 deactivate → 必须一一对应
|
|
142
|
+
❌ sequenceDiagram 消息文本内换行 → 不支持,保持单行
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 5. 渲染前强制检查清单
|
|
148
|
+
|
|
149
|
+
每张图创建完毕后,**逐项检查**以下 12 项,全部通过才能输出:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
□ 1. 图类型与语义匹配(见第 1 节选型矩阵)
|
|
153
|
+
□ 2. 主方向为 TD 或纵向(非 LR,除非有充分理由)
|
|
154
|
+
□ 3. subgraph 内每行 ≤ 3 个节点(超过已用 --- 配对)
|
|
155
|
+
□ 4. 无 emoji 字符(在任何标签/子图/边中)
|
|
156
|
+
□ 5. 无 · 中点符(全文替换为 /)
|
|
157
|
+
□ 6. 无 → 箭头字符(替换为 -> 或文字)
|
|
158
|
+
□ 7. 节点标签换行使用 <br/>,不使用 \n
|
|
159
|
+
□ 8. stateDiagram state ID 全为 ASCII
|
|
160
|
+
□ 9. gantt 日期格式为 YYYY-MM-DD
|
|
161
|
+
□ 10. quadrantChart 标签无 /
|
|
162
|
+
□ 11. timeline key 无 + % & 特殊字符
|
|
163
|
+
□ 12. 所有深色节点(fill 深于 #4A4A4A)已设 color:#fff
|
|
164
|
+
□ 13. sequenceDiagram 参与者名含中文/空格时已用 alias 或引号
|
|
165
|
+
□ 14. sequenceDiagram 每个 activate 有对应 deactivate
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
> 批量自动检测 → 运行 `scripts/check_mermaid.py`
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 6. 快速参考卡
|
|
173
|
+
|
|
174
|
+
### 图类型 → 场景
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
产业链/供应链 → flowchart TD + 3个subgraph + UP→MID→DOWN 箭头
|
|
178
|
+
板块地图 → flowchart TD + subgraph per layer + --- 矩阵节点
|
|
179
|
+
资本/数据流 → flowchart TD + 有向链 + 反馈环
|
|
180
|
+
系统交互/时序 → sequenceDiagram + participant alias + activate/deactivate
|
|
181
|
+
状态周期 → stateDiagram-v2 direction TB + ASCII state ID
|
|
182
|
+
时间轴 → timeline(事件点)/ gantt(时间段)
|
|
183
|
+
象限定位 → quadrantChart
|
|
184
|
+
知识树 → mindmap
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### RB 配色速查
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
上游子图 fill:#00205B stroke:#1E4A9A
|
|
191
|
+
中游子图 fill:#003E96 stroke:#1A6AC4
|
|
192
|
+
下游子图 fill:#1E5C9E stroke:#3A8ACC
|
|
193
|
+
上游节点 fill:#0A2E7A stroke:#1E4A9A
|
|
194
|
+
中游节点 fill:#0050B8 stroke:#1A6AC4
|
|
195
|
+
下游节点 fill:#2A6EAE stroke:#3A8ACC
|
|
196
|
+
预警红 fill:#7B1010 stroke:#B52020
|
|
197
|
+
机会绿 fill:#1A5E3A stroke:#2A7E50
|
|
198
|
+
投机紫 fill:#2E0078 stroke:#5A20A0
|
|
199
|
+
价值蓝绿 fill:#004060 stroke:#1A5E80
|
|
200
|
+
所有深色背景加 color:#fff
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 禁用字符替换
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
· (U+00B7) → /
|
|
207
|
+
→ (U+2192) → -> 或文字
|
|
208
|
+
— (U+2014) → -
|
|
209
|
+
"" (U+201C/D)→ "
|
|
210
|
+
emoji → 删除
|
|
211
|
+
\n 在节点 → <br/>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 矩阵布局模板
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
A["节点A"] --- B["节点B"] ← 第一行(2列)
|
|
218
|
+
C["节点C"] --- D["节点D"] ← 第二行(2列)
|
|
219
|
+
E["奇数节点"] ← 第三行(居中)
|
|
220
|
+
```
|