@topmindspace/tms-skills 0.5.0 → 0.7.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.en.md +6 -5
  3. package/README.md +14 -5
  4. package/package.json +2 -1
  5. package/top-ppt-html/README.en.md +2 -2
  6. package/top-ppt-html/README.md +1 -1
  7. package/topmind-briefs/CHANGELOG.md +22 -0
  8. package/topmind-briefs/README.en.md +40 -0
  9. package/topmind-briefs/README.md +37 -0
  10. package/topmind-briefs/SKILL.md +101 -0
  11. package/topmind-briefs/dist/topmind-briefs.manifest.json +21 -0
  12. package/topmind-briefs/dist/topmind-briefs.zip +0 -0
  13. package/topmind-briefs/package-lock.json +13 -0
  14. package/topmind-briefs/package.json +1 -0
  15. package/topmind-briefs/references/examples.md +127 -0
  16. package/topmind-briefs/references/platform-specs.md +60 -0
  17. package/topmind-briefs/references/style-guide.md +55 -0
  18. package/topmind-briefs/scripts/audit_css.py +39 -0
  19. package/topmind-briefs/scripts/audit_docs.py +80 -0
  20. package/topmind-briefs/scripts/audit_skill.py +60 -0
  21. package/topmind-briefs/scripts/audit_styles.py +54 -0
  22. package/topmind-briefs/scripts/negative_tests.py +88 -0
  23. package/topmind-briefs/scripts/package_skill.py +188 -0
  24. package/topmind-cover/README.en.md +4 -4
  25. package/topmind-cover/README.md +4 -4
  26. package/topmind-cover/SKILL.md +5 -5
  27. package/topmind-cover/assets/examples/README.md +2 -0
  28. package/topmind-cover/package.json +1 -1
  29. package/topmind-cover/references/cover-prompts.md +19 -19
  30. package/topmind-cover/references/cover-styles.md +27 -26
  31. package/topmind-cover/scripts/crop-cover.py +6 -6
  32. package/topmind-cover/scripts/negative_tests.py +1 -1
  33. package/topmind-wechat-post/README.en.md +1 -1
  34. package/topmind-wechat-post/README.md +1 -1
  35. package/topmind-wechat-post/SKILL.md +74 -38
  36. package/topmind-wechat-post/package.json +1 -1
  37. package/topmind-wechat-post/references/element-spec.md +3 -2
  38. package/topmind-wechat-post/scripts/md2wechat.py +2 -7
  39. package/topmind-x-article/README.en.md +6 -2
  40. package/topmind-x-article/README.md +4 -2
  41. package/topmind-x-article/SKILL.md +59 -4
  42. package/topmind-x-article/package.json +1 -1
  43. package/topmind-x-article/references/publish-checklist.md +1 -1
  44. package/topmind-x-article/references/x-html-format.md +13 -1
  45. package/topmind-x-article/scripts/md2x-html.py +179 -51
@@ -3,13 +3,15 @@
3
3
 
4
4
  用法:
5
5
  python3 md2x-html.py 原稿.md --out X长文.html \\
6
- --images 样张1.png 样张2.png ... [--cover cover-1200x675.png]
6
+ --images 样张1.png 样张2.png ... [--cover cover-1500x600.png]
7
7
 
8
8
  约定(与 md2x.py 对齐):
9
- - `[图N]` 独占一行 → 按顺序取 --images[N-1],转成内嵌 data-URI 的 <figure>
9
+ - `[图N]` 独占一行 → 按顺序取 --images[N-1],转成内嵌 data-URI 的 <figure>;
10
+ 点击图片弹出 lightbox 放大查看原图(右键可另存/截图,不拦截右键菜单)
10
11
  - ``` 围栏代码块(直出提示词)→ <blockquote> + 「复制提示词」按钮。
11
12
  原因:X 文章编辑器没有"粘贴 <pre> 即代码块"的识别,原生代码块只能走
12
- Insert 菜单;<blockquote> 粘贴后即 X 原生引用块,格式不丢、读者全选即拷
13
+ Insert 菜单;<blockquote> 粘贴后即 X 原生引用块,格式不丢、读者全选即拷。
14
+ 支持嵌套围栏(```` 开栏不被内层 ``` 提前闭合);未闭合围栏在文末自动收尾
13
15
  - `#` → h1(X 标题栏专用,复制全文时自动剔除);`##`/`###` → h2/h3
14
16
  (对应 X 的"标题/副标题"两级)
15
17
  - `- ` / `1. ` → ul/ol;`**x**` → strong;`` `x` `` → code(X 粘贴后变纯文本);
@@ -22,7 +24,8 @@
22
24
  复制前自动剔除按钮/UI 与 .no-paste 块(标题、封面预览),只剩 X 可识别的
23
25
  语义标签:h2/h3/p/ul/ol/blockquote/a/hr/img
24
26
  - 图片:data-URI 内嵌(X 若支持则随粘贴带入);每张图带 [图N] 编号 + 「下载图片」
25
- 按钮保底,图片没跟过去时按编号下载再上传,不用找文件、不用对顺序
27
+ 按钮保底,图片没跟过去时按编号下载再上传,不用找文件、不用对顺序;
28
+ 点击图片 lightbox 放大看原图,方便核对、截图或右键另存
26
29
  - 封面:X 有独立封面上传入口,始终单独文件;HTML 顶部仅作预览 + 下载链接
27
30
 
28
31
  只用 Python 标准库。
@@ -66,10 +69,11 @@ body { margin: 0; background: #f7f9fa; color: #0f1419;
66
69
  border-radius: 8px; padding: 8px 12px; margin-bottom: 8px; }
67
70
  .cover-note { background: #f7f9fa; border: 1px dashed #cfd9de; border-radius: 12px;
68
71
  padding: 14px 16px; font-size: 14px; color: #536471; margin-bottom: 24px; }
69
- .cover-note img { width: 100%; border-radius: 8px; display: block; margin-bottom: 10px; }
72
+ .cover-note img { width: 100%; border-radius: 8px; display: block; margin-bottom: 10px;
73
+ cursor: zoom-in; }
70
74
  figure.ximg { margin: 22px 0; }
71
75
  figure.ximg img { width: 100%; border-radius: 10px; display: block;
72
- border: 1px solid #eff3f4; }
76
+ border: 1px solid #eff3f4; cursor: zoom-in; }
73
77
  figure.ximg figcaption { display: flex; justify-content: space-between; align-items: center;
74
78
  margin-top: 8px; font-size: 13px; color: #536471; }
75
79
  .prompt { position: relative; margin: 14px 0 20px; }
@@ -82,10 +86,22 @@ figure.ximg figcaption { display: flex; justify-content: space-between; align-it
82
86
  font-size: 12px; }
83
87
  .toast { position: fixed; left: 50%; bottom: 40px; transform: translateX(-50%);
84
88
  background: #0f1419; color: #fff; font-size: 14px; padding: 10px 20px;
85
- border-radius: 9999px; opacity: 0; transition: opacity .25s; pointer-events: none; }
89
+ border-radius: 9999px; opacity: 0; transition: opacity .25s; pointer-events: none;
90
+ z-index: 200; }
91
+ /* 图片 lightbox:点击放大看原图;不拦截右键,可右键另存/截图 */
92
+ .lightbox { position: fixed; inset: 0; z-index: 100; background: rgba(10,14,20,.93);
93
+ display: none; align-items: center; justify-content: center; padding: 24px; }
94
+ .lightbox.open { display: flex; }
95
+ .lb-inner { max-width: 96vw; max-height: 94vh; display: flex; flex-direction: column;
96
+ gap: 10px; align-items: center; }
97
+ .lb-inner img { max-width: 94vw; max-height: 76vh; border-radius: 8px; background: #fff; }
98
+ .lb-bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: center; }
99
+ .lb-cap { color: #cfd9de; font-size: 13px; }
100
+ .lb-tip { color: #8a93a6; font-size: 12px; }
86
101
  """
87
102
 
88
103
  JS = """
104
+ var lbFname = '';
89
105
  function toast(msg){ var t=document.getElementById('toast'); t.textContent=msg;
90
106
  t.style.opacity=1; setTimeout(function(){ t.style.opacity=0; }, 1800); }
91
107
  function cleanClone(){
@@ -116,19 +132,48 @@ async function copyPrompt(btn){
116
132
  catch(e){ toast('复制失败,请手动复制'); }
117
133
  }
118
134
  function dlImg(btn){
119
- var img=btn.closest('figure').querySelector('img');
135
+ var fig=btn.closest('figure');
136
+ var img=fig ? fig.querySelector('img') : document.querySelector('#coverbox img');
137
+ if(!img){ toast('找不到图片'); return; }
120
138
  var a=document.createElement('a');
121
- a.href=img.src; a.download=btn.getAttribute('data-fname');
139
+ a.href=img.src; a.download=btn.getAttribute('data-fname') || 'image.png';
122
140
  document.body.appendChild(a); a.click(); a.remove();
123
- toast('开始下载 '+btn.getAttribute('data-fname'));
141
+ toast('开始下载 '+(btn.getAttribute('data-fname')||'图片'));
124
142
  }
125
- function dlCover(btn){
126
- var img=document.querySelector('#coverbox img');
143
+ /* lightbox:点击图片放大看原图。刻意不拦截右键,放大后可右键另存 / 直接截图。 */
144
+ function openLightbox(img){
145
+ var lb=document.getElementById('lightbox');
146
+ document.getElementById('lbimg').src=img.src;
147
+ document.getElementById('lbimg').alt=img.alt||'原图';
148
+ lbFname=img.getAttribute('data-fname')||'image.png';
149
+ document.getElementById('lbcap').textContent=(img.alt||'原图')+' · '+lbFname;
150
+ lb.classList.add('open');
151
+ document.body.style.overflow='hidden';
152
+ }
153
+ function closeLightbox(){
154
+ document.getElementById('lightbox').classList.remove('open');
155
+ document.body.style.overflow='';
156
+ }
157
+ function openFullsize(){
158
+ var src=document.getElementById('lbimg').src;
159
+ var parts=src.split(','), mime='image/png';
160
+ var m=parts[0].match(/:(.*?);/); if(m){ mime=m[1]; }
161
+ try{
162
+ var bin=atob(parts[1]), arr=new Uint8Array(bin.length);
163
+ for(var i=0;i<bin.length;i++){ arr[i]=bin.charCodeAt(i); }
164
+ var url=URL.createObjectURL(new Blob([arr],{type:mime}));
165
+ window.open(url,'_blank');
166
+ }catch(e){ toast('打开失败,可右键图片另存'); }
167
+ }
168
+ function dlLightbox(){
127
169
  var a=document.createElement('a');
128
- a.href=img.src; a.download=btn.getAttribute('data-fname');
170
+ a.href=document.getElementById('lbimg').src; a.download=lbFname;
129
171
  document.body.appendChild(a); a.click(); a.remove();
130
- toast('封面开始下载');
172
+ toast('开始下载 '+lbFname);
131
173
  }
174
+ document.addEventListener('keydown', function(e){
175
+ if(e.key==='Escape'){ closeLightbox(); }
176
+ });
132
177
  """
133
178
 
134
179
  HTML_TMPL = """<!DOCTYPE html>
@@ -136,28 +181,46 @@ HTML_TMPL = """<!DOCTYPE html>
136
181
  <head>
137
182
  <meta charset="utf-8">
138
183
  <meta name="viewport" content="width=device-width, initial-scale=1">
139
- <title>{title}</title>
140
- <style>{css}</style>
184
+ <title>@@TITLE@@</title>
185
+ <style>@@CSS@@</style>
141
186
  </head>
142
187
  <body>
143
188
  <div class="toolbar">
144
189
  <span class="steps">1. 标题填入 X 标题栏 → 2. 一键复制全文 → 3. 粘贴到 X 正文</span>
145
190
  <button class="btn primary" onclick="copyArticle()">一键复制全文</button>
146
- <span class="hint">图片随粘贴带入则直接用;没带入就按 [图N] 下载上传。封面始终单独上传。</span>
191
+ <span class="hint">图片随粘贴带入则直接用;没带入就按 [图N] 下载上传。封面始终单独上传。点击图片可放大查看原图。</span>
147
192
  </div>
148
193
  <article class="article" id="article">
149
- {body}
194
+ @@BODY@@
150
195
  </article>
196
+ <div class="lightbox" id="lightbox" onclick="if(event.target===this)closeLightbox()">
197
+ <div class="lb-inner">
198
+ <img id="lbimg" alt="原图">
199
+ <div class="lb-bar">
200
+ <span class="lb-cap" id="lbcap"></span>
201
+ <button class="btn" onclick="openFullsize()">新标签页打开原图</button>
202
+ <button class="btn" onclick="dlLightbox()">下载原图</button>
203
+ <button class="btn" onclick="closeLightbox()">关闭</button>
204
+ </div>
205
+ <div class="lb-tip">可右键图片另存 / 直接截图 · Esc 关闭</div>
206
+ </div>
207
+ </div>
151
208
  <div class="toast" id="toast"></div>
152
- <script>{js}</script>
209
+ <script>@@JS@@</script>
153
210
  </body>
154
211
  </html>
155
212
  """
156
213
 
214
+ COVER_FNAME = "cover-1500x600.png"
215
+
157
216
 
158
217
  def data_uri(path):
159
- with open(path, "rb") as f:
160
- raw = f.read()
218
+ try:
219
+ with open(path, "rb") as f:
220
+ raw = f.read()
221
+ except OSError:
222
+ print("图片读取失败:%s" % path, file=sys.stderr)
223
+ sys.exit(1)
161
224
  ext = os.path.splitext(path)[1].lower()
162
225
  mime = {"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg",
163
226
  "webp": "image/webp", "gif": "image/gif"}.get(ext.lstrip("."), "image/png")
@@ -177,7 +240,6 @@ def inline_md(s):
177
240
  def md_to_html(md_text, images, cover):
178
241
  lines = md_text.split("\n")
179
242
  out = []
180
- img_idx = 0
181
243
  cover_uri = data_uri(cover) if cover else None
182
244
 
183
245
  i = 0
@@ -187,7 +249,7 @@ def md_to_html(md_text, images, cover):
187
249
  i += 1
188
250
  i += 1
189
251
 
190
- in_code = False
252
+ in_fence = None # 开栏的反引号串(如 '```');None 表示不在围栏内
191
253
  code_buf = []
192
254
  in_ul = False
193
255
  in_ol = False
@@ -224,28 +286,38 @@ def md_to_html(md_text, images, cover):
224
286
  in_table = False
225
287
  table_buf = []
226
288
 
289
+ def flush_prompt():
290
+ body = "\n".join(code_buf)
291
+ esc = html.escape(body).replace("\n", "<br>")
292
+ out.append(
293
+ '<div class="prompt"><blockquote><code>%s</code></blockquote>'
294
+ '<button class="btn" onclick="copyPrompt(this)">复制提示词</button>'
295
+ "</div>" % esc
296
+ )
297
+
227
298
  while i < len(lines):
228
299
  line = lines[i]
229
300
  s = line.strip()
230
301
 
231
- if s.startswith("```"):
232
- if not in_code:
302
+ fm = re.match(r"^(`{3,})", s)
303
+ if fm:
304
+ fence = fm.group(1)
305
+ if in_fence is None:
306
+ # 开栏
233
307
  close_lists()
234
308
  flush_table()
235
- in_code = True
309
+ in_fence = fence
310
+ code_buf = []
311
+ elif len(fence) >= len(in_fence) and s.strip("`") == "":
312
+ # 闭栏:纯反引号行且长度不短于开栏(内层短围栏视为代码内容)
313
+ flush_prompt()
314
+ in_fence = None
236
315
  code_buf = []
237
316
  else:
238
- in_code = False
239
- body = "\n".join(code_buf)
240
- esc = html.escape(body).replace("\n", "<br>")
241
- out.append(
242
- '<div class="prompt"><blockquote><code>%s</code></blockquote>'
243
- '<button class="btn" onclick="copyPrompt(this)">复制提示词</button>'
244
- "</div>" % esc
245
- )
317
+ code_buf.append(line)
246
318
  i += 1
247
319
  continue
248
- if in_code:
320
+ if in_fence is not None:
249
321
  code_buf.append(line)
250
322
  i += 1
251
323
  continue
@@ -269,15 +341,16 @@ def md_to_html(md_text, images, cover):
269
341
  uri = data_uri(images[n - 1])
270
342
  fname = "图%d-%s" % (n, os.path.basename(images[n - 1]))
271
343
  out.append(
272
- '<figure class="ximg"><img src="%s" alt="图%d">'
344
+ '<figure class="ximg"><img src="%s" alt="图%d" data-fname="%s" '
345
+ 'onclick="openLightbox(this)">'
273
346
  '<figcaption><span class="cap">[图%d]</span>'
274
347
  '<button class="btn" data-fname="%s" '
275
348
  'onclick="dlImg(this)">下载图片</button></figcaption></figure>'
276
- % (uri, n, n, html.escape(fname, quote=True))
349
+ % (uri, n, html.escape(fname, quote=True), n,
350
+ html.escape(fname, quote=True))
277
351
  )
278
352
  else:
279
353
  out.append("<p>[图%d](图片缺失)</p>" % n)
280
- img_idx = max(img_idx, n)
281
354
  i += 1
282
355
  continue
283
356
 
@@ -337,6 +410,9 @@ def md_to_html(md_text, images, cover):
337
410
 
338
411
  close_lists()
339
412
  flush_table()
413
+ if in_fence is not None:
414
+ # 未闭合围栏:文末自动收尾,不丢内容
415
+ flush_prompt()
340
416
 
341
417
  body = "\n".join(out)
342
418
 
@@ -344,12 +420,12 @@ def md_to_html(md_text, images, cover):
344
420
  if cover_uri:
345
421
  cover_html = (
346
422
  '<div class="cover-note no-paste" id="coverbox">'
347
- '<img src="%s" alt="封面预览">'
348
- '<div>封面图预览(1200×675)。X 文章有独立封面上传入口,'
423
+ '<img src="%s" alt="封面预览(点击放大)" data-fname="%s" onclick="openLightbox(this)">'
424
+ '<div>封面图预览(1500×600 · 5:2)。X 文章有独立封面上传入口,'
349
425
  '请单独上传,不要随正文粘贴。<br>'
350
- '<button class="btn" data-fname="cover-1200x675.png" '
351
- 'onclick="dlCover(this)">下载封面图</button></div></div>'
352
- ) % cover_uri
426
+ '<button class="btn" data-fname="%s" '
427
+ 'onclick="dlImg(this)">下载封面图</button></div></div>'
428
+ ) % (cover_uri, COVER_FNAME, COVER_FNAME)
353
429
  return cover_html + "\n" + body
354
430
 
355
431
 
@@ -358,24 +434,76 @@ def main():
358
434
  ap.add_argument("md", help="原稿 markdown 路径")
359
435
  ap.add_argument("--out", required=True, help="输出 HTML 路径")
360
436
  ap.add_argument("--images", nargs="*", default=[], help="[图N] 对应的图片路径(按顺序)")
361
- ap.add_argument("--cover", default=None, help="封面图路径(1200×675)")
437
+ ap.add_argument("--images-from", default=None, metavar="WECHAT_MD",
438
+ help="从指定的 markdown(如公众号稿.md)中按文档顺序提取 ![]() 本地图片路径,"
439
+ "作为 [图N] 的图片来源。X 稿必须从公众号稿派生时用此项,"
440
+ "禁止手工拼 --images 顺序(曾因此出现配图错位)")
441
+ ap.add_argument("--cover", default=None, help="封面图路径(1500×600,5:2)")
362
442
  args = ap.parse_args()
363
443
 
364
444
  if not os.path.isfile(args.md):
365
445
  print("输入文件不存在:%s" % args.md, file=sys.stderr)
366
446
  sys.exit(1)
367
-
368
447
  with open(args.md, encoding="utf-8") as f:
369
448
  md_text = f.read()
370
449
 
371
- m = re.search(r"^#\s+(.+)$", md_text, re.M)
450
+ # 图片来源:优先 --images-from(从公众号稿等源 md 按文档顺序派生),杜绝手工排序错位
451
+ images = list(args.images)
452
+ if args.images_from:
453
+ if not os.path.isfile(args.images_from):
454
+ print("图片来源文件不存在:%s" % args.images_from, file=sys.stderr)
455
+ sys.exit(1)
456
+ with open(args.images_from, encoding="utf-8") as f:
457
+ src_md = f.read()
458
+ base = os.path.dirname(os.path.abspath(args.images_from))
459
+ found = []
460
+ for q in re.findall(r"!\[[^\]]*\]\(([^)\s]+)\)", src_md):
461
+ if re.match(r"https?://", q):
462
+ print("警告:跳过远程图片(无法内嵌):%s" % q, file=sys.stderr)
463
+ continue
464
+ fp = q if os.path.isabs(q) else os.path.normpath(os.path.join(base, q))
465
+ found.append(fp)
466
+ if args.images:
467
+ print("警告:同时给了 --images 和 --images-from,采用 --images-from 派生顺序",
468
+ file=sys.stderr)
469
+ images = found
470
+ print("从 %s 派生图片顺序:%d 张" % (args.images_from, len(images)), file=sys.stderr)
471
+
472
+ for p in images:
473
+ if not os.path.isfile(p):
474
+ print("图片不存在:%s" % p, file=sys.stderr)
475
+ sys.exit(1)
476
+
477
+ # 校验:[图N] 标记数量必须与图片数量一致,且编号连续 1..N
478
+ markers = sorted({int(n) for line in md_text.split("\n")
479
+ for n in re.findall(r"^\[图(\d+)\]$", line.strip())})
480
+ if markers:
481
+ if markers != list(range(1, len(markers) + 1)):
482
+ print("图编号不连续:发现 %s,应为 1..%d" % (markers, len(markers)), file=sys.stderr)
483
+ sys.exit(1)
484
+ if len(images) != len(markers):
485
+ print("图片数量不匹配:[图N] 标记 %d 个,图片 %d 张" % (len(markers), len(images)),
486
+ file=sys.stderr)
487
+ sys.exit(1)
488
+ if args.cover and not os.path.isfile(args.cover):
489
+ print("封面图不存在:%s" % args.cover, file=sys.stderr)
490
+ sys.exit(1)
491
+
492
+ # 标题取 frontmatter 之后的第一个 #(frontmatter 里可能有 # 开头的行)
493
+ body_lines = md_text.split("\n")
494
+ if body_lines and body_lines[0].strip() == "---":
495
+ j = 1
496
+ while j < len(body_lines) and body_lines[j].strip() != "---":
497
+ j += 1
498
+ body_lines = body_lines[j + 1:]
499
+ m = re.search(r"^#\s+(.+)$", "\n".join(body_lines), re.M)
372
500
  title = m.group(1).strip() if m else "X长文"
373
501
 
374
- body = md_to_html(md_text, args.images, args.cover)
375
- page = HTML_TMPL.replace("{title}", html.escape(title), 1) \
376
- .replace("{css}", CSS, 1) \
377
- .replace("{body}", body, 1) \
378
- .replace("{js}", JS, 1)
502
+ body = md_to_html(md_text, images, args.cover)
503
+ page = HTML_TMPL.replace("@@TITLE@@", html.escape(title)) \
504
+ .replace("@@CSS@@", CSS) \
505
+ .replace("@@BODY@@", body) \
506
+ .replace("@@JS@@", JS)
379
507
 
380
508
  out_dir = os.path.dirname(os.path.abspath(args.out))
381
509
  os.makedirs(out_dir, exist_ok=True)