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,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用抓包工具
|
|
3
|
+
*
|
|
4
|
+
* 使用方式:
|
|
5
|
+
* node dist/tools/capture.js <platform> [timeout]
|
|
6
|
+
*
|
|
7
|
+
* 示例:
|
|
8
|
+
* node dist/tools/capture.js csdn # 抓取 CSDN (60s 超时)
|
|
9
|
+
* node dist/tools/capture.js csdn 120 # 抓取 CSDN (120s 超时)
|
|
10
|
+
* node dist/tools/capture.js juejin # 抓取掘金
|
|
11
|
+
*/
|
|
12
|
+
import { chromium } from 'playwright';
|
|
13
|
+
import { ConfigStore } from '../config.js';
|
|
14
|
+
import fs from 'fs/promises';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
// 平台配置
|
|
17
|
+
const PLATFORM_CONFIGS = {
|
|
18
|
+
csdn: {
|
|
19
|
+
editorUrl: 'https://editor.csdn.net/md/',
|
|
20
|
+
uploadPatterns: [/uploadImg/i, /upload/i, /file/i, /image/i],
|
|
21
|
+
},
|
|
22
|
+
juejin: {
|
|
23
|
+
editorUrl: 'https://editor.juejin.cn/',
|
|
24
|
+
uploadPatterns: [/upload/i, /file/i, /image/i, /img/i],
|
|
25
|
+
},
|
|
26
|
+
zhihu: {
|
|
27
|
+
editorUrl: 'https://zhuanlan.zhihu.com/write',
|
|
28
|
+
uploadPatterns: [/upload/i, /file/i, /image/i, /img/i],
|
|
29
|
+
},
|
|
30
|
+
toutiao: {
|
|
31
|
+
editorUrl: 'https://mp.toutiao.com/',
|
|
32
|
+
uploadPatterns: [/upload/i, /file/i, /image/i, /img/i],
|
|
33
|
+
},
|
|
34
|
+
jianshu: {
|
|
35
|
+
editorUrl: 'https://www.jianshu.com/writer',
|
|
36
|
+
uploadPatterns: [/upload/i, /file/i, /image/i, /img/i],
|
|
37
|
+
},
|
|
38
|
+
weibo: {
|
|
39
|
+
editorUrl: 'https://weibo.com/compose',
|
|
40
|
+
uploadPatterns: [/upload/i, /file/i, /image/i, /img/i],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
function isUploadRequest(url, headers, postData) {
|
|
44
|
+
const contentType = headers['content-type'] || '';
|
|
45
|
+
const isMultipart = contentType.includes('multipart/form-data');
|
|
46
|
+
const isUploadUrl = PLATFORM_CONFIGS[Object.keys(PLATFORM_CONFIGS)[0]]?.uploadPatterns.some(p => p.test(url));
|
|
47
|
+
// 检查 URL 是否匹配上传模式
|
|
48
|
+
for (const config of Object.values(PLATFORM_CONFIGS)) {
|
|
49
|
+
if (config.uploadPatterns.some(p => p.test(url))) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// 如果是 multipart/form-data,很可能是上传
|
|
54
|
+
if (isMultipart) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
async function loadCookies(platform) {
|
|
60
|
+
try {
|
|
61
|
+
switch (platform) {
|
|
62
|
+
case 'csdn': return await ConfigStore.getCSDNCookies() || {};
|
|
63
|
+
case 'zhihu': return await ConfigStore.getZhihuCookies() || {};
|
|
64
|
+
case 'juejin': return await ConfigStore.getJuejinCookies() || {};
|
|
65
|
+
case 'toutiao': return await ConfigStore.getToutiaoCookies() || {};
|
|
66
|
+
case 'jianshu': return await ConfigStore.getJianshuCookies() || {};
|
|
67
|
+
case 'weibo': return await ConfigStore.getWeiboCookies() || {};
|
|
68
|
+
default: return {};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function getCookieDomains(platform) {
|
|
76
|
+
const domains = {
|
|
77
|
+
csdn: ['.csdn.net', '.bizapi.csdn.net'],
|
|
78
|
+
zhihu: ['.zhihu.com'],
|
|
79
|
+
juejin: ['.juejin.cn', '.掘金.com'],
|
|
80
|
+
toutiao: ['.toutiao.com'],
|
|
81
|
+
jianshu: ['.jianshu.com'],
|
|
82
|
+
weibo: ['.weibo.com', '.sina.com.cn'],
|
|
83
|
+
};
|
|
84
|
+
return domains[platform] || [];
|
|
85
|
+
}
|
|
86
|
+
export async function capture(platform, timeoutMs = 60000) {
|
|
87
|
+
const config = PLATFORM_CONFIGS[platform];
|
|
88
|
+
if (!config) {
|
|
89
|
+
console.error(`不支持的平台: ${platform}`);
|
|
90
|
+
console.error(`支持的平台: ${Object.keys(PLATFORM_CONFIGS).join(', ')}`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
console.log(`
|
|
94
|
+
========================================
|
|
95
|
+
通用抓包工具 - ${platform.toUpperCase()}
|
|
96
|
+
========================================
|
|
97
|
+
平台: ${platform}
|
|
98
|
+
编辑器: ${config.editorUrl}
|
|
99
|
+
超时: ${timeoutMs / 1000}秒
|
|
100
|
+
========================================
|
|
101
|
+
`);
|
|
102
|
+
// 加载 cookies
|
|
103
|
+
const cookies = await loadCookies(platform);
|
|
104
|
+
if (Object.keys(cookies).length === 0) {
|
|
105
|
+
console.warn(`⚠️ 未找到 ${platform} 的 cookies,可能需要先登录`);
|
|
106
|
+
console.warn(` 运行: node dist/cli/index.js login --platform ${platform}`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
console.log(`✅ 已加载 ${Object.keys(cookies).length} 个 cookies`);
|
|
110
|
+
}
|
|
111
|
+
// 启动浏览器
|
|
112
|
+
const browser = await chromium.launch({
|
|
113
|
+
headless: false,
|
|
114
|
+
channel: 'chromium',
|
|
115
|
+
});
|
|
116
|
+
const context = await browser.newContext();
|
|
117
|
+
const page = await context.newPage();
|
|
118
|
+
// 设置 cookies
|
|
119
|
+
const cookieDomains = getCookieDomains(platform);
|
|
120
|
+
for (const [name, value] of Object.entries(cookies)) {
|
|
121
|
+
for (const domain of cookieDomains) {
|
|
122
|
+
try {
|
|
123
|
+
await context.addCookies([{
|
|
124
|
+
name,
|
|
125
|
+
value,
|
|
126
|
+
domain,
|
|
127
|
+
path: '/',
|
|
128
|
+
}]);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// 忽略无效 cookie
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// 抓包数据
|
|
137
|
+
const requests = [];
|
|
138
|
+
let lastActivityTime = Date.now();
|
|
139
|
+
// 拦截所有请求
|
|
140
|
+
page.on('request', async (request) => {
|
|
141
|
+
const url = request.url();
|
|
142
|
+
const headers = request.headers();
|
|
143
|
+
const method = request.method();
|
|
144
|
+
const contentType = headers['content-type'] || '';
|
|
145
|
+
// 只记录 API 请求
|
|
146
|
+
if (!url.includes('api') && !url.includes('bizapi')) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const captured = {
|
|
150
|
+
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
method,
|
|
153
|
+
url,
|
|
154
|
+
headers,
|
|
155
|
+
matched: isUploadRequest(url, headers),
|
|
156
|
+
};
|
|
157
|
+
// 获取 post data
|
|
158
|
+
try {
|
|
159
|
+
const postData = await request.postData();
|
|
160
|
+
if (postData) {
|
|
161
|
+
captured.postData = postData;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// ignore
|
|
166
|
+
}
|
|
167
|
+
requests.push(captured);
|
|
168
|
+
lastActivityTime = Date.now();
|
|
169
|
+
// 打印关键请求
|
|
170
|
+
if (captured.matched || url.includes('upload')) {
|
|
171
|
+
console.log(`\n📤 [${method}] ${url}`);
|
|
172
|
+
if (contentType) {
|
|
173
|
+
console.log(` Content-Type: ${contentType.substring(0, 50)}...`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
// 拦截响应
|
|
178
|
+
page.on('response', async (response) => {
|
|
179
|
+
const url = response.url();
|
|
180
|
+
const status = response.status();
|
|
181
|
+
// 找到对应的请求并更新
|
|
182
|
+
const captured = requests.find(r => r.url === url);
|
|
183
|
+
if (captured) {
|
|
184
|
+
captured.responseStatus = status;
|
|
185
|
+
// 尝试获取响应体
|
|
186
|
+
try {
|
|
187
|
+
const body = await response.text();
|
|
188
|
+
if (body && body.length < 10000) {
|
|
189
|
+
captured.responseBody = body;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// ignore
|
|
194
|
+
}
|
|
195
|
+
// 打印上传响应
|
|
196
|
+
if (captured.matched) {
|
|
197
|
+
console.log(` ← ${status} ${captured.responseBody?.substring(0, 100) || ''}...`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
// 监听浏览器关闭
|
|
202
|
+
const browserClosedPromise = new Promise((resolve) => {
|
|
203
|
+
browser.on('disconnected', () => {
|
|
204
|
+
console.log('\n🔌 浏览器已关闭,停止抓包...');
|
|
205
|
+
resolve();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
// 超时 Promise
|
|
209
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
console.log(`\n⏱️ 超时 (${timeoutMs / 1000}秒),停止抓包...`);
|
|
212
|
+
resolve();
|
|
213
|
+
}, timeoutMs);
|
|
214
|
+
});
|
|
215
|
+
// 无操作超时检测
|
|
216
|
+
const activityCheckPromise = new Promise((resolve) => {
|
|
217
|
+
const checkInterval = setInterval(() => {
|
|
218
|
+
if (Date.now() - lastActivityTime > timeoutMs) {
|
|
219
|
+
console.log('\n⏱️ 长时间无活动,停止抓包...');
|
|
220
|
+
clearInterval(checkInterval);
|
|
221
|
+
resolve();
|
|
222
|
+
}
|
|
223
|
+
}, 5000);
|
|
224
|
+
});
|
|
225
|
+
// 打开编辑器
|
|
226
|
+
console.log(`\n🌐 打开编辑器: ${config.editorUrl}`);
|
|
227
|
+
await page.goto(config.editorUrl, { waitUntil: 'networkidle', timeout: 30000 });
|
|
228
|
+
console.log('✅ 编辑器已加载');
|
|
229
|
+
console.log('\n📋 操作提示:');
|
|
230
|
+
console.log(' 1. 在编辑器中找到上传图片功能');
|
|
231
|
+
console.log(' 2. 上传一张图片');
|
|
232
|
+
console.log(' 3. 完成后关闭浏览器');
|
|
233
|
+
console.log('\n🕐 开始抓包,等待操作...\n');
|
|
234
|
+
// 等待浏览器关闭或超时
|
|
235
|
+
await Promise.race([
|
|
236
|
+
browserClosedPromise,
|
|
237
|
+
Promise.race([timeoutPromise, activityCheckPromise]),
|
|
238
|
+
]);
|
|
239
|
+
// 关闭浏览器
|
|
240
|
+
try {
|
|
241
|
+
await browser.close();
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// ignore
|
|
245
|
+
}
|
|
246
|
+
// 分析抓包数据
|
|
247
|
+
const uploadEndpoints = [...new Set(requests.filter(r => r.matched).map(r => {
|
|
248
|
+
try {
|
|
249
|
+
return new URL(r.url).pathname;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return r.url;
|
|
253
|
+
}
|
|
254
|
+
}))];
|
|
255
|
+
const captureData = {
|
|
256
|
+
platform,
|
|
257
|
+
captureTime: new Date().toISOString(),
|
|
258
|
+
timeout: timeoutMs,
|
|
259
|
+
editorUrl: config.editorUrl,
|
|
260
|
+
uploadApiUrl: uploadEndpoints[0],
|
|
261
|
+
requests: requests.map(r => ({
|
|
262
|
+
...r,
|
|
263
|
+
responseBody: r.responseBody?.substring(0, 500), // 截断响应体
|
|
264
|
+
})),
|
|
265
|
+
summary: {
|
|
266
|
+
totalRequests: requests.length,
|
|
267
|
+
matchedRequests: requests.filter(r => r.matched).length,
|
|
268
|
+
uploadEndpoints,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
return captureData;
|
|
272
|
+
}
|
|
273
|
+
async function main() {
|
|
274
|
+
const args = process.argv.slice(2);
|
|
275
|
+
const platform = args[0] || 'csdn';
|
|
276
|
+
const timeout = parseInt(args[1] || '60', 10) * 1000;
|
|
277
|
+
if (!PLATFORM_CONFIGS[platform]) {
|
|
278
|
+
console.error(`不支持的平台: ${platform}`);
|
|
279
|
+
console.error(`支持的平台: ${Object.keys(PLATFORM_CONFIGS).join(', ')}`);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
const data = await capture(platform, timeout);
|
|
283
|
+
if (!data) {
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
// 保存到临时文件
|
|
287
|
+
const timestamp = Date.now();
|
|
288
|
+
const filename = `temp/capture-${platform}-${timestamp}.json`;
|
|
289
|
+
const filepath = path.resolve(process.cwd(), filename);
|
|
290
|
+
// 确保 temp 目录存在
|
|
291
|
+
await fs.mkdir(path.dirname(filepath), { recursive: true });
|
|
292
|
+
await fs.writeFile(filepath, JSON.stringify(data, null, 2), 'utf-8');
|
|
293
|
+
console.log(`
|
|
294
|
+
========================================
|
|
295
|
+
抓包完成
|
|
296
|
+
========================================
|
|
297
|
+
文件: ${filepath}
|
|
298
|
+
|
|
299
|
+
📊 统计:
|
|
300
|
+
总请求数: ${data.summary.totalRequests}
|
|
301
|
+
命中请求: ${data.summary.matchedRequests}
|
|
302
|
+
上传端点: ${data.summary.uploadEndpoints.join(', ') || '无'}
|
|
303
|
+
|
|
304
|
+
========================================
|
|
305
|
+
`);
|
|
306
|
+
if (data.summary.uploadEndpoints.length > 0) {
|
|
307
|
+
console.log('🎯 检测到上传请求!');
|
|
308
|
+
console.log(' 把这个文件路径告诉 AI,让它分析签名格式');
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
console.log('⚠️ 未检测到上传请求');
|
|
312
|
+
console.log(' 可能原因:');
|
|
313
|
+
console.log(' 1. 没有执行上传操作');
|
|
314
|
+
console.log(' 2. 上传功能需要先登录');
|
|
315
|
+
console.log(' 3. 平台上传 API 不在拦截范围内');
|
|
316
|
+
}
|
|
317
|
+
console.log('\n运行 AI 分析:');
|
|
318
|
+
console.log(` 帮我分析 ${platform} 的抓包数据: ${filepath}`);
|
|
319
|
+
}
|
|
320
|
+
// 仅在直接运行时执行(不在被 import 时执行)
|
|
321
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
322
|
+
if (isMainModule) {
|
|
323
|
+
main().catch(console.error);
|
|
324
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ToutiaoUploadResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
coverUrl?: string;
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* 通过浏览器自动化上传头条号封面
|
|
8
|
+
* 流程:打开发布页 -> 填写内容 -> 点击预览并发布 -> 在预览弹窗中上传封面 -> 确认
|
|
9
|
+
*/
|
|
10
|
+
export declare function uploadCoverViaBrowser(imagePath: string): Promise<ToutiaoUploadResult>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 头条号封面图片浏览器上传
|
|
3
|
+
*/
|
|
4
|
+
import { chromium } from 'playwright';
|
|
5
|
+
import { ConfigStore } from '../config.js';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
/**
|
|
8
|
+
* 通过浏览器自动化上传头条号封面
|
|
9
|
+
* 流程:打开发布页 -> 填写内容 -> 点击预览并发布 -> 在预览弹窗中上传封面 -> 确认
|
|
10
|
+
*/
|
|
11
|
+
export async function uploadCoverViaBrowser(imagePath) {
|
|
12
|
+
const browser = await chromium.launch({
|
|
13
|
+
headless: false,
|
|
14
|
+
args: [
|
|
15
|
+
'--disable-blink-features=AutomationControlled',
|
|
16
|
+
'--disable-devtools-shm-usage',
|
|
17
|
+
'--no-sandbox',
|
|
18
|
+
]
|
|
19
|
+
});
|
|
20
|
+
try {
|
|
21
|
+
const context = await browser.newContext({
|
|
22
|
+
viewport: { width: 1280, height: 900 },
|
|
23
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
24
|
+
});
|
|
25
|
+
const page = await context.newPage();
|
|
26
|
+
// CDP 隐藏自动化特征
|
|
27
|
+
const cdp = await context.newCDPSession(page);
|
|
28
|
+
await cdp.send('Page.addScriptToEvaluateOnNewDocument', {
|
|
29
|
+
source: `
|
|
30
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
31
|
+
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
|
|
32
|
+
Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en'] });
|
|
33
|
+
window.chrome = { runtime: {}, loadTimes: () => {}, csi: () => {} };
|
|
34
|
+
`
|
|
35
|
+
});
|
|
36
|
+
// 加载 cookies
|
|
37
|
+
const cookies = await ConfigStore.getToutiaoCookies();
|
|
38
|
+
if (!cookies || Object.keys(cookies).length === 0) {
|
|
39
|
+
return { success: false, error: '未配置头条号 Cookie' };
|
|
40
|
+
}
|
|
41
|
+
const toutiaoCookies = Object.entries(cookies).map(([name, value]) => ({
|
|
42
|
+
name,
|
|
43
|
+
value,
|
|
44
|
+
domain: '.toutiao.com',
|
|
45
|
+
path: '/',
|
|
46
|
+
}));
|
|
47
|
+
await context.addCookies(toutiaoCookies);
|
|
48
|
+
console.log('[ToutiaoUpload] 打开发布页...');
|
|
49
|
+
await page.goto('https://mp.toutiao.com/profile_v4/graphic/publish', {
|
|
50
|
+
waitUntil: 'networkidle',
|
|
51
|
+
timeout: 60000
|
|
52
|
+
});
|
|
53
|
+
await page.waitForTimeout(2000);
|
|
54
|
+
// 关闭可能存在的遮罩层
|
|
55
|
+
try {
|
|
56
|
+
const mask = page.locator('.byte-drawer-mask').first();
|
|
57
|
+
if (await mask.isVisible({ timeout: 1000 })) {
|
|
58
|
+
await mask.click({ force: true });
|
|
59
|
+
await page.waitForTimeout(500);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch { }
|
|
63
|
+
// 截图
|
|
64
|
+
await page.screenshot({ path: `temp/toutiao-upload-start-${Date.now()}.png` });
|
|
65
|
+
// 填写标题和内容(让用户之前已经填好了,这里只做演示)
|
|
66
|
+
const titleInput = page.locator('textarea').first();
|
|
67
|
+
if (await titleInput.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
68
|
+
const currentTitle = await titleInput.inputValue().catch(() => '');
|
|
69
|
+
if (!currentTitle) {
|
|
70
|
+
await titleInput.fill('临时标题-封面测试');
|
|
71
|
+
console.log('[ToutiaoUpload] 填写了临时标题');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// 填写内容(如果为空)
|
|
75
|
+
const contentEl = page.locator('div[contenteditable="true"]').first();
|
|
76
|
+
if (await contentEl.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
77
|
+
const currentContent = await contentEl.textContent().catch(() => '');
|
|
78
|
+
if (!currentContent || currentContent.trim() === '') {
|
|
79
|
+
await contentEl.click();
|
|
80
|
+
await page.keyboard.press('Control+a');
|
|
81
|
+
await page.keyboard.type('临时内容-封面测试');
|
|
82
|
+
console.log('[ToutiaoUpload] 填写了临时内容');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// 点击"预览并发布"按钮
|
|
86
|
+
console.log('[ToutiaoUpload] 点击预览并发布按钮...');
|
|
87
|
+
const publishBtn = page.locator('button:has-text("预览并发布")');
|
|
88
|
+
if (await publishBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
89
|
+
await publishBtn.click({ force: true });
|
|
90
|
+
console.log('[ToutiaoUpload] 已点击预览并发布');
|
|
91
|
+
await page.waitForTimeout(3000);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
return { success: false, error: '未找到预览并发布按钮' };
|
|
95
|
+
}
|
|
96
|
+
// 在预览弹窗中选择"单图"选项
|
|
97
|
+
console.log('[ToutiaoUpload] 选择单图选项...');
|
|
98
|
+
const singleImageOption = page.locator('text=单图').first();
|
|
99
|
+
if (await singleImageOption.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
100
|
+
await singleImageOption.click();
|
|
101
|
+
console.log('[ToutiaoUpload] 点击了单图');
|
|
102
|
+
await page.waitForTimeout(500);
|
|
103
|
+
}
|
|
104
|
+
// 截图
|
|
105
|
+
await page.screenshot({ path: `temp/toutiao-upload-select-cover-${Date.now()}.png` });
|
|
106
|
+
// 设置 filechooser 监听
|
|
107
|
+
const absolutePath = path.resolve(imagePath);
|
|
108
|
+
console.log(`[ToutiaoUpload] 准备上传: ${absolutePath}`);
|
|
109
|
+
page.on('filechooser', async (fileChooser) => {
|
|
110
|
+
console.log('[ToutiaoUpload] 收到 filechooser,设置文件');
|
|
111
|
+
await fileChooser.setFiles(absolutePath);
|
|
112
|
+
});
|
|
113
|
+
// 点击封面添加区域触发 filechooser
|
|
114
|
+
console.log('[ToutiaoUpload] 点击封面添加区域...');
|
|
115
|
+
const coverAdd = page.locator('.article-cover-add');
|
|
116
|
+
if (await coverAdd.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
117
|
+
await coverAdd.click({ force: true });
|
|
118
|
+
console.log('[ToutiaoUpload] 点击了 article-cover-add');
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
return { success: false, error: '未找到封面添加区域' };
|
|
122
|
+
}
|
|
123
|
+
// 等待文件选择
|
|
124
|
+
await page.waitForTimeout(2000);
|
|
125
|
+
// 点击"本地上传"按钮
|
|
126
|
+
console.log('[ToutiaoUpload] 点击本地上传按钮...');
|
|
127
|
+
const localUploadBtn = page.locator('text=本地上传');
|
|
128
|
+
if (await localUploadBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
129
|
+
await localUploadBtn.click();
|
|
130
|
+
console.log('[ToutiaoUpload] 点击了本地上传');
|
|
131
|
+
await page.waitForTimeout(2000);
|
|
132
|
+
}
|
|
133
|
+
// 截图看预览
|
|
134
|
+
await page.screenshot({ path: `temp/toutiao-upload-preview-${Date.now()}.png` });
|
|
135
|
+
// 点击"确定"按钮
|
|
136
|
+
console.log('[ToutiaoUpload] 点击确定按钮...');
|
|
137
|
+
const confirmBtn = page.locator('button:has-text("确定")').filter({ visible: true });
|
|
138
|
+
try {
|
|
139
|
+
if (await confirmBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
140
|
+
const disabled = await confirmBtn.first().isDisabled().catch(() => true);
|
|
141
|
+
if (!disabled) {
|
|
142
|
+
await confirmBtn.first().click();
|
|
143
|
+
console.log('[ToutiaoUpload] 点击了确定按钮');
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
console.log('[ToutiaoUpload] 确定按钮被禁用');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
console.log('[ToutiaoUpload] 未找到确定按钮');
|
|
152
|
+
}
|
|
153
|
+
// 等待保存
|
|
154
|
+
await page.waitForTimeout(3000);
|
|
155
|
+
await page.screenshot({ path: `temp/toutiao-upload-finish-${Date.now()}.png` });
|
|
156
|
+
console.log('[ToutiaoUpload] 封面上传流程完成');
|
|
157
|
+
return { success: true, coverUrl: 'uploaded' };
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.error('[ToutiaoUpload] 上传失败:', err);
|
|
161
|
+
return { success: false, error: err.message };
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
await browser.close();
|
|
165
|
+
}
|
|
166
|
+
}
|