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
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器自动化上传封面图片
|
|
3
|
+
* 在文章编辑页面右侧设置面板中上传封面
|
|
4
|
+
*/
|
|
5
|
+
import { chromium } from 'playwright';
|
|
6
|
+
import { ConfigStore } from '../config.js';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
/**
|
|
9
|
+
* 通过浏览器自动化上传封面图片
|
|
10
|
+
* 流程:打开文章编辑页 -> 在右侧设置面板上传封面
|
|
11
|
+
* @param articleId 文章ID(发布后返回的)
|
|
12
|
+
* @param imagePath 本地图片路径
|
|
13
|
+
* @returns 上传后的封面 URL
|
|
14
|
+
*/
|
|
15
|
+
export async function uploadCoverViaBrowser(articleId, imagePath) {
|
|
16
|
+
const browser = await chromium.launch({
|
|
17
|
+
headless: false,
|
|
18
|
+
channel: 'chromium',
|
|
19
|
+
});
|
|
20
|
+
try {
|
|
21
|
+
const context = await browser.newContext({
|
|
22
|
+
viewport: { width: 1280, height: 720 },
|
|
23
|
+
});
|
|
24
|
+
const page = await context.newPage();
|
|
25
|
+
// 加载 CSDN cookies
|
|
26
|
+
const cookies = await ConfigStore.getCSDNCookies();
|
|
27
|
+
if (!cookies || Object.keys(cookies).length === 0) {
|
|
28
|
+
return { success: false, error: '未配置 CSDN Cookie,请先登录' };
|
|
29
|
+
}
|
|
30
|
+
// 设置 cookies
|
|
31
|
+
const csdnCookies = Object.entries(cookies).map(([name, value]) => ({
|
|
32
|
+
name,
|
|
33
|
+
value,
|
|
34
|
+
domain: '.csdn.net',
|
|
35
|
+
path: '/',
|
|
36
|
+
}));
|
|
37
|
+
await context.addCookies(csdnCookies);
|
|
38
|
+
// 打开文章编辑页
|
|
39
|
+
console.log('[BrowserUpload] 打开文章编辑页...');
|
|
40
|
+
const articleUrl = `https://editor.csdn.net/md/?articleId=${articleId}`;
|
|
41
|
+
await page.goto(articleUrl, { waitUntil: 'networkidle', timeout: 60000 });
|
|
42
|
+
await page.waitForTimeout(2000);
|
|
43
|
+
// 先查找并点击"发布"按钮
|
|
44
|
+
console.log('[BrowserUpload] 查找发布按钮...');
|
|
45
|
+
const publishSelectors = [
|
|
46
|
+
'button:has-text("发布")',
|
|
47
|
+
'button:has-text("立即发布")',
|
|
48
|
+
'[class*="publish"] button',
|
|
49
|
+
];
|
|
50
|
+
let publishButton = null;
|
|
51
|
+
for (const selector of publishSelectors) {
|
|
52
|
+
try {
|
|
53
|
+
const btn = page.locator(selector).first();
|
|
54
|
+
if (await btn.isVisible({ timeout: 1000 })) {
|
|
55
|
+
publishButton = btn;
|
|
56
|
+
console.log(`[BrowserUpload] 找到发布按钮: ${selector}`);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// continue
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!publishButton) {
|
|
65
|
+
console.log('[BrowserUpload] 未找到发布按钮');
|
|
66
|
+
return { success: false, error: '未找到发布按钮' };
|
|
67
|
+
}
|
|
68
|
+
await publishButton.click();
|
|
69
|
+
console.log('[BrowserUpload] 点击发布按钮');
|
|
70
|
+
await page.waitForTimeout(2000);
|
|
71
|
+
// 查找弹窗/对话框中的"添加封面"
|
|
72
|
+
console.log('[BrowserUpload] 查找发布设置弹窗...');
|
|
73
|
+
// 等待弹窗出现
|
|
74
|
+
const dialogSelectors = [
|
|
75
|
+
'[class*="dialog"]',
|
|
76
|
+
'[class*="modal"]',
|
|
77
|
+
'[class*="popup"]',
|
|
78
|
+
'[class*="publish-dialog"]',
|
|
79
|
+
'[class*="publishModal"]',
|
|
80
|
+
];
|
|
81
|
+
for (const selector of dialogSelectors) {
|
|
82
|
+
try {
|
|
83
|
+
const dialog = page.locator(selector).first();
|
|
84
|
+
if (await dialog.isVisible({ timeout: 2000 })) {
|
|
85
|
+
console.log(`[BrowserUpload] 找到弹窗: ${selector}`);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// continue
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// 截图看看弹窗内容
|
|
94
|
+
await page.screenshot({ path: `temp/csdn-publish-dialog-${Date.now()}.png` });
|
|
95
|
+
// 查找"添加封面"标签后面的上传框
|
|
96
|
+
console.log('[BrowserUpload] 查找"添加封面"后面的上传框...');
|
|
97
|
+
let coverInput = null;
|
|
98
|
+
let foundSelector = '';
|
|
99
|
+
// 尝试查找包含"添加封面"文字的元素
|
|
100
|
+
const addCoverLabel = page.locator('text=/添加封面|设置封面|上传封面/i');
|
|
101
|
+
if (await addCoverLabel.count() > 0) {
|
|
102
|
+
console.log(`[BrowserUpload] 找到 ${await addCoverLabel.count()} 个"添加封面"相关元素`);
|
|
103
|
+
// 查找这个元素附近的 file input
|
|
104
|
+
// 尝试找包含"添加封面"的父元素下的 file input
|
|
105
|
+
const parentFileInput = await addCoverLabel.evaluate((el) => {
|
|
106
|
+
// 往上找父元素
|
|
107
|
+
let parent = el.parentElement;
|
|
108
|
+
for (let i = 0; i < 5 && parent; i++) {
|
|
109
|
+
// 找这个父元素下的 input[type="file"]
|
|
110
|
+
const fileInput = parent.querySelector('input[type="file"]');
|
|
111
|
+
if (fileInput) {
|
|
112
|
+
return 'found';
|
|
113
|
+
}
|
|
114
|
+
parent = parent.parentElement;
|
|
115
|
+
}
|
|
116
|
+
return 'not-found';
|
|
117
|
+
});
|
|
118
|
+
console.log(`[BrowserUpload] 父元素下有 file input: ${parentFileInput}`);
|
|
119
|
+
}
|
|
120
|
+
// 查找弹窗中的 file input
|
|
121
|
+
const dialogFileInputs = page.locator('[class*="dialog"] input[type="file"], [class*="modal"] input[type="file"], [class*="popup"] input[type="file"]');
|
|
122
|
+
const dialogInputCount = await dialogFileInputs.count();
|
|
123
|
+
console.log(`[BrowserUpload] 弹窗中有 ${dialogInputCount} 个 file input`);
|
|
124
|
+
if (dialogInputCount > 0) {
|
|
125
|
+
const firstInput = dialogFileInputs.first();
|
|
126
|
+
coverInput = firstInput;
|
|
127
|
+
foundSelector = 'dialog file input';
|
|
128
|
+
console.log(`[BrowserUpload] 使用弹窗中的 file input`);
|
|
129
|
+
}
|
|
130
|
+
// 通用方式:在页面中查找所有 file input
|
|
131
|
+
if (!coverInput) {
|
|
132
|
+
const allFileInputs = page.locator('input[type="file"]');
|
|
133
|
+
const count = await allFileInputs.count();
|
|
134
|
+
console.log(`[BrowserUpload] 页面上共有 ${count} 个 file input`);
|
|
135
|
+
// 优先查找容器封面上传的 input (.el-upload__input)
|
|
136
|
+
const coverUploadInput = page.locator('.el-upload__input');
|
|
137
|
+
if (await coverUploadInput.count() > 0) {
|
|
138
|
+
coverInput = coverUploadInput.first();
|
|
139
|
+
foundSelector = 'el-upload__input';
|
|
140
|
+
console.log(`[BrowserUpload] 使用封面上传 input`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// 回退到可见的 file input
|
|
144
|
+
for (let i = 0; i < count; i++) {
|
|
145
|
+
const element = allFileInputs.nth(i);
|
|
146
|
+
if (await element.isVisible({ timeout: 500 }).catch(() => false)) {
|
|
147
|
+
coverInput = element;
|
|
148
|
+
foundSelector = `file input ${i}`;
|
|
149
|
+
console.log(`[BrowserUpload] 使用 file input ${i}`);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// 最后的尝试:直接查找 input[type="file"] 并检查其容器
|
|
156
|
+
if (!coverInput) {
|
|
157
|
+
console.log('[BrowserUpload] 最后尝试: 查找所有 file input...');
|
|
158
|
+
const allFileInputs = page.locator('input[type="file"]');
|
|
159
|
+
const count = await allFileInputs.count();
|
|
160
|
+
console.log(`[BrowserUpload] 共有 ${count} 个 file input`);
|
|
161
|
+
for (let i = 0; i < count; i++) {
|
|
162
|
+
const element = allFileInputs.nth(i);
|
|
163
|
+
try {
|
|
164
|
+
// 检查这个 input 是否可见
|
|
165
|
+
if (await element.isVisible({ timeout: 500 })) {
|
|
166
|
+
// 检查父元素是否和封面相关
|
|
167
|
+
const parentCover = await element.evaluate((el) => {
|
|
168
|
+
let parent = el.closest('[class*="cover"]') || el.parentElement?.closest('[class*="cover"]');
|
|
169
|
+
return parent ? 'found' : 'not-found';
|
|
170
|
+
});
|
|
171
|
+
if (parentCover === 'found') {
|
|
172
|
+
coverInput = element;
|
|
173
|
+
foundSelector = `file input ${i} (parent is cover)`;
|
|
174
|
+
console.log(`[BrowserUpload] 找到封面 input: ${foundSelector}`);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
// 尝试直接使用这个 input(如果页面上只有 1 个)
|
|
178
|
+
if (count === 1) {
|
|
179
|
+
coverInput = element;
|
|
180
|
+
foundSelector = `file input ${i} (only one on page)`;
|
|
181
|
+
console.log(`[BrowserUpload] 使用唯一的 file input`);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// continue
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// 如果还是没找到,尝试查找"封面"文字并点击
|
|
192
|
+
if (!coverInput) {
|
|
193
|
+
console.log('[BrowserUpload] 尝试点击包含"封面"的元素...');
|
|
194
|
+
const coverText = page.locator('text=/封面|cover/i');
|
|
195
|
+
const textCount = await coverText.count();
|
|
196
|
+
console.log(`[BrowserUpload] 找到 ${textCount} 个包含"封面"的元素`);
|
|
197
|
+
for (let i = 0; i < textCount; i++) {
|
|
198
|
+
const el = coverText.nth(i);
|
|
199
|
+
try {
|
|
200
|
+
// 点击这个元素
|
|
201
|
+
await el.click();
|
|
202
|
+
await page.waitForTimeout(1000);
|
|
203
|
+
console.log(`[BrowserUpload] 点击了包含封面的元素 ${i}`);
|
|
204
|
+
// 点击后重新查找 file input
|
|
205
|
+
const newFileInputs = page.locator('input[type="file"]');
|
|
206
|
+
const newCount = await newFileInputs.count();
|
|
207
|
+
for (let j = 0; j < newCount; j++) {
|
|
208
|
+
const fi = newFileInputs.nth(j);
|
|
209
|
+
if (await fi.isVisible({ timeout: 500 }).catch(() => false)) {
|
|
210
|
+
coverInput = fi;
|
|
211
|
+
foundSelector = `file input after clicking cover text`;
|
|
212
|
+
console.log(`[BrowserUpload] 点击后找到 file input`);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// continue
|
|
219
|
+
}
|
|
220
|
+
if (coverInput)
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!coverInput) {
|
|
225
|
+
console.log('[BrowserUpload] 未找到封面上传入口');
|
|
226
|
+
return { success: false, error: '未找到封面上传入口' };
|
|
227
|
+
}
|
|
228
|
+
// 上传文件
|
|
229
|
+
console.log(`[BrowserUpload] 上传封面: ${imagePath}`);
|
|
230
|
+
const absolutePath = path.resolve(imagePath);
|
|
231
|
+
try {
|
|
232
|
+
// 先点击"从本地上传"按钮来激活上传
|
|
233
|
+
const uploadBtn = page.locator('.upload-img-box').first();
|
|
234
|
+
try {
|
|
235
|
+
if (await uploadBtn.isVisible({ timeout: 1000 })) {
|
|
236
|
+
await uploadBtn.click();
|
|
237
|
+
console.log('[BrowserUpload] 点击了从本地上传按钮');
|
|
238
|
+
await page.waitForTimeout(1000);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// ignore
|
|
243
|
+
}
|
|
244
|
+
// 如果找不到上传按钮,点击封面区域
|
|
245
|
+
if (!(await uploadBtn.isVisible({ timeout: 500 }).catch(() => false))) {
|
|
246
|
+
const coverArea = page.locator('[class*="cover"], [class*="add-cover"]').first();
|
|
247
|
+
try {
|
|
248
|
+
if (await coverArea.isVisible({ timeout: 1000 })) {
|
|
249
|
+
await coverArea.click();
|
|
250
|
+
console.log('[BrowserUpload] 点击了封面区域');
|
|
251
|
+
await page.waitForTimeout(500);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// ignore
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
console.error('[BrowserUpload] 点击上传按钮失败:', err);
|
|
261
|
+
}
|
|
262
|
+
// 执行文件上传
|
|
263
|
+
try {
|
|
264
|
+
await coverInput.setInputFiles(absolutePath);
|
|
265
|
+
console.log('[BrowserUpload] setInputFiles 执行成功');
|
|
266
|
+
// 触发 change 事件
|
|
267
|
+
await coverInput.dispatchEvent('change');
|
|
268
|
+
console.log('[BrowserUpload] 触发 change 事件');
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
console.error('[BrowserUpload] setInputFiles 失败:', err);
|
|
272
|
+
return { success: false, error: `上传失败: ${err.message}` };
|
|
273
|
+
}
|
|
274
|
+
// 等待裁剪弹窗出现
|
|
275
|
+
console.log('[BrowserUpload] 等待裁剪弹窗...');
|
|
276
|
+
await page.waitForTimeout(2000);
|
|
277
|
+
// 点击"确认上传"按钮
|
|
278
|
+
const confirmBtn = page.locator('.vicp-operate-btn:has-text("确认上传"), .el-upload__confirm-upload');
|
|
279
|
+
try {
|
|
280
|
+
if (await confirmBtn.isVisible({ timeout: 3000 })) {
|
|
281
|
+
await confirmBtn.click();
|
|
282
|
+
console.log('[BrowserUpload] 点击确认上传按钮');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
console.log('[BrowserUpload] 未找到确认上传按钮,继续');
|
|
287
|
+
}
|
|
288
|
+
// 等待裁剪弹窗关闭
|
|
289
|
+
await page.waitForTimeout(2000);
|
|
290
|
+
// 点击"保存为草稿"按钮来保存封面设置
|
|
291
|
+
const saveDraftBtn = page.locator('button:has-text("保存为草稿")');
|
|
292
|
+
try {
|
|
293
|
+
if (await saveDraftBtn.isVisible({ timeout: 3000 })) {
|
|
294
|
+
await saveDraftBtn.click();
|
|
295
|
+
console.log('[BrowserUpload] 点击保存为草稿按钮');
|
|
296
|
+
await page.waitForTimeout(2000);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
console.log('[BrowserUpload] 未找到保存为草稿按钮,继续');
|
|
301
|
+
}
|
|
302
|
+
// 等待上传
|
|
303
|
+
console.log('[BrowserUpload] 等待上传...');
|
|
304
|
+
await page.waitForTimeout(5000);
|
|
305
|
+
// 截图看看上传后的状态
|
|
306
|
+
await page.screenshot({ path: `temp/csdn-after-upload-${Date.now()}.png` });
|
|
307
|
+
// 等待上传
|
|
308
|
+
console.log('[BrowserUpload] 等待上传...');
|
|
309
|
+
await page.waitForTimeout(3000);
|
|
310
|
+
// 检查封面预览
|
|
311
|
+
const previewSelectors = [
|
|
312
|
+
'img[src*="obs"]',
|
|
313
|
+
'img[src*="csdn-img"]',
|
|
314
|
+
'[class*="cover"] img[src]',
|
|
315
|
+
'[class*="coverPreview"] img',
|
|
316
|
+
'img.cover',
|
|
317
|
+
// 弹窗中的预览图
|
|
318
|
+
'[class*="modal"] img[src]',
|
|
319
|
+
'[class*="dialog"] img[src]',
|
|
320
|
+
];
|
|
321
|
+
for (const selector of previewSelectors) {
|
|
322
|
+
try {
|
|
323
|
+
const imgs = page.locator(selector);
|
|
324
|
+
const count = await imgs.count();
|
|
325
|
+
for (let i = 0; i < count; i++) {
|
|
326
|
+
const img = imgs.nth(i);
|
|
327
|
+
const src = await img.getAttribute('src').catch(() => '');
|
|
328
|
+
if (src && (src.includes('obs') || src.includes('csdn-img') || src.includes('https'))) {
|
|
329
|
+
console.log(`[BrowserUpload] 封面上传成功: ${src}`);
|
|
330
|
+
return { success: true, coverUrl: src };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// continue
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// 如果没找到特定 URL,但上传操作执行了,就认为成功
|
|
339
|
+
console.log('[BrowserUpload] 封面上传完成(未检测到预览 URL)');
|
|
340
|
+
return { success: true, coverUrl: 'uploaded' };
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
console.error('[BrowserUpload] 上传失败:', err);
|
|
344
|
+
return { success: false, error: err.message };
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
await browser.close();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface CapturedRequest {
|
|
2
|
+
id: string;
|
|
3
|
+
timestamp: number;
|
|
4
|
+
method: string;
|
|
5
|
+
url: string;
|
|
6
|
+
headers: Record<string, string>;
|
|
7
|
+
postData?: string;
|
|
8
|
+
responseStatus?: number;
|
|
9
|
+
responseBody?: string;
|
|
10
|
+
matched: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface CaptureData {
|
|
13
|
+
platform: string;
|
|
14
|
+
captureTime: string;
|
|
15
|
+
timeout: number;
|
|
16
|
+
editorUrl: string;
|
|
17
|
+
uploadApiUrl?: string;
|
|
18
|
+
requests: CapturedRequest[];
|
|
19
|
+
summary: {
|
|
20
|
+
totalRequests: number;
|
|
21
|
+
matchedRequests: number;
|
|
22
|
+
uploadEndpoints: string[];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare function capture(platform: string, timeoutMs?: number): Promise<CaptureData | null>;
|
|
26
|
+
export {};
|