wechat-media-writer 2.2.1 → 2.2.2

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 CHANGED
@@ -25,8 +25,14 @@ git clone https://github.com/your-username/wechat-media-writer.git ~/.claude/ski
25
25
 
26
26
  ```bash
27
27
  npx wechat-media-writer cover "人类简史" "尤瓦尔·赫拉利"
28
- npx wechat-media-writer cover "肖申克的救赎" "弗兰克·德拉邦特"
29
- npx wechat-media-writer publish --title "标题" --content-file content.html --cover-dir /tmp/output
28
+ npx wechat-media-writer download books 5
29
+ npx wechat-media-writer publish \
30
+ --title "《人类简史》全书深度拆解" \
31
+ --content-file content.html \
32
+ --cover-dir /tmp/wx \
33
+ --book-title "人类简史" \
34
+ --book-author "尤瓦尔·赫拉利" \
35
+ --theme books
30
36
  ```
31
37
 
32
38
  ### 方式三:全局安装
@@ -47,31 +53,59 @@ wechat-media-writer cover "人类简史"
47
53
  }
48
54
  ```
49
55
 
56
+ access_token 自动缓存在 `~/.wechat/token_cache.json`(2小时有效期)。
57
+
50
58
  ## 命令
51
59
 
52
- ### 获取封面
60
+ ### `cover` — 获取封面
53
61
 
54
62
  ```bash
55
- npx wechat-media-writer cover <书名/电影名> [作者/导演]
63
+ npx wechat-media-writer cover <书名/电影名> [作者] [ISBN]
56
64
  ```
57
65
 
58
- 自动从 Google BooksOpen Library、豆瓣获取封面。
66
+ 按优先级自动从 Google BooksOpen Library → 豆瓣 获取封面;
67
+ 三个数据源均失败时回退到 Pexels 占位图(保证流程不中断)。
59
68
 
60
- ### 下载主题插图
69
+ ### `download` — 下载主题插图
61
70
 
62
71
  ```bash
63
- npx wechat-media-writer download [主题] [数量]
72
+ npx wechat-media-writer download [主题] [数量] [输出目录]
64
73
  ```
65
74
 
66
- 主题:`nature`、`technology`、`abstract`、`books`、`movies`、`business`
75
+ 主题:`abstract`、`books`、`nature`、`technology`、`business`,默认 `abstract`,数量默认 `5`,目录默认 `/tmp/images`。
67
76
 
68
- ### 发布文章
77
+ ### `publish` — 发布文章到微信草稿箱
69
78
 
70
79
  ```bash
71
- npx wechat-media-writer publish --title "标题" --content-file content.html --cover-dir /tmp/output
80
+ npx wechat-media-writer publish --title "标题" --content-file content.html --cover-dir <目录>
72
81
  ```
73
82
 
74
- ### 查看帮助
83
+ `publish` 是**完整一键发布**:
84
+
85
+ 1. 检查封面图,缺失则自动获取
86
+ 2. 检查主题插图,缺失则自动下载
87
+ 3. 加载微信配置、获取 token
88
+ 4. 上传封面(永久素材)+ 内容图(uploadimg)
89
+ 5. 创建草稿
90
+
91
+ #### 完整参数
92
+
93
+ | 参数 | 必填 | 说明 |
94
+ | --- | --- | --- |
95
+ | `--title` | 是 | 文章标题 |
96
+ | `--content-file` | 是 | HTML 内容文件路径 |
97
+ | `--cover-dir` | 是 | 封面和图片目录(自动创建) |
98
+ | `--author` | 否 | 文章作者,默认 `读书笔记` |
99
+ | `--digest` | 否 | 文章摘要 |
100
+ | `--theme` | 否 | 图片主题,默认 `abstract` |
101
+ | `--image-count` | 否 | 主题插图数量,默认 `5` |
102
+ | `--book-title` | 否 | 书籍标题(封面缺失时自动获取) |
103
+ | `--book-author` | 否 | 书籍作者 |
104
+ | `--book-isbn` | 否 | 书籍 ISBN |
105
+
106
+ 成功后输出草稿 ID,前往 https://mp.weixin.qq.com 预览发布。
107
+
108
+ ### `help` — 查看帮助
75
109
 
76
110
  ```bash
77
111
  npx wechat-media-writer help
@@ -87,11 +121,11 @@ wechat-media-writer/
87
121
  ├── bin/
88
122
  │ └── cli.js # CLI 入口
89
123
  ├── scripts/
90
- │ ├── book_cover.py # 封面获取(书/影)
91
- │ ├── image_downloader.py # 图片下载
92
- │ ├── wechat_api.py # 微信API封装
93
- │ ├── publish.py # 完整发布脚本
94
- │ └── install.js # npm postinstall
124
+ │ ├── book_cover.py # 书籍封面获取
125
+ │ ├── image_downloader.py # 主题插图下载
126
+ │ ├── wechat_api.py # 微信 API 封装(token/上传/草稿)
127
+ │ ├── publish.py # 一键发布主脚本(含封面/插图自动补齐)
128
+ │ └── install.js # npm postinstall(检查 Python/Pillow)
95
129
  └── references/
96
130
  └── publish_template.py # 发布脚本模板
97
131
  ```
@@ -105,4 +139,4 @@ npm publish
105
139
 
106
140
  ## License
107
141
 
108
- MIT
142
+ MIT
package/bin/cli.js CHANGED
@@ -43,16 +43,33 @@ function showHelp() {
43
43
  npx wechat-media-writer <command> [options]
44
44
 
45
45
  命令:
46
- cover <书名/电影名> [作者/导演] 获取封面
47
- download <主题> [数量] 下载主题插图
48
- publish 发布文章到微信
49
- help 显示帮助信息
46
+ cover <书名/电影名> [作者] [ISBN] 获取书籍封面
47
+ download <主题> [数量] 下载主题插图
48
+ publish 发布文章到微信(封面/插图缺失会自动补齐)
49
+ help 显示帮助信息
50
+
51
+ publish 选项:
52
+ --title <标题> 文章标题(必填)
53
+ --content-file <路径> HTML 内容文件(必填)
54
+ --cover-dir <目录> 封面和图片目录(必填)
55
+ --author <作者> 文章作者,默认"读书笔记"
56
+ --digest <摘要> 文章摘要
57
+ --theme <主题> 图片主题:abstract/books/nature/technology/business,默认 abstract
58
+ --image-count <数量> 主题插图数量,默认 5
59
+ --book-title <书名> 书籍标题(封面缺失时自动获取)
60
+ --book-author <作者> 书籍作者
61
+ --book-isbn <ISBN> 书籍ISBN
50
62
 
51
63
  示例:
52
64
  npx wechat-media-writer cover "人类简史" "尤瓦尔·赫拉利"
53
- npx wechat-media-writer cover "肖申克的救赎" "弗兰克·德拉邦特"
54
65
  npx wechat-media-writer download books 5
55
- npx wechat-media-writer publish --title "标题" --content-file content.html
66
+ npx wechat-media-writer publish \\
67
+ --title "《人类简史》全书深度拆解" \\
68
+ --content-file content.html \\
69
+ --cover-dir /tmp/wx \\
70
+ --book-title "人类简史" \\
71
+ --book-author "尤瓦尔·赫拉利" \\
72
+ --theme books
56
73
 
57
74
  配置:
58
75
  创建 ~/.wechat/config.json:
@@ -86,18 +103,25 @@ function main() {
86
103
  break;
87
104
 
88
105
  case 'download':
89
- const theme = args[1] || 'abstract';
90
- const count = args[2] || '5';
91
- execSync(`${python} -c "from scripts.image_downloader import download_theme_images; download_theme_images('${theme}', '/tmp/images', ${count})"`, {
92
- stdio: 'inherit',
93
- cwd: SKILL_DIR
94
- });
106
+ {
107
+ const theme = args[1] || 'abstract';
108
+ const count = args[2] || '5';
109
+ const outDir = args[3] || '/tmp/images';
110
+ execSync(
111
+ `${python} ${path.join(SCRIPTS_DIR, 'image_downloader.py')} "${theme}" "${outDir}" ${count}`,
112
+ { stdio: 'inherit' }
113
+ );
114
+ }
95
115
  break;
96
116
 
97
117
  case 'publish':
98
- // 传递所有参数给Python脚本
99
- const publishArgs = args.slice(1).join(' ');
100
- execSync(`${python} ${path.join(SCRIPTS_DIR, 'publish.py')} ${publishArgs}`, { stdio: 'inherit' });
118
+ {
119
+ const publishArgs = args.slice(1).join(' ');
120
+ execSync(
121
+ `${python} ${path.join(SCRIPTS_DIR, 'publish.py')} ${publishArgs}`,
122
+ { stdio: 'inherit' }
123
+ );
124
+ }
101
125
  break;
102
126
 
103
127
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wechat-media-writer",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "description": "微信公众号书评、影评文章自动生成与发布 - Skill",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -14,7 +14,11 @@
14
14
  "README.md"
15
15
  ],
16
16
  "scripts": {
17
- "postinstall": "node scripts/install.js"
17
+ "postinstall": "node scripts/install.js",
18
+ "release:patch": "npm version patch && git push origin main --tags && npm publish",
19
+ "release:minor": "npm version minor && git push origin main --tags && npm publish",
20
+ "release:major": "npm version major && git push origin main --tags && npm publish",
21
+ "prepare": "husky"
18
22
  },
19
23
  "keywords": [
20
24
  "wechat",
@@ -36,5 +40,10 @@
36
40
  },
37
41
  "engines": {
38
42
  "node": ">=14.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@commitlint/cli": "^21.1.0",
46
+ "@commitlint/config-conventional": "^21.1.0",
47
+ "husky": "^9.1.7"
39
48
  }
40
49
  }
@@ -72,7 +72,8 @@ def fetch_book_cover_douban(book_title, author=""):
72
72
  ctx.verify_mode = ssl.CERT_NONE
73
73
 
74
74
  query = f"{book_title} {author}".strip()
75
- url = f"https://search.douban.com/book/subject_search?search_text={urllib.parse.quote(query)}"
75
+ encoded_query = urllib.parse.quote(query)
76
+ url = f"https://search.douban.com/book/subject_search?search_text={encoded_query}"
76
77
  req = urllib.request.Request(
77
78
  url,
78
79
  headers={
@@ -81,9 +82,8 @@ def fetch_book_cover_douban(book_title, author=""):
81
82
  )
82
83
 
83
84
  try:
84
- html = (
85
- urllib.request.urlopen(req, context=ctx, timeout=15).read().decode("utf-8")
86
- )
85
+ raw = urllib.request.urlopen(req, context=ctx, timeout=15).read()
86
+ html = raw.decode("utf-8", errors="replace")
87
87
  match = re.search(
88
88
  r'<img[^>]+src="(https://img[0-9]+\.doubanio\.com/view/subject/[^"]+)"',
89
89
  html,
@@ -97,7 +97,7 @@ 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
101
  cover_url = None
102
102
 
103
103
  print(f"尝试从 Google Books 获取《{book_title}》封面...")
@@ -112,10 +112,10 @@ def fetch_book_cover(book_title, author="", isbn=""):
112
112
  cover_url = fetch_book_cover_douban(book_title, author)
113
113
 
114
114
  if not cover_url:
115
- print("⚠️ 无法自动获取书籍封面,请手动提供封面图")
116
- return None
115
+ print("⚠️ 三个数据源均未返回封面,使用占位图占位")
116
+ cover_url = "https://images.pexels.com/photos/159711/books-bookstore-book-reading-159711.jpeg?auto=compress&cs=tinysrgb&w=600"
117
117
 
118
- print(f"✓ 成功获取封面: {cover_url}")
118
+ print(f"✓ 封面URL: {cover_url}")
119
119
  return cover_url
120
120
 
121
121
 
@@ -36,20 +36,26 @@ def download_pexels_image(pid, output_path):
36
36
  def download_theme_images(theme, output_dir, count=5):
37
37
  """根据主题下载多张图片"""
38
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],
39
+ "nature": [2387873, 2387874, 2387875, 2387876, 2387877, 2387878, 2387879],
40
+ "technology": [3568520, 3568521, 3568522, 3568523, 3568524, 3568525],
41
+ "abstract": [2693208, 2693209, 2693210, 2693211, 2693212, 2693213],
42
+ "books": [159711, 1029141, 256450, 590493, 762687, 3568520],
43
+ "business": [3184292, 3184293, 3184294, 3184295, 3184296, 3184297],
44
44
  }
45
45
 
46
46
  pids = themes.get(theme, themes["abstract"])[:count]
47
47
  urls = []
48
+ fallback_pid = 159711
49
+ fallback_used = False
48
50
 
49
51
  for i, pid in enumerate(pids):
50
52
  output_path = os.path.join(output_dir, f"img_{i + 1}.jpg")
51
53
  if download_pexels_image(pid, output_path):
52
54
  urls.append(output_path)
55
+ elif not fallback_used:
56
+ if download_pexels_image(fallback_pid, output_path):
57
+ urls.append(output_path)
58
+ fallback_used = True
53
59
 
54
60
  return urls
55
61
 
@@ -58,9 +64,19 @@ if __name__ == "__main__":
58
64
  import sys
59
65
 
60
66
  if len(sys.argv) < 2:
61
- print("用法: python image_downloader.py <图片URL> [输出路径]")
67
+ print("用法:")
68
+ print(" python image_downloader.py <图片URL> [输出路径]")
69
+ print(" python image_downloader.py <主题> <输出目录> [数量]")
62
70
  sys.exit(1)
63
71
 
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)
72
+ arg1 = sys.argv[1]
73
+ if arg1.startswith("http://") or arg1.startswith("https://"):
74
+ url = arg1
75
+ output = sys.argv[2] if len(sys.argv) > 2 else "/tmp/downloaded_image.jpg"
76
+ download_image(url, output)
77
+ else:
78
+ theme = arg1
79
+ output_dir = sys.argv[2] if len(sys.argv) > 2 else "/tmp/images"
80
+ count = int(sys.argv[3]) if len(sys.argv) > 3 else 5
81
+ urls = download_theme_images(theme, output_dir, count)
82
+ print(f"\n✓ 共下载 {len(urls)} 张图片到 {output_dir}")
@@ -23,18 +23,80 @@ from wechat_api import (
23
23
  )
24
24
 
25
25
 
26
- def publish_article(title, author, digest, content_html, cover_dir, theme="abstract"):
27
- """完整发布流程"""
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)
42
+
43
+ print(f"封面图缺失,自动获取《{book_title}》封面...")
44
+ 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
+ existing = [
55
+ f
56
+ for f in os.listdir(cover_dir)
57
+ if f.startswith("img_") and f.endswith((".jpg", ".jpeg", ".png"))
58
+ ]
59
+ if len(existing) >= count:
60
+ print(f"✓ 主题插图已存在 {len(existing)} 张,跳过下载")
61
+ return
62
+
63
+ print(f"主题插图不足,自动下载 {count} 张(主题: {theme})...")
64
+ download_theme_images(theme, cover_dir, count)
65
+
66
+
67
+ def publish_article(
68
+ title,
69
+ author,
70
+ digest,
71
+ content_html,
72
+ cover_dir,
73
+ theme="abstract",
74
+ image_count=5,
75
+ book_title="",
76
+ book_author="",
77
+ book_isbn="",
78
+ ):
79
+ """完整发布流程:获取封面 -> 下载插图 -> 上传图片 -> 创建草稿"""
28
80
  print("=" * 50)
29
81
  print("微信公众号 - 书评发布")
30
82
  print("=" * 50)
31
83
 
32
- # 1. 加载配置和获取token
84
+ os.makedirs(cover_dir, exist_ok=True)
85
+
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] 准备主题插图...")
92
+ ensure_theme_images(cover_dir, theme, image_count)
93
+
94
+ # 3. 加载配置和获取token
33
95
  config = load_config()
34
96
  token = get_access_token(config)
35
97
  print("\n✓ Token 已获取\n")
36
98
 
37
- # 2. 上传封面图
99
+ # 4. 上传封面图
38
100
  cover_900_path = os.path.join(cover_dir, "cover_900x500.jpg")
39
101
  if not os.path.exists(cover_900_path):
40
102
  print(f"错误: 封面图不存在 {cover_900_path}", file=sys.stderr)
@@ -43,7 +105,7 @@ def publish_article(title, author, digest, content_html, cover_dir, theme="abstr
43
105
  print("上传封面图...")
44
106
  cover_mid = upload_cover(token, cover_900_path)
45
107
 
46
- # 3. 上传内容图片
108
+ # 5. 上传内容图片
47
109
  print("\n上传内容图片...")
48
110
  image_urls = {}
49
111
  for fname in sorted(os.listdir(cover_dir)):
@@ -54,14 +116,14 @@ def publish_article(title, author, digest, content_html, cover_dir, theme="abstr
54
116
  name = os.path.splitext(fname)[0]
55
117
  image_urls[name] = url
56
118
 
57
- # 4. 上传书籍封面图(用于文章内显示)
119
+ # 6. 上传书籍封面图(用于文章内显示)
58
120
  book_cover_path = os.path.join(cover_dir, "book_cover.jpg")
59
121
  if os.path.exists(book_cover_path):
60
122
  book_cover_url = upload_content_image(token, book_cover_path)
61
123
  if book_cover_url:
62
124
  image_urls["book_cover"] = book_cover_url
63
125
 
64
- # 5. 保存上传结果
126
+ # 7. 保存上传结果
65
127
  result_path = os.path.join(cover_dir, "upload_result.json")
66
128
  with open(result_path, "w") as f:
67
129
  json.dump(
@@ -75,7 +137,7 @@ def publish_article(title, author, digest, content_html, cover_dir, theme="abstr
75
137
  )
76
138
  print(f"\n✓ 上传结果已保存: {result_path}")
77
139
 
78
- # 6. 创建草稿
140
+ # 8. 创建草稿
79
141
  print("\n创建草稿...")
80
142
  draft_id = create_draft(token, title, author, digest, content_html, cover_mid)
81
143
 
@@ -99,6 +161,10 @@ def main():
99
161
  parser.add_argument("--content-file", required=True, help="HTML内容文件路径")
100
162
  parser.add_argument("--cover-dir", required=True, help="封面和图片目录")
101
163
  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")
102
168
  args = parser.parse_args()
103
169
 
104
170
  with open(args.content_file, "r", encoding="utf-8") as f:
@@ -111,6 +177,10 @@ def main():
111
177
  content_html,
112
178
  args.cover_dir,
113
179
  args.theme,
180
+ args.image_count,
181
+ args.book_title,
182
+ args.book_author,
183
+ args.book_isbn,
114
184
  )
115
185
 
116
186