@topmindspace/tms-skills 0.1.19 → 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 (59) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.en.md +5 -2
  3. package/README.md +4 -1
  4. package/package.json +5 -2
  5. package/topmind-cover/README.md +40 -0
  6. package/topmind-cover/SKILL.md +66 -0
  7. package/topmind-cover/package-lock.json +16 -0
  8. package/topmind-cover/package.json +16 -0
  9. package/topmind-cover/references/cover-styles.md +40 -0
  10. package/topmind-cover/scripts/audit_css.py +39 -0
  11. package/topmind-cover/scripts/audit_docs.py +58 -0
  12. package/topmind-cover/scripts/audit_skill.py +60 -0
  13. package/topmind-cover/scripts/audit_styles.py +54 -0
  14. package/topmind-cover/scripts/crop-cover.py +69 -0
  15. package/topmind-cover/scripts/negative_tests.py +71 -0
  16. package/topmind-cover/scripts/package_skill.py +188 -0
  17. package/topmind-wechat-post/README.md +49 -0
  18. package/topmind-wechat-post/SKILL.md +208 -0
  19. package/topmind-wechat-post/assets/themes/amber-review.json +35 -0
  20. package/topmind-wechat-post/assets/themes/graphite.json +35 -0
  21. package/topmind-wechat-post/assets/themes/minimal-ink.json +35 -0
  22. package/topmind-wechat-post/assets/themes/newsprint.json +35 -0
  23. package/topmind-wechat-post/assets/themes/tech-blue.json +35 -0
  24. package/topmind-wechat-post/package-lock.json +16 -0
  25. package/topmind-wechat-post/package.json +16 -0
  26. package/topmind-wechat-post/references/design.md +50 -0
  27. package/topmind-wechat-post/references/element-spec.md +162 -0
  28. package/topmind-wechat-post/references/known-pits.md +86 -0
  29. package/topmind-wechat-post/references/theme-map.md +139 -0
  30. package/topmind-wechat-post/references/typography-rules.md +62 -0
  31. package/topmind-wechat-post/references/wechat-constraints.md +106 -0
  32. package/topmind-wechat-post/references/workflow.md +102 -0
  33. package/topmind-wechat-post/references/writing-quality.md +129 -0
  34. package/topmind-wechat-post/scripts/audit_css.py +39 -0
  35. package/topmind-wechat-post/scripts/audit_docs.py +58 -0
  36. package/topmind-wechat-post/scripts/audit_skill.py +60 -0
  37. package/topmind-wechat-post/scripts/audit_styles.py +54 -0
  38. package/topmind-wechat-post/scripts/lint-wechat.py +446 -0
  39. package/topmind-wechat-post/scripts/md2wechat.py +1602 -0
  40. package/topmind-wechat-post/scripts/negative_tests.py +65 -0
  41. package/topmind-wechat-post/scripts/new-article.py +183 -0
  42. package/topmind-wechat-post/scripts/package_skill.py +188 -0
  43. package/topmind-wechat-post/scripts/push-to-topstream.py +226 -0
  44. package/topmind-wechat-post/scripts/scan_ai_flavor.py +464 -0
  45. package/topmind-wechat-post/scripts/sync-mapping.py +220 -0
  46. package/topmind-wechat-post/scripts/sync-status.py +187 -0
  47. package/topmind-x-article/README.md +40 -0
  48. package/topmind-x-article/SKILL.md +62 -0
  49. package/topmind-x-article/package-lock.json +16 -0
  50. package/topmind-x-article/package.json +16 -0
  51. package/topmind-x-article/references/publish-checklist.md +24 -0
  52. package/topmind-x-article/references/x-format.md +24 -0
  53. package/topmind-x-article/scripts/audit_css.py +39 -0
  54. package/topmind-x-article/scripts/audit_docs.py +58 -0
  55. package/topmind-x-article/scripts/audit_skill.py +60 -0
  56. package/topmind-x-article/scripts/audit_styles.py +54 -0
  57. package/topmind-x-article/scripts/md2x.py +121 -0
  58. package/topmind-x-article/scripts/negative_tests.py +74 -0
  59. package/topmind-x-article/scripts/package_skill.py +188 -0
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env python3
2
+ """md → X 长文可粘贴纯文本。
3
+
4
+ 把 Markdown 原稿转成可直接粘贴进 X Article 编辑器的纯文本:
5
+ 标题/正文保留,排版标记剥离,图片转为 [图N] 占位。
6
+
7
+ 用法:
8
+ python3 md2x.py <输入.md> --out <输出/X发布稿.txt>
9
+
10
+ 规则见 references/x-format.md。
11
+ """
12
+ import argparse
13
+ import re
14
+ import sys
15
+ from pathlib import Path
16
+
17
+
18
+ def convert(md: str):
19
+ images = []
20
+ out_lines = []
21
+ in_code = False
22
+
23
+ def img_repl(m):
24
+ alt = m.group(1).strip()
25
+ images.append(alt)
26
+ return f"[图{len(images)}]"
27
+
28
+ for raw in md.splitlines():
29
+ line = raw.rstrip()
30
+
31
+ # frontmatter 直接跳过
32
+ if line.strip() == "---":
33
+ continue
34
+
35
+ # 代码块:去围栏,内容缩进保留
36
+ if line.strip().startswith("```"):
37
+ in_code = not in_code
38
+ continue
39
+ if in_code:
40
+ out_lines.append(" " + line)
41
+ continue
42
+
43
+ # 图片占位
44
+ line = re.sub(r"!\[([^\]]*)\]\([^)]*\)", img_repl, line)
45
+
46
+ # 标题 → 纯文本行
47
+ m = re.match(r"^(#{1,6})\s+(.*)$", line)
48
+ if m:
49
+ out_lines.append("")
50
+ out_lines.append(m.group(2).strip())
51
+ out_lines.append("")
52
+ continue
53
+
54
+ # 引用 → 纯文本
55
+ line = re.sub(r"^>\s?", "", line)
56
+
57
+ # 分割线 → 空行
58
+ if re.match(r"^(\*{3,}|-{3,}|_{3,})\s*$", line):
59
+ out_lines.append("")
60
+ continue
61
+
62
+ # 表格行 → "项:值" 列表
63
+ if "|" in line and line.strip().startswith("|"):
64
+ cells = [c.strip() for c in line.strip().strip("|").split("|")]
65
+ if all(re.match(r"^:?-+:?$", c) for c in cells):
66
+ continue
67
+ label = cells[0]
68
+ rest = ",".join(c for c in cells[1:] if c)
69
+ out_lines.append(f"{label}:{rest}" if rest else label)
70
+ continue
71
+
72
+ # 行内标记剥离
73
+ line = re.sub(r"\*\*(.+?)\*\*", r"\1", line) # 加粗
74
+ line = re.sub(r"__(.+?)__", r"\1", line)
75
+ line = re.sub(r"\*(.+?)\*", r"\1", line) # 斜体
76
+ line = re.sub(r"`(.+?)`", r"\1", line) # 行内代码
77
+ line = re.sub(r"~~(.+?)~~", r"\1", line) # 删除线
78
+ line = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1(\2)", line) # 链接
79
+ line = re.sub(r"^(\s*)- \[ \]\s+", r"\1- ", line) # 任务列表
80
+ line = re.sub(r"^(\s*)- \[x\]\s+", r"\1- ", line, flags=re.I)
81
+
82
+ out_lines.append(line)
83
+
84
+ # 压缩 3+ 空行为 2 个
85
+ text = "\n".join(out_lines)
86
+ text = re.sub(r"\n{3,}", "\n\n", text).strip() + "\n"
87
+
88
+ # 文末附图注清单
89
+ if images:
90
+ text += "\n—— 配图清单 ——\n"
91
+ for i, alt in enumerate(images, 1):
92
+ text += f"[图{i}] {alt or '(未写图注)'}\n"
93
+
94
+ return text, images
95
+
96
+
97
+ def main() -> None:
98
+ ap = argparse.ArgumentParser()
99
+ ap.add_argument("input", help="输入 Markdown")
100
+ ap.add_argument("--out", required=True, help="输出 X发布稿.txt")
101
+ args = ap.parse_args()
102
+
103
+ inp = Path(args.input)
104
+ if not inp.is_file():
105
+ sys.exit(f"错误:找不到输入文件 {inp}")
106
+ md = inp.read_text(encoding="utf-8")
107
+ # 去掉首尾 frontmatter 块
108
+ md = re.sub(r"^---\n.*?\n---\n", "", md, flags=re.S)
109
+ text, images = convert(md)
110
+
111
+ out = Path(args.out)
112
+ out.parent.mkdir(parents=True, exist_ok=True)
113
+ out.write_text(text, encoding="utf-8")
114
+
115
+ chars = len(text.replace("\n", ""))
116
+ print(f"OK {out}")
117
+ print(f"正文约 {chars} 字,配图 {len(images)} 张")
118
+
119
+
120
+ if __name__ == "__main__":
121
+ main()
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """topmind-x-article 异常输入测试:md2x.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
+ MD2X = ROOT / "scripts" / "md2x.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(MD2X), *args], capture_output=True, text=True, timeout=30)
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-x-article")
34
+ with tempfile.TemporaryDirectory() as td:
35
+ td = Path(td)
36
+
37
+ # 1. 空文件
38
+ (td / "empty.md").write_text("", encoding="utf-8")
39
+ r = run([str(td / "empty.md"), "--out", str(td / "o1.txt")])
40
+ check("空文件不崩溃", r.returncode == 0 and "Traceback" not in r.stderr)
41
+
42
+ # 2. 只有 frontmatter
43
+ (td / "fm.md").write_text("---\ntitle: t\n---\n", encoding="utf-8")
44
+ r = run([str(td / "fm.md"), "--out", str(td / "o2.txt")])
45
+ check("纯 frontmatter 不崩溃", r.returncode == 0 and "Traceback" not in r.stderr)
46
+
47
+ # 3. 未闭合代码围栏
48
+ (td / "fence.md").write_text("# t\n\n```python\nprint(1)\n", encoding="utf-8")
49
+ r = run([str(td / "fence.md"), "--out", str(td / "o3.txt")])
50
+ check("未闭合围栏不崩溃", r.returncode == 0 and "Traceback" not in r.stderr)
51
+
52
+ # 4. 无分隔行的表格
53
+ (td / "tbl.md").write_text("# t\n\n| a | b |\n| 1 | 2 |\n", encoding="utf-8")
54
+ r = run([str(td / "tbl.md"), "--out", str(td / "o4.txt")])
55
+ check("异常表格不崩溃", r.returncode == 0 and "Traceback" not in r.stderr)
56
+
57
+ # 5. 不存在的输入 → 非零退出且无 Traceback
58
+ r = run([str(td / "nope.md"), "--out", str(td / "o5.txt")])
59
+ check("缺失输入干净报错", r.returncode != 0 and "Traceback" not in r.stderr)
60
+
61
+ # 6. 正常输入产出配图清单
62
+ (td / "ok.md").write_text("# t\n\n![a](x.png)\n", encoding="utf-8")
63
+ r = run([str(td / "ok.md"), "--out", str(td / "o6.txt")])
64
+ out = (td / "o6.txt").read_text(encoding="utf-8") if (td / "o6.txt").exists() else ""
65
+ check("正常输入含配图清单", r.returncode == 0 and "[图1]" in out)
66
+
67
+ if fails:
68
+ print(f"\n[negative_tests] {len(fails)} 项失败")
69
+ sys.exit(1)
70
+ print("\n[negative_tests] 全部通过")
71
+
72
+
73
+ if __name__ == "__main__":
74
+ 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()