multi-publisher 1.1.0 → 1.1.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 +16 -7
- package/dist/adapters/csdn.d.ts +15 -0
- package/dist/adapters/csdn.js +120 -1
- package/dist/adapters/toutiao.js +89 -38
- package/dist/cli/capture.d.ts +6 -0
- package/dist/cli/capture.js +49 -0
- package/dist/cli/index.js +7 -0
- package/dist/tools/browser-upload.d.ts +13 -0
- package/dist/tools/browser-upload.js +349 -0
- package/dist/tools/capture.d.ts +26 -0
- package/dist/tools/capture.js +324 -0
- package/dist/tools/toutiao-upload.d.ts +10 -0
- package/dist/tools/toutiao-upload.js +166 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**一行命令,Markdown 发布到全网。**
|
|
4
4
|
|
|
5
|
-
将排版好的 Markdown 文章一键发布到微信公众号、知乎、掘金、CSDN 等 20+ 平台,无需手动复制粘贴。支持浏览器自动登录获取 Cookie,
|
|
5
|
+
将排版好的 Markdown 文章一键发布到微信公众号、知乎、掘金、CSDN 等 20+ 平台,无需手动复制粘贴。支持浏览器自动登录获取 Cookie,14 套渲染主题,是内容创作者的效率神器。
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
| **多平台支持** | 微信公众号、知乎、掘金、CSDN、微博、小红书、B站等 20+ 平台 |
|
|
14
14
|
| **浏览器自动登录** | Playwright 驱动扫码/账号登录,Cookie 自动获取保存 |
|
|
15
15
|
| **14 套渲染主题** | default、wechat、modern、minimal、cyberpunk、nord、paper、darkelite、sunset、zen、retro、midnight、brutalism、neumorphism |
|
|
16
|
+
| **自动封面图生成** | AI 根据标题生成封面图,自动上传到微信 CDN |
|
|
16
17
|
| **Markdown 直发** | front-matter 元数据、代码高亮、LaTeX 公式 |
|
|
17
18
|
| **CI/CD 友好** | 纯命令行无需浏览器,配置文件统一管理 |
|
|
18
19
|
| **草稿箱发布** | 微信公众号 → 草稿箱,其他平台 → 各自草稿箱 |
|
|
@@ -95,6 +96,12 @@ mpub render -f article.md
|
|
|
95
96
|
|
|
96
97
|
# 指定主题渲染
|
|
97
98
|
mpub render -f article.md -t cyberpunk
|
|
99
|
+
|
|
100
|
+
# 自动封面图(文章无封面时 AI 生成)
|
|
101
|
+
mpub publish -f article.md -p weixin --auto-cover
|
|
102
|
+
|
|
103
|
+
# 指定封面图
|
|
104
|
+
mpub publish -f article.md -p weixin -c cover.jpg
|
|
98
105
|
```
|
|
99
106
|
|
|
100
107
|
---
|
|
@@ -199,14 +206,16 @@ pre { background: #f6f8fa; border-radius: 6px; }
|
|
|
199
206
|
|
|
200
207
|
### front-matter 元数据
|
|
201
208
|
|
|
209
|
+
> **重要**:front-matter 是必需的!缺少 front-matter 会导致标题显示为"无标题"。
|
|
210
|
+
|
|
202
211
|
```markdown
|
|
203
212
|
---
|
|
204
|
-
title: 文章标题
|
|
205
|
-
author: 作者名
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
source_url: https://original.url
|
|
209
|
-
tags: [技术, 前端, JavaScript]
|
|
213
|
+
title: 文章标题 # 必填,用于公众号标题
|
|
214
|
+
author: 作者名 # 必填
|
|
215
|
+
description: 文章描述(可选) # 用于公众号摘要
|
|
216
|
+
cover: https://example.com/cover.jpg # 可选,有 --auto-cover 可省略
|
|
217
|
+
source_url: https://original.url # 原文链接(可选)
|
|
218
|
+
tags: [技术, 前端, JavaScript] # 标签(可选)
|
|
210
219
|
---
|
|
211
220
|
|
|
212
221
|
正文内容...
|
package/dist/adapters/csdn.d.ts
CHANGED
|
@@ -24,6 +24,21 @@ export declare class CSDNAdapter implements IPlatformAdapter {
|
|
|
24
24
|
* 生成 CSDN API 签名
|
|
25
25
|
*/
|
|
26
26
|
private signRequest;
|
|
27
|
+
/**
|
|
28
|
+
* 上传图片到 CSDN
|
|
29
|
+
* 流程:1. 调用签名 API 获取 policy 2. 直接上传到华为云 OBS
|
|
30
|
+
* @param imagePath 本地文件路径或网络 URL
|
|
31
|
+
* @returns CSDN 图片 URL
|
|
32
|
+
*/
|
|
33
|
+
uploadImage(imagePath: string): Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* 获取华为云 OBS 上传签名
|
|
36
|
+
*/
|
|
37
|
+
private getUploadSignature;
|
|
38
|
+
/**
|
|
39
|
+
* 处理封面图:上传本地文件或使用网络 URL
|
|
40
|
+
*/
|
|
41
|
+
private processCover;
|
|
27
42
|
checkAuth(): Promise<AuthResult>;
|
|
28
43
|
publish(article: Article): Promise<SyncResult>;
|
|
29
44
|
}
|
package/dist/adapters/csdn.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { ConfigStore } from '../config.js';
|
|
2
|
+
import { uploadCoverViaBrowser } from '../tools/browser-upload.js';
|
|
2
3
|
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
3
7
|
// CSDN API 签名密钥
|
|
4
8
|
const API_KEY = '203803574';
|
|
5
9
|
const API_SECRET = '9znpamsyl2c7cdrr9sas0le9vbc3r6ba';
|
|
10
|
+
// 封面图上传 API 的 key 和 secret
|
|
11
|
+
const COVER_API_KEY = '260196572';
|
|
12
|
+
const COVER_API_SECRET = 't5PaqxVQpWoHgLGt7XPIvd5ipJcwJTU7';
|
|
6
13
|
export class CSDNAdapter {
|
|
7
14
|
meta = {
|
|
8
15
|
id: 'csdn',
|
|
@@ -75,6 +82,106 @@ export class CSDNAdapter {
|
|
|
75
82
|
}
|
|
76
83
|
return headers;
|
|
77
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* 上传图片到 CSDN
|
|
87
|
+
* 流程:1. 调用签名 API 获取 policy 2. 直接上传到华为云 OBS
|
|
88
|
+
* @param imagePath 本地文件路径或网络 URL
|
|
89
|
+
* @returns CSDN 图片 URL
|
|
90
|
+
*/
|
|
91
|
+
async uploadImage(imagePath) {
|
|
92
|
+
if (!this.cookieData?.cookies) {
|
|
93
|
+
throw new Error('未配置 CSDN Cookie');
|
|
94
|
+
}
|
|
95
|
+
// 如果是网络 URL,直接返回
|
|
96
|
+
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
|
|
97
|
+
return imagePath;
|
|
98
|
+
}
|
|
99
|
+
// 读取本地文件
|
|
100
|
+
const fileBuffer = await fs.readFile(imagePath);
|
|
101
|
+
const filename = path.basename(imagePath);
|
|
102
|
+
const ext = path.extname(filename).toLowerCase();
|
|
103
|
+
const imageSuffix = ext.replace('.', '');
|
|
104
|
+
try {
|
|
105
|
+
// 步骤 1: 获取上传签名 (policy)
|
|
106
|
+
const signatureData = await this.getUploadSignature(imageSuffix);
|
|
107
|
+
const { accessId, policy, signature, host, dir } = signatureData;
|
|
108
|
+
// 步骤 2: 直接上传到华为云 OBS
|
|
109
|
+
const objectKey = `${dir}${filename}`;
|
|
110
|
+
const obsUrl = `${host}/${objectKey}`;
|
|
111
|
+
const formData = new FormData();
|
|
112
|
+
formData.append('key', objectKey);
|
|
113
|
+
formData.append('OSSAccessKeyId', accessId);
|
|
114
|
+
formData.append('policy', policy);
|
|
115
|
+
formData.append('signature', signature);
|
|
116
|
+
formData.append('expire', signatureData.expire);
|
|
117
|
+
formData.append('file', new Blob([fileBuffer]), filename);
|
|
118
|
+
const response = await this.runtime.fetch(obsUrl, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
body: formData,
|
|
121
|
+
});
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
throw new Error(`OBS 上传失败: ${response.status}`);
|
|
124
|
+
}
|
|
125
|
+
// 返回 CSDN 图片 URL
|
|
126
|
+
return `https://csdn-img-blog.obs.cn-north-4.myhuaweicloud.com/${objectKey}`;
|
|
127
|
+
}
|
|
128
|
+
catch (apiError) {
|
|
129
|
+
throw new Error(`封面 API 上传失败: ${apiError.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 获取华为云 OBS 上传签名
|
|
134
|
+
*/
|
|
135
|
+
async getUploadSignature(imageSuffix) {
|
|
136
|
+
const cookieStr = this.buildCookieString(this.cookieData);
|
|
137
|
+
// CSDN 封面图签名 API 使用的 key 和之前的不同
|
|
138
|
+
const coverApiKey = '260196572';
|
|
139
|
+
const nonce = this.createUuid();
|
|
140
|
+
const timestamp = Date.now().toString();
|
|
141
|
+
const apiPath = '/resource-api/v1/image/direct/upload/signature';
|
|
142
|
+
// 按照 Lt 函数格式: method\naccept\n\ncontentType\ntimestamp\n(sorted headers)\nurl
|
|
143
|
+
const signStr = `POST\n*/*\n\napplication/json\n${timestamp}\nx-ca-key:${coverApiKey}\nx-ca-nonce:${nonce}\nx-ca-timestamp:${timestamp}\n${apiPath}`;
|
|
144
|
+
// 封面签名使用专用的 secret
|
|
145
|
+
const signature = await this.hmacSha256(signStr, COVER_API_SECRET);
|
|
146
|
+
const response = await this.runtime.fetch(`https://bizapi.csdn.net${apiPath}`, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: {
|
|
149
|
+
'accept': 'application/json',
|
|
150
|
+
'x-ca-key': coverApiKey,
|
|
151
|
+
'x-ca-nonce': nonce,
|
|
152
|
+
'x-ca-timestamp': timestamp,
|
|
153
|
+
'x-ca-signature': signature,
|
|
154
|
+
'x-ca-signature-headers': 'x-ca-key,x-ca-nonce,x-ca-timestamp',
|
|
155
|
+
'content-type': 'application/json',
|
|
156
|
+
'Cookie': cookieStr,
|
|
157
|
+
},
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
imageTemplate: '',
|
|
160
|
+
appName: 'direct_blog_coverimage',
|
|
161
|
+
imageSuffix,
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
const res = await response.json();
|
|
165
|
+
if (res.code !== 200 || !res.data) {
|
|
166
|
+
throw new Error(res.message || '获取上传签名失败');
|
|
167
|
+
}
|
|
168
|
+
return res.data;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 处理封面图:上传本地文件或使用网络 URL
|
|
172
|
+
*/
|
|
173
|
+
async processCover(cover) {
|
|
174
|
+
if (!cover)
|
|
175
|
+
return [];
|
|
176
|
+
try {
|
|
177
|
+
const coverUrl = await this.uploadImage(cover);
|
|
178
|
+
return [coverUrl];
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.warn(`[CSDN] 封面上传失败: ${err.message}`);
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
78
185
|
async checkAuth() {
|
|
79
186
|
if (!this.cookieData?.cookies) {
|
|
80
187
|
return { isAuthenticated: false, error: '未配置 CSDN Cookie,请运行: mpub cookie --platform csdn --set' };
|
|
@@ -118,6 +225,7 @@ export class CSDNAdapter {
|
|
|
118
225
|
// 生成签名
|
|
119
226
|
const apiPath = '/blog-console-api/v3/mdeditor/saveArticle';
|
|
120
227
|
const headers = await this.signRequest(apiPath);
|
|
228
|
+
// 先发布文章(不带封面,因为 API 签名有问题)
|
|
121
229
|
const response = await this.runtime.fetch(`https://bizapi.csdn.net${apiPath}`, {
|
|
122
230
|
method: 'POST',
|
|
123
231
|
headers: {
|
|
@@ -139,7 +247,7 @@ export class CSDNAdapter {
|
|
|
139
247
|
not_auto_saved: '1',
|
|
140
248
|
source: 'pc_mdeditor',
|
|
141
249
|
cover_images: [],
|
|
142
|
-
cover_type:
|
|
250
|
+
cover_type: 0,
|
|
143
251
|
is_new: 1,
|
|
144
252
|
vote_id: 0,
|
|
145
253
|
resource_id: '',
|
|
@@ -153,6 +261,17 @@ export class CSDNAdapter {
|
|
|
153
261
|
}
|
|
154
262
|
const postId = res.data.id;
|
|
155
263
|
const draftUrl = `https://editor.csdn.net/md?articleId=${postId}`;
|
|
264
|
+
// 如果有封面图片,通过浏览器自动化上传
|
|
265
|
+
if (article.cover && existsSync(article.cover)) {
|
|
266
|
+
console.log(`[CSDN] 封面上传中...`);
|
|
267
|
+
const coverResult = await uploadCoverViaBrowser(postId, article.cover);
|
|
268
|
+
if (coverResult.success && coverResult.coverUrl) {
|
|
269
|
+
console.log(`[CSDN] 封面 URL: ${coverResult.coverUrl}`);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
console.warn(`[CSDN] 封面上传失败: ${coverResult.error}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
156
275
|
return {
|
|
157
276
|
platform: this.meta.id,
|
|
158
277
|
success: true,
|
package/dist/adapters/toutiao.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { chromium } from 'playwright';
|
|
6
6
|
import { ConfigStore } from '../config.js';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
7
9
|
export class ToutiaoAdapter {
|
|
8
10
|
meta = {
|
|
9
11
|
id: 'toutiao',
|
|
@@ -22,7 +24,7 @@ export class ToutiaoAdapter {
|
|
|
22
24
|
}
|
|
23
25
|
const hasSession = this.cookieData['sessionid'] || this.cookieData['sid_tt'];
|
|
24
26
|
if (!hasSession) {
|
|
25
|
-
return { isAuthenticated: false, error: '
|
|
27
|
+
return { isAuthenticated: false, error: '登录已过期' };
|
|
26
28
|
}
|
|
27
29
|
return { isAuthenticated: true };
|
|
28
30
|
}
|
|
@@ -99,7 +101,6 @@ export class ToutiaoAdapter {
|
|
|
99
101
|
const contentEl = page.locator('div[contenteditable="true"]').first();
|
|
100
102
|
if (await contentEl.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
101
103
|
await contentEl.click();
|
|
102
|
-
// 使用 Ctrl+A 全选,然后输入内容(模拟用户输入)
|
|
103
104
|
await page.keyboard.press('Control+a');
|
|
104
105
|
await page.waitForTimeout(200);
|
|
105
106
|
const htmlContent = article.html || article.markdown || '';
|
|
@@ -107,59 +108,109 @@ export class ToutiaoAdapter {
|
|
|
107
108
|
const div = document.querySelector('[contenteditable="true"]');
|
|
108
109
|
if (div) {
|
|
109
110
|
div.innerHTML = el;
|
|
110
|
-
// 触发 input 事件
|
|
111
111
|
div.dispatchEvent(new InputEvent('input', { bubbles: true }));
|
|
112
112
|
}
|
|
113
113
|
}, htmlContent);
|
|
114
114
|
console.log('[toutiao] 已填写内容');
|
|
115
115
|
await page.waitForTimeout(1000);
|
|
116
116
|
}
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
// 如果有封面图片,先上传封面再发布
|
|
118
|
+
if (article.cover && existsSync(article.cover)) {
|
|
119
|
+
console.log('[toutiao] 检测到封面图片,准备上传...');
|
|
120
|
+
// 点击"预览并发布"按钮打开预览弹窗
|
|
121
|
+
const previewBtn = page.locator('button:has-text("预览并发布")');
|
|
122
|
+
if (await previewBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
123
|
+
await previewBtn.click({ force: true });
|
|
124
|
+
console.log('[toutiao] 点击预览并发布按钮');
|
|
125
|
+
await page.waitForTimeout(2000);
|
|
126
|
+
// 选择"单图"选项
|
|
127
|
+
const singleImg = page.locator('text=单图').first();
|
|
128
|
+
if (await singleImg.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
129
|
+
await singleImg.click();
|
|
130
|
+
console.log('[toutiao] 选择单图');
|
|
131
|
+
await page.waitForTimeout(500);
|
|
132
|
+
}
|
|
133
|
+
// 设置 filechooser 监听
|
|
134
|
+
const absolutePath = path.resolve(article.cover);
|
|
135
|
+
page.on('filechooser', async (fileChooser) => {
|
|
136
|
+
console.log('[toutiao] 收到 filechooser,设置封面文件');
|
|
137
|
+
await fileChooser.setFiles(absolutePath);
|
|
138
|
+
});
|
|
139
|
+
// 点击封面添加区域
|
|
140
|
+
const coverAdd = page.locator('.article-cover-add');
|
|
141
|
+
if (await coverAdd.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
142
|
+
await coverAdd.click({ force: true });
|
|
143
|
+
console.log('[toutiao] 点击了封面添加区域');
|
|
144
|
+
await page.waitForTimeout(1500);
|
|
145
|
+
}
|
|
146
|
+
// 点击"本地上传"
|
|
147
|
+
const localUpload = page.locator('text=本地上传');
|
|
148
|
+
if (await localUpload.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
149
|
+
await localUpload.click();
|
|
150
|
+
console.log('[toutiao] 点击本地上传');
|
|
151
|
+
await page.waitForTimeout(1500);
|
|
152
|
+
}
|
|
153
|
+
// 点击"确定"按钮
|
|
154
|
+
const confirmBtn = page.locator('button:has-text("确定")').filter({ visible: true });
|
|
155
|
+
try {
|
|
156
|
+
if (await confirmBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
157
|
+
const disabled = await confirmBtn.first().isDisabled().catch(() => true);
|
|
158
|
+
if (!disabled) {
|
|
159
|
+
await confirmBtn.first().click();
|
|
160
|
+
console.log('[toutiao] 点击确定按钮,封面上传完成');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
console.log('[toutiao] 确定按钮点击失败:', e.message);
|
|
166
|
+
}
|
|
167
|
+
// 等待封面上传保存(头条号会自动保存)
|
|
168
|
+
await page.waitForTimeout(5000);
|
|
124
169
|
}
|
|
125
|
-
await page.waitForTimeout(1000);
|
|
126
|
-
}
|
|
127
|
-
// 检查 URL 是否包含文章 ID
|
|
128
|
-
let currentUrl = page.url();
|
|
129
|
-
let idMatch = currentUrl.match(/[?&]id=(\d+)/);
|
|
130
|
-
let savedId = idMatch ? idMatch[1] : null;
|
|
131
|
-
if (idMatch) {
|
|
132
|
-
console.log('[toutiao] 检测到文章 ID:', idMatch[1]);
|
|
133
170
|
}
|
|
134
|
-
|
|
135
|
-
console.log('[toutiao] URL 仍未变化:', currentUrl);
|
|
136
|
-
}
|
|
137
|
-
// 尝试点击"发布"按钮触发保存
|
|
138
|
-
console.log('[toutiao] 尝试点击发布按钮...');
|
|
171
|
+
// 关闭预览弹窗(如果还在的话)
|
|
139
172
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
await
|
|
144
|
-
console.log('[toutiao] 已点击发布按钮');
|
|
145
|
-
// 等待保存完成
|
|
146
|
-
await page.waitForTimeout(10000);
|
|
173
|
+
const continueBtn = page.locator('button:has-text("继续编辑")');
|
|
174
|
+
if (await continueBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
175
|
+
await continueBtn.click({ force: true });
|
|
176
|
+
await page.waitForTimeout(2000);
|
|
147
177
|
}
|
|
148
178
|
else {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
await
|
|
153
|
-
|
|
154
|
-
|
|
179
|
+
const closeIcon = page.locator('[class*="close"], .ai-assistant-drawer-wrapper [class*="close"]');
|
|
180
|
+
if (await closeIcon.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
181
|
+
await closeIcon.click({ force: true });
|
|
182
|
+
await page.waitForTimeout(1000);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
// 关闭弹窗失败,继续
|
|
188
|
+
}
|
|
189
|
+
// 等待编辑器稳定
|
|
190
|
+
await page.waitForTimeout(2000);
|
|
191
|
+
// 点击发布按钮
|
|
192
|
+
console.log('[toutiao] 查找发布按钮...');
|
|
193
|
+
try {
|
|
194
|
+
// 尝试多种发布按钮选择器
|
|
195
|
+
const publishSelectors = [
|
|
196
|
+
'button:has-text("发布")',
|
|
197
|
+
'button:has-text("预览并发布")',
|
|
198
|
+
];
|
|
199
|
+
for (const selector of publishSelectors) {
|
|
200
|
+
const btn = page.locator(selector);
|
|
201
|
+
if (await btn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
202
|
+
await btn.click({ force: true });
|
|
203
|
+
console.log('[toutiao] 点击了:', selector);
|
|
204
|
+
break;
|
|
155
205
|
}
|
|
156
206
|
}
|
|
207
|
+
await page.waitForTimeout(10000);
|
|
157
208
|
}
|
|
158
209
|
catch (e) {
|
|
159
|
-
console.log('[toutiao]
|
|
210
|
+
console.log('[toutiao] 发布按钮点击失败:', e.message);
|
|
160
211
|
}
|
|
161
212
|
const finalUrl = page.url();
|
|
162
|
-
idMatch = finalUrl.match(/[?&]id=(\d+)/);
|
|
213
|
+
const idMatch = finalUrl.match(/[?&]id=(\d+)/);
|
|
163
214
|
if (idMatch) {
|
|
164
215
|
console.log('[toutiao] 保存成功,文章 ID:', idMatch[1]);
|
|
165
216
|
return {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* capture 命令入口
|
|
3
|
+
*/
|
|
4
|
+
import { capture } from '../tools/capture.js';
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
export async function runCapture(options) {
|
|
8
|
+
const platform = options.platform || 'csdn';
|
|
9
|
+
const timeoutSeconds = parseInt(options.timeout || '60', 10);
|
|
10
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
11
|
+
const data = await capture(platform, timeoutMs);
|
|
12
|
+
if (!data) {
|
|
13
|
+
console.error('抓包失败');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
// 保存到临时文件
|
|
17
|
+
const timestamp = Date.now();
|
|
18
|
+
const filename = `temp/capture-${platform}-${timestamp}.json`;
|
|
19
|
+
const filepath = path.resolve(process.cwd(), filename);
|
|
20
|
+
// 确保 temp 目录存在
|
|
21
|
+
await fs.mkdir(path.dirname(filepath), { recursive: true });
|
|
22
|
+
await fs.writeFile(filepath, JSON.stringify(data, null, 2), 'utf-8');
|
|
23
|
+
console.log(`
|
|
24
|
+
========================================
|
|
25
|
+
抓包完成
|
|
26
|
+
========================================
|
|
27
|
+
文件: ${filepath}
|
|
28
|
+
|
|
29
|
+
📊 统计:
|
|
30
|
+
总请求数: ${data.summary.totalRequests}
|
|
31
|
+
命中请求: ${data.summary.matchedRequests}
|
|
32
|
+
上传端点: ${data.summary.uploadEndpoints.join(', ') || '无'}
|
|
33
|
+
|
|
34
|
+
========================================
|
|
35
|
+
`);
|
|
36
|
+
if (data.summary.uploadEndpoints.length > 0) {
|
|
37
|
+
console.log('🎯 检测到上传请求!');
|
|
38
|
+
console.log(' 把这个文件路径告诉 AI,让它分析签名格式');
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.log('⚠️ 未检测到上传请求');
|
|
42
|
+
console.log(' 可能原因:');
|
|
43
|
+
console.log(' 1. 没有执行上传操作');
|
|
44
|
+
console.log(' 2. 上传功能需要先登录');
|
|
45
|
+
console.log(' 3. 平台上传 API 不在拦截范围内');
|
|
46
|
+
}
|
|
47
|
+
console.log('\n运行 AI 分析:');
|
|
48
|
+
console.log(` 帮我分析 ${platform} 的抓包数据: ${filepath}`);
|
|
49
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { runCredential } from './credential.js';
|
|
|
11
11
|
import { runCookie } from './cookie.js';
|
|
12
12
|
import { runLogin } from './login.js';
|
|
13
13
|
import { runPublishAll } from './publish-all.js';
|
|
14
|
+
import { runCapture } from './capture.js';
|
|
14
15
|
export function createProgram() {
|
|
15
16
|
const program = new Command();
|
|
16
17
|
program
|
|
@@ -68,6 +69,12 @@ export function createProgram() {
|
|
|
68
69
|
.option('--mac-style', '启用 Mac 风格代码块', true)
|
|
69
70
|
.option('--no-mac-style', '禁用 Mac 风格代码块')
|
|
70
71
|
.action(runPublishAll);
|
|
72
|
+
// capture 命令
|
|
73
|
+
const captureCmd = program.command('capture')
|
|
74
|
+
.description('抓包工具 - 打开浏览器并拦截上传请求')
|
|
75
|
+
.option('-p, --platform <platform>', '平台名称 (csdn|juejin|zhihu|toutiao|jianshu|weibo)', 'csdn')
|
|
76
|
+
.option('-t, --timeout <seconds>', '超时时间(秒)', '60')
|
|
77
|
+
.action(runCapture);
|
|
71
78
|
return program;
|
|
72
79
|
}
|
|
73
80
|
const program = createProgram();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface BrowserUploadResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
coverUrl?: string;
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* 通过浏览器自动化上传封面图片
|
|
8
|
+
* 流程:打开文章编辑页 -> 在右侧设置面板上传封面
|
|
9
|
+
* @param articleId 文章ID(发布后返回的)
|
|
10
|
+
* @param imagePath 本地图片路径
|
|
11
|
+
* @returns 上传后的封面 URL
|
|
12
|
+
*/
|
|
13
|
+
export declare function uploadCoverViaBrowser(articleId: string, imagePath: string): Promise<BrowserUploadResult>;
|