@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/README.en.md +6 -76
  3. package/README.md +7 -75
  4. package/package.json +1 -1
  5. package/top-ppt-html/README.en.md +19 -20
  6. package/top-ppt-html/README.md +36 -90
  7. package/top-ppt-html/SKILL.md +65 -60
  8. package/top-ppt-html/references/charts.md +0 -15
  9. package/top-ppt-html/references/components.md +0 -18
  10. package/top-ppt-html/references/design-system.md +2 -2
  11. package/top-ppt-html/references/icons.md +2 -2
  12. package/top-ppt-html/references/infographics.md +1 -5
  13. package/top-ppt-html/references/modes.md +4 -4
  14. package/top-ppt-html/references/tech-design.md +3 -3
  15. package/top-ppt-html/scripts/build_examples.py +4 -4
  16. package/top-ppt-html/scripts/build_pptx.js +240 -59
  17. package/top-ppt-html/scripts/check_triggers.py +10 -2
  18. package/top-ppt-html/scripts/extract_model.py +16 -5
  19. package/top-ppt-html/scripts/gen_channel_a.js +55 -10
  20. package/top-ppt-html/scripts/hydrate_charts.py +68 -13
  21. package/top-ppt-html/scripts/negative_tests.py +1 -1
  22. package/top-ppt-html/scripts/prepare_images.py +1 -1
  23. package/top-ppt-html/scripts/recommend_layout.py +26 -3
  24. package/top-ppt-html/scripts/render_from_model.py +78 -13
  25. package/top-ppt-html/scripts/scaffold_report.py +43 -3
  26. package/top-ppt-html/scripts/sync_runtime.py +32 -4
  27. package/top-ppt-html/scripts/test_feedback_gates.py +24 -0
  28. package/top-ppt-html/scripts/validate_pptx.py +10 -1
  29. package/top-ppt-html/scripts/validate_report.py +43 -7
  30. package/topmind-cover/README.en.md +4 -17
  31. package/topmind-cover/README.md +5 -18
  32. package/topmind-cover/SKILL.md +28 -35
  33. package/topmind-cover/assets/examples/README.md +15 -26
  34. package/topmind-cover/assets/examples/big-type-wechat.png +0 -0
  35. package/topmind-cover/assets/examples/big-type.png +0 -0
  36. package/topmind-cover/assets/examples/brand-launch-wechat.png +0 -0
  37. package/topmind-cover/assets/examples/brand-launch.png +0 -0
  38. package/topmind-cover/assets/examples/gan-huo-wechat.png +0 -0
  39. package/topmind-cover/assets/examples/gan-huo.png +0 -0
  40. package/topmind-cover/assets/examples/ip-fun-wechat.png +0 -0
  41. package/topmind-cover/assets/examples/ip-fun.png +0 -0
  42. package/topmind-cover/assets/examples/magazine-wechat.png +0 -0
  43. package/topmind-cover/assets/examples/magazine.png +0 -0
  44. package/topmind-cover/assets/examples/minimal-wechat.png +0 -0
  45. package/topmind-cover/assets/examples/minimal.png +0 -0
  46. package/topmind-cover/assets/examples/news-flash-wechat.png +0 -0
  47. package/topmind-cover/assets/examples/news-flash.png +0 -0
  48. package/topmind-cover/assets/examples/overview.png +0 -0
  49. package/topmind-cover/assets/examples/tutorial-steps-wechat.png +0 -0
  50. package/topmind-cover/assets/examples/tutorial-steps.png +0 -0
  51. package/topmind-cover/references/cover-styles.md +275 -249
  52. package/topmind-wechat-post/README.en.md +49 -1
  53. package/topmind-wechat-post/README.md +48 -2
  54. package/topmind-wechat-post/SKILL.md +35 -37
  55. package/topmind-wechat-post/references/element-spec.md +1 -1
  56. package/topmind-wechat-post/references/known-pits.md +2 -3
  57. package/topmind-wechat-post/references/typography-rules.md +1 -1
  58. package/topmind-wechat-post/references/workflow.md +3 -3
  59. package/topmind-wechat-post/references/writing-quality.md +8 -32
  60. package/topmind-wechat-post/scripts/md2wechat.py +117 -41
  61. package/topmind-wechat-post/scripts/negative_tests.py +32 -0
  62. package/topmind-x-article/README.en.md +40 -1
  63. package/topmind-x-article/README.md +42 -4
  64. package/topmind-x-article/SKILL.md +11 -5
  65. package/topmind-x-article/references/x-format.md +13 -10
  66. 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 = re.compile("|".join("(?P<%s>%s)" % (name, pat) for name, pat in rules), re.S)
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"&lt;u&gt;(.+?)&lt;/u&gt;")
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 = re.sub(r"`([^`]+)`", lambda m: stash(m.group(1)), 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
+ # 段落里的 ![alt](x.png) 被原样保留相对路径,粘贴到公众号后整图消失)。
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 = re.sub(r"!\[([^\]]*)\]\(([^)\s]+)\)", img_repl, 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
- if url not in [f["url"] for f in footnotes]:
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
- idx = [f["url"] for f in footnotes].index(url) + 1
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 = re.sub(r"\[([^\]]+)\]\(([^)\s]+)\)", link_repl, 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 = re.sub(
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 = re.sub(
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 = re.sub(r"\+\+([^+]+)\+\+", underline, text)
428
- text = re.sub(r"&lt;u&gt;(.+?)&lt;/u&gt;", underline, 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 = re.sub(
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 = re.sub(r"\*\*([^*]+)\*\*", r'<strong style="%s">\1</strong>' % strong, text)
440
- text = re.sub(r"(?<![*\w])\*([^*\n]+)\*(?!\*)", r'<em style="%s">\1</em>' % em, 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]+)\)\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
- return ('<p style="text-align:center;margin:32px 0;">'
746
- '<span style="display:inline-block;width:12%%;height:2px;'
747
- 'background:%s;"></span></p>' % theme["accent"])
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 里的 ![alt](src) 通过它走 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
- for pat, lv, msg in [
1391
- (r"linear-gradient", "ERROR", "linear-gradient(渐变被静默丢弃,退化成无背景。改纯色)"),
1392
- (r"box-shadow", "ERROR", "box-shadow(阴影被丢弃)"),
1393
- (r"rgba\(|hsla\(", "ERROR", "rgba()/hsla() 颜色(透明度不被支持,改纯色十六进制)"),
1394
- (r"var\(--", "ERROR", "CSS 变量(不支持,值必须写死)"),
1395
- (r"@media|@keyframes", "ERROR", "@media/@keyframes(媒体查询与动画不被支持)"),
1396
- (r"\sclass=", "ERROR", "class 属性(公众号会剥离,样式必须内联)"),
1397
- (r"\sid=", "WARN", "id 属性(正文里无意义,可能被剥离)"),
1398
- (r"<img[^>]*width=\"", "WARN", "<img> 上的 width 属性(公众号不保证保留,宽度用内联 style)"),
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
- with open(path, "w", encoding="utf-8") as fh:
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
- with open(args.input, encoding="utf-8") as fh:
1533
- md_text = fh.read()
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
- with open(html_path, "w", encoding="utf-8") as fh:
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段落 ![行内图](a.png) 后续文字。\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 (`---` / `***` / `___`) → blank lines, bold/italic markers removed (including `***bold-italic***`), links → `text(url)`, images → `[图N]` (image list appended at the end), tables → "item: value" lists.
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
+ ![发布会现场](cover.png)
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
- (含 `***粗斜体***`)、链接→`文字(url)`、图片→`[图N]`(文末附配图清单)、
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
+ ![发布会现场](cover.png)
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 字短推文发布(其 xurl 只覆盖短推文,不发长文)。
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
- ## Manus 2.0 实战沉淀
58
+ ## 实战沉淀
53
59
 
54
60
  - 补充信息(邀请码、链接、勘误)**不挤正文**,发在发布后首条评论并置顶
55
- - 长文标题 ≤140 字符,首段 3 行内出钩子
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 字短推文发布(其 xurl 只覆盖短推文,不发长文)。
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
- | `[文字](url)` | `文字(url)` |
11
- | `![](alt)` | `[图N]`,文末附"配图清单" |
12
- | `-` / `1.` 列表 | 保留原符号 |
13
- | `>` 引用 | 去 `>` 留文字 |
14
- | 表格 | 转"项:值"列表;复杂表拆成段落 |
15
- | `---` 分割线 | 空行 |
16
- | 代码块 | 去围栏,内容缩进 4 格 |
17
- | frontmatter | 整块丢弃 |
8
+ | `#` ~ `######` 标题 | 纯文本行,前后各一空行;标题内的加粗/链接等标记同样剥离 |
9
+ | `标题` + 下一行 `===`(Setext 一级标题) | 同上,转纯文本标题行 |
10
+ | `**加粗**` / `*斜体*` / `***粗斜体***` / `~~删除线~~` / `` `行内代码` `` | 去标记留文字(语气靠文字本身) |
11
+ | `[文字](url)` | `文字(url)`;url 里可含括号,`[文字](url "title")` 的 title 丢弃 |
12
+ | `[文字][ref]` / `[文字][]` / `[文字]`(引用式,有 `[ref]: url` 定义) | `文字(url)`;定义行不进正文 |
13
+ | `![alt](url)` / `![alt][ref]` | `[图N]`,文末附"配图清单"(`[图N] alt`;alt 为空记 `(未写图注)`) |
14
+ | `-` / `1.` 列表 | 保留原符号(含嵌套缩进);`- [ ]` / `- [x]` 任务列表都去勾选框留 `-`(完成状态不保留) |
15
+ | `>` 引用 | 去掉所有层级的 `>` 留文字;引用内的标题照转 |
16
+ | 表格(行首以 `|` 开头的标准写法;不带前导 `|` 的表格暂不转换) | 逐行转"项:值"列表(单元格内标记同样剥离);分隔行丢弃,复杂表请手动拆成段落 |
17
+ | `---` / `***` / `___` 分割线(允许空格,如 `* * *`) | 空行 |
18
+ | 代码块(``` 围栏;`~~~` 围栏暂不支持) | 去围栏,内容缩进 4 格;块内行原样保留(块内的 `---` 不再误判分割线) |
19
+ | `\` 转义(如 `\*`) | 去反斜杠留字符 |
20
+ | frontmatter | 整块丢弃(块内非空行须都含 `:` 才认定,文首的 `---` 分割线不会被误吞) |
18
21
 
19
22
  ## 写作时就按 X 想
20
23