@topmindspace/topmind-writing-skills 0.7.1
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 +931 -0
- package/LICENSE +21 -0
- package/README.en.md +124 -0
- package/README.md +124 -0
- package/bin/topmind-writing-skills.js +388 -0
- package/package.json +56 -0
- package/topmind-briefs/CHANGELOG.md +22 -0
- package/topmind-briefs/README.en.md +41 -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 +131 -0
- package/topmind-briefs/references/platform-specs.md +63 -0
- package/topmind-briefs/references/style-guide.md +60 -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 +54 -0
- package/topmind-cover/README.md +54 -0
- package/topmind-cover/SKILL.md +134 -0
- package/topmind-cover/assets/examples/README.md +34 -0
- package/topmind-cover/assets/examples/bg-blur-alt.png +0 -0
- package/topmind-cover/assets/examples/bg-blur-wechat.png +0 -0
- package/topmind-cover/assets/examples/bg-blur.png +0 -0
- package/topmind-cover/assets/examples/big-type-wechat.png +0 -0
- package/topmind-cover/assets/examples/big-type.png +0 -0
- package/topmind-cover/assets/examples/brand-launch-wechat.png +0 -0
- package/topmind-cover/assets/examples/brand-launch.png +0 -0
- package/topmind-cover/assets/examples/gan-huo-wechat.png +0 -0
- package/topmind-cover/assets/examples/gan-huo.png +0 -0
- package/topmind-cover/assets/examples/ip-fun-wechat.png +0 -0
- package/topmind-cover/assets/examples/ip-fun.png +0 -0
- package/topmind-cover/assets/examples/magazine-wechat.png +0 -0
- package/topmind-cover/assets/examples/magazine.png +0 -0
- package/topmind-cover/assets/examples/minimal-wechat.png +0 -0
- package/topmind-cover/assets/examples/minimal.png +0 -0
- package/topmind-cover/assets/examples/news-flash-wechat.png +0 -0
- package/topmind-cover/assets/examples/news-flash.png +0 -0
- package/topmind-cover/assets/examples/overview.png +0 -0
- package/topmind-cover/assets/examples/paper-collage-alt.png +0 -0
- package/topmind-cover/assets/examples/paper-collage-wechat.png +0 -0
- package/topmind-cover/assets/examples/paper-collage.png +0 -0
- package/topmind-cover/assets/examples/tutorial-steps-wechat.png +0 -0
- package/topmind-cover/assets/examples/tutorial-steps.png +0 -0
- package/topmind-cover/assets/examples/white-clean-alt-wechat.png +0 -0
- package/topmind-cover/assets/examples/white-clean-alt.png +0 -0
- package/topmind-cover/assets/examples/white-clean-wechat.png +0 -0
- package/topmind-cover/assets/examples/white-clean.png +0 -0
- package/topmind-cover/package-lock.json +16 -0
- package/topmind-cover/package.json +16 -0
- package/topmind-cover/references/cover-prompts.md +436 -0
- package/topmind-cover/references/cover-styles.md +496 -0
- package/topmind-cover/references/impact-language.md +94 -0
- package/topmind-cover/references/ref-white-clean-1.png +0 -0
- package/topmind-cover/references/ref-white-clean-2.png +0 -0
- package/topmind-cover/scripts/audit_css.py +39 -0
- package/topmind-cover/scripts/audit_docs.py +80 -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 +74 -0
- package/topmind-cover/scripts/negative_tests.py +71 -0
- package/topmind-cover/scripts/package_skill.py +188 -0
- package/topmind-wechat-post/README.en.md +111 -0
- package/topmind-wechat-post/README.md +111 -0
- package/topmind-wechat-post/SKILL.md +251 -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 +163 -0
- package/topmind-wechat-post/references/known-pits.md +85 -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 +111 -0
- package/topmind-wechat-post/references/workflow.md +102 -0
- package/topmind-wechat-post/references/writing-quality.md +105 -0
- package/topmind-wechat-post/scripts/audit_css.py +39 -0
- package/topmind-wechat-post/scripts/audit_docs.py +80 -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 +1792 -0
- package/topmind-wechat-post/scripts/negative_tests.py +171 -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.en.md +109 -0
- package/topmind-x-article/README.md +104 -0
- package/topmind-x-article/SKILL.md +156 -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 +31 -0
- package/topmind-x-article/references/x-format.md +27 -0
- package/topmind-x-article/references/x-html-format.md +85 -0
- package/topmind-x-article/scripts/audit_css.py +39 -0
- package/topmind-x-article/scripts/audit_docs.py +80 -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-html.py +522 -0
- package/topmind-x-article/scripts/md2x.py +323 -0
- package/topmind-x-article/scripts/negative_tests.py +91 -0
- package/topmind-x-article/scripts/package_skill.py +188 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""push-to-topstream.py — 反向回推:公众号稿 → topstream/notes 纯 Markdown。
|
|
4
|
+
|
|
5
|
+
反向链路的最后一公里最容易出错:`::: 容器`、`==高亮==`、`[!徽章]` 等公众号专用语法
|
|
6
|
+
绝不能写进 notes/(仓库原文保持纯 Markdown)。本脚本做「降级转换」并生成 README 索引条目。
|
|
7
|
+
|
|
8
|
+
降级规则:
|
|
9
|
+
- 剥离 frontmatter
|
|
10
|
+
- `:::` 围栏行删除,容器内文本原样保留
|
|
11
|
+
- `==高亮==` → 去掉 `==`
|
|
12
|
+
- `[!徽章]` / `[!xxx]` → 整段删除
|
|
13
|
+
- `[文字](url)` 外链保留(GitHub markdown 支持)
|
|
14
|
+
- 中英文盘古之白保留(对 notes 同样适用)
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python3 push-to-topstream.py <包目录> [--target notes/xxx.md] [--apply]
|
|
18
|
+
# 默认 dry-run:打印降级预览 + 语法统计 + README 索引条目,不写盘
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import shutil
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
DEFAULT_BASE = os.path.join(
|
|
28
|
+
os.path.expanduser("~"), "TopWorkSpace", "topmind-workspace",
|
|
29
|
+
"40-创作", "2026-公众号")
|
|
30
|
+
DEFAULT_TOPSTREAM = os.path.join(os.path.expanduser("~"), "TopWorkSpace", "topstream")
|
|
31
|
+
|
|
32
|
+
CONTAINER_RE = re.compile(r"^:::\s*\w*\s*$")
|
|
33
|
+
HIGHLIGHT_RE = re.compile(r"==([^=]+)==")
|
|
34
|
+
BADGE_RE = re.compile(r"\[![^\]]*\]")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def split_fm(text):
|
|
38
|
+
if not text.startswith("---\n"):
|
|
39
|
+
return None, text
|
|
40
|
+
i = text.find("\n---", 3)
|
|
41
|
+
if i < 0:
|
|
42
|
+
return None, text
|
|
43
|
+
return text[4:i], text[i + 4:].lstrip("\n")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_field(fm, key, default=""):
|
|
47
|
+
if not fm:
|
|
48
|
+
return default
|
|
49
|
+
m = re.search(r"^%s:\s*(.*)$" % re.escape(key), fm, re.M)
|
|
50
|
+
if not m:
|
|
51
|
+
return default
|
|
52
|
+
v = m.group(1).strip()
|
|
53
|
+
return v.strip('"').strip("'") if v else default
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def downgrade(body):
|
|
57
|
+
"""公众号稿正文 → notes 纯 Markdown。返回 (文本, 统计)"""
|
|
58
|
+
stats = {"container": 0, "highlight": 0, "badge": 0}
|
|
59
|
+
out = []
|
|
60
|
+
for line in body.split("\n"):
|
|
61
|
+
if CONTAINER_RE.match(line.strip()):
|
|
62
|
+
stats["container"] += 1
|
|
63
|
+
continue
|
|
64
|
+
if BADGE_RE.search(line):
|
|
65
|
+
stats["badge"] += len(BADGE_RE.findall(line))
|
|
66
|
+
line = BADGE_RE.sub("", line).strip()
|
|
67
|
+
if not line:
|
|
68
|
+
continue
|
|
69
|
+
line, n = HIGHLIGHT_RE.subn(r"\1", line)
|
|
70
|
+
stats["highlight"] += n
|
|
71
|
+
out.append(line)
|
|
72
|
+
return "\n".join(out).strip("\n") + "\n", stats
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
IMG_REF_RE = re.compile(r"(!\[[^\]]*\]\()(images/[^)\s]+)(\))")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def push_assets(body_md, pkg_path, topstream, note_target, name_map, dry=False):
|
|
79
|
+
"""把正文引用的本地图片搬进 topstream 并改写为仓库约定路径。
|
|
80
|
+
|
|
81
|
+
`notes/xxx.md` 里写 `images/xx.png` 在 GitHub 上是死链——仓库没有 `notes/images/`。
|
|
82
|
+
仓库既有约定:图放 `assets/images/<note-slug>/`,正文用 `../assets/images/<slug>/xx.png`。
|
|
83
|
+
返回 (新正文, 搬移明细, 未找到的源图)。
|
|
84
|
+
"""
|
|
85
|
+
slug = os.path.splitext(os.path.basename(note_target))[0]
|
|
86
|
+
dst_dir = os.path.join(topstream, "assets", "images", slug)
|
|
87
|
+
moved, missing = [], []
|
|
88
|
+
|
|
89
|
+
def repl(m):
|
|
90
|
+
src = m.group(2)
|
|
91
|
+
base = os.path.basename(src)
|
|
92
|
+
src_path = os.path.join(pkg_path, src)
|
|
93
|
+
if not os.path.exists(src_path):
|
|
94
|
+
missing.append(src)
|
|
95
|
+
return m.group(0)
|
|
96
|
+
new_name = name_map.get(base, base)
|
|
97
|
+
if not dry: # dry-run 只看映射,不落盘
|
|
98
|
+
os.makedirs(dst_dir, exist_ok=True)
|
|
99
|
+
shutil.copy2(src_path, os.path.join(dst_dir, new_name))
|
|
100
|
+
moved.append((base, new_name))
|
|
101
|
+
return m.group(1) + "../assets/images/%s/%s" % (slug, new_name) + m.group(3)
|
|
102
|
+
|
|
103
|
+
return IMG_REF_RE.sub(repl, body_md), moved, missing
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def make_readme_entry(title, target, tags):
|
|
107
|
+
tag_str = " ".join("`%s`" % t for t in tags)
|
|
108
|
+
return (
|
|
109
|
+
"- [**%s**](%s) \n"
|
|
110
|
+
" 描述:<一句话介绍这篇文章解决了什么问题、读者能带走什么> \n"
|
|
111
|
+
" %s\n" % (title, target, tag_str)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main():
|
|
116
|
+
ap = argparse.ArgumentParser()
|
|
117
|
+
ap.add_argument("pkg", help="包目录名(相对 base)或绝对路径")
|
|
118
|
+
ap.add_argument("--base", default=DEFAULT_BASE)
|
|
119
|
+
ap.add_argument("--topstream", default=DEFAULT_TOPSTREAM)
|
|
120
|
+
ap.add_argument("--target", help="回推目标 notes/xxx.md(缺省读 frontmatter target_file)")
|
|
121
|
+
ap.add_argument("--apply", action="store_true", help="实际写盘(默认只预览)")
|
|
122
|
+
ap.add_argument("--force", action="store_true", help="目标已存在时覆盖")
|
|
123
|
+
ap.add_argument("--assets", action="store_true",
|
|
124
|
+
help="把正文图片搬进 <topstream>/assets/images/<note-slug>/ 并改写引用(GitHub 必需)")
|
|
125
|
+
ap.add_argument("--asset-slug", default="", help="覆盖资源子目录名(默认取 note 文件名)")
|
|
126
|
+
ap.add_argument("--asset-names", default="",
|
|
127
|
+
help='文件名重命名映射,如 "00-封面.jpg=01-cover.jpg,01-分工.png=02-division-of-labor.png"')
|
|
128
|
+
args = ap.parse_args()
|
|
129
|
+
|
|
130
|
+
pkg_path = args.pkg if os.path.isabs(args.pkg) else os.path.join(args.base, args.pkg)
|
|
131
|
+
draft = os.path.join(pkg_path, "公众号稿.md")
|
|
132
|
+
if not os.path.exists(draft):
|
|
133
|
+
print("✗ 找不到公众号稿:%s" % draft)
|
|
134
|
+
return 1
|
|
135
|
+
|
|
136
|
+
text = open(draft, encoding="utf-8").read()
|
|
137
|
+
fm, body = split_fm(text)
|
|
138
|
+
title = get_field(fm, "title") or get_field(fm, "title", "")
|
|
139
|
+
status = get_field(fm, "status", "草稿")
|
|
140
|
+
target_file = get_field(fm, "target_file", "")
|
|
141
|
+
tags = []
|
|
142
|
+
mt = re.search(r"^tags:\s*\[(.*)\]$", fm or "", re.M)
|
|
143
|
+
if mt:
|
|
144
|
+
tags = [t.strip().strip('"').strip("'") for t in mt.group(1).split(",") if t.strip()]
|
|
145
|
+
tags = [t for t in tags if t not in ("公众号", "排版")]
|
|
146
|
+
|
|
147
|
+
target = args.target or (target_file if target_file != "pending" else "")
|
|
148
|
+
if not target:
|
|
149
|
+
print("✗ 需要回推目标:--target notes/xxx.md(frontmatter target_file 仍为 pending)")
|
|
150
|
+
return 1
|
|
151
|
+
if not target.startswith("notes/"):
|
|
152
|
+
print("✗ target 应以 notes/ 开头:%s" % target)
|
|
153
|
+
return 1
|
|
154
|
+
|
|
155
|
+
body_md, stats = downgrade(body)
|
|
156
|
+
|
|
157
|
+
name_map = {}
|
|
158
|
+
for pair in args.asset_names.split(","):
|
|
159
|
+
if "=" in pair:
|
|
160
|
+
k, v = pair.split("=", 1)
|
|
161
|
+
name_map[k.strip()] = v.strip()
|
|
162
|
+
|
|
163
|
+
if args.assets:
|
|
164
|
+
if args.asset_slug:
|
|
165
|
+
target = os.path.join(os.path.dirname(target), args.asset_slug + ".md")
|
|
166
|
+
body_md, moved, missing = push_assets(
|
|
167
|
+
body_md, pkg_path, args.topstream, target, name_map,
|
|
168
|
+
dry=not args.apply)
|
|
169
|
+
else:
|
|
170
|
+
moved, missing = [], []
|
|
171
|
+
|
|
172
|
+
# 确保有 H1 标题开头
|
|
173
|
+
if not body_md.lstrip().startswith("# "):
|
|
174
|
+
body_md = "# %s\n\n%s" % (title, body_md)
|
|
175
|
+
|
|
176
|
+
out_path = os.path.join(args.topstream, target)
|
|
177
|
+
|
|
178
|
+
print("回推预览:%s" % os.path.basename(pkg_path))
|
|
179
|
+
print(" 标题 : %s" % title)
|
|
180
|
+
print(" 状态 : %s" % status)
|
|
181
|
+
print(" 目标 : %s" % target)
|
|
182
|
+
print(" 降级统计 : 容器 %d · 高亮 %d · 徽章 %d"
|
|
183
|
+
% (stats["container"], stats["highlight"], stats["badge"]))
|
|
184
|
+
if status != "定稿":
|
|
185
|
+
print(" ⚠ status=%s,回推前应先定稿(sync-status.py --set 定稿)" % status)
|
|
186
|
+
print("")
|
|
187
|
+
if moved or missing:
|
|
188
|
+
print(" 图片搬运 : %d 张 → assets/images/%s/"
|
|
189
|
+
% (len(moved), os.path.splitext(os.path.basename(target))[0]))
|
|
190
|
+
for zh, en in moved:
|
|
191
|
+
if zh != en:
|
|
192
|
+
print(" %s → %s" % (zh, en))
|
|
193
|
+
non_ascii = [en for _, en in moved if not en.isascii()]
|
|
194
|
+
if non_ascii:
|
|
195
|
+
print(" ⚠ 文件名含非 ASCII 字符:%s" % "、".join(non_ascii))
|
|
196
|
+
print(" 仓库约定用 ASCII(--asset-names 传映射,如 \"00-封面.jpg=01-cover.jpg\")")
|
|
197
|
+
if missing:
|
|
198
|
+
print(" ✗ 源图缺失(引用仍是 images/,会 404):%s" % "、".join(missing))
|
|
199
|
+
elif "images/" in body_md:
|
|
200
|
+
print(" ⚠ 正文引用了本地图但未加 --assets:GitHub 上 images/ 是死链,请补 --assets")
|
|
201
|
+
print("")
|
|
202
|
+
print("—— README 索引条目(复制到 topstream/README.md 的「研析心得」段)——")
|
|
203
|
+
print(make_readme_entry(title, target, tags))
|
|
204
|
+
|
|
205
|
+
if args.apply:
|
|
206
|
+
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
|
207
|
+
if os.path.exists(out_path) and not args.force:
|
|
208
|
+
print("✗ 目标已存在,加 --force 覆盖:%s" % out_path)
|
|
209
|
+
return 1
|
|
210
|
+
with open(out_path, "w", encoding="utf-8") as fh:
|
|
211
|
+
fh.write(body_md)
|
|
212
|
+
print("")
|
|
213
|
+
print("✓ 已写入 %s(%d 字节)" % (out_path, len(body_md.encode("utf-8"))))
|
|
214
|
+
print(" 下一步:把上面的 README 条目插进 topstream/README.md;")
|
|
215
|
+
print(" (图片已按上面明细落 assets/,正文引用已改写为 ../assets/...)")
|
|
216
|
+
print(" 然后回本包改 frontmatter:target_file 从 pending 改为 %s" % target)
|
|
217
|
+
print(" (可用:sync-status.py 之外手动改 target_file 字段,或用编辑器)")
|
|
218
|
+
else:
|
|
219
|
+
print("")
|
|
220
|
+
print("[dry-run] 加 --apply 实际写盘。写盘不会自动改 README 与 frontmatter,")
|
|
221
|
+
print(" 这两步由你/agent 手工完成,保证索引不漂移。")
|
|
222
|
+
return 0
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
sys.exit(main())
|