@topmindspace/tms-skills 0.6.0 → 0.7.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 +36 -0
- package/README.en.md +5 -4
- package/README.md +13 -4
- package/package.json +2 -1
- package/topmind-briefs/CHANGELOG.md +22 -0
- package/topmind-briefs/README.en.md +40 -0
- package/topmind-briefs/README.md +37 -0
- package/topmind-briefs/SKILL.md +101 -0
- package/topmind-briefs/dist/topmind-briefs.manifest.json +21 -0
- package/topmind-briefs/dist/topmind-briefs.zip +0 -0
- package/topmind-briefs/package-lock.json +13 -0
- package/topmind-briefs/package.json +1 -0
- package/topmind-briefs/references/examples.md +127 -0
- package/topmind-briefs/references/platform-specs.md +60 -0
- package/topmind-briefs/references/style-guide.md +55 -0
- package/topmind-briefs/scripts/audit_css.py +39 -0
- package/topmind-briefs/scripts/audit_docs.py +80 -0
- package/topmind-briefs/scripts/audit_skill.py +60 -0
- package/topmind-briefs/scripts/audit_styles.py +54 -0
- package/topmind-briefs/scripts/negative_tests.py +88 -0
- package/topmind-briefs/scripts/package_skill.py +188 -0
- package/topmind-cover/README.en.md +1 -1
- package/topmind-cover/README.md +1 -1
- package/topmind-wechat-post/README.en.md +1 -1
- package/topmind-wechat-post/README.md +1 -1
- package/topmind-wechat-post/SKILL.md +74 -38
- package/topmind-wechat-post/package.json +1 -1
- package/topmind-wechat-post/references/element-spec.md +3 -2
- package/topmind-wechat-post/scripts/md2wechat.py +2 -7
- package/topmind-x-article/README.en.md +8 -6
- package/topmind-x-article/README.md +6 -5
- package/topmind-x-article/SKILL.md +56 -2
- package/topmind-x-article/package.json +1 -1
- package/topmind-x-article/references/x-html-format.md +6 -1
- package/topmind-x-article/scripts/md2x-html.py +43 -5
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""文档审计:references 完整性与内部链接可达性(任一不过 → 退出码 1)。"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
12
|
+
except Exception:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
16
|
+
NAME = ROOT.name
|
|
17
|
+
errors: list[str] = []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check(cond: bool, msg: str) -> None:
|
|
21
|
+
print((" ✓ " if cond else " ✗ ") + msg)
|
|
22
|
+
if not cond:
|
|
23
|
+
errors.append(msg)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> None:
|
|
27
|
+
print(f"[audit_docs] {NAME}")
|
|
28
|
+
md_files = [p for p in ROOT.rglob("*.md") if ".git" not in p.parts and "dist" not in p.parts]
|
|
29
|
+
check(len(md_files) >= 2, f"md 文档 ≥2(实 {len(md_files)},含 SKILL.md/README.md)")
|
|
30
|
+
|
|
31
|
+
refdir = ROOT / "references"
|
|
32
|
+
if refdir.is_dir():
|
|
33
|
+
refs = sorted(refdir.glob("*.md"))
|
|
34
|
+
check(len(refs) >= 1, f"references/*.md ≥1(实 {len(refs)})")
|
|
35
|
+
for md in refs:
|
|
36
|
+
check(md.stat().st_size > 0, f"references/{md.name} 非空")
|
|
37
|
+
|
|
38
|
+
for md in md_files:
|
|
39
|
+
text = md.read_text(encoding="utf-8")
|
|
40
|
+
rel = md.relative_to(ROOT).as_posix()
|
|
41
|
+
for link in set(re.findall(r"\]\(([^)]+)\)", text)):
|
|
42
|
+
if link.startswith(("http://", "https://", "#", "mailto:")):
|
|
43
|
+
continue
|
|
44
|
+
target = (md.parent / link.split("#")[0]).resolve()
|
|
45
|
+
if link and not target.exists():
|
|
46
|
+
# 允许指向脚本/资源等非 md 文件的相对链接缺失检查放宽到 md
|
|
47
|
+
if target.suffix == ".md":
|
|
48
|
+
check(False, f"{rel} 悬空链接: {link}")
|
|
49
|
+
check(True, "内部 md 链接可达")
|
|
50
|
+
|
|
51
|
+
# 反引号代码引用中的路径可达(审计 轻微-15 门禁盲区:之前只查 [text](path) 形式,
|
|
52
|
+
# `references/pan-style.md` 这类反引号引用曾漏网)。抓取反引号内的 references/*.md
|
|
53
|
+
# 与 scripts/* 路径并校验存在;glob/占位符写法(* ? < > |)跳过。
|
|
54
|
+
backtick_path_re = re.compile(r"(references|scripts)/([\w.\-/]+)")
|
|
55
|
+
line_suffix_re = re.compile(r":\d+(?:-\d+)?$")
|
|
56
|
+
backtick_bad = 0
|
|
57
|
+
for md in md_files:
|
|
58
|
+
text = md.read_text(encoding="utf-8")
|
|
59
|
+
rel = md.relative_to(ROOT).as_posix()
|
|
60
|
+
for span in set(re.findall(r"`([^`]+)`", text)):
|
|
61
|
+
for kind, p in backtick_path_re.findall(span):
|
|
62
|
+
p = line_suffix_re.sub("", p)
|
|
63
|
+
if not p or any(c in p for c in "*?<>|"):
|
|
64
|
+
continue
|
|
65
|
+
target = md.parent / kind / p
|
|
66
|
+
if not target.exists():
|
|
67
|
+
target = ROOT / kind / p
|
|
68
|
+
if not target.exists():
|
|
69
|
+
backtick_bad += 1
|
|
70
|
+
check(False, f"{rel} 反引号引用不可达: `{kind}/{p}`")
|
|
71
|
+
check(backtick_bad == 0, f"反引号内 references/scripts 引用可达(扫描 {len(md_files)} 个 md 文件)")
|
|
72
|
+
|
|
73
|
+
if errors:
|
|
74
|
+
print(f"\n[audit_docs] {NAME}: {len(errors)} 项失败")
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
print(f"\n[audit_docs] {NAME}: 全部通过")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""技能入口审计:SKILL.md 结构完整性(任一不过 → 退出码 1)。"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
12
|
+
except Exception:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
16
|
+
NAME = ROOT.name
|
|
17
|
+
errors: list[str] = []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check(cond: bool, msg: str) -> None:
|
|
21
|
+
print((" ✓ " if cond else " ✗ ") + msg)
|
|
22
|
+
if not cond:
|
|
23
|
+
errors.append(msg)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> None:
|
|
27
|
+
print(f"[audit_skill] {NAME}")
|
|
28
|
+
text = (ROOT / "SKILL.md").read_text(encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
m = re.search(r"^triggers:\s*\n((?: - .+\n?)+)", text, re.M)
|
|
31
|
+
triggers = re.findall(r"^ - (.+)$", m.group(1), re.M) if m else []
|
|
32
|
+
check(len(triggers) >= 1, f"triggers ≥1(实 {len(triggers)})")
|
|
33
|
+
|
|
34
|
+
check("When NOT to use" in text or "何时不用" in text, "含 When NOT to use / 何时不用")
|
|
35
|
+
|
|
36
|
+
has_workflow = bool(re.search(r"工作流|Workflow|用法|Usage", text))
|
|
37
|
+
check(has_workflow, "含工作流/用法说明")
|
|
38
|
+
|
|
39
|
+
fm = re.match(r"^---\r?\n([\s\S]*?)\r?\n---", text)
|
|
40
|
+
desc = ""
|
|
41
|
+
if fm:
|
|
42
|
+
dm = re.search(r'^description:\s*"([^"]*)"\s*$', fm.group(1), re.M)
|
|
43
|
+
if dm:
|
|
44
|
+
desc = dm.group(1).strip()
|
|
45
|
+
else:
|
|
46
|
+
dm = re.search(r"^description:\s*>\-\s*\n((?:[ \t]+.*\n?)+)", fm.group(1), re.M)
|
|
47
|
+
if dm:
|
|
48
|
+
desc = " ".join(l.strip() for l in dm.group(1).splitlines())
|
|
49
|
+
check("Use when" in desc or "用" in desc, "description 含使用场景")
|
|
50
|
+
check("Do NOT use" in desc or "不用" in desc, "description 含不适用场景")
|
|
51
|
+
check(len(desc) <= 1024, f"description ≤1024(实 {len(desc)})")
|
|
52
|
+
|
|
53
|
+
if errors:
|
|
54
|
+
print(f"\n[audit_skill] {NAME}: {len(errors)} 项失败")
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
print(f"\n[audit_skill] {NAME}: 全部通过")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""样式资产审计:主题/风格资源有效性(任一不过 → 退出码 1)。
|
|
4
|
+
|
|
5
|
+
本组技能无 CSS 样式表:若存在 assets/themes/*.json 则校验 JSON 可解析;
|
|
6
|
+
references 风格文档非空(已由 audit_docs 覆盖,此处做存在性断言)。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
16
|
+
except Exception:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
20
|
+
NAME = ROOT.name
|
|
21
|
+
errors: list[str] = []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def check(cond: bool, msg: str) -> None:
|
|
25
|
+
print((" ✓ " if cond else " ✗ ") + msg)
|
|
26
|
+
if not cond:
|
|
27
|
+
errors.append(msg)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> None:
|
|
31
|
+
print(f"[audit_styles] {NAME}")
|
|
32
|
+
themes = sorted((ROOT / "assets" / "themes").glob("*.json")) if (ROOT / "assets" / "themes").is_dir() else []
|
|
33
|
+
if themes:
|
|
34
|
+
for t in themes:
|
|
35
|
+
try:
|
|
36
|
+
json.loads(t.read_text(encoding="utf-8"))
|
|
37
|
+
check(True, f"assets/themes/{t.name} JSON 可解析")
|
|
38
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
39
|
+
check(False, f"assets/themes/{t.name} 解析失败: {e}")
|
|
40
|
+
else:
|
|
41
|
+
check(True, "无主题 JSON 资产(本技能不依赖主题文件)")
|
|
42
|
+
|
|
43
|
+
refdir = ROOT / "references"
|
|
44
|
+
has_style_doc = refdir.is_dir() and any(refdir.glob("*.md"))
|
|
45
|
+
check(has_style_doc, "references 风格/格式文档存在")
|
|
46
|
+
|
|
47
|
+
if errors:
|
|
48
|
+
print(f"\n[audit_styles] {NAME}: {len(errors)} 项失败")
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
print(f"\n[audit_styles] {NAME}: 全部通过")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""topmind-briefs 异常输入测试:技能工具链在坏输入下干净失败、不崩溃(任一失败 → 退出码 1)。
|
|
4
|
+
|
|
5
|
+
覆盖:
|
|
6
|
+
1. 工作流引用的跨技能脚本存在(md2x-html.py / md2wechat.py 缺失即失败,不静默)
|
|
7
|
+
2. audit_skill.py 在 frontmatter 损坏的 SKILL.md 上退出码 1(不抛未捕获异常)
|
|
8
|
+
3. package_skill.py --check 在缺 README.md 的技能目录上退出码 1(不抛未捕获异常)
|
|
9
|
+
4. scripts/*.py 全部可编译
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
+
REPO = ROOT.parent
|
|
26
|
+
PY = sys.executable
|
|
27
|
+
fails: list[str] = []
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check(name: str, cond: bool, detail: str = "") -> None:
|
|
31
|
+
print((" ✓ " if cond else " ✗ ") + name + (f" — {detail}" if detail and not cond else ""))
|
|
32
|
+
if not cond:
|
|
33
|
+
fails.append(name)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_quiet(args: list[str], cwd: Path) -> subprocess.CompletedProcess:
|
|
37
|
+
return subprocess.run([PY, *args], capture_output=True, text=True, timeout=60, cwd=str(cwd))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main() -> None:
|
|
41
|
+
print("[negative_tests] topmind-briefs")
|
|
42
|
+
|
|
43
|
+
# 1. 跨技能脚本依赖存在
|
|
44
|
+
for rel in ("topmind-x-article/scripts/md2x-html.py",
|
|
45
|
+
"topmind-wechat-post/scripts/md2wechat.py"):
|
|
46
|
+
p = REPO / rel
|
|
47
|
+
check(f"跨技能依赖存在: {rel}", p.is_file())
|
|
48
|
+
|
|
49
|
+
# 2. frontmatter 损坏 → audit_skill 退出 1 且不崩溃
|
|
50
|
+
with tempfile.TemporaryDirectory() as td:
|
|
51
|
+
t = Path(td)
|
|
52
|
+
shutil.copytree(ROOT, t / "skill", ignore=shutil.ignore_patterns("dist", "__pycache__"))
|
|
53
|
+
sk = t / "skill" / "SKILL.md"
|
|
54
|
+
sk.write_text("no frontmatter here\n# broken\n", encoding="utf-8")
|
|
55
|
+
r = run_quiet(["scripts/audit_skill.py"], t / "skill")
|
|
56
|
+
check("audit_skill 在坏 frontmatter 上退出码=1",
|
|
57
|
+
r.returncode == 1, f"rc={r.returncode}")
|
|
58
|
+
check("audit_skill 在坏 frontmatter 上无未捕获 Traceback",
|
|
59
|
+
"Traceback" not in (r.stderr or ""), (r.stderr or "")[-200:])
|
|
60
|
+
|
|
61
|
+
# 3. 缺 README.md → package_skill --check 退出 1 且不崩溃
|
|
62
|
+
with tempfile.TemporaryDirectory() as td:
|
|
63
|
+
t = Path(td)
|
|
64
|
+
shutil.copytree(ROOT, t / "skill", ignore=shutil.ignore_patterns("dist", "__pycache__"))
|
|
65
|
+
(t / "skill" / "README.md").unlink()
|
|
66
|
+
(t / "skill" / "README.en.md").unlink(missing_ok=True)
|
|
67
|
+
r = run_quiet(["scripts/package_skill.py", "--check"], t / "skill")
|
|
68
|
+
check("package_skill --check 在缺 README.md 时退出码=1",
|
|
69
|
+
r.returncode == 1, f"rc={r.returncode}")
|
|
70
|
+
check("package_skill --check 在缺 README.md 时无未捕获 Traceback",
|
|
71
|
+
"Traceback" not in (r.stderr or ""), (r.stderr or "")[-200:])
|
|
72
|
+
|
|
73
|
+
# 4. scripts/*.py 可编译(用内置 compile 只验语法,不写 .pyc,避免污染工作区)
|
|
74
|
+
for py in sorted((ROOT / "scripts").glob("*.py")):
|
|
75
|
+
try:
|
|
76
|
+
compile(py.read_text(encoding="utf-8"), str(py), "exec")
|
|
77
|
+
check(f"可编译: scripts/{py.name}", True)
|
|
78
|
+
except SyntaxError as e:
|
|
79
|
+
check(f"可编译: scripts/{py.name}", False, str(e))
|
|
80
|
+
|
|
81
|
+
if fails:
|
|
82
|
+
print(f"\n[negative_tests] topmind-briefs: {len(fails)} 项失败")
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
print("\n[negative_tests] topmind-briefs: 全部通过")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""技能分发包打包器(发布前完整性校验 + 发布清单)。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python scripts/package_skill.py # 校验 → 打包 → 写发布清单
|
|
7
|
+
python scripts/package_skill.py --check # 只校验不打包(CI / 提交前自检)
|
|
8
|
+
|
|
9
|
+
产物:
|
|
10
|
+
dist/<skill>.zip 分发包(解压目录名 = 技能名)
|
|
11
|
+
dist/<skill>.manifest.json 发布清单(版本 / 条目 / 大小 / SHA-256)
|
|
12
|
+
|
|
13
|
+
校验(任一不过 → 退出码 1):
|
|
14
|
+
· SKILL.md frontmatter 含 name / description;name 与目录名一致;
|
|
15
|
+
description ≤ 1024 字符(平台截断阈值)
|
|
16
|
+
· README.md / package.json 存在;package.json name 与技能名一致
|
|
17
|
+
· scripts/*.py 全部可编译;references/*.md 非空
|
|
18
|
+
· SKILL.md 引用的 references/<file> 全部存在
|
|
19
|
+
· 无 scripts/_* / __pycache__ / .env 等不应进包的文件
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
import zipfile
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
37
|
+
NAME = ROOT.name
|
|
38
|
+
OUT = ROOT / "dist" / f"{NAME}.zip"
|
|
39
|
+
MANIFEST = ROOT / "dist" / f"{NAME}.manifest.json"
|
|
40
|
+
DESC_LIMIT = 1024
|
|
41
|
+
|
|
42
|
+
INCLUDE = ["SKILL.md", "README.md", "package.json", "assets/", "references/", "scripts/", "evals/", "agents/"]
|
|
43
|
+
EXCLUDE_RES = [
|
|
44
|
+
re.compile(r"(^|/)scripts/_"),
|
|
45
|
+
re.compile(r"(^|/)\.env($|\.)"),
|
|
46
|
+
re.compile(r"(^|/)__pycache__(/|$)"),
|
|
47
|
+
re.compile(r"(^|/)node_modules(/|$)"),
|
|
48
|
+
re.compile(r"(^|/)dist(/|$)"),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
errors: list[str] = []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def fail(msg: str) -> None:
|
|
55
|
+
errors.append(msg)
|
|
56
|
+
print(f" ✗ {msg}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def ok(msg: str) -> None:
|
|
60
|
+
print(f" ✓ {msg}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def read_frontmatter() -> dict:
|
|
64
|
+
text = (ROOT / "SKILL.md").read_text(encoding="utf-8")
|
|
65
|
+
m = re.match(r"^---\r?\n([\s\S]*?)\r?\n---", text)
|
|
66
|
+
if not m:
|
|
67
|
+
fail("SKILL.md 缺 frontmatter")
|
|
68
|
+
return {}
|
|
69
|
+
fm = m.group(1)
|
|
70
|
+
name = re.search(r"^name:\s*(\S+)\s*$", fm, re.M)
|
|
71
|
+
desc = ""
|
|
72
|
+
dm = re.search(r'^description:\s*"([^"]*)"\s*$', fm, re.M)
|
|
73
|
+
if dm:
|
|
74
|
+
desc = dm.group(1).strip()
|
|
75
|
+
else:
|
|
76
|
+
dm = re.search(r"^description:\s*>\-\s*\n((?:[ \t]+.*\n?)+)", fm, re.M)
|
|
77
|
+
if dm:
|
|
78
|
+
desc = " ".join(line.strip() for line in dm.group(1).splitlines() if line.strip())
|
|
79
|
+
return {"name": name.group(1) if name else "", "description": desc}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def check() -> bool:
|
|
83
|
+
print(f"[check] {NAME}")
|
|
84
|
+
fm = read_frontmatter()
|
|
85
|
+
if fm.get("name") != NAME:
|
|
86
|
+
fail(f"frontmatter name={fm.get('name')!r} 与目录名 {NAME!r} 不一致")
|
|
87
|
+
else:
|
|
88
|
+
ok("name 与目录名一致")
|
|
89
|
+
desc = fm.get("description", "")
|
|
90
|
+
if not desc:
|
|
91
|
+
fail("description 为空")
|
|
92
|
+
elif len(desc) > DESC_LIMIT:
|
|
93
|
+
fail(f"description {len(desc)} 字符,超过 {DESC_LIMIT}")
|
|
94
|
+
else:
|
|
95
|
+
ok(f"description {len(desc)} 字符")
|
|
96
|
+
|
|
97
|
+
for f in ("README.md", "package.json"):
|
|
98
|
+
if not (ROOT / f).is_file():
|
|
99
|
+
fail(f"缺 {f}")
|
|
100
|
+
else:
|
|
101
|
+
ok(f"有 {f}")
|
|
102
|
+
try:
|
|
103
|
+
pkg = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
|
|
104
|
+
if pkg.get("name") != NAME:
|
|
105
|
+
fail(f"package.json name={pkg.get('name')!r} 与技能名不一致")
|
|
106
|
+
if not re.match(r"^\d+\.\d+\.\d+", str(pkg.get("version", ""))):
|
|
107
|
+
fail("package.json version 非 semver")
|
|
108
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
109
|
+
fail(f"package.json 解析失败: {e}")
|
|
110
|
+
|
|
111
|
+
for p in ROOT.rglob("*"):
|
|
112
|
+
if not p.is_file() or ".git" in p.parts:
|
|
113
|
+
continue
|
|
114
|
+
rel = p.relative_to(ROOT).as_posix()
|
|
115
|
+
for rx in EXCLUDE_RES:
|
|
116
|
+
if rx.search(rel):
|
|
117
|
+
fail(f"不应进包的文件: {rel}")
|
|
118
|
+
break
|
|
119
|
+
ok("无禁用文件")
|
|
120
|
+
|
|
121
|
+
for py in sorted((ROOT / "scripts").glob("*.py")):
|
|
122
|
+
try:
|
|
123
|
+
compile(py.read_text(encoding="utf-8"), str(py), "exec")
|
|
124
|
+
except SyntaxError as e:
|
|
125
|
+
fail(f"{py.name} 编译失败: {e}")
|
|
126
|
+
ok("scripts/*.py 编译通过")
|
|
127
|
+
|
|
128
|
+
refdir = ROOT / "references"
|
|
129
|
+
if refdir.is_dir():
|
|
130
|
+
for md in sorted(refdir.glob("*.md")):
|
|
131
|
+
if md.stat().st_size == 0:
|
|
132
|
+
fail(f"references/{md.name} 为空")
|
|
133
|
+
skill_text = (ROOT / "SKILL.md").read_text(encoding="utf-8")
|
|
134
|
+
for ref in sorted(set(re.findall(r"references/([\w\-.]+\.md)", skill_text))):
|
|
135
|
+
if not (refdir / ref).is_file():
|
|
136
|
+
fail(f"SKILL.md 引用的 references/{ref} 不存在")
|
|
137
|
+
ok("references 完整性通过")
|
|
138
|
+
|
|
139
|
+
if errors:
|
|
140
|
+
print(f"\n[check] {NAME}: {len(errors)} 项失败")
|
|
141
|
+
return False
|
|
142
|
+
print(f"\n[check] {NAME}: 全部通过")
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def package() -> None:
|
|
147
|
+
if not check():
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
(ROOT / "dist").mkdir(exist_ok=True)
|
|
150
|
+
entries: list[str] = []
|
|
151
|
+
with zipfile.ZipFile(OUT, "w", zipfile.ZIP_DEFLATED) as z:
|
|
152
|
+
for p in sorted(ROOT.rglob("*")):
|
|
153
|
+
if not p.is_file() or ".git" in p.parts:
|
|
154
|
+
continue
|
|
155
|
+
rel = p.relative_to(ROOT)
|
|
156
|
+
rel_posix = rel.as_posix()
|
|
157
|
+
if any(rx.search(rel_posix) for rx in EXCLUDE_RES):
|
|
158
|
+
continue
|
|
159
|
+
if not any(
|
|
160
|
+
rel_posix == inc.rstrip("/") or rel_posix.startswith(inc)
|
|
161
|
+
for inc in INCLUDE
|
|
162
|
+
if inc.endswith("/")
|
|
163
|
+
) and rel_posix not in INCLUDE:
|
|
164
|
+
continue
|
|
165
|
+
z.write(p, arcname=f"{NAME}/{rel_posix}")
|
|
166
|
+
entries.append(rel_posix)
|
|
167
|
+
sha = hashlib.sha256(OUT.read_bytes()).hexdigest()
|
|
168
|
+
pkg = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
|
|
169
|
+
manifest = {
|
|
170
|
+
"name": NAME,
|
|
171
|
+
"version": pkg.get("version"),
|
|
172
|
+
"built_at": datetime.now(timezone.utc).isoformat(),
|
|
173
|
+
"files": len(entries),
|
|
174
|
+
"sha256": sha,
|
|
175
|
+
"entries": entries,
|
|
176
|
+
}
|
|
177
|
+
MANIFEST.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
178
|
+
print(f"\n[package] {OUT} ({len(entries)} 文件, sha256={sha[:12]}…)")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main() -> None:
|
|
182
|
+
if "--check" in sys.argv[1:]:
|
|
183
|
+
sys.exit(0 if check() else 1)
|
|
184
|
+
package()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
main()
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
A reusable skill for generating cover art for X long-form posts and WeChat articles. Goal: **striking, eye-catching, theme-focused**.
|
|
6
6
|
|
|
7
|
-
- Version: **v0.3.0** (same tag as `@topmindspace/tms-skills@0.
|
|
7
|
+
- Version: **v0.3.0** (same tag as `@topmindspace/tms-skills@0.7.0`)
|
|
8
8
|
|
|
9
9
|
## Style gallery
|
|
10
10
|
|
package/topmind-cover/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Full lifecycle for WeChat articles: delivery package, review & rewrite, three quality gates, status sync, WeChat inline typography, and publish checklist.
|
|
6
6
|
|
|
7
|
-
- Version: **v0.
|
|
7
|
+
- Version: **v0.2.0** (same tag as `@topmindspace/tms-skills@0.7.0`)
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|