@topmindspace/tms-skills 0.5.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.en.md +6 -5
  3. package/README.md +14 -5
  4. package/package.json +2 -1
  5. package/top-ppt-html/README.en.md +2 -2
  6. package/top-ppt-html/README.md +1 -1
  7. package/topmind-briefs/CHANGELOG.md +22 -0
  8. package/topmind-briefs/README.en.md +40 -0
  9. package/topmind-briefs/README.md +37 -0
  10. package/topmind-briefs/SKILL.md +101 -0
  11. package/topmind-briefs/dist/topmind-briefs.manifest.json +21 -0
  12. package/topmind-briefs/dist/topmind-briefs.zip +0 -0
  13. package/topmind-briefs/package-lock.json +13 -0
  14. package/topmind-briefs/package.json +1 -0
  15. package/topmind-briefs/references/examples.md +127 -0
  16. package/topmind-briefs/references/platform-specs.md +60 -0
  17. package/topmind-briefs/references/style-guide.md +55 -0
  18. package/topmind-briefs/scripts/audit_css.py +39 -0
  19. package/topmind-briefs/scripts/audit_docs.py +80 -0
  20. package/topmind-briefs/scripts/audit_skill.py +60 -0
  21. package/topmind-briefs/scripts/audit_styles.py +54 -0
  22. package/topmind-briefs/scripts/negative_tests.py +88 -0
  23. package/topmind-briefs/scripts/package_skill.py +188 -0
  24. package/topmind-cover/README.en.md +4 -4
  25. package/topmind-cover/README.md +4 -4
  26. package/topmind-cover/SKILL.md +5 -5
  27. package/topmind-cover/assets/examples/README.md +2 -0
  28. package/topmind-cover/package.json +1 -1
  29. package/topmind-cover/references/cover-prompts.md +19 -19
  30. package/topmind-cover/references/cover-styles.md +27 -26
  31. package/topmind-cover/scripts/crop-cover.py +6 -6
  32. package/topmind-cover/scripts/negative_tests.py +1 -1
  33. package/topmind-wechat-post/README.en.md +1 -1
  34. package/topmind-wechat-post/README.md +1 -1
  35. package/topmind-wechat-post/SKILL.md +74 -38
  36. package/topmind-wechat-post/package.json +1 -1
  37. package/topmind-wechat-post/references/element-spec.md +3 -2
  38. package/topmind-wechat-post/scripts/md2wechat.py +2 -7
  39. package/topmind-x-article/README.en.md +6 -2
  40. package/topmind-x-article/README.md +4 -2
  41. package/topmind-x-article/SKILL.md +59 -4
  42. package/topmind-x-article/package.json +1 -1
  43. package/topmind-x-article/references/publish-checklist.md +1 -1
  44. package/topmind-x-article/references/x-html-format.md +13 -1
  45. package/topmind-x-article/scripts/md2x-html.py +179 -51
@@ -0,0 +1,55 @@
1
+ # 风格指南 · 干货短文
2
+
3
+ 目标:读者划过去,3 秒内抓住那个数字/结论,心想"这篇有料"。
4
+
5
+ ## 三原则
6
+
7
+ **短**:一件事讲完就停。X 版 280 字符内;公众号版 500 字内。
8
+ **干**:每个字都承载信息。删掉所有"众所周知""值得一提的是"。
9
+ **准**:数字有来源,判断有依据。不确定的写"据 X 报道",不编。
10
+
11
+ ## 开头:四种起手(按真实帖子)
12
+
13
+ - 数据型(@ArtificialAnlys):"[反直觉判断一句话]。On [基准], [最扎眼数字]。"
14
+ - 速报型(@arena):"Big news: [模型] just landed #1 in [榜单] with [分数]!"
15
+ - 论文型(@RulinShao):"‼️[加粗判断]: [一句话]! Introducing [X]"
16
+ - 讲解型(@akshay_pachaar):"[X], clearly explained! [痛点一句话]"
17
+
18
+ ## 正文:列表 > 段落
19
+
20
+ 能用列表就不用段落。参考格式:
21
+
22
+ ```
23
+ [结论一句话]
24
+
25
+ - 关键数据 1
26
+ - 关键数据 2
27
+ - 关键数据 3
28
+
29
+ [图]
30
+
31
+ 来源:[链接]
32
+ ```
33
+
34
+ ## 禁止清单
35
+
36
+ - 禁止"首先、其次、最后""总而言之""值得一提的是""值得注意的是"
37
+ - 禁止"赋能""抓手""闭环""底层逻辑"
38
+ - 禁止铺垫三段再说结论
39
+ - 禁止没有数据的形容词:"大幅提升""显著改善"(改成具体数字)
40
+ - 禁止感叹号连用
41
+ - 禁止"家人们""老铁"式直播腔
42
+ - 禁止正确的废话
43
+
44
+ ## 鼓励
45
+
46
+ - 数字代替形容词:"快 3 倍"不说"大幅提升"
47
+ - 对比代替孤立数字:"58 分,比 Opus 5.5 高 2 分"不说"58 分,很高"
48
+ - 一句话判断收尾(可选):"这意味着 [对读者的意义]。"——只写一句,不展开
49
+ - 来源链接放在最后,不打断阅读
50
+
51
+ ## 自检三问
52
+
53
+ 1. 删掉一半字,核心结论还在吗?(在→删)
54
+ 2. 每个数字都能说出来源吗?(不能→删或补来源)
55
+ 3. 不看图,文字本身成立吗?不看文字,图本身有信息吗?(都成立→图和文各司其职)
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """CSS 审计:本组技能无 CSS 资产;若未来加入 .css 文件,则做括号配平基础检查。"""
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ try:
10
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
11
+ except Exception:
12
+ pass
13
+
14
+ ROOT = Path(__file__).resolve().parent.parent
15
+ NAME = ROOT.name
16
+ errors: list[str] = []
17
+
18
+
19
+ def main() -> None:
20
+ print(f"[audit_css] {NAME}")
21
+ css_files = [p for p in ROOT.rglob("*.css") if ".git" not in p.parts and "dist" not in p.parts]
22
+ if not css_files:
23
+ print(" ✓ 无 CSS 资产,跳过")
24
+ print(f"\n[audit_css] {NAME}: 通过")
25
+ return
26
+ for css in css_files:
27
+ text = css.read_text(encoding="utf-8")
28
+ balanced = text.count("{") == text.count("}")
29
+ print((" ✓ " if balanced else " ✗ ") + f"{css.name} 括号配平")
30
+ if not balanced:
31
+ errors.append(css.name)
32
+ if errors:
33
+ print(f"\n[audit_css] {NAME}: {len(errors)} 项失败")
34
+ sys.exit(1)
35
+ print(f"\n[audit_css] {NAME}: 全部通过")
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
@@ -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.2.0** (same tag as `@topmindspace/tms-skills@0.5.0`)
7
+ - Version: **v0.3.0** (same tag as `@topmindspace/tms-skills@0.7.0`)
8
8
 
9
9
  ## Style gallery
10
10
 
@@ -26,20 +26,20 @@ npx @topmindspace/tms-skills install topmind-cover
26
26
  1. **Refine the title**: lock the title copy first (usually ≤ 10 chars) using that style's title rules; rewrite until it lands — no drawing yet.
27
27
  2. **Composition brief**: one-sentence brief (theme / audience / mood / visual metaphor / palette direction) before composing the prompt.
28
28
  3. Input: article title + 3 theme keywords + platform (x / wechat / both, default both).
29
- 4. Generate with the agent's image-generation capability (landscape 16:9, 8% margin on all sides).
29
+ 4. Generate with the agent's image-generation capability (landscape 5:2, 8% margin on all sides).
30
30
  5. **Impact self-check** (required): pass every item in the [impact self-check list](./SKILL.md#冲击力自检清单); on failure, tweak the prompt and regenerate, max 3 attempts.
31
31
  6. Crop and save:
32
32
 
33
33
  ```bash
34
34
  python3 scripts/crop-cover.py <main-image> --out-dir <package>/images/
35
- # produces 00-封面.png (1200×675) + 00-封面-公众号.png (900×383, center crop)
35
+ # produces 00-封面.png (1500x600) + 00-封面-公众号.png (900x383, center crop)
36
36
  ```
37
37
 
38
38
  ## Sizes
39
39
 
40
40
  | Platform | Size | Aspect |
41
41
  |----------|------|--------|
42
- | X Article cover | 1200×675 | 16:9 (master image) |
42
+ | X Article cover | 1500x600 | 5:2 (master image) |
43
43
  | WeChat cover large image | 900×383 | 2.35:1 (center crop) |
44
44
 
45
45
  ## Design rules
@@ -4,7 +4,7 @@
4
4
 
5
5
  为 X 长文和公众号文章生成封面图的可复用技能。目标:**震撼、醒目、主题突出**。
6
6
 
7
- - 版本:**v0.2.0**(与 `@topmindspace/tms-skills@0.5.0` 同 tag)
7
+ - 版本:**v0.3.0**(与 `@topmindspace/tms-skills@0.7.0` 同 tag)
8
8
 
9
9
  ## 风格样张
10
10
 
@@ -26,20 +26,20 @@ npx @topmindspace/tms-skills install topmind-cover
26
26
  1. **标题提炼**:先定标题文案(一般 ≤10 字),用该风格章节的标题写法规范;不满意重写,不先画图。
27
27
  2. **构图简报**:一句话 brief(主题/受众/情绪/视觉隐喻/配色方向),组 prompt 前先写。
28
28
  3. 输入文章标题 + 3 个主题关键词 + 平台(x / wechat / both,默认 both)。
29
- 4. 用 agent 的图片生成能力出图(横构图 16:9,四周留白 8%)。
29
+ 4. 用 agent 的图片生成能力出图(横构图 5:2,四周留白 8%)。
30
30
  5. **冲击力自检**(必做):按 [SKILL.md 冲击力自检清单](./SKILL.md#冲击力自检清单) 逐项过;不通过→调 prompt 重生成,最多 3 次。
31
31
  6. 裁剪落盘:
32
32
 
33
33
  ```bash
34
34
  python3 scripts/crop-cover.py <主图> --out-dir <包>/images/
35
- # 产出 00-封面.png (1200×675) + 00-封面-公众号.png (900×383,中央裁剪)
35
+ # 产出 00-封面.png (1500×600) + 00-封面-公众号.png (900×383,中央裁剪)
36
36
  ```
37
37
 
38
38
  ## 尺寸
39
39
 
40
40
  | 平台 | 尺寸 | 比例 |
41
41
  |------|------|------|
42
- | X Article 封面 | 1200×675 | 16:9(主图) |
42
+ | X Article 封面 | 1500×600 | 5:2(主图) |
43
43
  | 公众号封面大图 | 900×383 | 2.35:1(中央裁) |
44
44
 
45
45
  ## 设计铁律
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: topmind-cover
3
- version: 0.2.0
3
+ version: 0.3.0
4
4
  description: "文章封面配图生成:X 长文与公众号共用。震撼、醒目、主题突出;平台尺寸规范、11 种风格模板、命名落盘、成图检查全流程覆盖。Use when 文章封面、封面图、头图、题图、cover。Do NOT use for 正文插图、PPT/报告封面。"
5
5
  action_category: write
6
6
  triggers:
@@ -75,10 +75,10 @@ updated: 2026-09-29
75
75
 
76
76
  | 平台 | 尺寸 | 比例 | 备注 |
77
77
  |------|------|------|------|
78
- | X Article 封面 | 1200×675 | 16:9 | 主尺寸,先按这个生成 |
79
- | 公众号封面大图 | 900×383 | 2.35:1 | 从 16:9 中央裁剪 |
78
+ | X Article 封面 | 1500×600 | 5:2 | 主尺寸,先按这个生成 |
79
+ | 公众号封面大图 | 900×383 | 2.35:1 | 从 5:2 主图中央裁剪(左右各裁约 3%) |
80
80
 
81
- 生成 prompt 里写明"横构图 16:9";公众号版用 `scripts/crop-cover.py` 从主图中央裁出,不重新生成(保系列一致)。
81
+ 生成 prompt 里写明"横构图 5:2";公众号版用 `scripts/crop-cover.py` 从主图中央裁出,不重新生成(保系列一致)。
82
82
 
83
83
  ## 工作流
84
84
 
@@ -95,7 +95,7 @@ updated: 2026-09-29
95
95
  ```bash
96
96
  python3 scripts/crop-cover.py <主图> --out-dir <包>/images/
97
97
  ```
98
- 产出 `00-封面.png`(1200×675,X/公众号共用主文件)与 `00-封面-公众号.png`(900×383)。
98
+ 产出 `00-封面.png`(1500×600,X/公众号共用主文件)与 `00-封面-公众号.png`(900×383)。
99
99
  公众号包规约:正文引用 `images/00-封面.png`,`图片上传清单.md` 登记两版。
100
100
  7. **系列感**:同一系列固定色系 + 版式语言,prompt 复用同一风格模板。
101
101
 
@@ -5,6 +5,8 @@
5
5
  另附 `overview.png`(11 宫格总览:3 行 × 4 列缩略图 + 风格名标注,选风格时先看它)。
6
6
 
7
7
  - `<style>.png`(1200×675,X 长文封面 / 公众号共用主尺寸)
8
+ > 注:现行主尺寸已改为 1500×600(5:2),见 `../SKILL.md` 尺寸表。
9
+ > 本目录样张为 16:9 旧版,仅作风格参考,不重制。
8
10
  - `<style>-wechat.png`(900×383,公众号封面大图,中央裁剪版)
9
11
  - `<style>-alt.png`(1200×675,同风格第二张演示样张;白色清新 / 背景虚化 / 纸感拼贴
10
12
  三种新风格各有 2 张:主样张与 alt 样张用了不同的优选组合)