multi-publisher 1.0.2 → 1.0.4
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
|
@@ -22,23 +22,34 @@
|
|
|
22
22
|
## 安装
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
cd multi-publisher
|
|
28
|
-
npm install
|
|
29
|
-
npm run build
|
|
25
|
+
# 推荐:一键安装(Node.js ≥ 18)
|
|
26
|
+
npm install -g multi-publisher
|
|
30
27
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
mpub --help
|
|
28
|
+
# 验证安装
|
|
29
|
+
mpub --version
|
|
34
30
|
```
|
|
35
31
|
|
|
36
32
|
**Node.js ≥ 18** required.
|
|
37
33
|
|
|
34
|
+
### 配合 AI 使用
|
|
35
|
+
|
|
36
|
+
本工具已制作为 Claude Code Skill,可让 AI 直接帮你发布文章:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# 在任意 Claude Code 对话中,直接描述你的需求:
|
|
40
|
+
# "帮我把这篇 article.md 发布到微信公众号"
|
|
41
|
+
# "用 cyberpunk 主题渲染预览一下"
|
|
42
|
+
# AI 会调用 mpub 完成操作
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
查看支持的所有命令:`mpub --help`
|
|
46
|
+
|
|
38
47
|
---
|
|
39
48
|
|
|
40
49
|
## 快速开始
|
|
41
50
|
|
|
51
|
+
> **配合 AI 使用**:安装本工具后,在 Claude Code 中直接说"帮我发布文章到微信公众号",AI 会调用 `mpub` 自动完成。
|
|
52
|
+
|
|
42
53
|
### 1. 配置平台登录
|
|
43
54
|
|
|
44
55
|
**方式一:浏览器自动登录(推荐)**
|
package/dist/adapters/toutiao.js
CHANGED
|
@@ -114,18 +114,15 @@ export class ToutiaoAdapter {
|
|
|
114
114
|
console.log('[toutiao] 已填写内容');
|
|
115
115
|
await page.waitForTimeout(1000);
|
|
116
116
|
}
|
|
117
|
-
// 等待编辑器自动保存(最多5
|
|
118
|
-
console.log('[toutiao]
|
|
119
|
-
for (let i = 0; i <
|
|
117
|
+
// 等待编辑器自动保存(最多5秒)
|
|
118
|
+
console.log('[toutiao] 等待编辑器自动保存(5秒)...');
|
|
119
|
+
for (let i = 0; i < 5; i++) {
|
|
120
120
|
const url = page.url();
|
|
121
121
|
if (url.includes('id=')) {
|
|
122
122
|
console.log('[toutiao] 检测到文章 ID,保存完成');
|
|
123
123
|
break;
|
|
124
124
|
}
|
|
125
125
|
await page.waitForTimeout(1000);
|
|
126
|
-
if (i % 30 === 0 && i > 0) {
|
|
127
|
-
console.log(`[toutiao] 已等待 ${i} 秒...`);
|
|
128
|
-
}
|
|
129
126
|
}
|
|
130
127
|
// 检查 URL 是否包含文章 ID
|
|
131
128
|
let currentUrl = page.url();
|
|
@@ -15,9 +15,6 @@ export declare class WechatPublisher {
|
|
|
15
15
|
* 下载图片并以指定文件名上传到微信 CDN
|
|
16
16
|
*/
|
|
17
17
|
uploadImageFromUrl(imageUrl: string): Promise<string>;
|
|
18
|
-
/**
|
|
19
|
-
* 上传本地图片到微信 CDN
|
|
20
|
-
*/
|
|
21
18
|
/**
|
|
22
19
|
* 上传本地图片到微信 CDN(临时素材,用于正文嵌入)
|
|
23
20
|
*/
|
|
@@ -27,6 +24,11 @@ export declare class WechatPublisher {
|
|
|
27
24
|
* 使用 material/add_material 接口
|
|
28
25
|
*/
|
|
29
26
|
uploadPermanentImageFromPath(filePath: string, filename?: string): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* 生成占位封面图(当文章无图片时使用)
|
|
29
|
+
* 从占位图服务下载图片并上传为永久素材
|
|
30
|
+
*/
|
|
31
|
+
uploadPlaceholderCover(): Promise<string>;
|
|
30
32
|
/**
|
|
31
33
|
* 从 URL 下载图片并上传为永久素材(用于封面)
|
|
32
34
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
3
|
import FormData from 'form-data';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import os from 'node:os';
|
|
@@ -65,9 +65,6 @@ export class WechatPublisher {
|
|
|
65
65
|
this.uploadCache[imageUrl] = mediaId;
|
|
66
66
|
return mediaId;
|
|
67
67
|
}
|
|
68
|
-
/**
|
|
69
|
-
* 上传本地图片到微信 CDN
|
|
70
|
-
*/
|
|
71
68
|
/**
|
|
72
69
|
* 上传本地图片到微信 CDN(临时素材,用于正文嵌入)
|
|
73
70
|
*/
|
|
@@ -75,7 +72,7 @@ export class WechatPublisher {
|
|
|
75
72
|
const token = await this.getAccessToken();
|
|
76
73
|
const name = filename ?? path.basename(filePath);
|
|
77
74
|
const form = new FormData();
|
|
78
|
-
form.append('media',
|
|
75
|
+
form.append('media', readFileSync(filePath), { filename: name, contentType: 'image/png' });
|
|
79
76
|
const res = await axios.post(`https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${token}&type=image`, form, { headers: form.getHeaders(), maxBodyLength: Infinity, maxContentLength: Infinity });
|
|
80
77
|
const data = res.data;
|
|
81
78
|
if (!data.media_id) {
|
|
@@ -91,7 +88,7 @@ export class WechatPublisher {
|
|
|
91
88
|
const token = await this.getAccessToken();
|
|
92
89
|
const name = filename ?? path.basename(filePath);
|
|
93
90
|
const form = new FormData();
|
|
94
|
-
form.append('media',
|
|
91
|
+
form.append('media', readFileSync(filePath), { filename: name, contentType: 'image/png' });
|
|
95
92
|
form.append('type', 'image');
|
|
96
93
|
const res = await axios.post(`https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${token}&type=image`, form, { headers: form.getHeaders(), maxBodyLength: Infinity, maxContentLength: Infinity });
|
|
97
94
|
const data = res.data;
|
|
@@ -100,6 +97,27 @@ export class WechatPublisher {
|
|
|
100
97
|
}
|
|
101
98
|
return data.media_id;
|
|
102
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* 生成占位封面图(当文章无图片时使用)
|
|
102
|
+
* 从占位图服务下载图片并上传为永久素材
|
|
103
|
+
*/
|
|
104
|
+
async uploadPlaceholderCover() {
|
|
105
|
+
const token = await this.getAccessToken();
|
|
106
|
+
// 从 httpbin 下载标准测试图(8090 bytes 有效 PNG)
|
|
107
|
+
const res = await this.runtime.fetch('https://httpbin.org/image/png');
|
|
108
|
+
if (!res.ok)
|
|
109
|
+
throw new Error(`占位图下载失败: ${res.status}`);
|
|
110
|
+
const buf = await res.arrayBuffer();
|
|
111
|
+
const png = Buffer.from(buf);
|
|
112
|
+
const form = new FormData();
|
|
113
|
+
form.append('media', png, { filename: 'cover.png', contentType: 'image/png' });
|
|
114
|
+
form.append('type', 'image');
|
|
115
|
+
const uploadRes = await axios.post(`https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${token}&type=image`, form, { headers: form.getHeaders(), maxBodyLength: Infinity, maxContentLength: Infinity });
|
|
116
|
+
const data = uploadRes.data;
|
|
117
|
+
if (!data.media_id)
|
|
118
|
+
throw new Error(`占位图上传失败: ${data.errmsg || JSON.stringify(data)}`);
|
|
119
|
+
return data.media_id;
|
|
120
|
+
}
|
|
103
121
|
/**
|
|
104
122
|
* 从 URL 下载图片并上传为永久素材(用于封面)
|
|
105
123
|
*/
|
|
@@ -160,28 +178,42 @@ export class WechatPublisher {
|
|
|
160
178
|
async publishToDraft(options) {
|
|
161
179
|
const token = await this.getAccessToken();
|
|
162
180
|
const { html: processedHtml, firstMediaId } = await this.processImages(options.content);
|
|
163
|
-
//
|
|
181
|
+
// 上传封面图:优先用 front-matter 指定的封面图,其次用正文第一张图片的 media_id
|
|
164
182
|
let thumbMediaId;
|
|
165
183
|
try {
|
|
166
184
|
thumbMediaId = await this.uploadCover(options.cover);
|
|
167
185
|
}
|
|
168
186
|
catch (err) {
|
|
169
|
-
console.warn(`[WechatPublisher]
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
console.warn(`[WechatPublisher] 封面上传失败: ${err.message}`);
|
|
188
|
+
// 用 processImages 返回的第一张图片 media_id 作为封面
|
|
189
|
+
if (firstMediaId) {
|
|
190
|
+
thumbMediaId = firstMediaId;
|
|
191
|
+
console.log('[WechatPublisher] 使用正文第一张图片作为封面');
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
// fallback:直接从 processedHtml 中查找 img 标签
|
|
195
|
+
const imgMatches = [...processedHtml.matchAll(/<img\s+[^>]*src=["']([^"']+)["'][^>]*>/gi)];
|
|
196
|
+
for (const m of imgMatches) {
|
|
197
|
+
const src = m[1];
|
|
198
|
+
if (src.includes('mmbiz.qpic.cn') || src.includes('mmbiz.qlogo.cn'))
|
|
199
|
+
continue;
|
|
200
|
+
try {
|
|
201
|
+
thumbMediaId = await this.uploadImageFromUrl(src);
|
|
202
|
+
console.log(`[WechatPublisher] 用正文图片作封面: ${src}`);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
catch { /* continue */ }
|
|
179
206
|
}
|
|
180
|
-
catch { /* continue */ }
|
|
181
207
|
}
|
|
182
208
|
}
|
|
183
209
|
if (!thumbMediaId) {
|
|
184
|
-
|
|
210
|
+
try {
|
|
211
|
+
thumbMediaId = await this.uploadPlaceholderCover();
|
|
212
|
+
console.log('[WechatPublisher] 使用占位图作为封面');
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
console.warn('[WechatPublisher] 占位图上传失败,不传 thumb_media_id:', err.message);
|
|
216
|
+
}
|
|
185
217
|
}
|
|
186
218
|
const article = {
|
|
187
219
|
title: options.title,
|
|
@@ -190,8 +222,10 @@ export class WechatPublisher {
|
|
|
190
222
|
digest: options.title,
|
|
191
223
|
content_source_url: options.source_url,
|
|
192
224
|
};
|
|
193
|
-
|
|
225
|
+
// 只在有有效 thumbMediaId 时才传 thumb_media_id
|
|
226
|
+
if (thumbMediaId && thumbMediaId.trim() !== '') {
|
|
194
227
|
article.thumb_media_id = thumbMediaId;
|
|
228
|
+
}
|
|
195
229
|
const payload = { articles: [article] };
|
|
196
230
|
const res = await axios.post(`https://api.weixin.qq.com/cgi-bin/draft/add?access_token=${token}`, payload, { headers: { 'Content-Type': 'application/json' }, responseType: 'json' });
|
|
197
231
|
const data = res.data;
|