@topmindspace/tms-skills 0.1.18 → 0.2.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 (98) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.en.md +6 -3
  3. package/README.md +6 -3
  4. package/package.json +5 -2
  5. package/top-ppt-html/README.en.md +2 -2
  6. package/top-ppt-html/README.md +1 -1
  7. package/top-ppt-html/SKILL.md +4 -4
  8. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +24 -12
  9. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +24 -12
  10. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +24 -12
  11. package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.html +94 -165
  12. package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.model.json +32 -38
  13. package/top-ppt-html/assets/pptx-export.js +3 -2
  14. package/top-ppt-html/assets/style-gallery.html +2 -1
  15. package/top-ppt-html/assets/templates/architecture.html +38 -15
  16. package/top-ppt-html/assets/templates/engine.css +20 -9
  17. package/top-ppt-html/assets/templates/presentation.html +38 -17
  18. package/top-ppt-html/assets/templates/research.html +44 -23
  19. package/top-ppt-html/package-lock.json +2 -2
  20. package/top-ppt-html/package.json +1 -1
  21. package/top-ppt-html/references/components-atoms.md +1 -1
  22. package/top-ppt-html/references/content-rules.md +17 -11
  23. package/top-ppt-html/references/failure-modes.md +6 -4
  24. package/top-ppt-html/references/high-fidelity.md +14 -1
  25. package/top-ppt-html/references/icons.md +2 -2
  26. package/top-ppt-html/references/layout-grammar.md +3 -1
  27. package/top-ppt-html/references/layouts-research.md +1 -1
  28. package/top-ppt-html/references/pptx-export.md +2 -2
  29. package/top-ppt-html/references/tech-design.md +2 -0
  30. package/top-ppt-html/scripts/build_icon_assets.js +92 -0
  31. package/top-ppt-html/scripts/build_pptx.js +125 -29
  32. package/top-ppt-html/scripts/extract_model.py +21 -1
  33. package/top-ppt-html/scripts/icon_lib.js +53 -0
  34. package/top-ppt-html/scripts/icon_raster.js +86 -0
  35. package/top-ppt-html/scripts/layout-constants.json +4 -1
  36. package/top-ppt-html/scripts/lib_layout_regions.js +3 -1
  37. package/top-ppt-html/scripts/model-schema.json +7 -3
  38. package/top-ppt-html/scripts/package_skill.py +1 -0
  39. package/top-ppt-html/scripts/render_from_model.py +125 -24
  40. package/top-ppt-html/scripts/scaffold_report.py +47 -24
  41. package/top-ppt-html/scripts/sync_runtime.py +2 -1
  42. package/top-ppt-html/scripts/validate_pptx.py +14 -3
  43. package/top-ppt-html/scripts/validate_report.py +1 -1
  44. package/topmind-cover/README.md +40 -0
  45. package/topmind-cover/SKILL.md +66 -0
  46. package/topmind-cover/package-lock.json +16 -0
  47. package/topmind-cover/package.json +16 -0
  48. package/topmind-cover/references/cover-styles.md +40 -0
  49. package/topmind-cover/scripts/audit_css.py +39 -0
  50. package/topmind-cover/scripts/audit_docs.py +58 -0
  51. package/topmind-cover/scripts/audit_skill.py +60 -0
  52. package/topmind-cover/scripts/audit_styles.py +54 -0
  53. package/topmind-cover/scripts/crop-cover.py +69 -0
  54. package/topmind-cover/scripts/negative_tests.py +71 -0
  55. package/topmind-cover/scripts/package_skill.py +188 -0
  56. package/topmind-wechat-post/README.md +49 -0
  57. package/topmind-wechat-post/SKILL.md +208 -0
  58. package/topmind-wechat-post/assets/themes/amber-review.json +35 -0
  59. package/topmind-wechat-post/assets/themes/graphite.json +35 -0
  60. package/topmind-wechat-post/assets/themes/minimal-ink.json +35 -0
  61. package/topmind-wechat-post/assets/themes/newsprint.json +35 -0
  62. package/topmind-wechat-post/assets/themes/tech-blue.json +35 -0
  63. package/topmind-wechat-post/package-lock.json +16 -0
  64. package/topmind-wechat-post/package.json +16 -0
  65. package/topmind-wechat-post/references/design.md +50 -0
  66. package/topmind-wechat-post/references/element-spec.md +162 -0
  67. package/topmind-wechat-post/references/known-pits.md +86 -0
  68. package/topmind-wechat-post/references/theme-map.md +139 -0
  69. package/topmind-wechat-post/references/typography-rules.md +62 -0
  70. package/topmind-wechat-post/references/wechat-constraints.md +106 -0
  71. package/topmind-wechat-post/references/workflow.md +102 -0
  72. package/topmind-wechat-post/references/writing-quality.md +129 -0
  73. package/topmind-wechat-post/scripts/audit_css.py +39 -0
  74. package/topmind-wechat-post/scripts/audit_docs.py +58 -0
  75. package/topmind-wechat-post/scripts/audit_skill.py +60 -0
  76. package/topmind-wechat-post/scripts/audit_styles.py +54 -0
  77. package/topmind-wechat-post/scripts/lint-wechat.py +446 -0
  78. package/topmind-wechat-post/scripts/md2wechat.py +1602 -0
  79. package/topmind-wechat-post/scripts/negative_tests.py +65 -0
  80. package/topmind-wechat-post/scripts/new-article.py +183 -0
  81. package/topmind-wechat-post/scripts/package_skill.py +188 -0
  82. package/topmind-wechat-post/scripts/push-to-topstream.py +226 -0
  83. package/topmind-wechat-post/scripts/scan_ai_flavor.py +464 -0
  84. package/topmind-wechat-post/scripts/sync-mapping.py +220 -0
  85. package/topmind-wechat-post/scripts/sync-status.py +187 -0
  86. package/topmind-x-article/README.md +40 -0
  87. package/topmind-x-article/SKILL.md +62 -0
  88. package/topmind-x-article/package-lock.json +16 -0
  89. package/topmind-x-article/package.json +16 -0
  90. package/topmind-x-article/references/publish-checklist.md +24 -0
  91. package/topmind-x-article/references/x-format.md +24 -0
  92. package/topmind-x-article/scripts/audit_css.py +39 -0
  93. package/topmind-x-article/scripts/audit_docs.py +58 -0
  94. package/topmind-x-article/scripts/audit_skill.py +60 -0
  95. package/topmind-x-article/scripts/audit_styles.py +54 -0
  96. package/topmind-x-article/scripts/md2x.py +121 -0
  97. package/topmind-x-article/scripts/negative_tests.py +74 -0
  98. package/topmind-x-article/scripts/package_skill.py +188 -0
@@ -0,0 +1,40 @@
1
+ # 封面风格模板
2
+
3
+ 选风格先看题材,再看系列延续性。同一系列固定一个模板,只换主体与标题字。
4
+
5
+ ## 科技深空(默认 · AI/技术题材)
6
+
7
+ - 深色底(藏青/墨黑)+ 霓虹线条 + 细颗粒星空
8
+ - 主体:产品/人物半身,面光干净
9
+ - 标题:白色粗黑,字号压住上 1/3
10
+ - 例:Manus 2.0、Cue、AI 智能体题材
11
+
12
+ ## 清新浅色(人物/生活/轻题材)
13
+
14
+ - 米白/浅灰底 + 柔光 + 大面积留白
15
+ - 主体居中,神态自然
16
+ - 标题:深灰细黑,落在下方 1/4
17
+ - 例:卡牌系列人物、随笔
18
+
19
+ ## 国潮撞色(文化/历史题材)
20
+
21
+ - 朱红/黛青撞色 + 传统纹样暗纹
22
+ - 主体:国风人物或器物
23
+ - 标题:书法感粗体,竖排亦可
24
+
25
+ ## 硬核评测(产品评测题材)
26
+
27
+ - 深灰底 + 产品特写 + 数据流装饰
28
+ - 标题:黄/白撞色,配 1 个核心数字(如"32%")
29
+ - 忌:数字必须与正文一致,承重数字先核实
30
+
31
+ ## 通用 prompt 组装公式
32
+
33
+ ```
34
+ {风格模板一句话},主体:{主体描述},
35
+ 画面中央大标题“{≤10字标题}”,{暗底亮字/亮底深字},
36
+ 横构图16:9,主体占画面40%以上,四周留白8%,
37
+ 极简,不堆砌元素,电影感光影
38
+ ```
39
+
40
+ 标题字逐字写进 prompt;生成后第一件事就是检查标题字。
@@ -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,58 @@
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
+ if errors:
52
+ print(f"\n[audit_docs] {NAME}: {len(errors)} 项失败")
53
+ sys.exit(1)
54
+ print(f"\n[audit_docs] {NAME}: 全部通过")
55
+
56
+
57
+ if __name__ == "__main__":
58
+ 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,69 @@
1
+ #!/usr/bin/env python3
2
+ """封面图裁剪:从 16:9 主图中央裁出公众号 2.35:1 版,并统一落盘命名。
3
+
4
+ 用法:
5
+ python3 crop-cover.py <主图路径> --slug <slug> --out-dir <包>/images/
6
+
7
+ 产出:
8
+ <out-dir>/00-封面.png 1200x675 (X / 公众号共用主文件)
9
+ <out-dir>/00-封面-公众号.png 900x383 (公众号封面大图,中央裁剪)
10
+ """
11
+ import argparse
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ try:
16
+ from PIL import Image
17
+ except ImportError:
18
+ sys.exit("需要 Pillow:pip install pillow")
19
+
20
+ TARGETS = {
21
+ "00-封面.png": (1200, 675),
22
+ "00-封面-公众号.png": (900, 383),
23
+ }
24
+
25
+
26
+ def main() -> None:
27
+ ap = argparse.ArgumentParser()
28
+ ap.add_argument("src", help="16:9 主图路径")
29
+ ap.add_argument("--slug", default="cover")
30
+ ap.add_argument("--out-dir", required=True)
31
+ args = ap.parse_args()
32
+
33
+ src = Path(args.src)
34
+ out_dir = Path(args.out_dir)
35
+ out_dir.mkdir(parents=True, exist_ok=True)
36
+
37
+ if not src.is_file():
38
+ sys.exit(f"错误:找不到输入图片 {src}")
39
+ try:
40
+ img = Image.open(src).convert("RGB")
41
+ except Exception as e:
42
+ sys.exit(f"错误:无法读取图片 {src}({e})")
43
+ w, h = img.size
44
+
45
+ # 先按 16:9 中央裁一块基准
46
+ base_ratio = 16 / 9
47
+ if w / h > base_ratio:
48
+ cw, ch = int(h * base_ratio), h
49
+ else:
50
+ cw, ch = w, int(w / base_ratio)
51
+ x0, y0 = (w - cw) // 2, (h - ch) // 2
52
+ base = img.crop((x0, y0, x0 + cw, y0 + ch))
53
+
54
+ for name, (tw, th) in TARGETS.items():
55
+ ratio = tw / th
56
+ bw, bh = base.size
57
+ if bw / bh > ratio:
58
+ cw2, ch2 = int(bh * ratio), bh
59
+ else:
60
+ cw2, ch2 = bw, int(bw / ratio)
61
+ x1, y1 = (bw - cw2) // 2, (bh - ch2) // 2
62
+ out = base.crop((x1, y1, x1 + cw2, y1 + ch2)).resize((tw, th), Image.LANCZOS)
63
+ dest = out_dir / name
64
+ out.save(dest)
65
+ print(f"OK {dest} ({tw}x{th})")
66
+
67
+
68
+ if __name__ == "__main__":
69
+ main()
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """topmind-cover 异常输入测试:crop-cover.py 错误路径干净(任一失败 → 退出码 1)。"""
4
+ from __future__ import annotations
5
+
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ try:
12
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
13
+ except Exception:
14
+ pass
15
+
16
+ ROOT = Path(__file__).resolve().parent.parent
17
+ CROP = ROOT / "scripts" / "crop-cover.py"
18
+ PY = sys.executable
19
+ fails: list[str] = []
20
+
21
+
22
+ def run(args: list[str]) -> subprocess.CompletedProcess:
23
+ return subprocess.run([PY, str(CROP), *args], capture_output=True, text=True, timeout=60)
24
+
25
+
26
+ def check(name: str, cond: bool, detail: str = "") -> None:
27
+ print((" ✓ " if cond else " ✗ ") + name + (f" — {detail}" if detail and not cond else ""))
28
+ if not cond:
29
+ fails.append(name)
30
+
31
+
32
+ def main() -> None:
33
+ print("[negative_tests] topmind-cover")
34
+ with tempfile.TemporaryDirectory() as td:
35
+ td = Path(td)
36
+
37
+ # 1. 缺失输入 → 非零退出,无 Traceback
38
+ r = run([str(td / "nope.png"), "--out-dir", str(td / "o1")])
39
+ check("缺失输入干净报错", r.returncode != 0 and "Traceback" not in r.stderr, r.stderr[:100])
40
+
41
+ # 2. 非图片文件 → 非零退出,无 Traceback
42
+ (td / "notimg.txt").write_text("hello", encoding="utf-8")
43
+ r = run([str(td / "notimg.txt"), "--out-dir", str(td / "o2")])
44
+ check("非图片干净报错", r.returncode != 0 and "Traceback" not in r.stderr, r.stderr[:100])
45
+
46
+ # 3. 正常图片产出双尺寸
47
+ try:
48
+ from PIL import Image
49
+ img = Image.new("RGB", (1600, 900), (20, 30, 60))
50
+ img.save(td / "ok.png")
51
+ r = run([str(td / "ok.png"), "--out-dir", str(td / "o3")])
52
+ got_a = (td / "o3" / "00-封面.png").exists()
53
+ got_b = (td / "o3" / "00-封面-公众号.png").exists()
54
+ check("正常图片产出双尺寸", r.returncode == 0 and got_a and got_b, r.stderr[:100])
55
+ if got_a:
56
+ w, h = Image.open(td / "o3" / "00-封面.png").size
57
+ check("主图 1200×675", (w, h) == (1200, 675), f"实 {w}×{h}")
58
+ if got_b:
59
+ w, h = Image.open(td / "o3" / "00-封面-公众号.png").size
60
+ check("公众号图 900×383", (w, h) == (900, 383), f"实 {w}×{h}")
61
+ except ImportError:
62
+ print(" - 跳过图片生成测试(无 Pillow)")
63
+
64
+ if fails:
65
+ print(f"\n[negative_tests] {len(fails)} 项失败")
66
+ sys.exit(1)
67
+ print("\n[negative_tests] 全部通过")
68
+
69
+
70
+ if __name__ == "__main__":
71
+ 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()
@@ -0,0 +1,49 @@
1
+ # topmind-wechat-post · 公众号创作技能
2
+
3
+ 公众号文章全生命周期:交付包、审校改写、质量三关、状态同步、微信内联排版与发布清单。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npx @topmindspace/tms-skills install topmind-wechat-post
9
+ ```
10
+
11
+ ## 用法
12
+
13
+ ```bash
14
+ export TOPMIND_WORKSPACE=/path/to/workspace # 推荐;或用 --base 指定交付包根
15
+
16
+ # 新建交付包
17
+ python3 scripts/new-article.py --slug demo --title "标题" --direction reverse
18
+
19
+ # 排版体检 + 自动修复
20
+ python3 scripts/lint-wechat.py --input <包>/公众号稿.md --fix
21
+
22
+ # 去 AI 味扫描(目标 ≥85)
23
+ python3 scripts/scan_ai_flavor.py <包>/公众号稿.md
24
+
25
+ # Markdown → 全内联 HTML(必加 --embed-images,否则粘贴丢图)
26
+ python3 scripts/md2wechat.py --input <包>/公众号稿.md --out-dir <包> --slug demo --embed-images
27
+
28
+ # 状态同步
29
+ python3 scripts/sync-status.py --set 定稿 <包> --apply
30
+ ```
31
+
32
+ 脚本纯 Python 标准库,零依赖。
33
+
34
+ ## 三条路径
35
+
36
+ - **forward**:底稿 → 公众号(审校改写)
37
+ - **reverse**:选题原创 → 公众号 → 可选回推
38
+ - **站外拉取**:在线精选站 → 按 reverse 处理
39
+
40
+ ## 质量三关
41
+
42
+ 事实(一手来源)/ 逻辑(结构一致)/ 文字(去 AI 味 ≥85)。
43
+
44
+ ## 开发
45
+
46
+ ```bash
47
+ python3 scripts/package_skill.py --check
48
+ python3 scripts/negative_tests.py
49
+ ```