@topmindspace/topmind-writing-skills 0.8.44 → 0.8.45

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 (81) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.en.md +11 -11
  3. package/README.md +11 -11
  4. package/package.json +1 -1
  5. package/topmind-briefs/CHANGELOG.md +5 -0
  6. package/topmind-briefs/README.en.md +1 -1
  7. package/topmind-briefs/README.md +1 -1
  8. package/topmind-briefs/SKILL.md +1 -1
  9. package/topmind-briefs/package.json +1 -1
  10. package/topmind-briefs/references/image-sourcing.md +1 -1
  11. package/topmind-briefs/references/writing-principles.md +7 -3
  12. package/topmind-briefs/scripts/de_ai_lint.py +9 -2
  13. package/topmind-briefs/scripts/md2wechat.py +77 -19
  14. package/topmind-briefs/scripts/md2x-html.py +88 -7
  15. package/topmind-cover/README.en.md +1 -1
  16. package/topmind-cover/README.md +1 -1
  17. package/topmind-cover/SKILL.md +1 -1
  18. package/topmind-cover/package.json +1 -1
  19. package/topmind-cover/scripts/de_ai_lint.py +9 -2
  20. package/topmind-format/README.en.md +1 -1
  21. package/topmind-format/README.md +1 -1
  22. package/topmind-format/SKILL.md +1 -1
  23. package/topmind-format/package.json +1 -1
  24. package/topmind-format/scripts/de_ai_lint.py +9 -2
  25. package/topmind-poster/README.en.md +1 -1
  26. package/topmind-poster/README.md +1 -1
  27. package/topmind-revise/CHANGELOG.md +5 -0
  28. package/topmind-revise/README.en.md +1 -1
  29. package/topmind-revise/README.md +1 -1
  30. package/topmind-revise/SKILL.md +2 -1
  31. package/topmind-revise/package.json +1 -1
  32. package/topmind-revise/references/image-sourcing.md +1 -1
  33. package/topmind-revise/scripts/audit-provenance.py +12 -1
  34. package/topmind-revise/scripts/check_article.py +206 -0
  35. package/topmind-revise/scripts/de_ai_lint.py +9 -2
  36. package/topmind-revise/scripts/negative_tests.py +10 -1
  37. package/topmind-viral-posts/README.en.md +1 -1
  38. package/topmind-viral-posts/README.md +1 -1
  39. package/topmind-viral-posts/SKILL.md +1 -1
  40. package/topmind-viral-posts/package.json +1 -1
  41. package/topmind-viral-posts/references/image-sourcing.md +1 -1
  42. package/topmind-viral-posts/references/writing-principles.md +7 -3
  43. package/topmind-viral-posts/scripts/de_ai_lint.py +9 -2
  44. package/topmind-wechat-post/README.en.md +29 -17
  45. package/topmind-wechat-post/README.md +32 -20
  46. package/topmind-wechat-post/SKILL.md +72 -53
  47. package/topmind-wechat-post/package.json +1 -1
  48. package/topmind-wechat-post/references/element-spec.md +9 -6
  49. package/topmind-wechat-post/references/image-sourcing.md +1 -1
  50. package/topmind-wechat-post/references/known-pits.md +11 -3
  51. package/topmind-wechat-post/references/typography-rules.md +13 -15
  52. package/topmind-wechat-post/references/workflow.md +33 -23
  53. package/topmind-wechat-post/references/writing-principles.md +7 -3
  54. package/topmind-wechat-post/scripts/article_scaffold.py +95 -0
  55. package/topmind-wechat-post/scripts/audit-provenance.py +12 -1
  56. package/topmind-wechat-post/scripts/check_article.py +206 -0
  57. package/topmind-wechat-post/scripts/de_ai_lint.py +9 -2
  58. package/topmind-wechat-post/scripts/lint-wechat.py +12 -2
  59. package/topmind-wechat-post/scripts/md2wechat.py +77 -19
  60. package/topmind-wechat-post/scripts/negative_tests.py +77 -4
  61. package/topmind-wechat-post/scripts/new-article.py +80 -9
  62. package/topmind-wechat-post/scripts/push-to-topstream.py +5 -4
  63. package/topmind-wechat-post/scripts/sync-mapping.py +20 -12
  64. package/topmind-wechat-post/scripts/sync-status.py +19 -10
  65. package/topmind-wechat-post/scripts/wechat_paths.py +21 -0
  66. package/topmind-x-article/README.en.md +15 -11
  67. package/topmind-x-article/README.md +15 -10
  68. package/topmind-x-article/SKILL.md +67 -85
  69. package/topmind-x-article/package.json +1 -1
  70. package/topmind-x-article/references/image-sourcing.md +1 -1
  71. package/topmind-x-article/references/publish-checklist.md +10 -6
  72. package/topmind-x-article/references/writing-principles.md +7 -3
  73. package/topmind-x-article/references/x-format.md +13 -12
  74. package/topmind-x-article/references/x-html-format.md +8 -5
  75. package/topmind-x-article/scripts/article_scaffold.py +95 -0
  76. package/topmind-x-article/scripts/audit-provenance.py +12 -1
  77. package/topmind-x-article/scripts/check_article.py +206 -0
  78. package/topmind-x-article/scripts/de_ai_lint.py +9 -2
  79. package/topmind-x-article/scripts/md2x-html.py +88 -7
  80. package/topmind-x-article/scripts/md2x.py +26 -10
  81. package/topmind-x-article/scripts/negative_tests.py +52 -3
package/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## [0.8.45] - 2026-10-18
2
+
3
+ > 根包 0.8.44 → **0.8.45**(patch);技能版本:topmind-wechat-post 0.3.15 → 0.3.16、topmind-x-article 0.4.21 → 0.4.22、topmind-revise 0.1.0 → 0.1.1、topmind-briefs 0.2.23 → 0.2.24、topmind-cover 0.4.12 → 0.4.13、topmind-format 0.1.8 → 0.1.9、topmind-viral-posts 0.3.15 → 0.3.16(后四个只是随包共享副本更新);topmind-poster 0.1.6 不变。
4
+
5
+ ### 新增(长文交付改造:单源 Markdown,HTML 按需)
6
+
7
+ - 长文改为一个文章文件夹 `<文章名>/`:唯一源稿 `<文章名>.md`(frontmatter 五项 title / date / status / tags / cover,H1 = title = 文件名)、`images/`(按出现顺序编号)、`cover/`、私有 `素材来源.md`、`raw/`、`out/`(派生产物,可随时重建)。默认只交付 Markdown,Obsidian 里直接能看;要 X 版或公众号版时再派生到 `out/`。
8
+ - 新增共享脚本 `article_scaffold.py`(建文章文件夹;文件名安全化、最长 80 字;已存在就报错,不覆盖)和 `check_article.py`(E 级:frontmatter 五项、标题一致、图片存在、编号顺序与出现顺序一致(后出现的图序号反而更小才报)、无 `![[ ]]` / callout / `:::`;W 级:没被引用的图、远程图片、编号不连续或带 11a 这类插入后缀、自制图标注 W18;有 E 退出码 1)。
9
+ - topmind-wechat-post / topmind-x-article 默认组合 topmind-revise:写完先由 revise 执行改稿与质量三关(嵌入模式,完整报告落 `out/改稿报告.md`),HTML / 纯文本只在用户要时生成。
10
+
11
+ ### 变更
12
+
13
+ - `md2x-html.py`:源稿没有 `[图N]` 时直接按文档顺序读独占一行的 `![]()`(路径相对 md 所在目录,代码块内不动,远程图片跳过并告警),不再需要派生 `X长文.md`;图片下一行的「来源:」渲染成小号灰字图注;`--cover` 缺省读 frontmatter 的 `cover`;`--out` 缺省 `<文章文件夹>/out/X长文.html`;有 `[图N]` 却没给图片来源时直接报错。`--images-from` / `--images` 旧用法保留。
14
+ - `md2x.py`:列表默认 `plain`(「· 」「1)」,二级缩进两格),`--list-style emoji` 可选;`--out` 缺省 `out/X发布稿.txt`。
15
+ - `md2wechat.py`:二级标题有手写序号(「一、」「1.」)时原样保留,不再剥离、不再叠加自动编号,导读用「·」;新增 `--auto-number` 恢复旧行为;`--out-dir` 缺省 `<输入目录>/out`、`--slug` 缺省文件名、`--asset-root` 缺省输入目录;图片后的「来源:」行渲染成图注。
16
+ - wechat-post:`new-article.py` 默认建新结构,`--legacy --slug` 保留旧的 `YYYY-MM-DD-slug/公众号稿.md`;`sync-status.py` / `sync-mapping.py` / `push-to-topstream.py` 新旧结构都认,新结构只改 frontmatter 的 status、不改目录名,旧包的 `-released` 改名逻辑不变;`lint-wechat.py` 对新源稿里的 `:::` 报错(`公众号稿.md` 或 `--legacy` 只告警);`direction / source_file / target_file / word_count` 改为可选字段。
17
+ - `de_ai_lint.py` 的 W18:图片下一行写了「来源:自制,数据:…」也算标清楚;`audit-provenance.py` 的 `--draft` 可以直接给文章文件夹。
18
+ - 文档:wechat-post / x-article 的 SKILL.md、README 与 references(workflow、typography-rules、element-spec、known-pits、x-html-format、x-format、publish-checklist)换成新文件夹与按需派生;x-article 的「文本派生铁律」「HTML 占位符」「配图错位」三节合并为「派生规则」;`writing-principles.md` 第六节改为「单源、按需派生」。
19
+ - 旧交付包只读兼容,不自动迁移。
20
+
1
21
  ## [0.8.44] - 2026-10-18
2
22
 
3
23
  > 根包 0.8.43 → **0.8.44**(patch);技能版本:新增 topmind-revise **0.1.0**;topmind-briefs 0.2.22 → 0.2.23、topmind-cover 0.4.11 → 0.4.12、topmind-format 0.1.7 → 0.1.8、topmind-viral-posts 0.3.14 → 0.3.15、topmind-wechat-post 0.3.14 → 0.3.15、topmind-x-article 0.4.20 → 0.4.21;topmind-poster 0.1.6 不变。
package/README.en.md CHANGED
@@ -19,7 +19,7 @@
19
19
 
20
20
  ### topmind-wechat-post · WeChat article authoring
21
21
 
22
- Full lifecycle for WeChat articles: delivery package, review & rewrite, three quality gates (facts / logic / de-AI-flavor), WeChat inline typography (images must be embedded), publish checklist and status sync. Python stdlib only — zero dependencies.
22
+ Full lifecycle for WeChat articles: single-source article folder (`<title>/<title>.md`), review & rewrite, three quality gates (run by topmind-revise by default: facts / logic / de-AI-flavor), WeChat inline typography on request (images must be embedded, output goes to `out/`), publish checklist and status sync. Hand-written heading numbers are kept as written; legacy `公众号稿.md` packages stay readable. Python stdlib only — zero dependencies.
23
23
 
24
24
  ```bash
25
25
  npx @topmindspace/topmind-writing-skills install topmind-wechat-post
@@ -27,7 +27,7 @@ npx @topmindspace/topmind-writing-skills install topmind-wechat-post
27
27
 
28
28
  ### topmind-x-article · One-click X long-form publishing
29
29
 
30
- Markdown draft → one-click-copy HTML (rich text, images embedded) / plain-text fallback (`md2x.py`: images → `[图N]`, tables → "item: value" lists, lists → emoji bullets because plain-text X does not render Markdown lists) + cover art + publish checklist. Publishing is done by hand-pasting; fetch the text back for verification after posting.
30
+ Delivers one `<title>.md` by default (after topmind-revise); when you want to post on X, derives into `out/` on request: one-click-copy HTML (rich text; reads the source's `![]()` lines and embeds images in order, "来源:" lines become captions) / plain-text fallback (`md2x.py`: images → `[图N]`, tables → "item: value" lists, lists → "·" / "1)" with emoji optional, because plain-text X does not render Markdown lists) + cover art + publish checklist. Publishing is done by hand-pasting; fetch the text back for verification after posting.
31
31
 
32
32
  ```bash
33
33
  npx @topmindspace/topmind-writing-skills install topmind-x-article
@@ -119,7 +119,7 @@ The writing skills share one rule set (`shared/references/`, copied into each sk
119
119
 
120
120
  Also:
121
121
 
122
- - **Originals first** (`image-sourcing.md`): find official / original-post / third-party images before writing and log sources in a private list inside the delivery package; self-made charts are a fallback and must be labeled 自制 with the data source.
122
+ - **Originals first** (`image-sourcing.md`): find official / original-post / third-party images before writing and log sources in the private `素材来源.md` inside the article folder; self-made charts are a fallback and must be labeled 自制 with the data source.
123
123
  - **De-AI pass is mandatory**: rewrite boundaries first (change wording, never facts or certainty); W warnings need a decision rather than a zero count; the regression set `scripts/test_lint_rules.py` keeps normal writing from being flagged. topmind-format reports hits without rewriting.
124
124
  - **Fixed revision order** (`revision-workflow.md`): facts → logic → cut redundancy → de-AI → voice check → layout → fidelity check → over-edit check; back up first and report before/after word counts (`count_words.py`). `topmind-revise` runs it when installed.
125
125
 
@@ -145,16 +145,16 @@ Every skill ships sample outputs written by us (fictional or neutral topics; no
145
145
 
146
146
  | Skill | Version | What it does |
147
147
  |-------|---------|--------------|
148
- | [`topmind-wechat-post`](./topmind-wechat-post/) | **0.3.15** | WeChat article lifecycle: package, review, 3 quality gates, inline typography & publish checklist |
149
- | [`topmind-x-article`](./topmind-x-article/) | **0.4.21** | X long-form one-click publish: Markdown → one-click-copy HTML (images/prompt copy, click-to-zoom images) / plain-text fallback + cover + checklist |
150
- | [`topmind-cover`](./topmind-cover/) | **0.4.12** | Cover art for X / WeChat (X master 1500×600 / 5:2): striking, theme-focused; 22-style cover style library + 16:9 X short-post cover scene + 24 title treatments + 7-layer scene recipe + 55 example images (22 styles × 2 sizes + 11 variant samples) + 1 style overview |
151
- | [`topmind-briefs`](./topmind-briefs/) | **0.2.23** | Short-form briefs (WeChat + X): one topic, one chart; data rankings / paper explainers / product alerts / mechanism explainers; sources → images → content → layout; one copy-ready plain-text X block (no HTML by default, emoji optional, no AI tone), links / images sent separately; asks before generating any image |
152
- | [`topmind-viral-posts`](./topmind-viral-posts/) | **0.3.15** | X viral short posts: 18 template types + combinations, duanzi layout (blank lines, no emoji, plain bullet lines), one copy-ready plain-text block per post with hashtags last, links / images sent separately, classification tags listed separately; asks before generating an image |
153
- | [`topmind-format`](./topmind-format/) | **0.1.8** | X plain-text formatter: to duanzi (blank lines, no emoji) / to short post (paragraphing + sensible lists, emoji optional); layout only, words unchanged, with a fidelity check; one copy-ready plain-text body, links / images sent separately |
154
- | [`topmind-revise`](./topmind-revise/) | **0.1.0** | Revision and quality check: edit, tighten, de-AI, three quality gates and fidelity audit; four modes (audit / revise / embed / quick); every change maps to a rule code, with an over-edit guard; the long-form skills use it by default |
148
+ | [`topmind-wechat-post`](./topmind-wechat-post/) | **0.3.16** | WeChat article lifecycle: single-source article folder, review, 3 quality gates (revise by default), inline typography on request & publish checklist |
149
+ | [`topmind-x-article`](./topmind-x-article/) | **0.4.22** | X long-form: single-source Markdown (revise by default) → on-request one-click-copy HTML (images/prompt copy, click-to-zoom images) / plain-text fallback + cover + checklist |
150
+ | [`topmind-cover`](./topmind-cover/) | **0.4.13** | Cover art for X / WeChat (X master 1500×600 / 5:2): striking, theme-focused; 22-style cover style library + 16:9 X short-post cover scene + 24 title treatments + 7-layer scene recipe + 55 example images (22 styles × 2 sizes + 11 variant samples) + 1 style overview |
151
+ | [`topmind-briefs`](./topmind-briefs/) | **0.2.24** | Short-form briefs (WeChat + X): one topic, one chart; data rankings / paper explainers / product alerts / mechanism explainers; sources → images → content → layout; one copy-ready plain-text X block (no HTML by default, emoji optional, no AI tone), links / images sent separately; asks before generating any image |
152
+ | [`topmind-viral-posts`](./topmind-viral-posts/) | **0.3.16** | X viral short posts: 18 template types + combinations, duanzi layout (blank lines, no emoji, plain bullet lines), one copy-ready plain-text block per post with hashtags last, links / images sent separately, classification tags listed separately; asks before generating an image |
153
+ | [`topmind-format`](./topmind-format/) | **0.1.9** | X plain-text formatter: to duanzi (blank lines, no emoji) / to short post (paragraphing + sensible lists, emoji optional); layout only, words unchanged, with a fidelity check; one copy-ready plain-text body, links / images sent separately |
154
+ | [`topmind-revise`](./topmind-revise/) | **0.1.1** | Revision and quality check: edit, tighten, de-AI, three quality gates and fidelity audit; four modes (audit / revise / embed / quick); every change maps to a rule code, with an over-edit guard; the long-form skills use it by default |
155
155
  | [`topmind-poster`](./topmind-poster/) | **0.1.6** | HTML+CSS → high-res long image / poster / infographic PNG: headless Chrome 2× screenshot + auto-trim; "one image per section" batch workflow (generator + renderer) |
156
156
 
157
- Installer [`@topmindspace/topmind-writing-skills`](https://www.npmjs.com/package/@topmindspace/topmind-writing-skills) is **0.8.44** (whole-repo same-tag releases).
157
+ Installer [`@topmindspace/topmind-writing-skills`](https://www.npmjs.com/package/@topmindspace/topmind-writing-skills) is **0.8.45** (whole-repo same-tag releases).
158
158
 
159
159
  ### Which skill, and how they combine
160
160
 
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  ### topmind-wechat-post · 公众号创作
19
19
 
20
- 公众号文章全生命周期:交付包、审校改写、质量三关(事实 / 逻辑 / 去 AI 味)、微信内联排版(图片必内嵌)、发布清单与状态同步。脚本纯 Python 标准库,零依赖。
20
+ 公众号文章全生命周期:单源文章文件夹(`<文章名>/<文章名>.md`)、审校改写、质量三关(默认组合 topmind-revise:事实 / 逻辑 / 去 AI 味)、按需出微信内联排版(图片必内嵌,产物进 `out/`)、发布清单与状态同步。手写小标题序号原样保留;旧 `公众号稿.md` 交付包只读兼容。脚本纯 Python 标准库,零依赖。
21
21
 
22
22
  ```bash
23
23
  npx @topmindspace/topmind-writing-skills install topmind-wechat-post
@@ -25,7 +25,7 @@ npx @topmindspace/topmind-writing-skills install topmind-wechat-post
25
25
 
26
26
  ### topmind-x-article · X 长文一键发布
27
27
 
28
- Markdown 原稿 → 一键复制 HTML(富文本,配图内嵌)/ 纯文本兜底(`md2x.py`:图片→`[图N]`、表格→"项:值"列表、列表→emoji 项目符号,因为 X 纯文本不渲染 Markdown 列表)+ 封面图 + 发布清单。发布走人工粘贴,发布后抓回核对。
28
+ 默认交付一份 `<文章名>.md`(先过 topmind-revise);要发 X 时再按需派生到 `out/`:一键复制 HTML(富文本,直接读源稿的 `![]()` 按顺序内嵌配图,「来源:」行成图注)/ 纯文本兜底(`md2x.py`:图片→`[图N]`、表格→"项:值"列表、列表→「·」「1)」,emoji 可选,因为 X 纯文本不渲染 Markdown 列表)+ 封面图 + 发布清单。发布走人工粘贴,发布后抓回核对。
29
29
 
30
30
  ```bash
31
31
  npx @topmindspace/topmind-writing-skills install topmind-x-article
@@ -119,7 +119,7 @@ npx @topmindspace/topmind-writing-skills install topmind-poster
119
119
 
120
120
  另外:
121
121
 
122
- - **原图优先**(`image-sourcing.md`):写字之前先找官方 / 原帖 / 第三方原图,来源记进交付包里的私有信源清单;自制图只作兜底,标「自制」和数据来源。
122
+ - **原图优先**(`image-sourcing.md`):写字之前先找官方 / 原帖 / 第三方原图,来源记进文章文件夹里的私有 `素材来源.md`;自制图只作兜底,标「自制」和数据来源。
123
123
  - **去 AI 味必过**:先守改写边界(只改表达,不改事实和确定性);W 警告不要求清零,要求每条有结论;规则回归测试 `scripts/test_lint_rules.py` 守住「不应报」的正常写法。topmind-format 只报告命中、不改字。
124
124
  - **固定改稿流程**(`revision-workflow.md`):事实 → 逻辑 → 删冗余 → 去 AI 味 → 核心经验对照 → 排版 → 保真审计 → 过度改稿检查;每轮先备份,报改前 / 改后字数(`count_words.py`)。装了 `topmind-revise` 就由它执行。
125
125
 
@@ -145,16 +145,16 @@ npx @topmindspace/topmind-writing-skills install topmind-poster
145
145
 
146
146
  | 技能 | 版本 | 做什么 |
147
147
  |------|------|--------|
148
- | [`topmind-wechat-post`](./topmind-wechat-post/) | **0.3.15** | 公众号文章全生命周期:交付包、审校改写、质量三关、微信内联排版与发布清单 |
149
- | [`topmind-x-article`](./topmind-x-article/) | **0.4.21** | X 长文一键发布:Markdown 原稿 → 一键复制 HTML(含配图/提示词复制、图片点击放大)/ 纯文本兜底 + 封面图 + 发布清单 |
150
- | [`topmind-cover`](./topmind-cover/) | **0.4.12** | 文章封面配图(X / 公众号共用,X 主尺寸 1500×600 / 5:2):震撼醒目主题突出;22 种封面风格库 + X 短文 16:9 封面场景 + 24 种标题花式 + 七层场景配方 + 55 张示例图(22 种风格 × 2 尺寸 + 11 张变体样张) + 1 张风格总览图 |
151
- | [`topmind-briefs`](./topmind-briefs/) | **0.2.23** | 干货短文(公众号 + X 双平台):一件事讲透,数据榜单/论文解读/新品速递;信源 → 原图 → 内容 → 排版;X 版交付一段可复制纯文本(默认不出 HTML,emoji 可选,去 AI 味),链接 / 原图 / 配图另发,无原图先询问配图 |
152
- | [`topmind-viral-posts`](./topmind-viral-posts/) | **0.3.15** | X 引流段子/爆款短篇:18 大类型模板 + 组合玩法,段子体(行间空行,零 emoji,分点不加符号)、每条一段可复制纯文本、话题标签在末行,链接 / 原图另发、分类标签另列;无原图先询问再走 topmind-cover |
153
- | [`topmind-format`](./topmind-format/) | **0.1.8** | X 纯文本格式化:转段子(行间空行,零 emoji)/ 格式化成短文(分段 + 合理列表,emoji 可选),只动排版不改字,带原文保真校验;交付一段可复制纯文本,链接 / 原图另发 |
154
- | [`topmind-revise`](./topmind-revise/) | **0.1.0** | 改稿与质检:改稿、精简、去 AI 味、质量三关与保真审计;四种模式(只审不改 / 改稿 / 嵌入 / 快审),每处改动对应规则编号,防过度改稿;长文技能默认组合 |
148
+ | [`topmind-wechat-post`](./topmind-wechat-post/) | **0.3.16** | 公众号文章全生命周期:单源文章文件夹、审校改写、质量三关(默认组合 revise)、按需出微信内联排版与发布清单 |
149
+ | [`topmind-x-article`](./topmind-x-article/) | **0.4.22** | X 长文:单源 Markdown(默认组合 revise)→ 按需出一键复制 HTML(含配图/提示词复制、图片点击放大)/ 纯文本兜底 + 封面图 + 发布清单 |
150
+ | [`topmind-cover`](./topmind-cover/) | **0.4.13** | 文章封面配图(X / 公众号共用,X 主尺寸 1500×600 / 5:2):震撼醒目主题突出;22 种封面风格库 + X 短文 16:9 封面场景 + 24 种标题花式 + 七层场景配方 + 55 张示例图(22 种风格 × 2 尺寸 + 11 张变体样张) + 1 张风格总览图 |
151
+ | [`topmind-briefs`](./topmind-briefs/) | **0.2.24** | 干货短文(公众号 + X 双平台):一件事讲透,数据榜单/论文解读/新品速递;信源 → 原图 → 内容 → 排版;X 版交付一段可复制纯文本(默认不出 HTML,emoji 可选,去 AI 味),链接 / 原图 / 配图另发,无原图先询问配图 |
152
+ | [`topmind-viral-posts`](./topmind-viral-posts/) | **0.3.16** | X 引流段子/爆款短篇:18 大类型模板 + 组合玩法,段子体(行间空行,零 emoji,分点不加符号)、每条一段可复制纯文本、话题标签在末行,链接 / 原图另发、分类标签另列;无原图先询问再走 topmind-cover |
153
+ | [`topmind-format`](./topmind-format/) | **0.1.9** | X 纯文本格式化:转段子(行间空行,零 emoji)/ 格式化成短文(分段 + 合理列表,emoji 可选),只动排版不改字,带原文保真校验;交付一段可复制纯文本,链接 / 原图另发 |
154
+ | [`topmind-revise`](./topmind-revise/) | **0.1.1** | 改稿与质检:改稿、精简、去 AI 味、质量三关与保真审计;四种模式(只审不改 / 改稿 / 嵌入 / 快审),每处改动对应规则编号,防过度改稿;长文技能默认组合 |
155
155
  | [`topmind-poster`](./topmind-poster/) | **0.1.6** | HTML+CSS → 高清长图 / 海报 / 信息图 PNG:无头 Chrome 2× 截图 + 自动裁白;「一段一图」分段图批量出图(生成器 + 渲染器) |
156
156
 
157
- 安装器 [`@topmindspace/topmind-writing-skills`](https://www.npmjs.com/package/@topmindspace/topmind-writing-skills) 为 **0.8.44**(整仓同 tag 发版)。
157
+ 安装器 [`@topmindspace/topmind-writing-skills`](https://www.npmjs.com/package/@topmindspace/topmind-writing-skills) 为 **0.8.45**(整仓同 tag 发版)。
158
158
 
159
159
  ### 怎么选、怎么组合
160
160
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topmindspace/topmind-writing-skills",
3
- "version": "0.8.44",
3
+ "version": "0.8.45",
4
4
  "description": "TopMindSpace 写作技能合集 — 通过 npm / npx 安装技能(公众号长文、X 长文、干货短文、X 引流短帖、改稿与质检、封面图、长图海报)。只出稿与发布包,不代发。",
5
5
  "license": "MIT",
6
6
  "author": "TopMindspace",
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.24 (2026-10-18)
4
+
5
+ - 随包共享脚本更新:`md2x-html.py` 能直接读源稿的 `![]()` 按顺序嵌图、「来源:」行渲染成图注、`--out` 缺省 `out/X长文.html`(原有 `--images-from` 用法不变);`md2wechat.py` 默认保留手写小标题序号(`--auto-number` 恢复旧行为),`--out-dir` / `--slug` 有缺省值
6
+ - `de_ai_lint.py` 的 W18:图片下一行写了「来源:自制,数据:…」也算标清楚
7
+
3
8
  ## 0.2.23 (2026-10-18)
4
9
 
5
10
  - 写法规则改为引用共享的 `core-voice.md`:首行一句话题陈述、事实讲完就停(去掉收尾句)、列表默认「·」、话题标签默认 2 个;引语正文写「人名:中文意思」;未完全确认的热点首行用一次「?!」,不写泛泛的「据报道」
@@ -7,7 +7,7 @@ A brief is not a long-form article — **no HTML by default**: the workflow is v
7
7
  The X version is one copy-ready plain-text block (emoji optional); **links (including sources), original images and any generated images are sent separately**, never mixed into the body.
8
8
  The WeChat version is ~300–800 Chinese characters of Markdown. With no original image it asks before generating one.
9
9
 
10
- - Version: **v0.2.23** (ships with `@topmindspace/topmind-writing-skills@0.8.44`)
10
+ - Version: **v0.2.24** (ships with `@topmindspace/topmind-writing-skills@0.8.45`)
11
11
 
12
12
  ## Install
13
13
 
@@ -7,7 +7,7 @@
7
7
  X 版交付一段可直接复制的纯文本(emoji 可选),**链接(含来源)、原图、配图另发**,不和正文混在一起;公众号版约 300–800 字 Markdown。
8
8
  没有原图时先问要不要配图(`topmind-cover`);链接那一条写明来源名称、发布时间与 URL。
9
9
 
10
- - 版本:**v0.2.23**(随 `@topmindspace/topmind-writing-skills@0.8.44` 发布)
10
+ - 版本:**v0.2.24**(随 `@topmindspace/topmind-writing-skills@0.8.45` 发布)
11
11
 
12
12
  ## 安装
13
13
 
@@ -3,7 +3,7 @@ name: topmind-briefs
3
3
  description: "干货短文:一件事讲透,X + 公众号双平台。短文不是长文——默认不出 HTML:工作流按「核实一手信源 → 找原帖原图 → 写内容 → 排版」;X 版交付一段可直接复制的纯文本(首行一句话题陈述、合理分段与列表、去 AI 味、emoji 可选且只用 TopMind 体系、末行默认 2 个话题标签),链接(含来源)、原图(1–2 张)、配图另发,绝不混进正文;公众号版是 300–800 字 Markdown;没有原图时主动询问是否配图,不擅自生图。尊重原始信息、原图、原文。只交付不代发。Use when 写短文、公众号短文、X 短文、发快讯、简讯、速报、数据榜单、榜单速报、论文解读、新品速递、来篇短的、短平快、brief;信息密度型(数据、榜单、发布)走本技能。Do NOT use for 把现成文字格式化、转段子、排版(→ topmind-format)、X 长文与发 X 文章(→ topmind-x-article)、公众号长文与公众号排版 HTML(→ topmind-wechat-post)、引流互动型短帖、段子、涨粉帖、互关帖、爆款短篇(→ topmind-viral-posts)、单独出长图/海报/信息图 PNG(→ topmind-poster)、封面与头图(→ topmind-cover)、单独改稿、去 AI 味与质检(→ topmind-revise)、把稿子发出去(→ topmind-x,须用户确认)。"
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.2.23"
6
+ version: "0.2.24"
7
7
  action_category: "write"
8
8
  triggers: "写短文, 公众号短文, X 短文, 发快讯, 快讯, 简讯, 速报, 干货短文, 论文解读, 数据榜单, 榜单速报, 新品速递, brief, 来篇短的, 短平快, 发个快讯"
9
9
  author: "TopMindSpace"
@@ -9,5 +9,5 @@
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/topmindspace/topmind-writing-skills.git"
11
11
  },
12
- "version": "0.2.23"
12
+ "version": "0.2.24"
13
13
  }
@@ -13,7 +13,7 @@
13
13
  ## 二、自制图规则
14
14
 
15
15
  - 图内或图注写明「自制」和数据来源,例如「自制,数据:xx 官方 2026-09」。
16
- - 公众号稿 / X长文.md 的图片行写成 `![自制,数据:xx](images/NN-xx.png)`;`lint_x_post.py --md` / `--long` 会对没标清的自制图报 W18。
16
+ - 长文源稿 `<文章名>.md` 的自制图写成 `![自制,数据:xx](images/NN-xx.png)`,或在图片下一行写「来源:自制,数据:xx」;`lint_x_post.py --md` / `--long` 会对没标清的自制图报 W18。
17
17
  - 不用 AI 生成的图冒充原图或数据图;示意图标「示意」。
18
18
 
19
19
  ## 三、私有信源清单(不进公开仓库)
@@ -33,7 +33,7 @@
33
33
  > 能用可视化传达的,不用文字啰嗦。这是 X 和公众号共同的内容要求。
34
34
 
35
35
  1. **表格 > 段落**:对比类内容用简单表格,不写大段文字。
36
- 2. **列表 > 段落**:并列要点用列表,不写成长段。公众号稿用 Markdown 列表;X 纯文本帖不渲染 Markdown,用 emoji 项目符号(🔹 / 1️⃣,规则见 `x-post-format.md`)。
36
+ 2. **列表 > 段落**:并列要点用列表,不写成长段。长文源稿用 Markdown 列表;X 纯文本帖不渲染 Markdown,默认用「·」「1)」,emoji 项目符号可选(规则见 `x-post-format.md`)。
37
37
  3. **"概要 + 列表"**:信息密集的节,先给一句概要(这节的结论),再用列表展开论据。
38
38
  不写长段论述;非写不可的长段,拆成多个短段或转成"概要+列表"。
39
39
  4. **图表 > 描述**:数据趋势用图表,图片用素材原图。
@@ -165,7 +165,8 @@ AI 味多数来自句式,不是单个词。下面每一类都给出改法,
165
165
  > 以下为 topmind 工作区默认规约;其他用户可通过环境变量或 CLI 参数覆盖,
166
166
  > 技能本身不强制依赖特定目录结构。
167
167
 
168
- 1. **目录规约(默认)**:公众号+X 创作包放在工作区的长文类专题下(按类别**名称**找「长文」,
168
+ 1. **目录规约(默认)**:一篇长文一个文章文件夹 `<文章名>/`,里面只有一份源稿 `<文章名>.md`
169
+ (外加 `images/`、`cover/`、`素材来源.md`、`raw/`、`out/`),放在工作区的长文类专题下(按类别**名称**找「长文」,
169
170
  兼容旧名「创作」「专题」;专题默认 `{当年}-公众号`),正式稿 HTML 交付到 role:delivery 类别
170
171
  (以工作区 `topmind.yaml` 的实际目录为准,例如 `88-输出/`)。编号与年份不写死。
171
172
  覆盖方式:`--base` 参数 > `TOPMIND_WECHAT_BASE` > `TOPMIND_WORKSPACE`(按名称发现类别);
@@ -173,7 +174,10 @@ AI 味多数来自句式,不是单个词。下面每一类都给出改法,
173
174
  2. **云端备份**:所有文章创作结果默认在云端工作区按规约格式目录备份存档,
174
175
  方便找回/暂存。
175
176
  3. **封面默认 2 版**:每篇长文默认提供至少 2 版不同风格 cover 供用户选择。
176
- 4. **双平台同步**:默认 X + 公众号双版,不分长短文。
177
+ 4. **单源、按需派生**:长文默认只交付一份 Markdown(`<文章名>.md`,Obsidian 里直接能看);
178
+ 用户要哪个平台的版本,再从它派生到 `out/`(X 长文 HTML / 纯文本 → `topmind-x-article`,
179
+ 公众号 HTML → `topmind-wechat-post`)。`out/` 是生成文件,不手改;改稿永远只改源稿,改完重新派生。
180
+ 短文、段子、回复不变:一段可复制纯文本,链接、原图另发。旧的「公众号稿 + 派生 X 稿」交付包只读兼容。
177
181
 
178
182
  ## 七、研究深度要求(用户给定主题)
179
183
 
@@ -290,8 +290,15 @@ def image_label_hits(lines: list[str], start: int):
290
290
  if not m:
291
291
  continue
292
292
  alt, path = m.group(1), m.group(2)
293
- if SELFMADE_HINT_RE.search(alt + " " + path) and not (SELFMADE_HINT_RE.search(alt) and SOURCE_HINT_RE.search(alt)):
294
- out.append((start + off, "W18", "自制图没标清楚:图注(alt)里写「自制」和数据来源,例如「自制,数据:xx 官方」"))
293
+ # 新结构里出处写在图片下一行(可隔一个空行)的「来源:」,也算图注
294
+ nxt = ""
295
+ for k in (off + 1, off + 2):
296
+ if k < len(lines) and lines[k].strip():
297
+ nxt = lines[k].strip() if re.match(r"^来源[::]", lines[k].strip()) else ""
298
+ break
299
+ label = alt + " " + nxt
300
+ if SELFMADE_HINT_RE.search(alt + " " + path) and not (SELFMADE_HINT_RE.search(label) and (SOURCE_HINT_RE.search(alt) or nxt)):
301
+ out.append((start + off, "W18", "自制图没标清楚:图注(alt)或下一行「来源:」里写「自制」和数据来源,例如「来源:自制,数据:xx 官方」"))
295
302
  return out
296
303
 
297
304
 
@@ -14,9 +14,16 @@ Supported beyond CommonMark:
14
14
  - four heading-2 style variants
15
15
 
16
16
  Usage:
17
+ python3 md2wechat.py --input <文章名>/<文章名>.md --embed-images
18
+ # 文章文件夹结构:--out-dir 缺省 <文章名>/out,--slug 缺省文件名,
19
+ # --asset-root 缺省文章文件夹
17
20
  python3 md2wechat.py --input 稿.md --out-dir 交付包 --slug foo \
18
21
  --asset-root /path/to/repo [--theme theme.json] [--link-mode footnote] \
19
- [--embed-images]
22
+ [--embed-images] [--auto-number]
23
+
24
+ Headings: 二级标题写了「一、」「1.」这类手写序号就原样保留,不再自动编号;
25
+ 都没写时按主题样式自动编号。--auto-number 恢复旧行为(剥离手写序号后统一编号)。
26
+ Images: 图片后紧跟(或隔一个空行)的「来源:……」行渲染成图注样式。
20
27
 
21
28
  Outputs:
22
29
  <out-dir>/<slug>-公众号版.html full page, copy body section into WeChat
@@ -581,13 +588,16 @@ def match_img_only(line):
581
588
  return None
582
589
  return line[2:k], src
583
590
  CAPTION_RE = re.compile(r"^\*(.+)\*\s*$")
591
+ # 图片后紧跟的「来源:」行渲染成图注(core-voice V14);中间最多隔一个空行
592
+ SOURCE_RE = re.compile(r"^(来源[::].+)$")
584
593
  TABLE_SEP_RE = re.compile(r"^\|?[\s:|-]+\|[\s:|-]*$")
585
594
  CONTAINER_RE = re.compile(r"^:::+\s*(\w+)?\s*(.*)$")
586
595
 
587
596
  CN_NUM = "一二三四五六七八九十壹贰叁肆伍陆柒捌玖拾"
588
597
 
589
- # 标题里手写的中文/阿拉伯序号。序号应由排版层生成(可换样式、可加引用),
590
- # 写进标题文本后就变成了不可编程的字符串。这里统一剥离后重新编号。
598
+ # 标题里手写的中文/阿拉伯序号。默认:稿子里的二级标题带手写序号(「一、」「1.」)就原样保留,
599
+ # 排版层不再自动编号;二级标题都没写序号时才按主题样式自动编号。--auto-number 恢复旧行为:
600
+ # 一律剥离手写序号后由排版层重新编号。
591
601
  # 注意:\d{1,2}[、..](?!\d) 带负向断言,避免把「3.4× 成本」这类
592
602
  # 以数字开头的标题误剥成「4× 成本」。
593
603
  HAND_NUM_RE = re.compile(
@@ -625,6 +635,16 @@ def para_style(theme):
625
635
  % (theme["para_gap"], theme["line_height"], theme["align"]))
626
636
 
627
637
 
638
+ def keep_hand_num(theme):
639
+ return bool(theme.get("_keep_hand_num"))
640
+
641
+
642
+ def detect_hand_numbers(lines):
643
+ """二级标题里有没有手写序号(「一、」「1.」「第一章」)。"""
644
+ return any(HAND_NUM_RE.match(l.strip()[3:].strip())
645
+ for l in lines if l.strip().startswith("## "))
646
+
647
+
628
648
  def render_heading(level, text, theme, index=None):
629
649
  accent = theme["accent"]
630
650
  if level == 1:
@@ -633,12 +653,18 @@ def render_heading(level, text, theme, index=None):
633
653
  return ('<p style="font-size:20px;font-weight:700;color:%s;'
634
654
  'text-align:center;margin:0 0 18px;line-height:1.5;'
635
655
  'letter-spacing:0.5px;">%s</p>'
636
- % (theme["text_strong"], render_inline(HAND_NUM_RE.sub("", text).strip() or text)))
656
+ % (theme["text_strong"], render_inline(text.strip() if keep_hand_num(theme)
657
+ else (HAND_NUM_RE.sub("", text).strip() or text))))
637
658
 
638
659
  if level == 2:
639
660
  style = theme.get("h2_style", "number")
640
- # 先剥离手写序号,再按主题样式决定是否由排版层补编号
641
- plain = HAND_NUM_RE.sub("", text).strip() or text.strip()
661
+ if keep_hand_num(theme):
662
+ # 保留手写序号:标题原样输出,不再补排版层编号
663
+ plain = text.strip()
664
+ index = None
665
+ else:
666
+ # 先剥离手写序号,再按主题样式决定是否由排版层补编号
667
+ plain = HAND_NUM_RE.sub("", text).strip() or text.strip()
642
668
  t = render_inline(plain)
643
669
  num = ""
644
670
  if "number" in style and index:
@@ -672,7 +698,8 @@ def render_heading(level, text, theme, index=None):
672
698
  % (theme["text_strong"], accent, num, t))
673
699
 
674
700
  if level == 3:
675
- t = render_inline(HAND_NUM_RE.sub("", text).strip() or text)
701
+ t = render_inline(text.strip() if keep_hand_num(theme)
702
+ else (HAND_NUM_RE.sub("", text).strip() or text))
676
703
  return ('<h3 style="font-size:16px;font-weight:600;color:%s;'
677
704
  'margin:26px 0 12px;line-height:1.5;">%s</h3>'
678
705
  % (theme["text_strong"], t))
@@ -878,12 +905,18 @@ def render_hr(theme):
878
905
  'margin:0 auto;"></section></section>' % theme["accent"])
879
906
 
880
907
 
881
- def render_figure(src, caption, theme, alt=""):
908
+ def render_figure(src, caption, theme, alt="", source=None):
882
909
  cap = ""
883
910
  if caption:
884
911
  cap = ('<p style="font-size:13px;color:%s;text-align:center;'
885
912
  'margin:9px 0 %s;line-height:1.6;">%s</p>'
886
- % (theme["text_light"], theme["para_gap"], render_inline(caption)))
913
+ % (theme["text_light"], "4px" if source else theme["para_gap"], render_inline(caption)))
914
+ if source:
915
+ cap += ('<p style="font-size:12px;color:%s;text-align:center;'
916
+ 'margin:%s 0 %s;line-height:1.6;">%s</p>'
917
+ % (theme["text_light"], "0" if caption else "9px", theme["para_gap"],
918
+ render_inline(source)))
919
+ caption = caption or source
887
920
  border = "" # MD3 优雅:图片无边框,只保留圆角
888
921
  altattr = ' alt="%s"' % esc(alt) if alt else ""
889
922
  # 图片一律 max-width:100%,绝不用 width:100%:正文容器约 677px,
@@ -1093,7 +1126,9 @@ def convert_blocks(lines, md_path, out_dir, theme, ctx):
1093
1126
  toc_at = None # 导读落点:首个正文段落之后
1094
1127
 
1095
1128
  # 预扫描:末章是否为收束类,决定它的编号用数字还是 ∞
1096
- h2_texts = [HAND_NUM_RE.sub("", l.strip()[3:]).strip() or l.strip()[3:].strip()
1129
+ keep = keep_hand_num(theme)
1130
+ h2_texts = [(l.strip()[3:].strip() if keep else
1131
+ (HAND_NUM_RE.sub("", l.strip()[3:]).strip() or l.strip()[3:].strip()))
1097
1132
  for l in lines if l.strip().startswith("## ")]
1098
1133
  h2_total = len(h2_texts)
1099
1134
  last_is_summary = bool(h2_total) and bool(SUMMARY_H2_RE.match(h2_texts[-1]))
@@ -1144,8 +1179,8 @@ def convert_blocks(lines, md_path, out_dir, theme, ctx):
1144
1179
  marker = h2_index
1145
1180
  if level == 2:
1146
1181
  h2_index += 1
1147
- h2_titles.append(HAND_NUM_RE.sub("", m.group(2)).strip()
1148
- or m.group(2).strip())
1182
+ h2_titles.append(m.group(2).strip() if keep else
1183
+ (HAND_NUM_RE.sub("", m.group(2)).strip() or m.group(2).strip()))
1149
1184
  marker = ("∞" if (last_is_summary and h2_index == h2_total)
1150
1185
  else h2_index)
1151
1186
  out.append(render_heading(level, m.group(2).strip(), theme, marker))
@@ -1199,14 +1234,24 @@ def convert_blocks(lines, md_path, out_dir, theme, ctx):
1199
1234
  m = match_img_only(stripped)
1200
1235
  if m:
1201
1236
  caption = None
1237
+ source = None
1202
1238
  if i + 1 < n:
1203
1239
  cm = CAPTION_RE.match(lines[i + 1].strip())
1204
1240
  if cm:
1205
1241
  caption = cm.group(1)
1206
1242
  i += 1
1243
+ # 「来源:」行(紧跟或隔一个空行)→ 图注样式
1244
+ j = i + 1
1245
+ if j < n and not lines[j].strip() and j + 1 < n:
1246
+ j += 1
1247
+ if j < n:
1248
+ sm = SOURCE_RE.match(lines[j].strip())
1249
+ if sm:
1250
+ source = sm.group(1)
1251
+ i = j
1207
1252
  src = resolve_image(m[1], md_path, out_dir,
1208
1253
  ctx["assets"], ctx["asset_root"])
1209
- out.append(render_figure(src, caption, theme, alt=m[0]))
1254
+ out.append(render_figure(src, caption, theme, alt=m[0], source=source))
1210
1255
  i += 1
1211
1256
  continue
1212
1257
 
@@ -1243,8 +1288,10 @@ def convert_blocks(lines, md_path, out_dir, theme, ctx):
1243
1288
 
1244
1289
  def convert(md_text, md_path, out_dir, theme, link_mode="footnote",
1245
1290
  render_mermaid=False, asset_root=None, use_pangu=True,
1246
- toc=True, sign_mode="auto", author="", author_bio=""):
1291
+ toc=True, sign_mode="auto", author="", author_bio="", auto_number=False):
1247
1292
  lines = strip_frontmatter(md_text.split("\n"))
1293
+ theme = dict(theme)
1294
+ theme["_keep_hand_num"] = (not auto_number) and detect_hand_numbers(lines)
1248
1295
  han_chars = sum(len(HAN_RE.findall(l)) for l in lines)
1249
1296
  ctx = {"assets": [], "diagrams": [], "footnotes": [],
1250
1297
  "render_mermaid": render_mermaid, "asset_root": asset_root,
@@ -1320,7 +1367,7 @@ def render_toc(titles, theme):
1320
1367
  # 导读的标记必须跟正文标题一致:
1321
1368
  # - 标题带编号(h2_style 含 number)→ 导读也用编号;末章收束类用 ∞,避免「导读 04 / 正文 ∞」
1322
1369
  # - 标题不带编号(center/underline/plain)→ 导读只用中性圆点,不要凭空造出 01/02
1323
- numbered = "number" in theme.get("h2_style", "number")
1370
+ numbered = "number" in theme.get("h2_style", "number") and not keep_hand_num(theme)
1324
1371
  last_summary = bool(titles) and bool(SUMMARY_H2_RE.match(titles[-1]))
1325
1372
 
1326
1373
  def label(k):
@@ -1696,6 +1743,9 @@ def main():
1696
1743
  ap.add_argument("--author", default="",
1697
1744
  help="签名区署名;留空则不输出署名行(不再写 {{作者名}} 占位)")
1698
1745
  ap.add_argument("--author-bio", default="", help="签名区一句话简介")
1746
+ ap.add_argument("--auto-number", action="store_true",
1747
+ help="二级标题一律剥离手写序号、由排版层重新编号(旧行为);"
1748
+ "默认:稿里写了「一、」这类序号就原样保留")
1699
1749
  args = ap.parse_args()
1700
1750
 
1701
1751
  if args.list_themes:
@@ -1704,10 +1754,17 @@ def main():
1704
1754
  "、".join(t["genre"]) or "(未声明题材)"))
1705
1755
  return
1706
1756
 
1707
- missing = [n for n, v in (("--input", args.input), ("--out-dir", args.out_dir),
1708
- ("--slug", args.slug)) if not v]
1709
- if missing:
1710
- sys.exit("缺少必填参数:%s" % "、".join(missing))
1757
+ if not args.input:
1758
+ sys.exit("缺少必填参数:--input")
1759
+ # 文章文件夹结构(<文章名>/<文章名>.md):--out-dir 缺省 <文章文件夹>/out,
1760
+ # --slug 缺省为文件名,--asset-root 缺省为文章文件夹
1761
+ in_dir = os.path.dirname(os.path.abspath(args.input))
1762
+ if not args.out_dir:
1763
+ args.out_dir = os.path.join(in_dir, "out")
1764
+ if not args.slug:
1765
+ args.slug = os.path.splitext(os.path.basename(args.input))[0]
1766
+ if not args.asset_root:
1767
+ args.asset_root = in_dir
1711
1768
 
1712
1769
  if os.path.isdir(args.input):
1713
1770
  sys.exit("错误:输入是目录不是文件 %s" % args.input)
@@ -1733,6 +1790,7 @@ def main():
1733
1790
  not args.no_pangu,
1734
1791
  toc=not args.no_toc, sign_mode=args.signature,
1735
1792
  author=args.author, author_bio=args.author_bio,
1793
+ auto_number=args.auto_number,
1736
1794
  )
1737
1795
 
1738
1796
  title = args.title
@@ -2,10 +2,16 @@
2
2
  """md2x-html.py — Markdown 原稿 → 可直接复制的 X 长文 HTML(单文件)。
3
3
 
4
4
  用法:
5
- python3 md2x-html.py 原稿.md --out X长文.html \\
6
- --images 样张1.png 样张2.png ... [--cover cover-1500x600.png]
5
+ python3 md2x-html.py <文章名>/<文章名>.md
6
+ # 文章文件夹结构:直接读源稿里的 ![说明](images/xx.png),按文档顺序嵌图;
7
+ # --out 缺省 <文章名>/out/X长文.html,--cover 缺省读 frontmatter 的 cover
8
+ python3 md2x-html.py X长文.md --out X长文.html --images-from 公众号稿.md # 旧结构
9
+ python3 md2x-html.py 原稿.md --out X长文.html --images 1.png 2.png [--cover c.png]
7
10
 
8
11
  约定(与 md2x.py 对齐):
12
+ - 源稿没有 `[图N]`、只有独占一行的 `![说明](本地路径)` 时,按文档顺序直接取图(路径相对 md 所在目录),
13
+ 内部换成 `[图N]`,不需要再派生 X长文.md;远程图片无法内嵌,跳过并告警
14
+ - 图片后紧跟(或隔一个空行)的「来源:……」行渲染成小号灰字图注,复制时作为一段文字保留
9
15
  - `[图N]` 独占一行 → 按顺序取 --images[N-1],转成内嵌 data-URI 的 <figure>;
10
16
  点击图片弹出 lightbox 放大查看原图(右键可另存/截图,不拦截右键菜单)
11
17
  - ``` 围栏代码块(直出提示词)→ <blockquote> + 「复制提示词」按钮。
@@ -78,6 +84,7 @@ body { margin: 0; background: #f7f9fa; color: #0f1419;
78
84
  figure.ximg { margin: 22px 0; }
79
85
  figure.ximg img { width: 100%; border-radius: 10px; display: block;
80
86
  border: 1px solid #eff3f4; cursor: zoom-in; }
87
+ .article p.src { font-size: 13px; line-height: 1.6; color: #536471; margin: -12px 0 18px; }
81
88
  figure.ximg figcaption { display: flex; justify-content: space-between; align-items: center;
82
89
  margin-top: 8px; font-size: 13px; color: #536471; }
83
90
  .prompt { position: relative; margin: 14px 0 20px; }
@@ -341,6 +348,51 @@ def inline_md(s):
341
348
  return s
342
349
 
343
350
 
351
+ IMG_LINE_RE = re.compile(r"^\s*!\[([^\]]*)\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)\s*$")
352
+
353
+
354
+ def inline_images(md_text, base):
355
+ """把独占一行的 ![说明](本地路径) 换成 [图N],返回 (新文本, 图片绝对路径列表)。
356
+
357
+ 围栏代码块里的行不动;远程图片无法内嵌,换成一行「(远程图片:url)」并告警。"""
358
+ out, images, fence = [], [], None
359
+ for line in md_text.split("\n"):
360
+ st = line.strip()
361
+ fm = re.match(r"^(`{3,})", st)
362
+ if fm:
363
+ if fence is None:
364
+ fence = fm.group(1)
365
+ elif len(fm.group(1)) >= len(fence) and st.strip("`") == "":
366
+ fence = None
367
+ out.append(line)
368
+ continue
369
+ m = IMG_LINE_RE.match(line) if fence is None else None
370
+ if not m:
371
+ out.append(line)
372
+ continue
373
+ src = m.group(2)
374
+ if re.match(r"https?://", src):
375
+ print("警告:跳过远程图片(无法内嵌):%s" % src, file=sys.stderr)
376
+ out.append("(远程图片:%s)" % src)
377
+ continue
378
+ images.append(src if os.path.isabs(src) else os.path.normpath(os.path.join(base, src)))
379
+ out.append("[图%d]" % len(images))
380
+ return "\n".join(out), images
381
+
382
+
383
+ def frontmatter_value(md_text, key):
384
+ lines = md_text.split("\n")
385
+ if not lines or lines[0].strip() != "---":
386
+ return None
387
+ for ln in lines[1:]:
388
+ if ln.strip() == "---":
389
+ break
390
+ m = re.match(r"^%s:\s*(.+?)\s*$" % re.escape(key), ln)
391
+ if m:
392
+ return m.group(1).strip().strip("\"'")
393
+ return None
394
+
395
+
344
396
  def md_to_html(md_text, images, cover):
345
397
  lines = md_text.split("\n")
346
398
  out = []
@@ -510,6 +562,11 @@ def md_to_html(md_text, images, cover):
510
562
  continue
511
563
 
512
564
  close_lists()
565
+ # 图片后的「来源:」行 → 小号灰字图注(上一个输出块是图片时)
566
+ if re.match(r"^来源[::]", s) and out and out[-1].startswith('<figure class="ximg"'):
567
+ out.append('<p class="src">%s</p>' % inline_md(s))
568
+ i += 1
569
+ continue
513
570
  # 保留行首缩进(X 稿里用 emoji 项目符号表示二级条目时需要缩进):
514
571
  # 行首 ≥2 个空格 / 全角空格转成 &nbsp;,避免 HTML 把缩进吞掉
515
572
  lead = len(line) - len(line.lstrip(" \u3000"))
@@ -541,14 +598,15 @@ def md_to_html(md_text, images, cover):
541
598
 
542
599
  def main():
543
600
  ap = argparse.ArgumentParser(description="Markdown 原稿 → X 长文 HTML(一键复制)")
544
- ap.add_argument("md", help="原稿 markdown 路径")
545
- ap.add_argument("--out", required=True, help="输出 HTML 路径")
601
+ ap.add_argument("md", help="原稿 markdown 路径(文章文件夹结构下就是 <文章名>.md)")
602
+ ap.add_argument("--out", default=None, help="输出 HTML 路径(缺省 <md 所在目录>/out/X长文.html)")
546
603
  ap.add_argument("--images", nargs="*", default=[], help="[图N] 对应的图片路径(按顺序)")
547
604
  ap.add_argument("--images-from", default=None, metavar="WECHAT_MD",
548
- help="从指定的 markdown(如公众号稿.md)中按文档顺序提取 ![]() 本地图片路径,"
549
- "作为 [图N] 的图片来源。X 稿必须从公众号稿派生时用此项,"
605
+ help="从指定的 markdown(如旧结构的公众号稿.md)中按文档顺序提取 ![]() 本地图片路径,"
606
+ "作为 [图N] 的图片来源。缺省:源稿自己有 ![]() 就直接读它;"
550
607
  "禁止手工拼 --images 顺序(曾因此出现配图错位)")
551
- ap.add_argument("--cover", default=None, help="封面图路径(1500×600,5:2)")
608
+ ap.add_argument("--cover", default=None,
609
+ help="封面图路径(1500×600,5:2);缺省读 frontmatter 的 cover(相对 md 所在目录)")
552
610
  args = ap.parse_args()
553
611
 
554
612
  if not os.path.isfile(args.md):
@@ -556,6 +614,29 @@ def main():
556
614
  sys.exit(1)
557
615
  with open(args.md, encoding="utf-8") as f:
558
616
  md_text = f.read()
617
+ md_dir = os.path.dirname(os.path.abspath(args.md))
618
+ if not args.out:
619
+ args.out = os.path.join(md_dir, "out", "X长文.html")
620
+ if not args.cover:
621
+ fc = frontmatter_value(md_text, "cover")
622
+ if fc:
623
+ cp = fc if os.path.isabs(fc) else os.path.normpath(os.path.join(md_dir, fc))
624
+ if os.path.isfile(cp):
625
+ args.cover = cp
626
+ else:
627
+ print("警告:frontmatter 的 cover 文件不存在,跳过封面:%s" % fc, file=sys.stderr)
628
+
629
+ has_markers = any(re.fullmatch(r"\[图\d+\]", ln.strip()) for ln in md_text.split("\n"))
630
+ if not has_markers and not args.images and not args.images_from:
631
+ # 新结构:源稿只有 ![](),直接按文档顺序取图
632
+ md_text, derived = inline_images(md_text, md_dir)
633
+ if derived:
634
+ print("从源稿直接取图:%d 张" % len(derived), file=sys.stderr)
635
+ args.images = derived
636
+ elif has_markers and not args.images and not args.images_from:
637
+ print("源稿里有 [图N] 但没给图片来源:加 --images-from <带 ![]() 的稿子>,"
638
+ "或把 [图N] 换回 ![说明](images/xx.png)", file=sys.stderr)
639
+ sys.exit(1)
559
640
 
560
641
  # 图片来源:优先 --images-from(从公众号稿等源 md 按文档顺序派生),杜绝手工排序错位
561
642
  images = list(args.images)