@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.
- package/CHANGELOG.md +66 -0
- package/README.en.md +6 -3
- package/README.md +6 -3
- package/package.json +5 -2
- package/top-ppt-html/README.en.md +2 -2
- package/top-ppt-html/README.md +1 -1
- package/top-ppt-html/SKILL.md +4 -4
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +24 -12
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +24 -12
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +24 -12
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.html +94 -165
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.model.json +32 -38
- package/top-ppt-html/assets/pptx-export.js +3 -2
- package/top-ppt-html/assets/style-gallery.html +2 -1
- package/top-ppt-html/assets/templates/architecture.html +38 -15
- package/top-ppt-html/assets/templates/engine.css +20 -9
- package/top-ppt-html/assets/templates/presentation.html +38 -17
- package/top-ppt-html/assets/templates/research.html +44 -23
- package/top-ppt-html/package-lock.json +2 -2
- package/top-ppt-html/package.json +1 -1
- package/top-ppt-html/references/components-atoms.md +1 -1
- package/top-ppt-html/references/content-rules.md +17 -11
- package/top-ppt-html/references/failure-modes.md +6 -4
- package/top-ppt-html/references/high-fidelity.md +14 -1
- package/top-ppt-html/references/icons.md +2 -2
- package/top-ppt-html/references/layout-grammar.md +3 -1
- package/top-ppt-html/references/layouts-research.md +1 -1
- package/top-ppt-html/references/pptx-export.md +2 -2
- package/top-ppt-html/references/tech-design.md +2 -0
- package/top-ppt-html/scripts/build_icon_assets.js +92 -0
- package/top-ppt-html/scripts/build_pptx.js +125 -29
- package/top-ppt-html/scripts/extract_model.py +21 -1
- package/top-ppt-html/scripts/icon_lib.js +53 -0
- package/top-ppt-html/scripts/icon_raster.js +86 -0
- package/top-ppt-html/scripts/layout-constants.json +4 -1
- package/top-ppt-html/scripts/lib_layout_regions.js +3 -1
- package/top-ppt-html/scripts/model-schema.json +7 -3
- package/top-ppt-html/scripts/package_skill.py +1 -0
- package/top-ppt-html/scripts/render_from_model.py +125 -24
- package/top-ppt-html/scripts/scaffold_report.py +47 -24
- package/top-ppt-html/scripts/sync_runtime.py +2 -1
- package/top-ppt-html/scripts/validate_pptx.py +14 -3
- package/top-ppt-html/scripts/validate_report.py +1 -1
- package/topmind-cover/README.md +40 -0
- package/topmind-cover/SKILL.md +66 -0
- package/topmind-cover/package-lock.json +16 -0
- package/topmind-cover/package.json +16 -0
- package/topmind-cover/references/cover-styles.md +40 -0
- package/topmind-cover/scripts/audit_css.py +39 -0
- package/topmind-cover/scripts/audit_docs.py +58 -0
- package/topmind-cover/scripts/audit_skill.py +60 -0
- package/topmind-cover/scripts/audit_styles.py +54 -0
- package/topmind-cover/scripts/crop-cover.py +69 -0
- package/topmind-cover/scripts/negative_tests.py +71 -0
- package/topmind-cover/scripts/package_skill.py +188 -0
- package/topmind-wechat-post/README.md +49 -0
- package/topmind-wechat-post/SKILL.md +208 -0
- package/topmind-wechat-post/assets/themes/amber-review.json +35 -0
- package/topmind-wechat-post/assets/themes/graphite.json +35 -0
- package/topmind-wechat-post/assets/themes/minimal-ink.json +35 -0
- package/topmind-wechat-post/assets/themes/newsprint.json +35 -0
- package/topmind-wechat-post/assets/themes/tech-blue.json +35 -0
- package/topmind-wechat-post/package-lock.json +16 -0
- package/topmind-wechat-post/package.json +16 -0
- package/topmind-wechat-post/references/design.md +50 -0
- package/topmind-wechat-post/references/element-spec.md +162 -0
- package/topmind-wechat-post/references/known-pits.md +86 -0
- package/topmind-wechat-post/references/theme-map.md +139 -0
- package/topmind-wechat-post/references/typography-rules.md +62 -0
- package/topmind-wechat-post/references/wechat-constraints.md +106 -0
- package/topmind-wechat-post/references/workflow.md +102 -0
- package/topmind-wechat-post/references/writing-quality.md +129 -0
- package/topmind-wechat-post/scripts/audit_css.py +39 -0
- package/topmind-wechat-post/scripts/audit_docs.py +58 -0
- package/topmind-wechat-post/scripts/audit_skill.py +60 -0
- package/topmind-wechat-post/scripts/audit_styles.py +54 -0
- package/topmind-wechat-post/scripts/lint-wechat.py +446 -0
- package/topmind-wechat-post/scripts/md2wechat.py +1602 -0
- package/topmind-wechat-post/scripts/negative_tests.py +65 -0
- package/topmind-wechat-post/scripts/new-article.py +183 -0
- package/topmind-wechat-post/scripts/package_skill.py +188 -0
- package/topmind-wechat-post/scripts/push-to-topstream.py +226 -0
- package/topmind-wechat-post/scripts/scan_ai_flavor.py +464 -0
- package/topmind-wechat-post/scripts/sync-mapping.py +220 -0
- package/topmind-wechat-post/scripts/sync-status.py +187 -0
- package/topmind-x-article/README.md +40 -0
- package/topmind-x-article/SKILL.md +62 -0
- package/topmind-x-article/package-lock.json +16 -0
- package/topmind-x-article/package.json +16 -0
- package/topmind-x-article/references/publish-checklist.md +24 -0
- package/topmind-x-article/references/x-format.md +24 -0
- package/topmind-x-article/scripts/audit_css.py +39 -0
- package/topmind-x-article/scripts/audit_docs.py +58 -0
- package/topmind-x-article/scripts/audit_skill.py +60 -0
- package/topmind-x-article/scripts/audit_styles.py +54 -0
- package/topmind-x-article/scripts/md2x.py +121 -0
- package/topmind-x-article/scripts/negative_tests.py +74 -0
- package/topmind-x-article/scripts/package_skill.py +188 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""sync-status.py — 同步公众号交付包的「状态 ⇄ 目录名 ⇄ 字数」。
|
|
4
|
+
|
|
5
|
+
规则(与专题 topic.md 的目录规范一致):
|
|
6
|
+
status = 草稿 → 目录名 YYYY-MM-DD-<中文名>
|
|
7
|
+
status = 定稿 / 已发布 → 目录名 YYYY-MM-DD-<中文名>-released
|
|
8
|
+
|
|
9
|
+
同时刷新 frontmatter 的 word_count(正文中文字数)。
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
python3 sync-status.py # 扫描全部,只报告(dry-run)
|
|
13
|
+
python3 sync-status.py --apply # 实际重命名并回写字数
|
|
14
|
+
python3 sync-status.py --set 定稿 <包目录> # 改状态 + 同步目录名
|
|
15
|
+
python3 sync-status.py --base <专题目录>
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
def _default_base():
|
|
24
|
+
"""交付包根:CLI --base → env TOPMIND_WECHAT_BASE → env TOPMIND_WORKSPACE 发现 → 惯例路径。"""
|
|
25
|
+
env = os.environ.get("TOPMIND_WECHAT_BASE") or os.environ.get("TOPMIND_WECHAT_PACKAGE_ROOT")
|
|
26
|
+
if env:
|
|
27
|
+
return os.path.abspath(os.path.expanduser(env))
|
|
28
|
+
ws = os.environ.get("TOPMIND_WORKSPACE")
|
|
29
|
+
if ws:
|
|
30
|
+
for rel in (
|
|
31
|
+
("40-创作", "2026-公众号"),
|
|
32
|
+
("20-专题", "2026-公众号"),
|
|
33
|
+
("88-交付", "2026-公众号"),
|
|
34
|
+
):
|
|
35
|
+
p = os.path.join(ws, *rel)
|
|
36
|
+
if os.path.isdir(p):
|
|
37
|
+
return p
|
|
38
|
+
return os.path.join(ws, "40-创作", "2026-公众号")
|
|
39
|
+
# 惯例:父工作区(可被 CLI 覆盖)
|
|
40
|
+
return os.path.join(
|
|
41
|
+
os.path.expanduser("~"), "TopWorkSpace", "topmind-workspace",
|
|
42
|
+
"40-创作", "2026-公众号")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _default_topstream():
|
|
46
|
+
env = os.environ.get("TOPSTREAM_ROOT") or os.environ.get("TOPMIND_TOPSTREAM")
|
|
47
|
+
if env:
|
|
48
|
+
return os.path.abspath(os.path.expanduser(env))
|
|
49
|
+
return os.path.join(os.path.expanduser("~"), "TopWorkSpace", "topstream")
|
|
50
|
+
|
|
51
|
+
DEFAULT_BASE = _default_base()
|
|
52
|
+
|
|
53
|
+
FINAL_STATUSES = ("定稿", "已发布")
|
|
54
|
+
VALID_STATUSES = ("草稿",) + FINAL_STATUSES
|
|
55
|
+
RELEASED_SUFFIX = "-released"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def split_fm(text):
|
|
59
|
+
"""返回 (frontmatter, body, head, tail_sep);无 frontmatter 时 fm=None"""
|
|
60
|
+
if not text.startswith("---\n"):
|
|
61
|
+
return None, text, "", ""
|
|
62
|
+
i = text.find("\n---", 3)
|
|
63
|
+
if i < 0:
|
|
64
|
+
return None, text, "", ""
|
|
65
|
+
return text[4:i], text[i + 4:].lstrip("\n"), text[:4], text[i:i + 4]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_field(fm, key):
|
|
69
|
+
m = re.search(r"^%s:\s*(.*)$" % re.escape(key), fm, re.M)
|
|
70
|
+
return m.group(1).strip().strip('"') if m else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def set_field(fm, key, val):
|
|
74
|
+
line = "%s: %s" % (key, val)
|
|
75
|
+
pat = re.compile(r"^%s:\s*.*$" % re.escape(key), re.M)
|
|
76
|
+
if pat.search(fm):
|
|
77
|
+
return pat.sub(line, fm, count=1)
|
|
78
|
+
return fm.rstrip("\n") + "\n" + line + "\n"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def scan(base):
|
|
82
|
+
"""返回 [(dirname, status, word_count, needs_rename, target_name)]"""
|
|
83
|
+
rows = []
|
|
84
|
+
for name in sorted(os.listdir(base)):
|
|
85
|
+
pkg = os.path.join(base, name)
|
|
86
|
+
draft = os.path.join(pkg, "公众号稿.md")
|
|
87
|
+
if not os.path.isdir(pkg) or not os.path.exists(draft):
|
|
88
|
+
continue
|
|
89
|
+
text = open(draft, encoding="utf-8").read()
|
|
90
|
+
fm, body, head, sep = split_fm(text)
|
|
91
|
+
if fm is None:
|
|
92
|
+
rows.append((name, None, None, False, None, "无 frontmatter"))
|
|
93
|
+
continue
|
|
94
|
+
status = get_field(fm, "status") or "草稿"
|
|
95
|
+
cn = len(re.findall(r"[\u4e00-\u9fff]", body))
|
|
96
|
+
|
|
97
|
+
want_final = status in FINAL_STATUSES
|
|
98
|
+
stem = name[:-len(RELEASED_SUFFIX)] if name.endswith(RELEASED_SUFFIX) else name
|
|
99
|
+
target = stem + (RELEASED_SUFFIX if want_final else "")
|
|
100
|
+
rows.append((name, status, cn, target != name, target, ""))
|
|
101
|
+
return rows
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def apply_rename(base, name, target, draft_path, fm, body, head, new_fm):
|
|
105
|
+
"""写回 frontmatter 后重命名目录"""
|
|
106
|
+
open(draft_path, "w", encoding="utf-8").write("---\n" + new_fm + "\n---\n\n" + body)
|
|
107
|
+
if target != name:
|
|
108
|
+
os.rename(os.path.join(base, name), os.path.join(base, target))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main():
|
|
112
|
+
ap = argparse.ArgumentParser()
|
|
113
|
+
ap.add_argument("--base", default=DEFAULT_BASE)
|
|
114
|
+
ap.add_argument("--apply", action="store_true", help="实际执行(默认只报告)")
|
|
115
|
+
ap.add_argument("--set", metavar="STATUS", choices=VALID_STATUSES,
|
|
116
|
+
help="把指定包改成该状态(需配合包名)")
|
|
117
|
+
ap.add_argument("pkg", nargs="?", help="包目录名(配合 --set 使用)")
|
|
118
|
+
args = ap.parse_args()
|
|
119
|
+
|
|
120
|
+
if not os.path.isdir(args.base):
|
|
121
|
+
print("✗ 专题目录不存在:%s" % args.base)
|
|
122
|
+
return 1
|
|
123
|
+
|
|
124
|
+
# 单包改状态
|
|
125
|
+
if args.set:
|
|
126
|
+
if not args.pkg:
|
|
127
|
+
print("✗ --set 需要指定包目录名")
|
|
128
|
+
return 1
|
|
129
|
+
draft = os.path.join(args.base, args.pkg, "公众号稿.md")
|
|
130
|
+
if not os.path.exists(draft):
|
|
131
|
+
print("✗ 找不到:%s" % draft)
|
|
132
|
+
return 1
|
|
133
|
+
text = open(draft, encoding="utf-8").read()
|
|
134
|
+
fm, body, head, sep = split_fm(text)
|
|
135
|
+
if fm is None:
|
|
136
|
+
print("✗ 该文件无 frontmatter")
|
|
137
|
+
return 1
|
|
138
|
+
fm = set_field(fm, "status", args.set)
|
|
139
|
+
cn = len(re.findall(r"[\u4e00-\u9fff]", body))
|
|
140
|
+
fm = set_field(fm, "word_count", str(cn))
|
|
141
|
+
|
|
142
|
+
want_final = args.set in FINAL_STATUSES
|
|
143
|
+
stem = args.pkg[:-len(RELEASED_SUFFIX)] if args.pkg.endswith(RELEASED_SUFFIX) else args.pkg
|
|
144
|
+
target = stem + (RELEASED_SUFFIX if want_final else "")
|
|
145
|
+
|
|
146
|
+
if args.apply:
|
|
147
|
+
apply_rename(args.base, args.pkg, target, draft, fm, body, head, fm)
|
|
148
|
+
print("✓ %s → status=%s,目录 %s(%d 中文字)" % (
|
|
149
|
+
args.pkg, args.set, target if target != args.pkg else "不变", cn))
|
|
150
|
+
else:
|
|
151
|
+
print("[dry-run] %s → status=%s,目录将变为 %s" % (args.pkg, args.set, target))
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
rows = scan(args.base)
|
|
155
|
+
if not rows:
|
|
156
|
+
print("专题下没有交付包:%s" % args.base)
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
print("%-44s %-6s %8s %s" % ("包目录", "状态", "中文字", "待同步"))
|
|
160
|
+
print("-" * 84)
|
|
161
|
+
changed = 0
|
|
162
|
+
for name, status, cn, needs, target, note in rows:
|
|
163
|
+
if status is None:
|
|
164
|
+
print("%-44s %-6s %8s %s" % (name, "-", "-", note))
|
|
165
|
+
continue
|
|
166
|
+
flag = "→ " + target if needs else ""
|
|
167
|
+
print("%-44s %-6s %8s %s" % (name, status, cn, flag))
|
|
168
|
+
|
|
169
|
+
if args.apply:
|
|
170
|
+
draft = os.path.join(args.base, name, "公众号稿.md")
|
|
171
|
+
text = open(draft, encoding="utf-8").read()
|
|
172
|
+
fm, body, head, sep = split_fm(text)
|
|
173
|
+
fm = set_field(fm, "word_count", str(cn))
|
|
174
|
+
apply_rename(args.base, name, target, draft, fm, body, head, fm)
|
|
175
|
+
if needs:
|
|
176
|
+
changed += 1
|
|
177
|
+
|
|
178
|
+
if args.apply:
|
|
179
|
+
print("\n✓ 已刷新 %d 个包的 word_count,重命名 %d 个目录" % (len(rows), changed))
|
|
180
|
+
else:
|
|
181
|
+
pending = sum(1 for r in rows if r[3])
|
|
182
|
+
print("\n[dry-run] %d 个包目录名与状态不一致,加 --apply 执行" % pending)
|
|
183
|
+
return 0
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
sys.exit(main())
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# topmind-x-article · X 长文一键发布
|
|
2
|
+
|
|
3
|
+
把 Markdown 原稿变成"复制 → 粘贴 → 发"的 X 长文(Article)发布包。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @topmindspace/tms-skills install topmind-x-article
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 用法
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 1. 原稿转可粘贴纯文本(一键复制的核心)
|
|
15
|
+
python3 scripts/md2x.py <原稿>.md --out <包>/X发布稿.txt
|
|
16
|
+
|
|
17
|
+
# 2. 封面图:用 topmind-cover 生成 1200×675
|
|
18
|
+
|
|
19
|
+
# 3. 按 references/publish-checklist.md 逐项发布
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 转换规则
|
|
23
|
+
|
|
24
|
+
X Article 编辑器对 markdown 支持弱且不稳定,`md2x.py` 按 `references/x-format.md`
|
|
25
|
+
转纯文本:标题→纯文本行、加粗/斜体去标记、链接→`文字(url)`、
|
|
26
|
+
图片→`[图N]`(文末附配图清单)、表格→"项:值"列表。
|
|
27
|
+
|
|
28
|
+
转完必须人工通读一遍。
|
|
29
|
+
|
|
30
|
+
## 发布
|
|
31
|
+
|
|
32
|
+
X 长文目前走人工粘贴发布(X Article 编辑器),API 不发长文。
|
|
33
|
+
发布后按清单做:首条评论置顶补信息、全文抓回核对。
|
|
34
|
+
|
|
35
|
+
## 开发
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python3 scripts/package_skill.py --check # 发布前校验
|
|
39
|
+
python3 scripts/negative_tests.py # 异常输入测试
|
|
40
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: topmind-x-article
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: "X 长文(Article)一键发布:Markdown 原稿 → 可直接粘贴的 X发布稿.txt + 封面图 + 发布清单。沉淀自实战。Use when X 长文、发 X 文章、X article、长文发 X。Do NOT use for 短推文(走推文流程)、公众号。"
|
|
5
|
+
action_category: write
|
|
6
|
+
triggers:
|
|
7
|
+
- X 长文
|
|
8
|
+
- 发 X 文章
|
|
9
|
+
- X article
|
|
10
|
+
- 长文发 X
|
|
11
|
+
triggers_cn:
|
|
12
|
+
- 写 X 长文
|
|
13
|
+
- X 长文发布
|
|
14
|
+
author: Muse
|
|
15
|
+
license: MIT
|
|
16
|
+
updated: 2026-09-29
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# topmind-x-article · X 长文一键发布
|
|
20
|
+
|
|
21
|
+
把一篇 Markdown 原稿变成"复制 → 粘贴 → 发"的 X 长文发布包。
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
原稿.md → md2x.py → X发布稿.txt → 封面图(topmind-cover) → 发布清单 → 人工粘贴发布
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 工作流
|
|
28
|
+
|
|
29
|
+
1. **转文本**(一键复制的核心):
|
|
30
|
+
```bash
|
|
31
|
+
python3 scripts/md2x.py <原稿>.md --out <包>/X发布稿.txt
|
|
32
|
+
```
|
|
33
|
+
转换规则见 `references/x-format.md`。转完**通读一遍**:X 不渲染 markdown,
|
|
34
|
+
标记剥离后断句、 emphasis 全靠文字本身,读不顺就回原稿改。
|
|
35
|
+
2. **封面图**:调 `topmind-cover`,平台选 `x`,得 1200×675 主图。
|
|
36
|
+
3. **发布清单**:按 `references/publish-checklist.md` 逐项过。
|
|
37
|
+
4. **发布**:用户在 X Article 编辑器粘贴 `X发布稿.txt` 全文、上传封面与配图,
|
|
38
|
+
人工点发布。**API 不发长文**(topmind-x 的 xurl 只覆盖短推文)。
|
|
39
|
+
5. **收尾**:记录发布时间与链接;全文抓回核对一遍(标题、数字、图序)。
|
|
40
|
+
|
|
41
|
+
## 目录约定
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
<包>/
|
|
45
|
+
├── 原稿.md # 输入(或复用既有原稿)
|
|
46
|
+
├── X发布稿.txt # 唯一复制入口:一键全选复制
|
|
47
|
+
├── cover-1200x675.png # 封面(topmind-cover 产出)
|
|
48
|
+
└── 发布清单.md # 本次发布 checklist(含时间、链接)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Manus 2.0 实战沉淀
|
|
52
|
+
|
|
53
|
+
- 补充信息(邀请码、链接、勘误)**不挤正文**,发在发布后首条评论并置顶
|
|
54
|
+
- 长文标题 ≤140 字符,首段 3 行内出钩子
|
|
55
|
+
- 配图逐张按 `[图N]` 顺序上传,传完对照清单点一遍
|
|
56
|
+
- 事实修正只改发布包,不回头改已发原文(除非勘误)
|
|
57
|
+
|
|
58
|
+
## When NOT to use
|
|
59
|
+
|
|
60
|
+
- 280 字短推文 → `topmind-x`
|
|
61
|
+
- 公众号 → `topmind-wechat`
|
|
62
|
+
- 只想存档不发布 → `topmind-capture`
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "topmind-x-article",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "topmind-x-article",
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "topmind-x-article",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "X 长文(Article)一键发布:Markdown 原稿转可直接粘贴的纯文本 + 封面图 + 发布清单。",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/topmindspace/tms-skills.git",
|
|
10
|
+
"directory": "topmind-x-article"
|
|
11
|
+
},
|
|
12
|
+
"type": "commonjs",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# X 长文发布清单
|
|
2
|
+
|
|
3
|
+
发布前逐项打勾,发布后填时间与链接归档。
|
|
4
|
+
|
|
5
|
+
## 发布前
|
|
6
|
+
|
|
7
|
+
- [ ] `X发布稿.txt` 通读一遍:断句顺、无 markdown 残留标记
|
|
8
|
+
- [ ] 标题 ≤140 字符;首段 3 行内出钩子
|
|
9
|
+
- [ ] 承重数字与原稿一致(转格式不改数字)
|
|
10
|
+
- [ ] 封面图 1200×675 已备好,标题字无错别字
|
|
11
|
+
- [ ] 配图按 `[图N]` 顺序排好,清单与文件一一对应
|
|
12
|
+
- [ ] 话题标签想好(放文末,不超过 3 个)
|
|
13
|
+
|
|
14
|
+
## 发布时
|
|
15
|
+
|
|
16
|
+
- [ ] 全文粘贴 → 封面上传 → 配图逐张按序插入
|
|
17
|
+
- [ ] 预览一遍:图序、段落、标题
|
|
18
|
+
|
|
19
|
+
## 发布后
|
|
20
|
+
|
|
21
|
+
- [ ] 首条评论补信息(邀请码 / 链接 / 补充说明),置顶
|
|
22
|
+
- [ ] 记录:发布时间、文章链接
|
|
23
|
+
- [ ] 全文抓回核对一遍(标题、数字、图序),有误用评论勘误
|
|
24
|
+
- [ ] `发布清单.md` 归档进包
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# X Article 粘贴格式规则
|
|
2
|
+
|
|
3
|
+
X Article 编辑器对 markdown 支持弱且不稳定。`md2x.py` 按以下规则转纯文本,
|
|
4
|
+
转完人工通读一遍是硬性步骤。
|
|
5
|
+
|
|
6
|
+
| Markdown | 转后 |
|
|
7
|
+
|----------|------|
|
|
8
|
+
| `#` / `##` 标题 | 纯文本行,前后各一空行 |
|
|
9
|
+
| `**加粗**` / `*斜体*` | 去标记留文字(语气靠文字本身) |
|
|
10
|
+
| `[文字](url)` | `文字(url)` |
|
|
11
|
+
| `` | `[图N]`,文末附"配图清单" |
|
|
12
|
+
| `-` / `1.` 列表 | 保留原符号 |
|
|
13
|
+
| `>` 引用 | 去 `>` 留文字 |
|
|
14
|
+
| 表格 | 转"项:值"列表;复杂表拆成段落 |
|
|
15
|
+
| `---` 分割线 | 空行 |
|
|
16
|
+
| 代码块 | 去围栏,内容缩进 4 格 |
|
|
17
|
+
| frontmatter | 整块丢弃 |
|
|
18
|
+
|
|
19
|
+
## 写作时就按 X 想
|
|
20
|
+
|
|
21
|
+
- 段落短:手机阅读每段 ≤4 行
|
|
22
|
+
- 小标题就是一句话,不依赖加粗撑结构
|
|
23
|
+
- 数字、专有名词第一次出现就写全(剥离标记后无高亮可依靠)
|
|
24
|
+
- 需要强调的词:换说法前置,不要靠 `**`
|
|
@@ -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()
|