@topmindspace/topmind-skills 4.15.0 → 4.15.2
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 +33 -0
- package/INSTALL.md +10 -2
- package/README.en.md +18 -14
- package/README.md +19 -15
- package/README.zh-CN.md +19 -15
- package/bin/install-skills.mjs +48 -8
- package/evals/evals.json +108 -3
- package/install-targets/claude-code.json +1 -2
- package/install-targets/codex.json +1 -2
- package/install-targets/hermes.json +1 -2
- package/install-targets/mimocode.json +1 -2
- package/install-targets/opencode.json +1 -2
- package/install-targets/universal.json +1 -2
- package/package.json +3 -3
- package/shared/host-loading.md +3 -3
- package/shared/trigger-disambiguation.md +21 -2
- package/skills.md +1 -1
- package/topmind/SKILL.md +20 -23
- package/topmind/references/multi-intent.md +2 -0
- package/topmind-capture/SKILL.md +11 -34
- package/topmind-ledger/SKILL.md +11 -18
- package/topmind-loop/SKILL.md +11 -23
- package/topmind-maintain/SKILL.md +11 -24
- package/topmind-memory/SKILL.md +11 -21
- package/topmind-organize/SKILL.md +15 -36
- package/topmind-pack.json +49 -29
- package/topmind-weread/SKILL.md +11 -18
- package/topmind-write/SKILL.md +15 -32
- package/topmind-x/SKILL.md +13 -19
- package/topmind-wechat/DESIGN.md +0 -50
- package/topmind-wechat/SKILL.md +0 -229
- package/topmind-wechat/assets/themes/amber-review.json +0 -35
- package/topmind-wechat/assets/themes/graphite.json +0 -35
- package/topmind-wechat/assets/themes/minimal-ink.json +0 -35
- package/topmind-wechat/assets/themes/newsprint.json +0 -35
- package/topmind-wechat/assets/themes/tech-blue.json +0 -35
- package/topmind-wechat/references/element-spec.md +0 -162
- package/topmind-wechat/references/known-pits.md +0 -86
- package/topmind-wechat/references/theme-map.md +0 -139
- package/topmind-wechat/references/typography-rules.md +0 -62
- package/topmind-wechat/references/wechat-constraints.md +0 -106
- package/topmind-wechat/references/workflow.md +0 -113
- package/topmind-wechat/references/writing-quality.md +0 -137
- package/topmind-wechat/scripts/audit-provenance.py +0 -366
- package/topmind-wechat/scripts/lint-wechat.py +0 -446
- package/topmind-wechat/scripts/md2wechat.py +0 -1599
- package/topmind-wechat/scripts/new-article.py +0 -183
- package/topmind-wechat/scripts/push-to-topstream.py +0 -226
- package/topmind-wechat/scripts/scan_ai_flavor.py +0 -101
- package/topmind-wechat/scripts/sync-mapping.py +0 -220
- package/topmind-wechat/scripts/sync-status.py +0 -187
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""sync-mapping.py — 校验「frontmatter ⇄ 目录名 ⇄ topstream 文件 ⇄ 派生视图」映射一致性。
|
|
4
|
-
|
|
5
|
-
frontmatter 是映射唯一真源,topic.md 与 topstream `docs/公众号映射.md` 是派生视图。
|
|
6
|
-
派生视图靠手改,容易与真源漂移。本脚本只读校验,报告差异,不改文件。
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
python3 sync-mapping.py # 校验全部包(dry-run)
|
|
10
|
-
python3 sync-mapping.py --topstream <path> # 额外校验 notes 文件是否存在
|
|
11
|
-
python3 sync-mapping.py --check-topic <topic.md> # 校验 topic.md 总表是否与磁盘一致
|
|
12
|
-
python3 sync-mapping.py --apply # 刷新 word_count(仅这一项可自动修)
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
import argparse
|
|
16
|
-
import os
|
|
17
|
-
import re
|
|
18
|
-
import sys
|
|
19
|
-
|
|
20
|
-
def _default_base():
|
|
21
|
-
"""交付包根:CLI --base → env TOPMIND_WECHAT_BASE → env TOPMIND_WORKSPACE 发现 → 惯例路径。"""
|
|
22
|
-
env = os.environ.get("TOPMIND_WECHAT_BASE") or os.environ.get("TOPMIND_WECHAT_PACKAGE_ROOT")
|
|
23
|
-
if env:
|
|
24
|
-
return os.path.abspath(os.path.expanduser(env))
|
|
25
|
-
ws = os.environ.get("TOPMIND_WORKSPACE")
|
|
26
|
-
if ws:
|
|
27
|
-
for rel in (
|
|
28
|
-
("40-创作", "2026-公众号"),
|
|
29
|
-
("20-专题", "2026-公众号"),
|
|
30
|
-
("88-交付", "2026-公众号"),
|
|
31
|
-
):
|
|
32
|
-
p = os.path.join(ws, *rel)
|
|
33
|
-
if os.path.isdir(p):
|
|
34
|
-
return p
|
|
35
|
-
return os.path.join(ws, "40-创作", "2026-公众号")
|
|
36
|
-
# 惯例:父工作区(可被 CLI 覆盖)
|
|
37
|
-
return os.path.join(
|
|
38
|
-
os.path.expanduser("~"), "TopWorkSpace", "topmind-workspace",
|
|
39
|
-
"40-创作", "2026-公众号")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _default_topstream():
|
|
43
|
-
env = os.environ.get("TOPSTREAM_ROOT") or os.environ.get("TOPMIND_TOPSTREAM")
|
|
44
|
-
if env:
|
|
45
|
-
return os.path.abspath(os.path.expanduser(env))
|
|
46
|
-
return os.path.join(os.path.expanduser("~"), "TopWorkSpace", "topstream")
|
|
47
|
-
|
|
48
|
-
DEFAULT_BASE = _default_base()
|
|
49
|
-
|
|
50
|
-
VALID_STATUSES = ("草稿", "定稿", "已发布")
|
|
51
|
-
FINAL_STATUSES = ("定稿", "已发布")
|
|
52
|
-
RELEASED_SUFFIX = "-released"
|
|
53
|
-
DIR_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})-([\u4e00-\u9fffA-Za-z0-9][\u4e00-\u9fffA-Za-z0-9-]*?)(-released)?$")
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def split_fm(text):
|
|
57
|
-
if not text.startswith("---\n"):
|
|
58
|
-
return None, text
|
|
59
|
-
i = text.find("\n---", 3)
|
|
60
|
-
if i < 0:
|
|
61
|
-
return None, text
|
|
62
|
-
return text[4:i], text[i + 4:].lstrip("\n")
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def get_field(fm, key):
|
|
66
|
-
m = re.search(r"^%s:\s*(.*)$" % re.escape(key), fm, re.M)
|
|
67
|
-
if not m:
|
|
68
|
-
return None
|
|
69
|
-
v = m.group(1).strip()
|
|
70
|
-
if v in ("", '""', "''"):
|
|
71
|
-
return ""
|
|
72
|
-
return v.strip('"').strip("'")
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def cjk_count(body):
|
|
76
|
-
return len(re.findall(r"[\u4e00-\u9fff]", body))
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def collect(base):
|
|
80
|
-
"""返回 [(dirname, pkg_path, fm, body)]"""
|
|
81
|
-
out = []
|
|
82
|
-
if not os.path.isdir(base):
|
|
83
|
-
return out
|
|
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 = split_fm(text)
|
|
91
|
-
out.append((name, pkg, fm, body))
|
|
92
|
-
return out
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def check(base, topstream, check_topic):
|
|
96
|
-
rows = collect(base)
|
|
97
|
-
if not rows:
|
|
98
|
-
print("✗ 专题下没有交付包:%s" % base)
|
|
99
|
-
return 1
|
|
100
|
-
|
|
101
|
-
errors, warns = [], []
|
|
102
|
-
|
|
103
|
-
for name, pkg, fm, body in rows:
|
|
104
|
-
tag = "[" + name + "]"
|
|
105
|
-
if fm is None:
|
|
106
|
-
errors.append("%s 无 frontmatter" % tag)
|
|
107
|
-
continue
|
|
108
|
-
|
|
109
|
-
direction = get_field(fm, "direction") or ""
|
|
110
|
-
status = get_field(fm, "status") or "草稿"
|
|
111
|
-
source_file = get_field(fm, "source_file") or ""
|
|
112
|
-
target_file = get_field(fm, "target_file") or ""
|
|
113
|
-
wc = get_field(fm, "word_count")
|
|
114
|
-
|
|
115
|
-
# 字段完备性
|
|
116
|
-
if direction not in ("forward", "reverse"):
|
|
117
|
-
errors.append("%s direction 非法:%r(应为 forward/reverse)" % (tag, direction))
|
|
118
|
-
if status not in VALID_STATUSES:
|
|
119
|
-
errors.append("%s status 非法:%r" % (tag, status))
|
|
120
|
-
|
|
121
|
-
if direction == "forward" and not source_file:
|
|
122
|
-
errors.append("%s 是 forward 但缺 source_file(应填 notes/xxx.md)" % tag)
|
|
123
|
-
if direction == "reverse" and target_file == "":
|
|
124
|
-
errors.append("%s 是 reverse 但 target_file 为空(应为 pending 或 notes/xxx.md)" % tag)
|
|
125
|
-
if direction == "forward" and source_file and not source_file.startswith("notes/"):
|
|
126
|
-
warns.append("%s source_file 应以 notes/ 开头:%s" % (tag, source_file))
|
|
127
|
-
|
|
128
|
-
# 目录名与状态
|
|
129
|
-
m = DIR_RE.match(name)
|
|
130
|
-
if not m:
|
|
131
|
-
errors.append("%s 目录名不符合 YYYY-MM-DD-<短名>[-released]:%s" % (tag, name))
|
|
132
|
-
else:
|
|
133
|
-
has_sfx = name.endswith(RELEASED_SUFFIX)
|
|
134
|
-
if status in FINAL_STATUSES and not has_sfx:
|
|
135
|
-
errors.append("%s status=%s 但目录缺 -released 后缀(用 sync-status.py --set 修)"
|
|
136
|
-
% (tag, status))
|
|
137
|
-
if status == "草稿" and has_sfx:
|
|
138
|
-
errors.append("%s status=草稿 但目录带 -released 后缀" % tag)
|
|
139
|
-
|
|
140
|
-
# 字数是否落盘最新
|
|
141
|
-
cn = cjk_count(body)
|
|
142
|
-
if wc and wc.isdigit() and int(wc) != cn:
|
|
143
|
-
warns.append("%s word_count=%s 与实际中文字 %d 不一致(sync-status.py --apply 刷新)"
|
|
144
|
-
% (tag, wc, cn))
|
|
145
|
-
|
|
146
|
-
# topstream 文件存在性
|
|
147
|
-
if topstream:
|
|
148
|
-
for field, rel in (("source_file", source_file), ("target_file", target_file)):
|
|
149
|
-
if rel and rel not in ("pending", "") and rel.startswith("notes/"):
|
|
150
|
-
p = os.path.join(topstream, rel)
|
|
151
|
-
if not os.path.exists(p):
|
|
152
|
-
errors.append("%s %s 指向不存在的文件:%s" % (tag, field, rel))
|
|
153
|
-
|
|
154
|
-
# 派生视图漂移:topic.md 总表里的目录名应与磁盘集合一致
|
|
155
|
-
if check_topic:
|
|
156
|
-
if not os.path.exists(check_topic):
|
|
157
|
-
errors.append("topic.md 不存在:%s" % check_topic)
|
|
158
|
-
else:
|
|
159
|
-
topic = open(check_topic, encoding="utf-8").read()
|
|
160
|
-
listed = set()
|
|
161
|
-
for x in re.findall(r"`(\d{4}-\d{2}-\d{2}-[^`]+?/)`", topic):
|
|
162
|
-
m = DIR_RE.match(x.rstrip("/"))
|
|
163
|
-
if m:
|
|
164
|
-
listed.add(m.group(0))
|
|
165
|
-
on_disk = set(n for n, _, _, _ in rows) # 文章包(含 公众号稿.md)
|
|
166
|
-
on_disk_all = set(n for n in os.listdir(base)
|
|
167
|
-
if os.path.isdir(os.path.join(base, n))) # 含策划包等
|
|
168
|
-
missing_in_topic = on_disk - listed
|
|
169
|
-
stale_in_topic = listed - on_disk_all
|
|
170
|
-
if missing_in_topic:
|
|
171
|
-
warns.append("磁盘有但 topic.md 总表未列出:%s" % "、".join(sorted(missing_in_topic)))
|
|
172
|
-
if stale_in_topic:
|
|
173
|
-
warns.append("topic.md 总表列了但磁盘不存在:%s" % "、".join(sorted(stale_in_topic)))
|
|
174
|
-
|
|
175
|
-
# 输出
|
|
176
|
-
print("映射校验:%s" % base)
|
|
177
|
-
print("-" * 80)
|
|
178
|
-
for name, pkg, fm, body in rows:
|
|
179
|
-
if fm is None:
|
|
180
|
-
continue
|
|
181
|
-
d = get_field(fm, "direction")
|
|
182
|
-
s = get_field(fm, "status")
|
|
183
|
-
sf = get_field(fm, "source_file") or "-"
|
|
184
|
-
tf = get_field(fm, "target_file") or "-"
|
|
185
|
-
print("%-40s %-8s %-4s src=%-28s tgt=%s" % (name, d, s, sf[:28], tf[:28]))
|
|
186
|
-
print("-" * 80)
|
|
187
|
-
if errors:
|
|
188
|
-
print("错误 %d:" % len(errors))
|
|
189
|
-
for e in errors:
|
|
190
|
-
print(" ✗ %s" % e)
|
|
191
|
-
print("")
|
|
192
|
-
if warns:
|
|
193
|
-
print("警告 %d:" % len(warns))
|
|
194
|
-
for w in warns:
|
|
195
|
-
print(" ! %s" % w)
|
|
196
|
-
print("")
|
|
197
|
-
if not errors and not warns:
|
|
198
|
-
print("✓ 映射一致,无差异")
|
|
199
|
-
return 1 if errors else 0
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def main():
|
|
203
|
-
ap = argparse.ArgumentParser()
|
|
204
|
-
ap.add_argument("--base", default=DEFAULT_BASE)
|
|
205
|
-
ap.add_argument("--topstream", default=None,
|
|
206
|
-
help="topstream 仓库路径(默认 TOPSTREAM_ROOT 或 ~/TopWorkSpace/topstream)")
|
|
207
|
-
ap.add_argument("--check-topic", default=os.path.join(
|
|
208
|
-
DEFAULT_BASE, "topic.md"),
|
|
209
|
-
help="topic.md 路径,校验总表是否与磁盘一致")
|
|
210
|
-
ap.add_argument("--no-topstream", action="store_true", help="不校验 topstream")
|
|
211
|
-
ap.add_argument("--no-topic", action="store_true", help="不校验 topic.md")
|
|
212
|
-
args = ap.parse_args()
|
|
213
|
-
|
|
214
|
-
topstream = None if args.no_topstream else (args.topstream or _default_topstream())
|
|
215
|
-
check_topic = None if args.no_topic else args.check_topic
|
|
216
|
-
return check(args.base, topstream, check_topic)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if __name__ == "__main__":
|
|
220
|
-
sys.exit(main())
|
|
@@ -1,187 +0,0 @@
|
|
|
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())
|