wechat-media-writer 2.2.3 → 2.2.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/SKILL.md CHANGED
@@ -137,9 +137,9 @@ Token 缓存在 `~/.wechat/token_cache.json`(自动管理,2小时有效期
137
137
  <span>副标题(一句话点睛)</span>
138
138
  </p>
139
139
 
140
- <!-- 书籍封面图(第一张图,使用书籍原封面) -->
140
+ <!-- 主题配图(第一张图,使用主题贴图,非书籍原图) -->
141
141
  <p style="margin:0 0 20px;text-align:center;">
142
- <img src="{BOOK_COVER_URL}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="《书名》封面" />
142
+ <img src="{THEME_IMG_1}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图" />
143
143
  </p>
144
144
 
145
145
  <!-- 章节(无数字编号,直接标题) -->
@@ -150,11 +150,26 @@ Token 缓存在 `~/.wechat/token_cache.json`(自动管理,2小时有效期
150
150
  <span>正文段落。所有段落必须有 text-indent:2em。</span>
151
151
  </p>
152
152
 
153
- <!-- 引用块 -->
153
+ <!-- 主题配图(章节间穿插,与文风统一) -->
154
+ <p style="margin:0 0 20px;text-align:center;">
155
+ <img src="{THEME_IMG_2}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图" />
156
+ </p>
157
+
158
+ <!-- 金句引用块(用主题色高亮,加左侧色条) -->
154
159
  <p
155
- style="font-size:17px;line-height:1.9;color:rgb(T_R,T_G,T_B);font-weight:bold;text-indent:2em;margin:0 0 20px;padding:14px 18px;background:rgb(CBG);border-radius:8px;"
160
+ style="font-size:17px;line-height:1.9;color:{T};font-weight:bold;text-indent:2em;margin:0 0 20px;padding:14px 18px;background:{CBG};border-left:4px solid {T2};border-radius:6px;"
156
161
  >
157
- <span>金句或核心观点引用。</span>
162
+ <span style="color:{T2};">▍</span> 金句或核心观点引用。整段用主题主色加粗,加左侧强调色条,与正文形成视觉对比。
163
+ </p>
164
+
165
+ <!-- 重点启发语句(行内主题色 + 强调色高亮) -->
166
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
167
+ <span>正文段落中关键句子用 <strong style="color:{T};background:{CBG};padding:1px 6px;border-radius:3px;">主题主色高亮</strong>,核心概念用 <span style="color:{T2};font-weight:bold;">强调色加粗</span>,让读者一眼锁定重点。</span>
168
+ </p>
169
+
170
+ <!-- 主题配图(章节中段) -->
171
+ <p style="margin:0 0 20px;text-align:center;">
172
+ <img src="{THEME_IMG_3}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图" />
158
173
  </p>
159
174
 
160
175
  <!-- 分隔符(章节之间) -->
@@ -162,54 +177,52 @@ Token 缓存在 `~/.wechat/token_cache.json`(自动管理,2小时有效期
162
177
  </section>
163
178
  ```
164
179
 
165
- ### 关键排版规则
180
+ ### 关键排版规则
166
181
 
167
182
  1. **所有正文段落必须有 `text-indent:2em`** — 首行缩进
168
183
  2. **图片URL必须用完整的 `mmbiz.qpic.cn` 硬编码URL** — 不用模板变量
169
184
  3. **`<img>` 必须包裹在 `<p>` 中**,且 `style="width:100%;display:block;border-radius:4px;"`
170
185
  4. **行高统一 `line-height:2`** — 阅读舒适
171
- 5. **引用块用 `<p>` + `background` + `border-radius:8px`** — 不是 `<blockquote>`
186
+ 5. **引用块用 `<p>` + `background` + `border-radius:8px` + 左侧 `border-left:4px solid {T2}` 色条** — 不是 `<blockquote>`
172
187
  6. **章节标题不使用数字编号** — 直接用"作者背景与创作时代",不用"壹"
173
188
  7. **强调文字用 `<strong style="color:{T};">`** — 不是加粗或改色
174
- 8. **星标用 `<span>★★★★☆</span>`** `font-size:28px;color:{T2}`
175
- 9. **每篇文章只在一个 `<section>` 内** 不嵌套多个 section
176
- 10. **第一张图必须是书籍封面**使用书籍原封面图,居中显示,max-width:600px
189
+ 8. **重点语句/概念高亮**:用 `<strong style="color:{T};background:{CBG};padding:1px 6px;border-radius:3px;">` 或 `<span style="color:{T2};font-weight:bold;">` — 让主题色贯穿全文
190
+ 9. **星标用 `<span>★★★★☆</span>`**`font-size:28px;color:{T2}`
191
+ 10. **每篇文章只在一个 `<section>` 内** 不嵌套多个 section
192
+ 11. **全文插图统一为主题贴图** — 不再使用书籍原封面(书籍原图往往与文章主题风格不搭),全部用主题贴图
193
+ 12. **图片在章节间穿插** — 每 1-2 段正文插一张图,避免长段无图的视觉疲劳
177
194
 
178
195
  ## 图片工作流
179
196
 
180
- ### 第一步:获取书籍封面图
197
+ ### 第一步:准备主题贴图(不再使用书籍原图作为正文配图)
181
198
 
182
- **书评文章的第一张图必须是书籍原封面**
199
+ **书评文章的所有插图统一为主题贴图**(Pexels 等无版权图库),与文章主题、章节氛围贴合。
200
+ 不推荐使用书籍原图作为正文封面——书籍封面图风格往往与内容图不一致,且某些封面图清晰度/分辨率不适合正文展示。
183
201
 
184
- 使用 `scripts/book_cover.py` 模块自动获取:
202
+ 使用 `scripts/image_downloader.py` 下载主题贴图:
185
203
 
186
204
  ```python
187
- from book_cover import fetch_book_cover, download_and_process_cover
188
-
189
- # 自动获取封面URL(Google Books -> Open Library -> 豆瓣)
190
- cover_url = fetch_book_cover("书名", "作者", "ISBN")
205
+ from image_downloader import download_theme_images
191
206
 
192
- # 下载并处理(生成600px文章封面 + 900x500公众号封面)
193
- if cover_url:
194
- cover_path, cover_900_path = download_and_process_cover(cover_url, "/tmp/output")
207
+ # 根据主题下载 6-8 张贴图(用于正文穿插)
208
+ urls = download_theme_images("books", "/tmp/images", count=6)
195
209
  ```
196
210
 
197
- **封面尺寸要求**:
198
- - 文章内封面:宽度 600px,高度按比例缩放
199
- - 公众号封面:900x500px(用于文章列表展示)
211
+ ### 第二步:下载公众号封面图(精确 900x500)
200
212
 
201
- ### 第二步:下载主题插图
213
+ **公众号封面必须是 900x500 精确尺寸的精美图片**,与正文主题风格统一。
214
+ 不再从主题贴图裁剪(裁剪可能损失画面关键内容)——直接从 Pexels CDN 请求 900x500 精确裁切。
202
215
 
203
- 使用 `scripts/image_downloader.py` 下载:
216
+ 每个主题预存 3 张高质量 Pexels 候选图(见 `scripts/book_cover.py` 的 `THEME_COVER_CANDIDATES`),
217
+ 通过 `?w=900&h=500&fit=crop` 参数让 Pexels 服务端精确裁切为 900x500:
204
218
 
205
219
  ```python
206
- from image_downloader import download_theme_images
207
-
208
- # 根据主题下载5张插图
209
- urls = download_theme_images("nature", "/tmp/images", count=5)
220
+ from book_cover import download_cover_900x500
221
+ # 下载一张 books 主题的 900x500 封面(seed=0/1/2 轮换候选图)
222
+ download_cover_900x500("books", "/tmp/wx", seed=0)
210
223
  ```
211
224
 
212
- 每篇文章需要 5-6 张内容图 + 1张书籍封面图 + 1张封面图(900x500用于公众号封面)。
225
+ **支持的主题**:`abstract` / `books` / `nature` / `technology` / `business`
213
226
 
214
227
  ### 第三步:上传图片到微信
215
228
 
@@ -230,6 +243,8 @@ img_url = upload_content_image(token, "image.jpg")
230
243
  将上传后的 media_id 和 mmbiz URL **硬编码到发布脚本中**,不用模板变量。
231
244
  这是因为微信图片URL有时效性,硬编码已验证的URL最可靠。
232
245
 
246
+ 每篇文章需要:1张公众号封面 + 6-8张正文主题贴图。
247
+
233
248
  ## 微信 API 集成
234
249
 
235
250
  ### API 列表
@@ -354,15 +369,32 @@ python3 scripts/publish.py --title "标题" --content-file content.html --cover-
354
369
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 8px;text-indent:2em;">
355
370
  <strong style="color:{T};">适合人群:</strong>
356
371
  </p>
357
- <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;padding-left:2em;">
358
- <span>✓ 创业者/管理者——具体理由<br />✓ 陷入职业倦怠期的人——具体理由<br />✓ 对某领域感兴趣的读者——具体理由</span>
372
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
373
+ <span>✅ 创业者/管理者——具体理由<br />✅ 陷入职业倦怠期的人——具体理由<br />✅ 对某领域感兴趣的读者——具体理由</span>
359
374
  </p>
360
375
 
361
376
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 8px;text-indent:2em;">
362
377
  <strong style="color:{T};">不适合人群:</strong>
363
378
  </p>
364
- <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;padding-left:2em;">
365
- <span>✗ 已读过类似书籍的人——内容高度重复<br />✗ 讨厌某种风格的人——具体理由<br />✗ 寻找其他类型内容的人——具体理由</span>
379
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
380
+ <span>🚫 已读过类似书籍的人——内容高度重复<br />🚫 讨厌某种风格的人——具体理由<br />🚫 寻找其他类型内容的人——具体理由</span>
381
+ </p>
382
+
383
+ <!-- 结尾页(与正文风格延续,使用主题色) -->
384
+ <p style="margin:0 0 16px;text-align:center;">
385
+ <img src="{THEME_IMG_END}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图" />
386
+ </p>
387
+
388
+ <p style="font-size:14px;color:#ccc;text-align:center;letter-spacing:8px;margin:24px 0 16px;">· · · · · ·</p>
389
+
390
+ <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 8px;line-height:1.6;">
391
+ <span>📖 《书名》</span>
392
+ </p>
393
+ <p style="font-size:14px;color:{BODY};text-align:center;margin:0 0 4px;line-height:1.6;">
394
+ <span>作者 · 读书笔记</span>
395
+ </p>
396
+ <p style="font-size:14px;color:{T2};text-align:center;margin:0 0 4px;line-height:1.8;font-weight:bold;">
397
+ <span>「 一句话总结:核心洞察 」</span>
366
398
  </p>
367
399
  ```
368
400
 
@@ -371,6 +403,8 @@ python3 scripts/publish.py --title "标题" --content-file content.html --cover-
371
403
  1. **星级显示**:`font-size:28px;color:{T2}`,居中
372
404
  2. **分数显示**:`4 / 5 星`,在星标下方
373
405
  3. **评分理由**:1-2句话解释为什么给这个分数
374
- 4. **适合人群**:用 标记,每条用——连接理由
375
- 5. **不适合人群**:用 标记,每条用——连接理由
376
- 6. **不要直接放链接**——所有内容用文案表达
406
+ 4. **适合人群**:用 `✅ ` 标记,每条用——连接理由
407
+ 5. **不适合人群**:用 `🚫 ` 标记,每条用——连接理由
408
+ 6. **人群列表与首行对齐**:用 `text-indent:2em`(不用 `padding-left`),保证列表项与"适合人群:"首行视觉对齐
409
+ 7. **结尾页**:用主题主色显示书名+作者,强调色显示一句话总结,配一张主题贴图作收尾
410
+ 8. **不要直接放链接**——所有内容用文案表达
package/bin/cli.js CHANGED
@@ -43,9 +43,9 @@ function showHelp() {
43
43
  npx wechat-media-writer <command> [options]
44
44
 
45
45
  命令:
46
- cover <书名/电影名> [作者] [ISBN] 获取书籍封面
47
- download <主题> [数量] 下载主题插图
48
- publish 发布文章到微信(封面/插图缺失会自动补齐)
46
+ cover <书名/电影名> [作者] [ISBN] 获取书籍原封面(仅用于书籍插图占位)
47
+ download <主题> [数量] [输出目录] 下载主题贴图
48
+ publish 发布文章到微信(主题贴图/封面缺失会自动补齐)
49
49
  help 显示帮助信息
50
50
 
51
51
  publish 选项:
@@ -55,20 +55,14 @@ publish 选项:
55
55
  --author <作者> 文章作者,默认"读书笔记"
56
56
  --digest <摘要> 文章摘要
57
57
  --theme <主题> 图片主题:abstract/books/nature/technology/business,默认 abstract
58
- --image-count <数量> 主题插图数量,默认 5
59
- --book-title <书名> 书籍标题(封面缺失时自动获取)
60
- --book-author <作者> 书籍作者
61
- --book-isbn <ISBN> 书籍ISBN
58
+ --image-count <数量> 主题贴图数量,默认 6(5正文 + 1结尾)
62
59
 
63
60
  示例:
64
- npx wechat-media-writer cover "人类简史" "尤瓦尔·赫拉利"
65
- npx wechat-media-writer download books 5
66
- npx wechat-media-writer publish \\
67
- --title "《人类简史》全书深度拆解" \\
68
- --content-file content.html \\
69
- --cover-dir /tmp/wx \\
70
- --book-title "人类简史" \\
71
- --book-author "尤瓦尔·赫拉利" \\
61
+ npx wechat-media-writer download books 6
62
+ npx wechat-media-writer publish ^
63
+ --title "《人类简史》全书深度拆解" ^
64
+ --content-file content.html ^
65
+ --cover-dir /tmp/wx ^
72
66
  --theme books
73
67
 
74
68
  配置:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wechat-media-writer",
3
- "version": "2.2.3",
3
+ "version": "2.2.5",
4
4
  "description": "微信公众号书评、影评文章自动生成与发布 - Skill",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -5,14 +5,13 @@
5
5
 
6
6
  用法:
7
7
  1. 填写下方的变量(标题、摘要、HTML内容等)
8
- 2. 确保封面图已准备好(使用 scripts/book_cover.py 获取)
8
+ 2. 确保封面图已准备好(使用 scripts/image_downloader.py 下载主题贴图)
9
9
  3. 运行: python3 references/publish_template.py
10
10
  """
11
11
 
12
12
  import os
13
13
  import sys
14
14
 
15
- # 添加 scripts 目录到模块搜索路径
16
15
  scripts_dir = os.path.join(
17
16
  os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts"
18
17
  )
@@ -23,12 +22,11 @@ from wechat_api import load_config, get_access_token, upload_cover, create_draft
23
22
 
24
23
  # ========== 替换以下内容 ==========
25
24
 
26
- # 标题和摘要
27
25
  TITLE = "《书名》全书深度拆解:犀利洞察"
28
26
  AUTHOR = "读书笔记"
29
27
  DIGEST = "摘要文本,不超过120字。"
30
28
 
31
- # 封面图目录(包含 cover_900x500.jpg
29
+ # 封面图目录(包含 cover_900x500.jpg + img_*.jpg 主题贴图)
32
30
  COVER_DIR = "/tmp/book_output"
33
31
 
34
32
  # 主题色:主色(深色)+ 强调色(暖/亮色)
@@ -37,7 +35,16 @@ T2 = "#D4A24C" # 强调色
37
35
  BODY = "#555" # 正文色
38
36
  CBG = "#F0EDE5" # 引用块背景
39
37
 
40
- # 文章 HTML 内容(7段式,无数字编号)
38
+ # 图片 URL(替换为上传微信后返回的 mmbiz.qpic.cn URL)
39
+ # 通过 publish.py 自动上传后,从 upload_result.json 中获取
40
+ THEME_IMG_1 = "http://mmbiz.qpic.cn/REPLACE_WITH_THEME_IMG_1"
41
+ THEME_IMG_2 = "http://mmbiz.qpic.cn/REPLACE_WITH_THEME_IMG_2"
42
+ THEME_IMG_3 = "http://mmbiz.qpic.cn/REPLACE_WITH_THEME_IMG_3"
43
+ THEME_IMG_4 = "http://mmbiz.qpic.cn/REPLACE_WITH_THEME_IMG_4"
44
+ THEME_IMG_5 = "http://mmbiz.qpic.cn/REPLACE_WITH_THEME_IMG_5"
45
+ THEME_IMG_END = "http://mmbiz.qpic.cn/REPLACE_WITH_THEME_IMG_END"
46
+
47
+ # 文章 HTML 内容(7段式,无数字编号,全文主题贴图穿插)
41
48
  HTML = f"""<section style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;color:#333;background:#FBF8FC;">
42
49
 
43
50
  <p style="font-size:12px;color:{T2};text-align:center;letter-spacing:4px;margin:24px 0 8px;line-height:3em;">
@@ -52,25 +59,94 @@ HTML = f"""<section style="margin:0;padding:0;font-family:-apple-system,BlinkMac
52
59
  <span>副标题(一句话点睛)</span>
53
60
  </p>
54
61
 
55
- <!-- 书籍封面图(第一张图) -->
62
+ <!-- 主题配图1(头部) -->
56
63
  <p style="margin:0 0 20px;text-align:center;">
57
- <img src="http://mmbiz.qpic.cn/REPLACE_WITH_BOOK_COVER_URL" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="《书名》封面"/>
64
+ <img src="{THEME_IMG_1}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图"/>
58
65
  </p>
59
66
 
60
- <!-- 作者背景与创作时代(无数字编号) -->
67
+ <!-- 作者背景与创作时代 -->
61
68
  <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
62
69
  <span>作者背景与创作时代</span>
63
70
  </p>
71
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
72
+ <span>正文段落。所有段落必须有 text-indent:2em。重点句用 <strong style="color:{T};background:{CBG};padding:1px 6px;border-radius:3px;">主题主色高亮</strong>,核心概念用 <span style="color:{T2};font-weight:bold;">强调色加粗</span>。</span>
73
+ </p>
74
+
75
+ <!-- 主题配图2(作者背景后) -->
76
+ <p style="margin:0 0 20px;text-align:center;">
77
+ <img src="{THEME_IMG_2}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图"/>
78
+ </p>
79
+
80
+ <!-- 金句引用块(左侧色条 + 主题色加粗) -->
81
+ <p style="font-size:17px;line-height:1.9;color:{T};font-weight:bold;text-indent:2em;margin:0 0 20px;padding:14px 18px;background:{CBG};border-left:4px solid {T2};border-radius:6px;">
82
+ <span style="color:{T2};">▍</span> 金句或核心观点。整段用主题主色加粗,加左侧强调色条与正文形成视觉对比。
83
+ </p>
84
+
85
+ <!-- 全书完整逻辑思维导图 -->
86
+ <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
87
+ <span>全书完整逻辑思维导图</span>
88
+ </p>
89
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
90
+ <span>本章按章节脉络梳理<strong style="color:{T};background:{CBG};padding:1px 6px;border-radius:3px;">全书核心逻辑</strong>,用文字框图提炼主线。</span>
91
+ </p>
92
+
93
+ <!-- 主题配图3(逻辑导图后) -->
94
+ <p style="margin:0 0 20px;text-align:center;">
95
+ <img src="{THEME_IMG_3}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图"/>
96
+ </p>
97
+
98
+ <!-- 三大颠覆性核心论点 -->
99
+ <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
100
+ <span>三大颠覆性核心论点</span>
101
+ </p>
102
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
103
+ <span><span style="color:{T2};font-weight:bold;">论点一:</span><strong style="color:{T};background:{CBG};padding:1px 6px;border-radius:3px;">核心颠覆性观点</strong>,附书中具体案例支撑。</span>
104
+ </p>
105
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
106
+ <span><span style="color:{T2};font-weight:bold;">论点二:</span><strong style="color:{T};background:{CBG};padding:1px 6px;border-radius:3px;">核心颠覆性观点</strong>,附书中具体案例支撑。</span>
107
+ </p>
108
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
109
+ <span><span style="color:{T2};font-weight:bold;">论点三:</span><strong style="color:{T};background:{CBG};padding:1px 6px;border-radius:3px;">核心颠覆性观点</strong>,附书中具体案例支撑。</span>
110
+ </p>
111
+
112
+ <!-- 主题配图4(论点后) -->
113
+ <p style="margin:0 0 20px;text-align:center;">
114
+ <img src="{THEME_IMG_4}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图"/>
115
+ </p>
64
116
 
117
+ <!-- 批判性客观评析 -->
118
+ <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
119
+ <span>批判性客观评析</span>
120
+ </p>
65
121
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
66
- <span>正文段落。注意:所有段落必须有 text-indent:2em。</span>
122
+ <span>短板一:客观指出书中局限。<br/>短板二:逻辑漏洞分析。<br/>短板三:适用边界。<br/>短板四:时代局限。</span>
67
123
  </p>
68
124
 
69
- <p style="font-size:17px;line-height:1.9;color:{T};font-weight:bold;text-indent:2em;margin:0 0 20px;padding:14px 18px;background:{CBG};border-radius:8px;">
70
- <span>引用块:金句或核心观点。</span>
125
+ <!-- 金句二 -->
126
+ <p style="font-size:17px;line-height:1.9;color:{T};font-weight:bold;text-indent:2em;margin:0 0 20px;padding:14px 18px;background:{CBG};border-left:4px solid {T2};border-radius:6px;">
127
+ <span style="color:{T2};">▍</span> 第二个金句或关键启发。
71
128
  </p>
72
129
 
73
- <!-- 其他章节 -->
130
+ <!-- 收获与成长 -->
131
+ <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
132
+ <span>收获与成长</span>
133
+ </p>
134
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
135
+ <span>读完能带走的<strong style="color:{T};background:{CBG};padding:1px 6px;border-radius:3px;">三样东西</strong>,从个人成长视角展开。</span>
136
+ </p>
137
+
138
+ <!-- 主题配图5(收获与成长后) -->
139
+ <p style="margin:0 0 20px;text-align:center;">
140
+ <img src="{THEME_IMG_5}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图"/>
141
+ </p>
142
+
143
+ <!-- 现实落地应用方案 -->
144
+ <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
145
+ <span>现实落地应用方案</span>
146
+ </p>
147
+ <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
148
+ <span>方案一:具体执行步骤。<br/>方案二:可操作建议。<br/>方案三:拿来就用的行动项。</span>
149
+ </p>
74
150
 
75
151
  <p style="font-size:14px;color:#ccc;text-align:center;letter-spacing:8px;margin:20px 0;">· · · · · ·</p>
76
152
 
@@ -88,30 +164,38 @@ HTML = f"""<section style="margin:0;padding:0;font-family:-apple-system,BlinkMac
88
164
  </p>
89
165
 
90
166
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
91
- <span>为什么给X星?简要说明评分理由。</span>
167
+ <span>为什么给X星?简要说明评分理由,诚实评价,3-4星是常态,5星极少。</span>
92
168
  </p>
93
169
 
94
170
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 8px;text-indent:2em;">
95
171
  <strong style="color:{T};">适合人群:</strong>
96
172
  </p>
97
173
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
98
- <span>✓ 人群1——具体理由<br/>✓ 人群2——具体理由<br/>✓ 人群3——具体理由</span>
174
+ <span>✅&nbsp;&nbsp;创业者/管理者——理解系统脆弱性和反脆弱设计<br />✅&nbsp;&nbsp;对个人成长感兴趣的读者——掌握从压力中受益的方法<br />✅&nbsp;&nbsp;对决策科学感兴趣的读者——理解非线性风险的本质</span>
99
175
  </p>
100
176
 
101
177
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 8px;text-indent:2em;">
102
178
  <strong style="color:{T};">不适合人群:</strong>
103
179
  </p>
104
180
  <p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
105
- <span>✗ 人群1——具体理由<br/>✗ 人群2——具体理由<br/>✗ 人群3——具体理由</span>
181
+ <span>🚫&nbsp;&nbsp;已读过类似书籍的人——内容高度重复<br />🚫&nbsp;&nbsp;讨厌抽象论证的人——本书充满概念性思维<br />🚫&nbsp;&nbsp;寻找具体操作手册的人——本书偏哲学思辨</span>
106
182
  </p>
107
183
 
108
- <p style="font-size:14px;color:#ccc;text-align:center;letter-spacing:8px;margin:20px 0;">· · · · · ·</p>
184
+ <!-- 结尾页:主题配图 + 书名 + 作者 + 核心总结 -->
185
+ <p style="margin:0 0 16px;text-align:center;">
186
+ <img src="{THEME_IMG_END}" style="width:100%;max-width:600px;display:block;margin:0 auto;border-radius:4px;" alt="主题配图"/>
187
+ </p>
109
188
 
110
- <p style="font-size:16px;line-height:2;color:{T};text-align:center;margin:0 0 4px;font-weight:bold;text-indent:2em;">
111
- <span>📖 《书名》作者 · 读书笔记</span>
189
+ <p style="font-size:14px;color:#ccc;text-align:center;letter-spacing:8px;margin:24px 0 16px;">· · · · · ·</p>
190
+
191
+ <p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 8px;line-height:1.6;">
192
+ <span>📖 《书名》</span>
193
+ </p>
194
+ <p style="font-size:14px;color:{BODY};text-align:center;margin:0 0 4px;line-height:1.6;">
195
+ <span>作者 · 读书笔记</span>
112
196
  </p>
113
- <p style="font-size:14px;color:#999;text-align:center;margin:0 0 4px;text-indent:2em;">
114
- <span>一句话总结</span>
197
+ <p style="font-size:14px;color:{T2};text-align:center;margin:0 0 24px;line-height:1.8;font-weight:bold;">
198
+ <span>「 一句话总结:核心洞察 」</span>
115
199
  </p>
116
200
 
117
201
  </section>"""
@@ -122,22 +206,19 @@ def main():
122
206
  print("微信公众号 - 书名 发布")
123
207
  print("=" * 50)
124
208
 
125
- # 1. 加载配置和获取token
126
209
  config = load_config()
127
210
  token = get_access_token(config)
128
211
  print("\n✓ Token 已获取\n")
129
212
 
130
- # 2. 上传封面图
131
213
  cover_900_path = os.path.join(COVER_DIR, "cover_900x500.jpg")
132
214
  if not os.path.exists(cover_900_path):
133
215
  print(f"错误: 封面图不存在 {cover_900_path}", file=sys.stderr)
134
- print("请先运行: python3 scripts/book_cover.py '书名' '作者'", file=sys.stderr)
216
+ print("请先运行主题贴图下载与封面裁剪流程", file=sys.stderr)
135
217
  sys.exit(1)
136
218
 
137
219
  print("上传封面图...")
138
220
  cover_mid = upload_cover(token, cover_900_path)
139
221
 
140
- # 3. 创建草稿
141
222
  print("\n创建草稿...")
142
223
  draft_id = create_draft(token, TITLE, AUTHOR, DIGEST, HTML, cover_mid)
143
224
 
@@ -97,7 +97,12 @@ def fetch_book_cover_douban(book_title, author=""):
97
97
 
98
98
 
99
99
  def fetch_book_cover(book_title, author="", isbn=""):
100
- """按优先级尝试获取书籍封面,最后回退到占位图"""
100
+ """按优先级尝试获取书籍封面(书籍原图),最后回退到占位图
101
+
102
+ 注意:此函数返回的是书籍原图,仅用于 article 内首图占位。
103
+ 公众号封面(cover_900x500.jpg)现在改用主题贴图,与正文风格统一。
104
+ 如需主题封面,请调用 fetch_theme_cover()。
105
+ """
101
106
  cover_url = None
102
107
 
103
108
  print(f"尝试从 Google Books 获取《{book_title}》封面...")
@@ -112,15 +117,72 @@ def fetch_book_cover(book_title, author="", isbn=""):
112
117
  cover_url = fetch_book_cover_douban(book_title, author)
113
118
 
114
119
  if not cover_url:
115
- print("⚠️ 三个数据源均未返回封面,使用占位图占位")
120
+ print("⚠️ 三个数据源均未返回书籍封面,回退到占位图")
116
121
  cover_url = "https://images.pexels.com/photos/159711/books-bookstore-book-reading-159711.jpeg?auto=compress&cs=tinysrgb&w=600"
117
122
 
118
- print(f"✓ 封面URL: {cover_url}")
123
+ print(f"✓ 书籍封面URL: {cover_url}")
119
124
  return cover_url
120
125
 
121
126
 
127
+ # 每个主题对应 3 张精选的 900x500 Pexels 封面图(fit=crop 自动精确裁切)
128
+ # 选用宽高比接近 1.8 的高质量原图,避免裁切损失重要内容
129
+ THEME_COVER_CANDIDATES = {
130
+ "abstract": [1108572, 1762851, 3109808],
131
+ "books": [256450, 590493, 762687],
132
+ "nature": [2387873, 2387874, 2387876],
133
+ "technology": [3568520, 1181244, 546819],
134
+ "business": [3184292, 210607, 3184296],
135
+ }
136
+
137
+
138
+ def _build_cover_url(pid, w=900, h=500):
139
+ """构造 Pexels CDN URL,含精确 900x500 fit=crop 裁切参数"""
140
+ return f"https://images.pexels.com/photos/{pid}/pexels-photo-{pid}.jpeg?auto=compress&cs=tinysrgb&w={w}&h={h}&fit=crop"
141
+
142
+
143
+ def fetch_theme_cover(theme="abstract", seed=0):
144
+ """从 Pexels CDN 获取一张与主题对应的 900x500 精确封面 URL(不裁剪原图)"""
145
+ candidates = THEME_COVER_CANDIDATES.get(theme, THEME_COVER_CANDIDATES["abstract"])
146
+ pid = candidates[seed % len(candidates)]
147
+ url = _build_cover_url(pid)
148
+ print(f"✓ 主题封面URL ({theme}, pid={pid}): {url}")
149
+ return url
150
+
151
+
152
+ def download_cover_900x500(theme="abstract", output_dir="/tmp", seed=0):
153
+ """下载一张 900x500 的主题封面图(Pexels 服务端精确裁剪,无需本地处理)
154
+
155
+ 返回: cover_900x500.jpg 绝对路径
156
+ """
157
+ ctx = ssl.create_default_context()
158
+ ctx.check_hostname = False
159
+ ctx.verify_mode = ssl.CERT_NONE
160
+
161
+ os.makedirs(output_dir, exist_ok=True)
162
+ cover_900_path = os.path.join(output_dir, "cover_900x500.jpg")
163
+
164
+ candidates = THEME_COVER_CANDIDATES.get(theme, THEME_COVER_CANDIDATES["abstract"])
165
+ # 顺序尝试每个候选 PID,跳过 404
166
+ for i, pid in enumerate(candidates):
167
+ idx = (seed + i) % len(candidates)
168
+ pid_try = candidates[idx]
169
+ url = _build_cover_url(pid_try)
170
+ try:
171
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
172
+ data = urllib.request.urlopen(req, context=ctx, timeout=15).read()
173
+ with open(cover_900_path, "wb") as f:
174
+ f.write(data)
175
+ print(f"✓ 公众号封面已下载 ({theme}, pid={pid_try}): {cover_900_path}")
176
+ return cover_900_path
177
+ except Exception as e:
178
+ print(f" 候选 pid={pid_try} 失败: {e}")
179
+ continue
180
+
181
+ raise RuntimeError(f"主题 {theme} 的所有候选封面均下载失败")
182
+
183
+
122
184
  def download_and_process_cover(cover_url, output_dir):
123
- """下载并处理封面图"""
185
+ """从主题贴图URL下载并处理为 600px 文章封面(兼容旧调用)"""
124
186
  from PIL import Image
125
187
 
126
188
  ctx = ssl.create_default_context()
@@ -131,7 +193,7 @@ def download_and_process_cover(cover_url, output_dir):
131
193
  data = urllib.request.urlopen(req, context=ctx, timeout=15).read()
132
194
 
133
195
  os.makedirs(output_dir, exist_ok=True)
134
- original_path = os.path.join(output_dir, "book_cover_original.jpg")
196
+ original_path = os.path.join(output_dir, "theme_cover_original.jpg")
135
197
  with open(original_path, "wb") as f:
136
198
  f.write(data)
137
199
 
@@ -139,32 +201,17 @@ def download_and_process_cover(cover_url, output_dir):
139
201
  w, h = img.size
140
202
  if w > 600:
141
203
  ratio = 600 / w
142
- new_w = 600
143
- new_h = int(h * ratio)
144
- img = img.resize((new_w, new_h), Image.LANCZOS)
204
+ img = img.resize((600, int(h * ratio)), Image.LANCZOS)
205
+ img.save(os.path.join(output_dir, "theme_cover_600.jpg"), "JPEG", quality=90)
145
206
 
146
- cover_path = os.path.join(output_dir, "book_cover.jpg")
147
- img.save(cover_path, "JPEG", quality=90)
207
+ print(f"✓ 主题封面(600px)已保存: theme_cover_600.jpg")
208
+ return os.path.join(output_dir, "theme_cover_600.jpg"), None
148
209
 
149
- w, h = img.size
150
- target_ratio = 900 / 500
151
- if w / h > target_ratio:
152
- new_w = int(h * target_ratio)
153
- left = (w - new_w) // 2
154
- img = img.crop((left, 0, left + new_w, h))
155
- else:
156
- new_h = int(w / target_ratio)
157
- top = (h - new_h) // 2
158
- img = img.crop((0, top, w, top + new_h))
159
- img = img.resize((900, 500), Image.LANCZOS)
160
-
161
- cover_900_path = os.path.join(output_dir, "cover_900x500.jpg")
162
- img.save(cover_900_path, "JPEG", quality=90)
163
210
 
164
- print(f"✓ 封面图已保存: {cover_path}")
165
- print(f" 公众号封面已保存: {cover_900_path}")
166
-
167
- return cover_path, cover_900_path
211
+ def crop_cover_from_local(source_path, output_dir):
212
+ """兼容旧 API:建议改用 download_cover_900x500()"""
213
+ print("⚠️ crop_cover_from_local 已弃用,请改用 download_cover_900x500(theme=...)")
214
+ return None
168
215
 
169
216
 
170
217
  if __name__ == "__main__":
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  微信公众号读书拆解发布脚本
4
- 完整流程:获取封面 -> 下载插图 -> 上传图片 -> 创建草稿
4
+ 完整流程:准备主题贴图 -> 裁剪公众号封面 -> 上传图片 -> 创建草稿
5
5
  """
6
6
 
7
7
  import argparse
@@ -12,7 +12,7 @@ import sys
12
12
  # 添加当前目录到模块搜索路径
13
13
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
14
14
 
15
- from book_cover import fetch_book_cover, download_and_process_cover
15
+ from book_cover import download_cover_900x500
16
16
  from image_downloader import download_theme_images
17
17
  from wechat_api import (
18
18
  load_config,
@@ -23,34 +23,12 @@ from wechat_api import (
23
23
  )
24
24
 
25
25
 
26
- def ensure_book_cover(cover_dir, book_title="", book_author="", book_isbn=""):
27
- """确保封面图存在,缺失则自动获取"""
28
- cover_900_path = os.path.join(cover_dir, "cover_900x500.jpg")
29
- book_cover_path = os.path.join(cover_dir, "book_cover.jpg")
30
-
31
- if os.path.exists(cover_900_path) and os.path.exists(book_cover_path):
32
- print("✓ 封面图已存在,跳过获取")
33
- return
34
-
35
- if not book_title:
36
- print(
37
- f"⚠️ 缺少封面图且未提供 --book-title,无法自动获取。\n"
38
- f" 期望路径: {cover_900_path}",
39
- file=sys.stderr,
40
- )
41
- sys.exit(1)
26
+ def ensure_theme_images(cover_dir, theme="abstract", count=6):
27
+ """确保主题插图存在,缺失则自动下载
42
28
 
43
- print(f"封面图缺失,自动获取《{book_title}》封面...")
29
+ 默认 6 张:用于正文穿插(5 张)+ 结尾页(1 张)= 6 张
30
+ """
44
31
  os.makedirs(cover_dir, exist_ok=True)
45
- cover_url = fetch_book_cover(book_title, book_author, book_isbn)
46
- if not cover_url:
47
- print("错误: 无法获取书籍封面URL", file=sys.stderr)
48
- sys.exit(1)
49
- download_and_process_cover(cover_url, cover_dir)
50
-
51
-
52
- def ensure_theme_images(cover_dir, theme="abstract", count=5):
53
- """确保主题插图存在,缺失则自动下载"""
54
32
  existing = [
55
33
  f
56
34
  for f in os.listdir(cover_dir)
@@ -64,6 +42,21 @@ def ensure_theme_images(cover_dir, theme="abstract", count=5):
64
42
  download_theme_images(theme, cover_dir, count)
65
43
 
66
44
 
45
+ def ensure_cover_image(cover_dir, theme="abstract", seed=0):
46
+ """确保公众号封面(900x500)存在,缺失则下载一张对应主题的精确尺寸封面
47
+
48
+ 注意:不再从主题插图裁剪(裁剪可能损失画面重要内容),
49
+ 而是从 Pexels 直接请求 900x500 精确尺寸的对应主题图片。
50
+ """
51
+ cover_900_path = os.path.join(cover_dir, "cover_900x500.jpg")
52
+ if os.path.exists(cover_900_path):
53
+ print("✓ 公众号封面已存在,跳过下载")
54
+ return
55
+
56
+ print(f"封面图缺失,下载一张 {theme} 主题的 900x500 封面...")
57
+ download_cover_900x500(theme, cover_dir, seed=seed)
58
+
59
+
67
60
  def publish_article(
68
61
  title,
69
62
  author,
@@ -71,41 +64,39 @@ def publish_article(
71
64
  content_html,
72
65
  cover_dir,
73
66
  theme="abstract",
74
- image_count=5,
75
- book_title="",
76
- book_author="",
77
- book_isbn="",
67
+ image_count=6,
68
+ cover_seed=0,
78
69
  ):
79
- """完整发布流程:获取封面 -> 下载插图 -> 上传图片 -> 创建草稿"""
70
+ """完整发布流程:准备主题贴图 -> 下载公众号封面 -> 上传图片 -> 创建草稿"""
80
71
  print("=" * 50)
81
72
  print("微信公众号 - 书评发布")
82
73
  print("=" * 50)
83
74
 
84
75
  os.makedirs(cover_dir, exist_ok=True)
85
76
 
86
- # 1. 确保封面图存在
87
- print("\n[1/4] 准备封面图...")
88
- ensure_book_cover(cover_dir, book_title, book_author, book_isbn)
89
-
90
- # 2. 确保主题插图存在
91
- print("\n[2/4] 准备主题插图...")
77
+ # 1. 准备主题贴图
78
+ print("\n[1/4] 准备主题贴图...")
92
79
  ensure_theme_images(cover_dir, theme, image_count)
93
80
 
94
- # 3. 加载配置和获取token
81
+ # 2. 准备公众号封面(900x500 精确尺寸)
82
+ print("\n[2/4] 准备公众号封面...")
83
+ ensure_cover_image(cover_dir, theme, seed=cover_seed)
84
+
85
+ # 3. 加载配置和获取 token
95
86
  config = load_config()
96
87
  token = get_access_token(config)
97
88
  print("\n✓ Token 已获取\n")
98
89
 
99
- # 4. 上传封面图
90
+ # 4. 上传公众号封面
100
91
  cover_900_path = os.path.join(cover_dir, "cover_900x500.jpg")
101
92
  if not os.path.exists(cover_900_path):
102
93
  print(f"错误: 封面图不存在 {cover_900_path}", file=sys.stderr)
103
94
  sys.exit(1)
104
95
 
105
- print("上传封面图...")
96
+ print("上传公众号封面...")
106
97
  cover_mid = upload_cover(token, cover_900_path)
107
98
 
108
- # 5. 上传内容图片
99
+ # 5. 上传内容图片(img_*.jpg)
109
100
  print("\n上传内容图片...")
110
101
  image_urls = {}
111
102
  for fname in sorted(os.listdir(cover_dir)):
@@ -116,14 +107,7 @@ def publish_article(
116
107
  name = os.path.splitext(fname)[0]
117
108
  image_urls[name] = url
118
109
 
119
- # 6. 上传书籍封面图(用于文章内显示)
120
- book_cover_path = os.path.join(cover_dir, "book_cover.jpg")
121
- if os.path.exists(book_cover_path):
122
- book_cover_url = upload_content_image(token, book_cover_path)
123
- if book_cover_url:
124
- image_urls["book_cover"] = book_cover_url
125
-
126
- # 7. 保存上传结果
110
+ # 6. 保存上传结果
127
111
  result_path = os.path.join(cover_dir, "upload_result.json")
128
112
  with open(result_path, "w") as f:
129
113
  json.dump(
@@ -137,7 +121,7 @@ def publish_article(
137
121
  )
138
122
  print(f"\n✓ 上传结果已保存: {result_path}")
139
123
 
140
- # 8. 创建草稿
124
+ # 7. 创建草稿
141
125
  print("\n创建草稿...")
142
126
  draft_id = create_draft(token, title, author, digest, content_html, cover_mid)
143
127
 
@@ -161,10 +145,15 @@ def main():
161
145
  parser.add_argument("--content-file", required=True, help="HTML内容文件路径")
162
146
  parser.add_argument("--cover-dir", required=True, help="封面和图片目录")
163
147
  parser.add_argument("--theme", default="abstract", help="图片主题")
164
- parser.add_argument("--image-count", type=int, default=5, help="主题插图数量")
165
- parser.add_argument("--book-title", default="", help="书籍标题(缺封面时自动获取)")
166
- parser.add_argument("--book-author", default="", help="书籍作者")
167
- parser.add_argument("--book-isbn", default="", help="书籍ISBN")
148
+ parser.add_argument(
149
+ "--image-count", type=int, default=6, help="主题插图数量(默认6:5正文+1结尾)"
150
+ )
151
+ parser.add_argument(
152
+ "--cover-seed",
153
+ type=int,
154
+ default=0,
155
+ help="封面图候选序号(0/1/2,相同主题下可轮换)",
156
+ )
168
157
  args = parser.parse_args()
169
158
 
170
159
  with open(args.content_file, "r", encoding="utf-8") as f:
@@ -178,9 +167,7 @@ def main():
178
167
  args.cover_dir,
179
168
  args.theme,
180
169
  args.image_count,
181
- args.book_title,
182
- args.book_author,
183
- args.book_isbn,
170
+ args.cover_seed,
184
171
  )
185
172
 
186
173