@topmindspace/tms-skills 0.3.1 → 0.3.5
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 +159 -0
- package/README.en.md +6 -76
- package/README.md +7 -75
- package/package.json +1 -1
- package/top-ppt-html/README.en.md +19 -20
- package/top-ppt-html/README.md +36 -90
- package/top-ppt-html/SKILL.md +65 -60
- package/top-ppt-html/references/charts.md +0 -15
- package/top-ppt-html/references/components.md +0 -18
- package/top-ppt-html/references/design-system.md +2 -2
- package/top-ppt-html/references/icons.md +2 -2
- package/top-ppt-html/references/infographics.md +1 -5
- package/top-ppt-html/references/modes.md +4 -4
- package/top-ppt-html/references/tech-design.md +3 -3
- package/top-ppt-html/scripts/build_examples.py +4 -4
- package/top-ppt-html/scripts/build_pptx.js +240 -59
- package/top-ppt-html/scripts/check_triggers.py +10 -2
- package/top-ppt-html/scripts/extract_model.py +16 -5
- package/top-ppt-html/scripts/gen_channel_a.js +55 -10
- package/top-ppt-html/scripts/hydrate_charts.py +68 -13
- package/top-ppt-html/scripts/negative_tests.py +1 -1
- package/top-ppt-html/scripts/prepare_images.py +1 -1
- package/top-ppt-html/scripts/recommend_layout.py +26 -3
- package/top-ppt-html/scripts/render_from_model.py +78 -13
- package/top-ppt-html/scripts/scaffold_report.py +43 -3
- package/top-ppt-html/scripts/sync_runtime.py +32 -4
- package/top-ppt-html/scripts/test_feedback_gates.py +24 -0
- package/top-ppt-html/scripts/validate_pptx.py +10 -1
- package/top-ppt-html/scripts/validate_report.py +43 -7
- package/topmind-cover/README.en.md +4 -17
- package/topmind-cover/README.md +5 -18
- package/topmind-cover/SKILL.md +28 -35
- package/topmind-cover/assets/examples/README.md +15 -26
- 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/tutorial-steps-wechat.png +0 -0
- package/topmind-cover/assets/examples/tutorial-steps.png +0 -0
- package/topmind-cover/references/cover-styles.md +275 -249
- package/topmind-wechat-post/README.en.md +49 -1
- package/topmind-wechat-post/README.md +48 -2
- package/topmind-wechat-post/SKILL.md +35 -37
- package/topmind-wechat-post/references/element-spec.md +1 -1
- package/topmind-wechat-post/references/known-pits.md +2 -3
- package/topmind-wechat-post/references/typography-rules.md +1 -1
- package/topmind-wechat-post/references/workflow.md +3 -3
- package/topmind-wechat-post/references/writing-quality.md +8 -32
- package/topmind-wechat-post/scripts/md2wechat.py +117 -41
- package/topmind-wechat-post/scripts/negative_tests.py +32 -0
- package/topmind-x-article/README.en.md +40 -1
- package/topmind-x-article/README.md +42 -4
- package/topmind-x-article/SKILL.md +11 -5
- package/topmind-x-article/references/x-format.md +13 -10
- package/topmind-x-article/scripts/md2x.py +220 -40
|
@@ -217,7 +217,10 @@ def highlight(code, lang, scheme=None):
|
|
|
217
217
|
if not rules:
|
|
218
218
|
return html.escape(code, quote=False)
|
|
219
219
|
palette = scheme or CTX.get("hl_scheme") or HIGHLIGHT
|
|
220
|
-
pattern =
|
|
220
|
+
pattern = _HL_PATTERN_CACHE.get(key)
|
|
221
|
+
if pattern is None:
|
|
222
|
+
pattern = re.compile("|".join("(?P<%s>%s)" % (name, pat) for name, pat in rules), re.S)
|
|
223
|
+
_HL_PATTERN_CACHE[key] = pattern
|
|
221
224
|
out = []
|
|
222
225
|
pos = 0
|
|
223
226
|
for m in pattern.finditer(code):
|
|
@@ -335,6 +338,22 @@ HAN_RE = re.compile("[%s]" % HAN)
|
|
|
335
338
|
PANGU_A = re.compile(r"([%s])([A-Za-z0-9])" % HAN)
|
|
336
339
|
PANGU_B = re.compile(r"([A-Za-z0-9])([%s])" % HAN)
|
|
337
340
|
|
|
341
|
+
# render_inline() 的行内模式:预编译在模块级,避免每行重复走 re 缓存查找
|
|
342
|
+
# (666k 行级调用 × 10 个模式时,re.sub 的字符串模式查找是可观开销)。
|
|
343
|
+
INLINE_CODE_RE = re.compile(r"`([^`]+)`")
|
|
344
|
+
INLINE_IMG_RE = re.compile(r"!\[([^\]]*)\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)")
|
|
345
|
+
INLINE_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)\s]+)\)")
|
|
346
|
+
INLINE_BADGE_RE = re.compile(r"\[!([^\]]+)\]")
|
|
347
|
+
INLINE_HL_RE = re.compile(r"==([^=]+)==")
|
|
348
|
+
INLINE_UNDER_RE = re.compile(r"\+\+([^+]+)\+\+")
|
|
349
|
+
INLINE_U_RE = re.compile(r"<u>(.+?)</u>")
|
|
350
|
+
INLINE_MARK_RE = re.compile(r"~~([^~]+)~~")
|
|
351
|
+
INLINE_STRONG_RE = re.compile(r"\*\*([^*]+)\*\*")
|
|
352
|
+
INLINE_EM_RE = re.compile(r"(?<![*\w])\*([^*\n]+)\*(?!\*)")
|
|
353
|
+
|
|
354
|
+
# highlight() 的语言联合模式缓存:同一语言的多段代码块只编译一次
|
|
355
|
+
_HL_PATTERN_CACHE = {}
|
|
356
|
+
|
|
338
357
|
|
|
339
358
|
def pangu(text):
|
|
340
359
|
text = PANGU_A.sub(r"\1 \2", text)
|
|
@@ -363,7 +382,7 @@ def render_inline(raw):
|
|
|
363
382
|
return CODE_TOKEN % (len(spans) - 1)
|
|
364
383
|
|
|
365
384
|
text = esc(raw)
|
|
366
|
-
text =
|
|
385
|
+
text = INLINE_CODE_RE.sub(lambda m: stash(m.group(1)), text)
|
|
367
386
|
|
|
368
387
|
if use_pangu:
|
|
369
388
|
text = pangu(text)
|
|
@@ -371,22 +390,37 @@ def render_inline(raw):
|
|
|
371
390
|
strong = 'font-weight:600;color:%s;' % theme["text_strong"]
|
|
372
391
|
em = 'font-style:normal;color:%s;' % theme["text_muted"]
|
|
373
392
|
|
|
374
|
-
# images
|
|
393
|
+
# images(行内图与独立成行图共用同一正则语义)
|
|
375
394
|
def img_repl(m):
|
|
376
395
|
alt, src = m.group(1), m.group(2)
|
|
396
|
+
# 行内图片同样走图片管线:本地文件解析 → 复制到 images/ → 登记清单,
|
|
397
|
+
# 否则 --embed-images 找不到文件、清单漏项、计数为 0(历史 bug:
|
|
398
|
+
# 段落里的  被原样保留相对路径,粘贴到公众号后整图消失)。
|
|
399
|
+
resolver = CTX.get("img_resolve")
|
|
400
|
+
if resolver and not re.match(r"^(https?:|data:|//)", src):
|
|
401
|
+
src = resolver(html.unescape(src))
|
|
377
402
|
caption = ' alt="%s"' % alt if alt else ""
|
|
378
403
|
return ('<img src="%s"%s style="max-width:100%%;height:auto;'
|
|
379
404
|
'border-radius:3px;vertical-align:middle;">') % (src, caption)
|
|
380
405
|
|
|
381
|
-
text =
|
|
406
|
+
text = INLINE_IMG_RE.sub(img_repl, text)
|
|
382
407
|
|
|
383
408
|
# links
|
|
384
409
|
def link_repl(m):
|
|
385
410
|
label, url = m.group(1), m.group(2)
|
|
386
411
|
if link_mode == "footnote" and footnotes is not None:
|
|
387
|
-
|
|
412
|
+
# url→序号字典:原写法每次链接都扫全表两次(O(n²)),长文链接多时明显变慢
|
|
413
|
+
fidx = CTX.get("footnote_idx")
|
|
414
|
+
if fidx is not None and url in fidx:
|
|
415
|
+
idx = fidx[url]
|
|
416
|
+
elif fidx is not None:
|
|
388
417
|
footnotes.append({"url": url, "label": label})
|
|
389
|
-
|
|
418
|
+
idx = len(footnotes)
|
|
419
|
+
fidx[url] = idx
|
|
420
|
+
else: # 外部直接调 render_inline 且未初始化索引时,走原来的线性查找
|
|
421
|
+
if url not in [f["url"] for f in footnotes]:
|
|
422
|
+
footnotes.append({"url": url, "label": label})
|
|
423
|
+
idx = [f["url"] for f in footnotes].index(url) + 1
|
|
390
424
|
return ('%s<sup style="font-size:11px;color:%s;'
|
|
391
425
|
'padding-left:1px;">[%d]</sup>'
|
|
392
426
|
% (label, theme["accent"], idx))
|
|
@@ -396,13 +430,12 @@ def render_inline(raw):
|
|
|
396
430
|
return ('<span style="font-size:13px;color:%s;word-break:break-all;">'
|
|
397
431
|
'%s(%s)</span>' % (theme["text_light"], label, url))
|
|
398
432
|
|
|
399
|
-
text =
|
|
433
|
+
text = INLINE_LINK_RE.sub(link_repl, text)
|
|
400
434
|
|
|
401
435
|
# badges: [!文本]
|
|
402
436
|
# 不用 display:inline-block:公众号对非常规 display 值支持不稳定,
|
|
403
437
|
# 徽章是纯行内元素,padding 足够撑开视觉。
|
|
404
|
-
text =
|
|
405
|
-
r"\[!([^\]]+)\]",
|
|
438
|
+
text = INLINE_BADGE_RE.sub(
|
|
406
439
|
r'<span style="font-size:11px;line-height:1.6;'
|
|
407
440
|
r'color:' + theme["accent"] + r';background:' + theme["accent_soft"] + r';'
|
|
408
441
|
r'padding:1px 6px;border-radius:2px;margin:0 2px;'
|
|
@@ -411,8 +444,7 @@ def render_inline(raw):
|
|
|
411
444
|
)
|
|
412
445
|
|
|
413
446
|
# ==高亮== before bold so inner markup still works
|
|
414
|
-
text =
|
|
415
|
-
r"==([^=]+)==",
|
|
447
|
+
text = INLINE_HL_RE.sub(
|
|
416
448
|
r'<strong style="font-weight:600;color:' + theme["text_strong"] + r';'
|
|
417
449
|
r'background:' + theme["accent_soft"] + r';padding:1px 3px;'
|
|
418
450
|
r'border-radius:2px;">\1</strong>',
|
|
@@ -424,20 +456,19 @@ def render_inline(raw):
|
|
|
424
456
|
underline = ('<span style="border-bottom:2px solid %s;font-weight:600;'
|
|
425
457
|
'color:%s;">\\1</span>'
|
|
426
458
|
% (theme["accent_light"], theme["text_strong"]))
|
|
427
|
-
text =
|
|
428
|
-
text =
|
|
459
|
+
text = INLINE_UNDER_RE.sub(underline, text)
|
|
460
|
+
text = INLINE_U_RE.sub(underline, text)
|
|
429
461
|
|
|
430
462
|
# ~~荧光笔~~:半高亮。用纯色浅底,不用 linear-gradient
|
|
431
463
|
# (渐变在公众号会被静默丢弃,退化成无底)。
|
|
432
|
-
text =
|
|
433
|
-
r"~~([^~]+)~~",
|
|
464
|
+
text = INLINE_MARK_RE.sub(
|
|
434
465
|
r'<span style="background:%s;font-weight:600;color:%s;">\1</span>'
|
|
435
466
|
% (theme["accent_soft"], theme["text_strong"]),
|
|
436
467
|
text,
|
|
437
468
|
)
|
|
438
469
|
|
|
439
|
-
text =
|
|
440
|
-
text =
|
|
470
|
+
text = INLINE_STRONG_RE.sub(r'<strong style="%s">\1</strong>' % strong, text)
|
|
471
|
+
text = INLINE_EM_RE.sub(r'<em style="%s">\1</em>' % em, text)
|
|
441
472
|
|
|
442
473
|
for idx, code in enumerate(spans):
|
|
443
474
|
text = text.replace(
|
|
@@ -454,12 +485,12 @@ def render_inline(raw):
|
|
|
454
485
|
HEADING_RE = re.compile(r"^(#{1,6})\s+(.*)$")
|
|
455
486
|
FENCE_RE = re.compile(r"^```(\w*)\s*$")
|
|
456
487
|
HR_RE = re.compile(r"^(-{3,}|\*{3,}|_{3,})\s*$")
|
|
457
|
-
IMG_ONLY_RE = re.compile(r"^!\[([^\]]*)\]\(([^)\s]+)
|
|
488
|
+
IMG_ONLY_RE = re.compile(r"^!\[([^\]]*)\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)\s*$")
|
|
458
489
|
CAPTION_RE = re.compile(r"^\*(.+)\*\s*$")
|
|
459
490
|
TABLE_SEP_RE = re.compile(r"^\|?[\s:|-]+\|[\s:|-]*$")
|
|
460
491
|
CONTAINER_RE = re.compile(r"^:::+\s*(\w+)?\s*(.*)$")
|
|
461
492
|
|
|
462
|
-
CN_NUM = "
|
|
493
|
+
CN_NUM = "一二三四五六七八九十壹贰叁肆伍陆柒捌玖拾"
|
|
463
494
|
|
|
464
495
|
# 标题里手写的中文/阿拉伯序号。序号应由排版层生成(可换样式、可加引用),
|
|
465
496
|
# 写进标题文本后就变成了不可编程的字符串。这里统一剥离后重新编号。
|
|
@@ -741,10 +772,13 @@ def render_table_cards(head, body, theme):
|
|
|
741
772
|
|
|
742
773
|
|
|
743
774
|
def render_hr(theme):
|
|
744
|
-
# 宽度用百分比:公众号端 px
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
775
|
+
# 宽度用百分比:公众号端 px 固定值在不同屏宽下表现不一致。
|
|
776
|
+
# 不用 display:inline-block:audit_inline 会把它判为 WARN,而这是生成器
|
|
777
|
+
# 自己的输出、用户无从修复——自检永远带一条洗不掉的 WARN 等于没有自检。
|
|
778
|
+
# 块级 section + margin:0 auto 居中,视觉与原来完全一致。
|
|
779
|
+
return ('<section style="margin:32px 0;">'
|
|
780
|
+
'<section style="width:12%%;height:2px;background:%s;'
|
|
781
|
+
'margin:0 auto;"></section></section>' % theme["accent"])
|
|
748
782
|
|
|
749
783
|
|
|
750
784
|
def render_figure(src, caption, theme, alt=""):
|
|
@@ -1103,8 +1137,14 @@ def convert(md_text, md_path, out_dir, theme, link_mode="footnote",
|
|
|
1103
1137
|
CTX["theme"] = theme
|
|
1104
1138
|
CTX["link_mode"] = link_mode
|
|
1105
1139
|
CTX["footnotes"] = ctx["footnotes"]
|
|
1140
|
+
CTX["footnote_idx"] = {}
|
|
1106
1141
|
CTX["use_pangu"] = use_pangu
|
|
1107
1142
|
CTX["hl_scheme"] = build_code_scheme(theme)
|
|
1143
|
+
# 行内图片解析器:render_inline 里的  通过它走 resolve_image,
|
|
1144
|
+
# 与独立成行的图片共享「复制到 images/ + 登记清单」管线。
|
|
1145
|
+
abs_md = os.path.abspath(md_path)
|
|
1146
|
+
CTX["img_resolve"] = lambda s: resolve_image(
|
|
1147
|
+
s, abs_md, out_dir, ctx["assets"], ctx.get("asset_root"))
|
|
1108
1148
|
|
|
1109
1149
|
body = convert_blocks(lines, md_path, out_dir, theme, ctx)
|
|
1110
1150
|
|
|
@@ -1328,6 +1368,26 @@ def build_notice(embedded, n_images):
|
|
|
1328
1368
|
return head + tail
|
|
1329
1369
|
|
|
1330
1370
|
|
|
1371
|
+
# audit_inline 的值级检查:原来是 8 次全正文 re.search(re.I 下每次都扫完整 MB 级
|
|
1372
|
+
# body,大正文里 audit_inline 能占近一半耗时)。合并为一次单遍扫描:命中哪个分支
|
|
1373
|
+
# 报哪条,收集命中的分支序号后按原列表顺序输出——与原来逐条 search 的结果完全一致。
|
|
1374
|
+
_AUDIT_VALUE_PATTERNS = [
|
|
1375
|
+
("linear-gradient", "ERROR", "linear-gradient(渐变被静默丢弃,退化成无背景。改纯色)"),
|
|
1376
|
+
("box-shadow", "ERROR", "box-shadow(阴影被丢弃)"),
|
|
1377
|
+
("rgba\\(|hsla\\(", "ERROR", "rgba()/hsla() 颜色(透明度不被支持,改纯色十六进制)"),
|
|
1378
|
+
("var\\(--", "ERROR", "CSS 变量(不支持,值必须写死)"),
|
|
1379
|
+
("@media|@keyframes", "ERROR", "@media/@keyframes(媒体查询与动画不被支持)"),
|
|
1380
|
+
("\\sclass=", "ERROR", "class 属性(公众号会剥离,样式必须内联)"),
|
|
1381
|
+
("\\sid=", "WARN", "id 属性(正文里无意义,可能被剥离)"),
|
|
1382
|
+
("<img[^>]*width=\"", "WARN", "<img> 上的 width 属性(公众号不保证保留,宽度用内联 style)"),
|
|
1383
|
+
]
|
|
1384
|
+
_AUDIT_VALUE_RE = re.compile(
|
|
1385
|
+
"|".join("(?P<g%d>%s)" % (i, pat)
|
|
1386
|
+
for i, (pat, _lv, _msg) in enumerate(_AUDIT_VALUE_PATTERNS)),
|
|
1387
|
+
re.I,
|
|
1388
|
+
)
|
|
1389
|
+
|
|
1390
|
+
|
|
1331
1391
|
def audit_inline(body):
|
|
1332
1392
|
"""正文的平台合规自检,返回 [(级别, 说明)],空列表即通过。
|
|
1333
1393
|
|
|
@@ -1387,18 +1447,15 @@ def audit_inline(body):
|
|
|
1387
1447
|
elif prop == "border-radius":
|
|
1388
1448
|
add("WARN", "border-radius(圆角可能被丢弃,降级为直角;属可接受的优雅降级,勿依赖圆角做视觉区分)")
|
|
1389
1449
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
(
|
|
1399
|
-
]:
|
|
1400
|
-
if re.search(pat, body, re.I):
|
|
1401
|
-
add(lv, msg)
|
|
1450
|
+
hit = set()
|
|
1451
|
+
for m in _AUDIT_VALUE_RE.finditer(body):
|
|
1452
|
+
for i in range(len(_AUDIT_VALUE_PATTERNS)):
|
|
1453
|
+
if m.group("g%d" % i) is not None:
|
|
1454
|
+
hit.add(i)
|
|
1455
|
+
break
|
|
1456
|
+
for i in sorted(hit):
|
|
1457
|
+
_pat, lv, msg = _AUDIT_VALUE_PATTERNS[i]
|
|
1458
|
+
add(lv, msg)
|
|
1402
1459
|
|
|
1403
1460
|
# ---- 图片宽度:小图被拉伸是高频问题
|
|
1404
1461
|
for m in re.finditer(r'<img[^>]*style="([^"]*)"', body):
|
|
@@ -1418,6 +1475,22 @@ def audit_inline(body):
|
|
|
1418
1475
|
return out
|
|
1419
1476
|
|
|
1420
1477
|
|
|
1478
|
+
def atomic_write(path, text):
|
|
1479
|
+
"""原子写文件:先写临时文件再 rename,写一半失败(磁盘满/断电)时
|
|
1480
|
+
不会留下半截产物——要么是旧文件,要么是没有文件,绝不残缺。"""
|
|
1481
|
+
tmp = path + ".tmp"
|
|
1482
|
+
try:
|
|
1483
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
1484
|
+
fh.write(text)
|
|
1485
|
+
os.replace(tmp, path)
|
|
1486
|
+
except BaseException:
|
|
1487
|
+
try:
|
|
1488
|
+
os.unlink(tmp)
|
|
1489
|
+
except OSError:
|
|
1490
|
+
pass
|
|
1491
|
+
raise
|
|
1492
|
+
|
|
1493
|
+
|
|
1421
1494
|
def write_manifest(out_dir, slug, title, assets, diagrams, theme, footnotes,
|
|
1422
1495
|
embedded=False):
|
|
1423
1496
|
lines = ["# 图片上传清单 · %s" % title, ""]
|
|
@@ -1479,8 +1552,7 @@ def write_manifest(out_dir, slug, title, assets, diagrams, theme, footnotes,
|
|
|
1479
1552
|
theme.get("h2_style", "number"),
|
|
1480
1553
|
theme.get("code_scheme", "mono")))
|
|
1481
1554
|
path = os.path.join(out_dir, "图片上传清单.md")
|
|
1482
|
-
|
|
1483
|
-
fh.write("\n".join(lines))
|
|
1555
|
+
atomic_write(path, "\n".join(lines))
|
|
1484
1556
|
return path
|
|
1485
1557
|
|
|
1486
1558
|
|
|
@@ -1529,8 +1601,13 @@ def main():
|
|
|
1529
1601
|
sys.exit("错误:找不到输入文件 %s" % args.input)
|
|
1530
1602
|
|
|
1531
1603
|
theme = load_theme(args.theme)
|
|
1532
|
-
|
|
1533
|
-
|
|
1604
|
+
try:
|
|
1605
|
+
with open(args.input, encoding="utf-8") as fh:
|
|
1606
|
+
md_text = fh.read()
|
|
1607
|
+
except UnicodeDecodeError:
|
|
1608
|
+
sys.exit("错误:输入文件不是有效的 UTF-8 编码 %s" % args.input)
|
|
1609
|
+
except OSError as e:
|
|
1610
|
+
sys.exit("错误:无法读取输入文件 %s(%s)" % (args.input, e))
|
|
1534
1611
|
|
|
1535
1612
|
os.makedirs(args.out_dir, exist_ok=True)
|
|
1536
1613
|
body, assets, diagrams, footnotes = convert(
|
|
@@ -1567,8 +1644,7 @@ def main():
|
|
|
1567
1644
|
embed_stats["count"] if embed_stats else len(assets)),
|
|
1568
1645
|
)
|
|
1569
1646
|
html_path = os.path.join(args.out_dir, "%s-公众号版.html" % args.slug)
|
|
1570
|
-
|
|
1571
|
-
fh.write(page)
|
|
1647
|
+
atomic_write(html_path, page)
|
|
1572
1648
|
|
|
1573
1649
|
manifest = write_manifest(args.out_dir, args.slug, title, assets, diagrams,
|
|
1574
1650
|
theme, footnotes, embed_stats is not None)
|
|
@@ -55,6 +55,38 @@ def main() -> None:
|
|
|
55
55
|
r = run("sync-status.py", ["--set", "定稿", str(td / "nope-pkg")])
|
|
56
56
|
check("sync-status 缺失包干净报错", r.returncode != 0 and "Traceback" not in r.stderr, r.stderr[:100])
|
|
57
57
|
|
|
58
|
+
# 6. md2wechat 行内图片走图片管线(复制/embed/清单/计数)
|
|
59
|
+
png = ("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk"
|
|
60
|
+
"+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==")
|
|
61
|
+
import base64 as _b64
|
|
62
|
+
(td / "a.png").write_bytes(_b64.b64decode(png))
|
|
63
|
+
(td / "inline.md").write_text(
|
|
64
|
+
"# t\n\n段落  后续文字。\n", encoding="utf-8")
|
|
65
|
+
r = run("md2wechat.py", ["--input", str(td / "inline.md"),
|
|
66
|
+
"--out-dir", str(td / "o6"), "--slug", "t6",
|
|
67
|
+
"--embed-images"])
|
|
68
|
+
html = (td / "o6" / "t6-公众号版.html").read_text(encoding="utf-8")
|
|
69
|
+
manifest = (td / "o6" / "图片上传清单.md").read_text(encoding="utf-8")
|
|
70
|
+
check("md2wechat 行内图片被内嵌", r.returncode == 0 and
|
|
71
|
+
"data:image/png;base64," in html, r.stdout[:200])
|
|
72
|
+
check("md2wechat 行内图片进清单", "a.png" in manifest)
|
|
73
|
+
|
|
74
|
+
# 7. md2wechat 大写数字序号同样被剥离
|
|
75
|
+
(td / "num.md").write_text(
|
|
76
|
+
"# t\n\n## 第叁章 大写序号\n\n正文。\n", encoding="utf-8")
|
|
77
|
+
r = run("md2wechat.py", ["--input", str(td / "num.md"),
|
|
78
|
+
"--out-dir", str(td / "o7"), "--slug", "t7"])
|
|
79
|
+
html = (td / "o7" / "t7-公众号版.html").read_text(encoding="utf-8")
|
|
80
|
+
check("md2wechat 大写数字序号剥离",
|
|
81
|
+
"第叁章" not in html and ">01</span>" in html)
|
|
82
|
+
|
|
83
|
+
# 8. md2wechat 分隔线不触发自家合规 WARN(inline-block)
|
|
84
|
+
(td / "hr.md").write_text("# t\n\n---\n\n正文。\n", encoding="utf-8")
|
|
85
|
+
r = run("md2wechat.py", ["--input", str(td / "hr.md"),
|
|
86
|
+
"--out-dir", str(td / "o8"), "--slug", "t8"])
|
|
87
|
+
check("md2wechat 分隔线无 inline-block 告警",
|
|
88
|
+
"inline-block" not in r.stdout, r.stdout[:200])
|
|
89
|
+
|
|
58
90
|
if fails:
|
|
59
91
|
print(f"\n[negative_tests] {len(fails)} 项失败")
|
|
60
92
|
sys.exit(1)
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
Turn a Markdown draft into a "copy → paste → publish" X long-form (Article) package.
|
|
6
6
|
|
|
7
|
+
- Version: **v0.1.0** (same tag as `@topmindspace/tms-skills@0.3.5`)
|
|
8
|
+
|
|
7
9
|
## Install
|
|
8
10
|
|
|
9
11
|
```bash
|
|
@@ -23,10 +25,47 @@ python3 scripts/md2x.py <draft>.md --out <package>/X发布稿.txt
|
|
|
23
25
|
|
|
24
26
|
## Conversion rules
|
|
25
27
|
|
|
26
|
-
The X Article editor has weak, unstable markdown support, so `md2x.py` converts to plain text per `references/x-format.md`: headings → plain text lines, horizontal rules
|
|
28
|
+
The X Article editor has weak, unstable markdown support, so `md2x.py` converts to plain text per `references/x-format.md`: headings → plain text lines, horizontal rules → blank lines, emphasis markers removed, links → `text(url)`, images → `[图N]` (image list appended at the end), tables → "label:value" lists, blockquotes unquoted, `\` escapes restored.
|
|
27
29
|
|
|
28
30
|
Read the result through once by hand after converting.
|
|
29
31
|
|
|
32
|
+
## Minimal example
|
|
33
|
+
|
|
34
|
+
Input (10 lines of markdown):
|
|
35
|
+
|
|
36
|
+
```markdown
|
|
37
|
+
# Manus 2.0 发布了
|
|
38
|
+
|
|
39
|
+
**从零重建**的 Agent,新增了 Cue 个人助手。
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
| Token 消耗 | -23.2% |
|
|
44
|
+
| 耗时 | -28.2% |
|
|
45
|
+
|
|
46
|
+
详见[官方博客](https://manus.im/blog)。
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`python3 scripts/md2x.py draft.md --out X发布稿.txt` produces:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Manus 2.0 发布了
|
|
53
|
+
|
|
54
|
+
从零重建的 Agent,新增了 Cue 个人助手。
|
|
55
|
+
|
|
56
|
+
[图1]
|
|
57
|
+
|
|
58
|
+
Token 消耗:-23.2%
|
|
59
|
+
耗时:-28.2%
|
|
60
|
+
|
|
61
|
+
详见官方博客(https://manus.im/blog)。
|
|
62
|
+
|
|
63
|
+
—— 配图清单 ——
|
|
64
|
+
[图1] 发布会现场
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Copy the whole `X发布稿.txt` → paste into the X Article editor → upload images in `[图N]` order → publish.
|
|
68
|
+
|
|
30
69
|
## Publishing
|
|
31
70
|
|
|
32
71
|
X long-form posts are currently published by hand-pasting into the X Article editor; the API does not publish long-form. After publishing, do per the checklist: pin a first comment with the extra info, fetch the full text back for verification.
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
把 Markdown 原稿变成"复制 → 粘贴 → 发"的 X 长文(Article)发布包。
|
|
6
6
|
|
|
7
|
+
- 版本:**v0.1.0**(与 `@topmindspace/tms-skills@0.3.5` 同 tag)
|
|
8
|
+
|
|
7
9
|
## 安装
|
|
8
10
|
|
|
9
11
|
```bash
|
|
@@ -24,12 +26,48 @@ python3 scripts/md2x.py <原稿>.md --out <包>/X发布稿.txt
|
|
|
24
26
|
## 转换规则
|
|
25
27
|
|
|
26
28
|
X Article 编辑器对 markdown 支持弱且不稳定,`md2x.py` 按 `references/x-format.md`
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
表格→"项:值"列表。
|
|
29
|
+
转纯文本:标题→纯文本行、分隔线→空行、加粗/斜体/行内代码去标记、链接→`文字(url)`、
|
|
30
|
+
图片→`[图N]`(文末附配图清单)、表格→"项:值"列表、引用去 `>`、`\\` 转义还原。
|
|
30
31
|
|
|
31
32
|
转完必须人工通读一遍。
|
|
32
33
|
|
|
34
|
+
## 最小示例
|
|
35
|
+
|
|
36
|
+
输入(10 行 markdown):
|
|
37
|
+
|
|
38
|
+
```markdown
|
|
39
|
+
# Manus 2.0 发布了
|
|
40
|
+
|
|
41
|
+
**从零重建**的 Agent,新增了 Cue 个人助手。
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
| Token 消耗 | -23.2% |
|
|
46
|
+
| 耗时 | -28.2% |
|
|
47
|
+
|
|
48
|
+
详见[官方博客](https://manus.im/blog)。
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`python3 scripts/md2x.py 原稿.md --out X发布稿.txt` 得到:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Manus 2.0 发布了
|
|
55
|
+
|
|
56
|
+
从零重建的 Agent,新增了 Cue 个人助手。
|
|
57
|
+
|
|
58
|
+
[图1]
|
|
59
|
+
|
|
60
|
+
Token 消耗:-23.2%
|
|
61
|
+
耗时:-28.2%
|
|
62
|
+
|
|
63
|
+
详见官方博客(https://manus.im/blog)。
|
|
64
|
+
|
|
65
|
+
—— 配图清单 ——
|
|
66
|
+
[图1] 发布会现场
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
复制 `X发布稿.txt` 全文 → 粘贴进 X Article 编辑器 → 按 `[图N]` 顺序上传配图 → 发布。
|
|
70
|
+
|
|
33
71
|
## 发布
|
|
34
72
|
|
|
35
73
|
X 长文目前走人工粘贴发布(X Article 编辑器),API 不发长文。
|
|
@@ -40,7 +78,7 @@ X 长文目前走人工粘贴发布(X Article 编辑器),API 不发长文
|
|
|
40
78
|
以下技能**不在本仓库**(一般随用户侧 workbuddy 环境提供)。缺失时对应路由能力
|
|
41
79
|
不可用,不影响本技能核心流程(原稿转文本、封面、发布清单):
|
|
42
80
|
|
|
43
|
-
- `topmind-x`:280
|
|
81
|
+
- `topmind-x`:280 字短推文发布(xurl 只覆盖短推文,不发长文)。
|
|
44
82
|
- `topmind-capture`:「只想存档不发布」时的收录路由。
|
|
45
83
|
|
|
46
84
|
## 开发
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: topmind-x-article
|
|
3
3
|
version: 0.1.0
|
|
4
|
-
description: "X 长文(Article)一键发布:Markdown 原稿 → 可直接粘贴的 X发布稿.txt + 封面图 + 发布清单。沉淀自实战。Use when X 长文、发 X 文章、X article、长文发 X。Do NOT use for 短推文(→ topmind-x
|
|
4
|
+
description: "X 长文(Article)一键发布:Markdown 原稿 → 可直接粘贴的 X发布稿.txt + 封面图 + 发布清单。沉淀自实战。Use when X 长文、发 X 文章、X article、长文发 X。Do NOT use for 短推文(→ topmind-x)、公众号(→ topmind-wechat-post)。"
|
|
5
5
|
action_category: write
|
|
6
6
|
triggers:
|
|
7
7
|
- X 长文
|
|
8
8
|
- 发 X 文章
|
|
9
9
|
- X article
|
|
10
10
|
- 长文发 X
|
|
11
|
+
- X 发长文
|
|
12
|
+
- 转纯文本
|
|
13
|
+
- X 纯文本
|
|
14
|
+
- 发 article
|
|
11
15
|
triggers_cn:
|
|
12
16
|
- 写 X 长文
|
|
13
17
|
- X 长文发布
|
|
18
|
+
- 推特长文
|
|
19
|
+
- twitter 长文
|
|
14
20
|
author: TopMindspace
|
|
15
21
|
license: MIT
|
|
16
22
|
homepage: https://github.com/topmindspace/tms-skills#readme
|
|
@@ -49,11 +55,10 @@ updated: 2026-09-29
|
|
|
49
55
|
└── 发布清单.md # 本次发布 checklist(含时间、链接)
|
|
50
56
|
```
|
|
51
57
|
|
|
52
|
-
##
|
|
58
|
+
## 实战沉淀
|
|
53
59
|
|
|
54
60
|
- 补充信息(邀请码、链接、勘误)**不挤正文**,发在发布后首条评论并置顶
|
|
55
|
-
-
|
|
56
|
-
- 配图逐张按 `[图N]` 顺序上传,传完对照清单点一遍
|
|
61
|
+
- 标题 ≤140 字符,首段 3 行内出钩子;配图逐张按 `[图N]` 顺序上传,传完对照清单点一遍
|
|
57
62
|
- 事实修正只改发布包,不回头改已发原文(除非勘误)
|
|
58
63
|
|
|
59
64
|
## When NOT to use
|
|
@@ -61,11 +66,12 @@ updated: 2026-09-29
|
|
|
61
66
|
- 280 字短推文 → `topmind-x`
|
|
62
67
|
- 公众号 → `topmind-wechat-post`
|
|
63
68
|
- 只想存档不发布 → `topmind-capture`
|
|
69
|
+
- 写稿(只有发布需求、无原稿时)→ 先走写作技能再回本技能
|
|
64
70
|
|
|
65
71
|
## 外部依赖
|
|
66
72
|
|
|
67
73
|
以下技能**不在本仓库**(一般随用户侧 workbuddy 环境提供);缺失时对应路由能力不可用,
|
|
68
74
|
本技能核心流程(原稿转文本、封面、发布清单)不受影响:
|
|
69
75
|
|
|
70
|
-
- `topmind-x`:280
|
|
76
|
+
- `topmind-x`:280 字短推文发布(xurl 只覆盖短推文,不发长文)。
|
|
71
77
|
- `topmind-capture`:「只想存档不发布」时的收录路由。
|
|
@@ -5,16 +5,19 @@ X Article 编辑器对 markdown 支持弱且不稳定。`md2x.py` 按以下规
|
|
|
5
5
|
|
|
6
6
|
| Markdown | 转后 |
|
|
7
7
|
|----------|------|
|
|
8
|
-
| `#`
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
8
|
+
| `#` ~ `######` 标题 | 纯文本行,前后各一空行;标题内的加粗/链接等标记同样剥离 |
|
|
9
|
+
| `标题` + 下一行 `===`(Setext 一级标题) | 同上,转纯文本标题行 |
|
|
10
|
+
| `**加粗**` / `*斜体*` / `***粗斜体***` / `~~删除线~~` / `` `行内代码` `` | 去标记留文字(语气靠文字本身) |
|
|
11
|
+
| `[文字](url)` | `文字(url)`;url 里可含括号,`[文字](url "title")` 的 title 丢弃 |
|
|
12
|
+
| `[文字][ref]` / `[文字][]` / `[文字]`(引用式,有 `[ref]: url` 定义) | `文字(url)`;定义行不进正文 |
|
|
13
|
+
| `` / `![alt][ref]` | `[图N]`,文末附"配图清单"(`[图N] alt`;alt 为空记 `(未写图注)`) |
|
|
14
|
+
| `-` / `1.` 列表 | 保留原符号(含嵌套缩进);`- [ ]` / `- [x]` 任务列表都去勾选框留 `-`(完成状态不保留) |
|
|
15
|
+
| `>` 引用 | 去掉所有层级的 `>` 留文字;引用内的标题照转 |
|
|
16
|
+
| 表格(行首以 `|` 开头的标准写法;不带前导 `|` 的表格暂不转换) | 逐行转"项:值"列表(单元格内标记同样剥离);分隔行丢弃,复杂表请手动拆成段落 |
|
|
17
|
+
| `---` / `***` / `___` 分割线(允许空格,如 `* * *`) | 空行 |
|
|
18
|
+
| 代码块(``` 围栏;`~~~` 围栏暂不支持) | 去围栏,内容缩进 4 格;块内行原样保留(块内的 `---` 不再误判分割线) |
|
|
19
|
+
| `\` 转义(如 `\*`) | 去反斜杠留字符 |
|
|
20
|
+
| frontmatter | 整块丢弃(块内非空行须都含 `:` 才认定,文首的 `---` 分割线不会被误吞) |
|
|
18
21
|
|
|
19
22
|
## 写作时就按 X 想
|
|
20
23
|
|