wechat-media-writer 2.2.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.
- package/README.md +108 -0
- package/SKILL.md +376 -0
- package/bin/cli.js +110 -0
- package/package.json +40 -0
- package/references/publish_template.py +155 -0
- package/scripts/book_cover.py +182 -0
- package/scripts/image_downloader.py +66 -0
- package/scripts/install.js +29 -0
- package/scripts/publish.py +118 -0
- package/scripts/wechat_api.py +216 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
微信公众号读书拆解发布脚本模板
|
|
4
|
+
使用 scripts/ 目录下的模块
|
|
5
|
+
|
|
6
|
+
用法:
|
|
7
|
+
1. 填写下方的变量(标题、摘要、HTML内容等)
|
|
8
|
+
2. 确保封面图已准备好(使用 scripts/book_cover.py 获取)
|
|
9
|
+
3. 运行: python3 references/publish_template.py
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
# 添加 scripts 目录到模块搜索路径
|
|
16
|
+
scripts_dir = os.path.join(
|
|
17
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts"
|
|
18
|
+
)
|
|
19
|
+
sys.path.insert(0, scripts_dir)
|
|
20
|
+
|
|
21
|
+
from wechat_api import load_config, get_access_token, upload_cover, create_draft
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ========== 替换以下内容 ==========
|
|
25
|
+
|
|
26
|
+
# 标题和摘要
|
|
27
|
+
TITLE = "《书名》全书深度拆解:犀利洞察"
|
|
28
|
+
AUTHOR = "读书笔记"
|
|
29
|
+
DIGEST = "摘要文本,不超过120字。"
|
|
30
|
+
|
|
31
|
+
# 封面图目录(包含 cover_900x500.jpg)
|
|
32
|
+
COVER_DIR = "/tmp/book_output"
|
|
33
|
+
|
|
34
|
+
# 主题色:主色(深色)+ 强调色(暖/亮色)
|
|
35
|
+
T = "#2E5266" # 主色
|
|
36
|
+
T2 = "#D4A24C" # 强调色
|
|
37
|
+
BODY = "#555" # 正文色
|
|
38
|
+
CBG = "#F0EDE5" # 引用块背景
|
|
39
|
+
|
|
40
|
+
# 文章 HTML 内容(7段式,无数字编号)
|
|
41
|
+
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
|
+
|
|
43
|
+
<p style="font-size:12px;color:{T2};text-align:center;letter-spacing:4px;margin:24px 0 8px;line-height:3em;">
|
|
44
|
+
<span>📖 读书拆解</span>
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
<p style="font-size:22px;font-weight:bold;color:{T};text-align:center;margin:0 0 6px;line-height:1.5;">
|
|
48
|
+
<span>《书名》全书深度拆解</span>
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
<p style="font-size:14px;color:#999;text-align:center;margin:0 0 20px;line-height:3em;">
|
|
52
|
+
<span>副标题(一句话点睛)</span>
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<!-- 书籍封面图(第一张图) -->
|
|
56
|
+
<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="《书名》封面"/>
|
|
58
|
+
</p>
|
|
59
|
+
|
|
60
|
+
<!-- 作者背景与创作时代(无数字编号) -->
|
|
61
|
+
<p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
|
|
62
|
+
<span>作者背景与创作时代</span>
|
|
63
|
+
</p>
|
|
64
|
+
|
|
65
|
+
<p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
|
|
66
|
+
<span>正文段落。注意:所有段落必须有 text-indent:2em。</span>
|
|
67
|
+
</p>
|
|
68
|
+
|
|
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>
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
<!-- 其他章节 -->
|
|
74
|
+
|
|
75
|
+
<p style="font-size:14px;color:#ccc;text-align:center;letter-spacing:8px;margin:20px 0;">· · · · · ·</p>
|
|
76
|
+
|
|
77
|
+
<!-- 星级评分与适配人群 -->
|
|
78
|
+
<p style="font-size:18px;font-weight:bold;color:{T};text-align:center;margin:0 0 16px;line-height:1em;">
|
|
79
|
+
<span>星级评分与适配人群</span>
|
|
80
|
+
</p>
|
|
81
|
+
|
|
82
|
+
<p style="font-size:28px;text-align:center;margin:0 0 8px;">
|
|
83
|
+
<span style="color:{T2};">★★★★☆</span>
|
|
84
|
+
</p>
|
|
85
|
+
|
|
86
|
+
<p style="font-size:16px;color:{BODY};text-align:center;margin:0 0 16px;">
|
|
87
|
+
<span>4 / 5 星</span>
|
|
88
|
+
</p>
|
|
89
|
+
|
|
90
|
+
<p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 16px;text-indent:2em;">
|
|
91
|
+
<span>为什么给X星?简要说明评分理由。</span>
|
|
92
|
+
</p>
|
|
93
|
+
|
|
94
|
+
<p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 8px;text-indent:2em;">
|
|
95
|
+
<strong style="color:{T};">适合人群:</strong>
|
|
96
|
+
</p>
|
|
97
|
+
<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>
|
|
99
|
+
</p>
|
|
100
|
+
|
|
101
|
+
<p style="font-size:16px;line-height:2;color:{BODY};margin:0 0 8px;text-indent:2em;">
|
|
102
|
+
<strong style="color:{T};">不适合人群:</strong>
|
|
103
|
+
</p>
|
|
104
|
+
<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>
|
|
106
|
+
</p>
|
|
107
|
+
|
|
108
|
+
<p style="font-size:14px;color:#ccc;text-align:center;letter-spacing:8px;margin:20px 0;">· · · · · ·</p>
|
|
109
|
+
|
|
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>
|
|
112
|
+
</p>
|
|
113
|
+
<p style="font-size:14px;color:#999;text-align:center;margin:0 0 4px;text-indent:2em;">
|
|
114
|
+
<span>一句话总结</span>
|
|
115
|
+
</p>
|
|
116
|
+
|
|
117
|
+
</section>"""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main():
|
|
121
|
+
print("=" * 50)
|
|
122
|
+
print("微信公众号 - 书名 发布")
|
|
123
|
+
print("=" * 50)
|
|
124
|
+
|
|
125
|
+
# 1. 加载配置和获取token
|
|
126
|
+
config = load_config()
|
|
127
|
+
token = get_access_token(config)
|
|
128
|
+
print("\n✓ Token 已获取\n")
|
|
129
|
+
|
|
130
|
+
# 2. 上传封面图
|
|
131
|
+
cover_900_path = os.path.join(COVER_DIR, "cover_900x500.jpg")
|
|
132
|
+
if not os.path.exists(cover_900_path):
|
|
133
|
+
print(f"错误: 封面图不存在 {cover_900_path}", file=sys.stderr)
|
|
134
|
+
print("请先运行: python3 scripts/book_cover.py '书名' '作者'", file=sys.stderr)
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
|
|
137
|
+
print("上传封面图...")
|
|
138
|
+
cover_mid = upload_cover(token, cover_900_path)
|
|
139
|
+
|
|
140
|
+
# 3. 创建草稿
|
|
141
|
+
print("\n创建草稿...")
|
|
142
|
+
draft_id = create_draft(token, TITLE, AUTHOR, DIGEST, HTML, cover_mid)
|
|
143
|
+
|
|
144
|
+
print("\n" + "=" * 50)
|
|
145
|
+
print("发布成功!")
|
|
146
|
+
print("=" * 50)
|
|
147
|
+
print(f"标题: {TITLE}")
|
|
148
|
+
print(f"作者: {AUTHOR}")
|
|
149
|
+
print(f"草稿ID: {draft_id}")
|
|
150
|
+
print(f"\n请前往微信公众平台查看草稿:")
|
|
151
|
+
print(f"https://mp.weixin.qq.com")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
main()
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
书籍封面图获取模块
|
|
4
|
+
支持:Google Books API、Open Library API、豆瓣搜索
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import ssl
|
|
11
|
+
import urllib.request
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def fetch_book_cover_google(book_title, author=""):
|
|
15
|
+
"""通过 Google Books API 获取书籍封面"""
|
|
16
|
+
ctx = ssl.create_default_context()
|
|
17
|
+
ctx.check_hostname = False
|
|
18
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
19
|
+
|
|
20
|
+
query = f"intitle:{book_title}"
|
|
21
|
+
if author:
|
|
22
|
+
query += f"+inauthor:{author}"
|
|
23
|
+
|
|
24
|
+
url = f"https://www.googleapis.com/books/v1/volumes?q={query}&maxResults=1"
|
|
25
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
data = urllib.request.urlopen(req, context=ctx, timeout=15).read()
|
|
29
|
+
result = json.loads(data)
|
|
30
|
+
|
|
31
|
+
if "items" in result and len(result["items"]) > 0:
|
|
32
|
+
volume = result["items"][0]["volumeInfo"]
|
|
33
|
+
if "imageLinks" in volume:
|
|
34
|
+
cover_url = volume["imageLinks"].get("large") or volume[
|
|
35
|
+
"imageLinks"
|
|
36
|
+
].get("thumbnail")
|
|
37
|
+
if cover_url:
|
|
38
|
+
cover_url = cover_url.replace("http://", "https://").replace(
|
|
39
|
+
"&edge=curl", ""
|
|
40
|
+
)
|
|
41
|
+
return cover_url
|
|
42
|
+
except Exception as e:
|
|
43
|
+
print(f"Google Books API 获取失败: {e}")
|
|
44
|
+
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def fetch_book_cover_openlibrary(isbn):
|
|
49
|
+
"""通过 Open Library 获取书籍封面(需要 ISBN)"""
|
|
50
|
+
ctx = ssl.create_default_context()
|
|
51
|
+
ctx.check_hostname = False
|
|
52
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
53
|
+
|
|
54
|
+
url = f"https://covers.openlibrary.org/b/isbn/{isbn}-L.jpg"
|
|
55
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
response = urllib.request.urlopen(req, context=ctx, timeout=15)
|
|
59
|
+
if response.status == 200:
|
|
60
|
+
return url
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print(f"Open Library 获取失败: {e}")
|
|
63
|
+
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def fetch_book_cover_douban(book_title, author=""):
|
|
68
|
+
"""通过豆瓣搜索获取书籍封面(需解析 HTML)"""
|
|
69
|
+
ctx = ssl.create_default_context()
|
|
70
|
+
ctx.check_hostname = False
|
|
71
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
72
|
+
|
|
73
|
+
query = f"{book_title} {author}".strip()
|
|
74
|
+
url = f"https://search.douban.com/book/subject_search?search_text={query}"
|
|
75
|
+
req = urllib.request.Request(
|
|
76
|
+
url,
|
|
77
|
+
headers={
|
|
78
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
html = (
|
|
84
|
+
urllib.request.urlopen(req, context=ctx, timeout=15).read().decode("utf-8")
|
|
85
|
+
)
|
|
86
|
+
match = re.search(
|
|
87
|
+
r'<img[^>]+src="(https://img[0-9]+\.doubanio\.com/view/subject/[^"]+)"',
|
|
88
|
+
html,
|
|
89
|
+
)
|
|
90
|
+
if match:
|
|
91
|
+
return match.group(1)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"豆瓣搜索获取失败: {e}")
|
|
94
|
+
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def fetch_book_cover(book_title, author="", isbn=""):
|
|
99
|
+
"""按优先级尝试获取书籍封面"""
|
|
100
|
+
cover_url = None
|
|
101
|
+
|
|
102
|
+
print(f"尝试从 Google Books 获取《{book_title}》封面...")
|
|
103
|
+
cover_url = fetch_book_cover_google(book_title, author)
|
|
104
|
+
|
|
105
|
+
if not cover_url and isbn:
|
|
106
|
+
print(f"尝试从 Open Library 获取封面...")
|
|
107
|
+
cover_url = fetch_book_cover_openlibrary(isbn)
|
|
108
|
+
|
|
109
|
+
if not cover_url:
|
|
110
|
+
print(f"尝试从豆瓣获取封面...")
|
|
111
|
+
cover_url = fetch_book_cover_douban(book_title, author)
|
|
112
|
+
|
|
113
|
+
if not cover_url:
|
|
114
|
+
print("⚠️ 无法自动获取书籍封面,请手动提供封面图")
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
print(f"✓ 成功获取封面: {cover_url}")
|
|
118
|
+
return cover_url
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def download_and_process_cover(cover_url, output_dir):
|
|
122
|
+
"""下载并处理封面图"""
|
|
123
|
+
from PIL import Image
|
|
124
|
+
|
|
125
|
+
ctx = ssl.create_default_context()
|
|
126
|
+
ctx.check_hostname = False
|
|
127
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
128
|
+
|
|
129
|
+
req = urllib.request.Request(cover_url, headers={"User-Agent": "Mozilla/5.0"})
|
|
130
|
+
data = urllib.request.urlopen(req, context=ctx, timeout=15).read()
|
|
131
|
+
|
|
132
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
133
|
+
original_path = os.path.join(output_dir, "book_cover_original.jpg")
|
|
134
|
+
with open(original_path, "wb") as f:
|
|
135
|
+
f.write(data)
|
|
136
|
+
|
|
137
|
+
img = Image.open(original_path)
|
|
138
|
+
w, h = img.size
|
|
139
|
+
if w > 600:
|
|
140
|
+
ratio = 600 / w
|
|
141
|
+
new_w = 600
|
|
142
|
+
new_h = int(h * ratio)
|
|
143
|
+
img = img.resize((new_w, new_h), Image.LANCZOS)
|
|
144
|
+
|
|
145
|
+
cover_path = os.path.join(output_dir, "book_cover.jpg")
|
|
146
|
+
img.save(cover_path, "JPEG", quality=90)
|
|
147
|
+
|
|
148
|
+
w, h = img.size
|
|
149
|
+
target_ratio = 900 / 500
|
|
150
|
+
if w / h > target_ratio:
|
|
151
|
+
new_w = int(h * target_ratio)
|
|
152
|
+
left = (w - new_w) // 2
|
|
153
|
+
img = img.crop((left, 0, left + new_w, h))
|
|
154
|
+
else:
|
|
155
|
+
new_h = int(w / target_ratio)
|
|
156
|
+
top = (h - new_h) // 2
|
|
157
|
+
img = img.crop((0, top, w, top + new_h))
|
|
158
|
+
img = img.resize((900, 500), Image.LANCZOS)
|
|
159
|
+
|
|
160
|
+
cover_900_path = os.path.join(output_dir, "cover_900x500.jpg")
|
|
161
|
+
img.save(cover_900_path, "JPEG", quality=90)
|
|
162
|
+
|
|
163
|
+
print(f"✓ 封面图已保存: {cover_path}")
|
|
164
|
+
print(f"✓ 公众号封面已保存: {cover_900_path}")
|
|
165
|
+
|
|
166
|
+
return cover_path, cover_900_path
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
import sys
|
|
171
|
+
|
|
172
|
+
if len(sys.argv) < 2:
|
|
173
|
+
print("用法: python book_cover.py <书名> [作者] [ISBN]")
|
|
174
|
+
sys.exit(1)
|
|
175
|
+
|
|
176
|
+
title = sys.argv[1]
|
|
177
|
+
author = sys.argv[2] if len(sys.argv) > 2 else ""
|
|
178
|
+
isbn = sys.argv[3] if len(sys.argv) > 3 else ""
|
|
179
|
+
|
|
180
|
+
url = fetch_book_cover(title, author, isbn)
|
|
181
|
+
if url:
|
|
182
|
+
download_and_process_cover(url, "/tmp/book_cover")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
图片下载模块
|
|
4
|
+
支持:Pexels CDN、通用URL下载
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import ssl
|
|
9
|
+
import urllib.request
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def download_image(url, output_path):
|
|
13
|
+
"""下载图片到指定路径"""
|
|
14
|
+
ctx = ssl.create_default_context()
|
|
15
|
+
ctx.check_hostname = False
|
|
16
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
17
|
+
|
|
18
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
|
19
|
+
try:
|
|
20
|
+
data = urllib.request.urlopen(req, context=ctx, timeout=15).read()
|
|
21
|
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
22
|
+
with open(output_path, "wb") as f:
|
|
23
|
+
f.write(data)
|
|
24
|
+
return True
|
|
25
|
+
except Exception as e:
|
|
26
|
+
print(f"下载失败: {e}")
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def download_pexels_image(pid, output_path):
|
|
31
|
+
"""从 Pexels CDN 下载图片"""
|
|
32
|
+
url = f"https://images.pexels.com/photos/{pid}/pexels-photo-{pid}.jpeg?auto=compress&cs=tinysrgb&w=1080"
|
|
33
|
+
return download_image(url, output_path)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def download_theme_images(theme, output_dir, count=5):
|
|
37
|
+
"""根据主题下载多张图片"""
|
|
38
|
+
themes = {
|
|
39
|
+
"nature": [15286, 15287, 15288, 15289, 15290],
|
|
40
|
+
"technology": [1181244, 1181245, 1181246, 1181247, 1181248],
|
|
41
|
+
"abstract": [2693208, 2693209, 2693210, 2693211, 2693212],
|
|
42
|
+
"books": [159711, 159712, 159713, 159714, 159715],
|
|
43
|
+
"business": [3184292, 3184293, 3184294, 3184295, 3184296],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pids = themes.get(theme, themes["abstract"])[:count]
|
|
47
|
+
urls = []
|
|
48
|
+
|
|
49
|
+
for i, pid in enumerate(pids):
|
|
50
|
+
output_path = os.path.join(output_dir, f"img_{i + 1}.jpg")
|
|
51
|
+
if download_pexels_image(pid, output_path):
|
|
52
|
+
urls.append(output_path)
|
|
53
|
+
|
|
54
|
+
return urls
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
import sys
|
|
59
|
+
|
|
60
|
+
if len(sys.argv) < 2:
|
|
61
|
+
print("用法: python image_downloader.py <图片URL> [输出路径]")
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
|
|
64
|
+
url = sys.argv[1]
|
|
65
|
+
output = sys.argv[2] if len(sys.argv) > 2 else "/tmp/downloaded_image.jpg"
|
|
66
|
+
download_image(url, output)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
console.log('正在检查 Python 环境...');
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
// 检查 Python 3
|
|
9
|
+
execSync('python3 --version', { stdio: 'pipe' });
|
|
10
|
+
console.log('✓ Python 3 已安装');
|
|
11
|
+
|
|
12
|
+
// 检查并安装 Pillow
|
|
13
|
+
try {
|
|
14
|
+
execSync('python3 -c "from PIL import Image"', { stdio: 'pipe' });
|
|
15
|
+
console.log('✓ Pillow 已安装');
|
|
16
|
+
} catch {
|
|
17
|
+
console.log('正在安装 Pillow...');
|
|
18
|
+
execSync('pip3 install Pillow', { stdio: 'inherit' });
|
|
19
|
+
console.log('✓ Pillow 安装完成');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log('\n安装完成!使用方法:');
|
|
23
|
+
console.log(' npx wechat-book-review help');
|
|
24
|
+
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error('\n警告: Python 3 未安装');
|
|
27
|
+
console.error('请手动安装 Python 3: https://www.python.org/downloads/');
|
|
28
|
+
console.error('\n安装后仍可使用,但需要手动运行 Python 脚本');
|
|
29
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
微信公众号读书拆解发布脚本
|
|
4
|
+
完整流程:获取封面 -> 下载插图 -> 上传图片 -> 创建草稿
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
# 添加当前目录到模块搜索路径
|
|
13
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
14
|
+
|
|
15
|
+
from book_cover import fetch_book_cover, download_and_process_cover
|
|
16
|
+
from image_downloader import download_theme_images
|
|
17
|
+
from wechat_api import (
|
|
18
|
+
load_config,
|
|
19
|
+
get_access_token,
|
|
20
|
+
upload_cover,
|
|
21
|
+
upload_content_image,
|
|
22
|
+
create_draft,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def publish_article(title, author, digest, content_html, cover_dir, theme="abstract"):
|
|
27
|
+
"""完整发布流程"""
|
|
28
|
+
print("=" * 50)
|
|
29
|
+
print("微信公众号 - 书评发布")
|
|
30
|
+
print("=" * 50)
|
|
31
|
+
|
|
32
|
+
# 1. 加载配置和获取token
|
|
33
|
+
config = load_config()
|
|
34
|
+
token = get_access_token(config)
|
|
35
|
+
print("\n✓ Token 已获取\n")
|
|
36
|
+
|
|
37
|
+
# 2. 上传封面图
|
|
38
|
+
cover_900_path = os.path.join(cover_dir, "cover_900x500.jpg")
|
|
39
|
+
if not os.path.exists(cover_900_path):
|
|
40
|
+
print(f"错误: 封面图不存在 {cover_900_path}", file=sys.stderr)
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
print("上传封面图...")
|
|
44
|
+
cover_mid = upload_cover(token, cover_900_path)
|
|
45
|
+
|
|
46
|
+
# 3. 上传内容图片
|
|
47
|
+
print("\n上传内容图片...")
|
|
48
|
+
image_urls = {}
|
|
49
|
+
for fname in sorted(os.listdir(cover_dir)):
|
|
50
|
+
if fname.startswith("img_") and fname.endswith((".jpg", ".jpeg", ".png")):
|
|
51
|
+
fpath = os.path.join(cover_dir, fname)
|
|
52
|
+
url = upload_content_image(token, fpath)
|
|
53
|
+
if url:
|
|
54
|
+
name = os.path.splitext(fname)[0]
|
|
55
|
+
image_urls[name] = url
|
|
56
|
+
|
|
57
|
+
# 4. 上传书籍封面图(用于文章内显示)
|
|
58
|
+
book_cover_path = os.path.join(cover_dir, "book_cover.jpg")
|
|
59
|
+
if os.path.exists(book_cover_path):
|
|
60
|
+
book_cover_url = upload_content_image(token, book_cover_path)
|
|
61
|
+
if book_cover_url:
|
|
62
|
+
image_urls["book_cover"] = book_cover_url
|
|
63
|
+
|
|
64
|
+
# 5. 保存上传结果
|
|
65
|
+
result_path = os.path.join(cover_dir, "upload_result.json")
|
|
66
|
+
with open(result_path, "w") as f:
|
|
67
|
+
json.dump(
|
|
68
|
+
{
|
|
69
|
+
"cover_mid": cover_mid,
|
|
70
|
+
"image_urls": image_urls,
|
|
71
|
+
},
|
|
72
|
+
f,
|
|
73
|
+
indent=2,
|
|
74
|
+
ensure_ascii=False,
|
|
75
|
+
)
|
|
76
|
+
print(f"\n✓ 上传结果已保存: {result_path}")
|
|
77
|
+
|
|
78
|
+
# 6. 创建草稿
|
|
79
|
+
print("\n创建草稿...")
|
|
80
|
+
draft_id = create_draft(token, title, author, digest, content_html, cover_mid)
|
|
81
|
+
|
|
82
|
+
print("\n" + "=" * 50)
|
|
83
|
+
print("发布成功!")
|
|
84
|
+
print("=" * 50)
|
|
85
|
+
print(f"标题: {title}")
|
|
86
|
+
print(f"作者: {author}")
|
|
87
|
+
print(f"草稿ID: {draft_id}")
|
|
88
|
+
print(f"\n请前往微信公众平台查看草稿:")
|
|
89
|
+
print(f"https://mp.weixin.qq.com")
|
|
90
|
+
|
|
91
|
+
return draft_id
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def main():
|
|
95
|
+
parser = argparse.ArgumentParser(description="微信公众号书评发布工具")
|
|
96
|
+
parser.add_argument("--title", required=True, help="文章标题")
|
|
97
|
+
parser.add_argument("--author", default="读书笔记", help="作者名称")
|
|
98
|
+
parser.add_argument("--digest", default="", help="文章摘要")
|
|
99
|
+
parser.add_argument("--content-file", required=True, help="HTML内容文件路径")
|
|
100
|
+
parser.add_argument("--cover-dir", required=True, help="封面和图片目录")
|
|
101
|
+
parser.add_argument("--theme", default="abstract", help="图片主题")
|
|
102
|
+
args = parser.parse_args()
|
|
103
|
+
|
|
104
|
+
with open(args.content_file, "r", encoding="utf-8") as f:
|
|
105
|
+
content_html = f.read()
|
|
106
|
+
|
|
107
|
+
publish_article(
|
|
108
|
+
args.title,
|
|
109
|
+
args.author,
|
|
110
|
+
args.digest,
|
|
111
|
+
content_html,
|
|
112
|
+
args.cover_dir,
|
|
113
|
+
args.theme,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
main()
|