|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
} from './base.js';
|
|
|
import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
|
|
|
import { logger } from '../../utils/logger.js';
|
|
|
+import { aiService } from '../../ai/index.js';
|
|
|
|
|
|
// 小红书 Python API 服务配置
|
|
|
const XHS_PYTHON_SERVICE_URL = process.env.XHS_SERVICE_URL || 'http://localhost:5005';
|
|
|
@@ -25,7 +26,7 @@ const SERVER_ROOT = path.resolve(process.cwd());
|
|
|
export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
readonly platform: PlatformType = 'xiaohongshu';
|
|
|
readonly loginUrl = 'https://creator.xiaohongshu.com/';
|
|
|
- readonly publishUrl = 'https://creator.xiaohongshu.com/publish/publish';
|
|
|
+ readonly publishUrl = 'https://creator.xiaohongshu.com/publish/publish?from=menu&target=video';
|
|
|
readonly creatorHomeUrl = 'https://creator.xiaohongshu.com/creator/home';
|
|
|
readonly contentManageUrl = 'https://creator.xiaohongshu.com/creator/content';
|
|
|
|
|
|
@@ -141,6 +142,75 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 关闭页面上可能存在的弹窗对话框
|
|
|
+ */
|
|
|
+ private async closeModalDialogs(): Promise<boolean> {
|
|
|
+ if (!this.page) return false;
|
|
|
+
|
|
|
+ let closedAny = false;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 检查并关闭 Element UI / Vue 弹窗
|
|
|
+ const modalSelectors = [
|
|
|
+ // Element UI 弹窗关闭按钮
|
|
|
+ '.el-dialog__close',
|
|
|
+ '.el-dialog__headerbtn',
|
|
|
+ '.el-message-box__close',
|
|
|
+ '.el-overlay-dialog .el-icon-close',
|
|
|
+ // 通用关闭按钮
|
|
|
+ '[class*="modal"] [class*="close"]',
|
|
|
+ '[class*="dialog"] [class*="close"]',
|
|
|
+ '[role="dialog"] button[aria-label="close"]',
|
|
|
+ '[role="dialog"] [class*="close"]',
|
|
|
+ // 取消/关闭按钮
|
|
|
+ '.el-dialog__footer button:has-text("取消")',
|
|
|
+ '.el-dialog__footer button:has-text("关闭")',
|
|
|
+ '[role="dialog"] button:has-text("取消")',
|
|
|
+ '[role="dialog"] button:has-text("关闭")',
|
|
|
+ // 遮罩层(点击遮罩关闭)
|
|
|
+ '.el-overlay[style*="display: none"]',
|
|
|
+ ];
|
|
|
+
|
|
|
+ for (const selector of modalSelectors) {
|
|
|
+ try {
|
|
|
+ const closeBtn = this.page.locator(selector).first();
|
|
|
+ if (await closeBtn.count() > 0 && await closeBtn.isVisible()) {
|
|
|
+ logger.info(`[Xiaohongshu] Found modal close button: ${selector}`);
|
|
|
+ await closeBtn.click({ timeout: 2000 });
|
|
|
+ closedAny = true;
|
|
|
+ await this.page.waitForTimeout(500);
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 忽略错误,继续尝试下一个选择器
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 尝试按 ESC 键关闭弹窗
|
|
|
+ if (!closedAny) {
|
|
|
+ const hasModal = await this.page.locator('.el-overlay-dialog, [role="dialog"]').count();
|
|
|
+ if (hasModal > 0) {
|
|
|
+ logger.info('[Xiaohongshu] Trying ESC key to close modal...');
|
|
|
+ await this.page.keyboard.press('Escape');
|
|
|
+ await this.page.waitForTimeout(500);
|
|
|
+
|
|
|
+ // 检查是否关闭成功
|
|
|
+ const stillHasModal = await this.page.locator('.el-overlay-dialog, [role="dialog"]').count();
|
|
|
+ closedAny = stillHasModal < hasModal;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (closedAny) {
|
|
|
+ logger.info('[Xiaohongshu] Successfully closed modal dialog');
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ logger.warn('[Xiaohongshu] Error closing modal:', error);
|
|
|
+ }
|
|
|
+
|
|
|
+ return closedAny;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 获取账号信息
|
|
|
* 通过拦截 API 响应获取准确数据
|
|
|
*/
|
|
|
@@ -483,7 +553,14 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
const useHeadless = options?.headless ?? true;
|
|
|
|
|
|
try {
|
|
|
+ logger.info('[Xiaohongshu Publish] Initializing browser...');
|
|
|
await this.initBrowser({ headless: useHeadless });
|
|
|
+
|
|
|
+ if (!this.page) {
|
|
|
+ throw new Error('浏览器初始化失败,page 为 null');
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.info('[Xiaohongshu Publish] Setting cookies...');
|
|
|
await this.setCookies(cookies);
|
|
|
|
|
|
if (!useHeadless) {
|
|
|
@@ -491,7 +568,8 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
onProgress?.(1, '已打开浏览器窗口,请注意查看...');
|
|
|
}
|
|
|
|
|
|
- if (!this.page) throw new Error('Page not initialized');
|
|
|
+ // 再次检查 page 状态
|
|
|
+ if (!this.page) throw new Error('Page not initialized after setCookies');
|
|
|
|
|
|
// 检查视频文件是否存在
|
|
|
const fs = await import('fs');
|
|
|
@@ -530,90 +608,86 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
}
|
|
|
} catch {}
|
|
|
|
|
|
- // 上传视频文件 - 小红书需要点击"上传视频"按钮触发文件选择
|
|
|
+ // 上传视频 - 优先使用 AI 截图分析找到上传入口
|
|
|
let uploadTriggered = false;
|
|
|
-
|
|
|
- // 方法1: 点击"上传视频"按钮触发 file chooser
|
|
|
+
|
|
|
+ // 方法1: AI 截图分析找到上传入口
|
|
|
+ logger.info('[Xiaohongshu Publish] Using AI to find upload entry...');
|
|
|
try {
|
|
|
- logger.info('[Xiaohongshu Publish] Looking for upload button...');
|
|
|
-
|
|
|
- // 小红书的上传按钮通常显示"上传视频"文字
|
|
|
- const uploadBtnSelectors = [
|
|
|
- 'button:has-text("上传视频")',
|
|
|
- 'div:has-text("上传视频"):not(:has(*))', // 纯文字的 div
|
|
|
- '[class*="upload-btn"]',
|
|
|
- '[class*="upload"] button',
|
|
|
- 'span:has-text("上传视频")',
|
|
|
- ];
|
|
|
-
|
|
|
- for (const selector of uploadBtnSelectors) {
|
|
|
+ const screenshot = await this.screenshotBase64();
|
|
|
+ const guide = await aiService.getPageOperationGuide(screenshot, 'xiaohongshu', '找到视频上传入口并点击上传按钮');
|
|
|
+ logger.info(`[Xiaohongshu Publish] AI analysis result:`, guide);
|
|
|
+
|
|
|
+ if (guide.hasAction && guide.targetSelector) {
|
|
|
+ logger.info(`[Xiaohongshu Publish] AI suggested selector: ${guide.targetSelector}`);
|
|
|
try {
|
|
|
- const uploadBtn = this.page.locator(selector).first();
|
|
|
- if (await uploadBtn.count() > 0 && await uploadBtn.isVisible()) {
|
|
|
- logger.info(`[Xiaohongshu Publish] Found upload button via: ${selector}`);
|
|
|
- const [fileChooser] = await Promise.all([
|
|
|
- this.page.waitForEvent('filechooser', { timeout: 15000 }),
|
|
|
- uploadBtn.click(),
|
|
|
- ]);
|
|
|
- await fileChooser.setFiles(params.videoPath);
|
|
|
- uploadTriggered = true;
|
|
|
- logger.info('[Xiaohongshu Publish] File selected via file chooser (button click)');
|
|
|
- break;
|
|
|
- }
|
|
|
+ const [fileChooser] = await Promise.all([
|
|
|
+ this.page.waitForEvent('filechooser', { timeout: 15000 }),
|
|
|
+ this.page.click(guide.targetSelector),
|
|
|
+ ]);
|
|
|
+ await fileChooser.setFiles(params.videoPath);
|
|
|
+ uploadTriggered = true;
|
|
|
+ logger.info('[Xiaohongshu Publish] Upload triggered via AI selector');
|
|
|
} catch (e) {
|
|
|
- logger.warn(`[Xiaohongshu Publish] Button click failed for ${selector}`);
|
|
|
+ logger.warn(`[Xiaohongshu Publish] AI selector click failed: ${e}`);
|
|
|
}
|
|
|
}
|
|
|
} catch (e) {
|
|
|
- logger.warn('[Xiaohongshu Publish] Upload button method failed:', e);
|
|
|
+ logger.warn(`[Xiaohongshu Publish] AI analysis failed: ${e}`);
|
|
|
}
|
|
|
|
|
|
- // 方法2: 点击上传区域(拖拽区域)
|
|
|
+ // 方法2: 尝试点击常见的上传区域触发 file chooser
|
|
|
if (!uploadTriggered) {
|
|
|
- try {
|
|
|
- logger.info('[Xiaohongshu Publish] Trying click upload area...');
|
|
|
- const uploadAreaSelectors = [
|
|
|
- '[class*="upload-wrapper"]',
|
|
|
- '[class*="upload-area"]',
|
|
|
- '[class*="drag-area"]',
|
|
|
- '[class*="drop"]',
|
|
|
- 'div:has-text("拖拽视频到此")',
|
|
|
- ];
|
|
|
-
|
|
|
- for (const selector of uploadAreaSelectors) {
|
|
|
- const uploadArea = this.page.locator(selector).first();
|
|
|
- if (await uploadArea.count() > 0 && await uploadArea.isVisible()) {
|
|
|
- logger.info(`[Xiaohongshu Publish] Found upload area via: ${selector}`);
|
|
|
+ logger.info('[Xiaohongshu Publish] Trying common upload selectors...');
|
|
|
+ const uploadSelectors = [
|
|
|
+ // 小红书常见上传区域选择器
|
|
|
+ '[class*="upload-area"]',
|
|
|
+ '[class*="upload-btn"]',
|
|
|
+ '[class*="upload-trigger"]',
|
|
|
+ '[class*="upload-container"]',
|
|
|
+ '[class*="drag-area"]',
|
|
|
+ 'div[class*="upload"] div',
|
|
|
+ '.upload-wrapper',
|
|
|
+ '.video-upload',
|
|
|
+ 'button:has-text("上传")',
|
|
|
+ 'div:has-text("上传视频"):not(:has(div))',
|
|
|
+ 'span:has-text("上传视频")',
|
|
|
+ '[class*="add-video"]',
|
|
|
+ ];
|
|
|
+
|
|
|
+ for (const selector of uploadSelectors) {
|
|
|
+ if (uploadTriggered) break;
|
|
|
+ try {
|
|
|
+ const element = this.page.locator(selector).first();
|
|
|
+ if (await element.count() > 0 && await element.isVisible()) {
|
|
|
+ logger.info(`[Xiaohongshu Publish] Trying selector: ${selector}`);
|
|
|
const [fileChooser] = await Promise.all([
|
|
|
- this.page.waitForEvent('filechooser', { timeout: 15000 }),
|
|
|
- uploadArea.click(),
|
|
|
+ this.page.waitForEvent('filechooser', { timeout: 5000 }),
|
|
|
+ element.click(),
|
|
|
]);
|
|
|
await fileChooser.setFiles(params.videoPath);
|
|
|
uploadTriggered = true;
|
|
|
- logger.info('[Xiaohongshu Publish] File selected via file chooser (area click)');
|
|
|
- break;
|
|
|
+ logger.info(`[Xiaohongshu Publish] Upload triggered via selector: ${selector}`);
|
|
|
}
|
|
|
+ } catch (e) {
|
|
|
+ // 继续尝试下一个选择器
|
|
|
}
|
|
|
- } catch (e) {
|
|
|
- logger.warn('[Xiaohongshu Publish] Upload area method failed:', e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 方法3: 直接设置隐藏的 file input(最后尝试)
|
|
|
+ // 方法3: 直接设置 file input
|
|
|
if (!uploadTriggered) {
|
|
|
logger.info('[Xiaohongshu Publish] Trying direct file input...');
|
|
|
- const uploadSelectors = [
|
|
|
- 'input[type="file"][accept*="video"]',
|
|
|
- 'input[type="file"]',
|
|
|
- ];
|
|
|
-
|
|
|
- for (const selector of uploadSelectors) {
|
|
|
+ const fileInputs = await this.page.$$('input[type="file"]');
|
|
|
+ logger.info(`[Xiaohongshu Publish] Found ${fileInputs.length} file inputs`);
|
|
|
+
|
|
|
+ for (const fileInput of fileInputs) {
|
|
|
try {
|
|
|
- const fileInput = await this.page.$(selector);
|
|
|
- if (fileInput) {
|
|
|
+ const accept = await fileInput.getAttribute('accept');
|
|
|
+ if (!accept || accept.includes('video') || accept.includes('*')) {
|
|
|
await fileInput.setInputFiles(params.videoPath);
|
|
|
uploadTriggered = true;
|
|
|
- logger.info(`[Xiaohongshu Publish] File set via direct input: ${selector}`);
|
|
|
+ logger.info('[Xiaohongshu Publish] Upload triggered via file input');
|
|
|
|
|
|
// 直接设置后需要等待一下,让页面响应
|
|
|
await this.page.waitForTimeout(2000);
|
|
|
@@ -623,30 +697,23 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
if (hasChange) {
|
|
|
logger.info('[Xiaohongshu Publish] Page responded to file input');
|
|
|
break;
|
|
|
- } else {
|
|
|
- // 如果页面没有响应,尝试触发 change 事件
|
|
|
- await this.page.evaluate((sel) => {
|
|
|
- const input = document.querySelector(sel) as HTMLInputElement;
|
|
|
- if (input) {
|
|
|
- input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
- }
|
|
|
- }, selector);
|
|
|
- await this.page.waitForTimeout(2000);
|
|
|
- logger.info('[Xiaohongshu Publish] Dispatched change event');
|
|
|
}
|
|
|
- break;
|
|
|
}
|
|
|
} catch (e) {
|
|
|
- logger.warn(`[Xiaohongshu Publish] Failed with selector ${selector}`);
|
|
|
+ logger.warn(`[Xiaohongshu Publish] File input method failed: ${e}`);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (!uploadTriggered) {
|
|
|
// 截图调试
|
|
|
- const screenshotPath = `uploads/debug/xhs_no_upload_${Date.now()}.png`;
|
|
|
- await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
- logger.info(`[Xiaohongshu Publish] Screenshot saved: ${screenshotPath}`);
|
|
|
+ try {
|
|
|
+ if (this.page) {
|
|
|
+ const screenshotPath = `uploads/debug/xhs_no_upload_${Date.now()}.png`;
|
|
|
+ await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
+ logger.info(`[Xiaohongshu Publish] Screenshot saved: ${screenshotPath}`);
|
|
|
+ }
|
|
|
+ } catch {}
|
|
|
throw new Error('无法上传视频文件');
|
|
|
}
|
|
|
|
|
|
@@ -655,8 +722,11 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
// 等待视频上传完成
|
|
|
const maxWaitTime = 300000; // 5分钟
|
|
|
const startTime = Date.now();
|
|
|
+ let lastAiCheckTime = 0;
|
|
|
+ const aiCheckInterval = 10000; // 每10秒使用AI检测一次
|
|
|
+ let uploadComplete = false;
|
|
|
|
|
|
- while (Date.now() - startTime < maxWaitTime) {
|
|
|
+ while (Date.now() - startTime < maxWaitTime && !uploadComplete) {
|
|
|
await this.page.waitForTimeout(3000);
|
|
|
|
|
|
// 检查当前URL是否变化(上传成功后可能跳转)
|
|
|
@@ -665,7 +735,8 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
logger.info(`[Xiaohongshu Publish] URL changed to: ${newUrl}`);
|
|
|
}
|
|
|
|
|
|
- // 检查上传进度
|
|
|
+ // 检查上传进度(通过DOM)
|
|
|
+ let progressDetected = false;
|
|
|
const progressText = await this.page.locator('[class*="progress"]').first().textContent().catch(() => '');
|
|
|
if (progressText) {
|
|
|
const match = progressText.match(/(\d+)%/);
|
|
|
@@ -673,6 +744,12 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
const progress = parseInt(match[1]);
|
|
|
onProgress?.(15 + Math.floor(progress * 0.4), `视频上传中: ${progress}%`);
|
|
|
logger.info(`[Xiaohongshu Publish] Upload progress: ${progress}%`);
|
|
|
+ progressDetected = true;
|
|
|
+ if (progress >= 100) {
|
|
|
+ logger.info('[Xiaohongshu Publish] Upload progress reached 100%');
|
|
|
+ uploadComplete = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -691,17 +768,52 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
const count = await this.page.locator(selector).count();
|
|
|
if (count > 0) {
|
|
|
logger.info(`[Xiaohongshu Publish] Video upload completed, found: ${selector}`);
|
|
|
+ uploadComplete = true;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ if (uploadComplete) break;
|
|
|
+
|
|
|
// 如果标题输入框出现,说明可以开始填写了
|
|
|
const titleInput = await this.page.locator('input[placeholder*="标题"]').count();
|
|
|
if (titleInput > 0) {
|
|
|
logger.info('[Xiaohongshu Publish] Title input found, upload must be complete');
|
|
|
+ uploadComplete = true;
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
+ // 使用AI检测上传进度(每隔一段时间检测一次)
|
|
|
+ if (!progressDetected && !uploadComplete && Date.now() - lastAiCheckTime > aiCheckInterval) {
|
|
|
+ lastAiCheckTime = Date.now();
|
|
|
+ try {
|
|
|
+ const screenshot = await this.screenshotBase64();
|
|
|
+ const uploadStatus = await aiService.analyzeUploadProgress(screenshot, 'xiaohongshu');
|
|
|
+ logger.info(`[Xiaohongshu Publish] AI upload status:`, uploadStatus);
|
|
|
+
|
|
|
+ if (uploadStatus.isComplete) {
|
|
|
+ logger.info('[Xiaohongshu Publish] AI detected upload complete');
|
|
|
+ uploadComplete = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (uploadStatus.isFailed) {
|
|
|
+ throw new Error(`视频上传失败: ${uploadStatus.statusDescription}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (uploadStatus.progress !== null) {
|
|
|
+ onProgress?.(15 + Math.floor(uploadStatus.progress * 0.4), `视频上传中: ${uploadStatus.progress}%`);
|
|
|
+ if (uploadStatus.progress >= 100) {
|
|
|
+ logger.info('[Xiaohongshu Publish] AI detected progress 100%');
|
|
|
+ uploadComplete = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (aiError) {
|
|
|
+ logger.warn('[Xiaohongshu Publish] AI progress check failed:', aiError);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 检查是否上传失败
|
|
|
const failText = await this.page.locator('[class*="error"], [class*="fail"]').first().textContent().catch(() => '');
|
|
|
if (failText && failText.includes('失败')) {
|
|
|
@@ -795,13 +907,20 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
const stillInEditMode = await this.page.locator('input[placeholder*="标题"], [class*="video-preview"]').count() > 0;
|
|
|
if (!stillInEditMode) {
|
|
|
logger.error('[Xiaohongshu Publish] Page is no longer in edit mode!');
|
|
|
- const screenshotPath = `uploads/debug/xhs_not_in_edit_${Date.now()}.png`;
|
|
|
- await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
+ try {
|
|
|
+ if (this.page) {
|
|
|
+ const screenshotPath = `uploads/debug/xhs_not_in_edit_${Date.now()}.png`;
|
|
|
+ await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
+ }
|
|
|
+ } catch {}
|
|
|
throw new Error('页面状态异常,请重试');
|
|
|
}
|
|
|
|
|
|
onProgress?.(85, '正在发布...');
|
|
|
|
|
|
+ // 先关闭可能存在的弹窗
|
|
|
+ await this.closeModalDialogs();
|
|
|
+
|
|
|
// 滚动到页面底部,确保发布按钮可见
|
|
|
logger.info('[Xiaohongshu Publish] Scrolling to bottom...');
|
|
|
await this.page.evaluate(() => {
|
|
|
@@ -813,9 +932,13 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
logger.info('[Xiaohongshu Publish] Looking for publish button...');
|
|
|
|
|
|
// 先截图看当前页面状态
|
|
|
- const beforeClickPath = `uploads/debug/xhs_before_publish_${Date.now()}.png`;
|
|
|
- await this.page.screenshot({ path: beforeClickPath, fullPage: true });
|
|
|
- logger.info(`[Xiaohongshu Publish] Screenshot before publish: ${beforeClickPath}`);
|
|
|
+ try {
|
|
|
+ if (this.page) {
|
|
|
+ const beforeClickPath = `uploads/debug/xhs_before_publish_${Date.now()}.png`;
|
|
|
+ await this.page.screenshot({ path: beforeClickPath, fullPage: true });
|
|
|
+ logger.info(`[Xiaohongshu Publish] Screenshot before publish: ${beforeClickPath}`);
|
|
|
+ }
|
|
|
+ } catch {}
|
|
|
|
|
|
let publishClicked = false;
|
|
|
|
|
|
@@ -923,9 +1046,11 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
if (!publishClicked) {
|
|
|
// 截图调试
|
|
|
try {
|
|
|
- const screenshotPath = `uploads/debug/xhs_no_publish_btn_${Date.now()}.png`;
|
|
|
- await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
- logger.info(`[Xiaohongshu Publish] Screenshot saved: ${screenshotPath}`);
|
|
|
+ if (this.page) {
|
|
|
+ const screenshotPath = `uploads/debug/xhs_no_publish_btn_${Date.now()}.png`;
|
|
|
+ await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
+ logger.info(`[Xiaohongshu Publish] Screenshot saved: ${screenshotPath}`);
|
|
|
+ }
|
|
|
} catch {}
|
|
|
throw new Error('未找到发布按钮');
|
|
|
}
|
|
|
@@ -936,6 +1061,8 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
const publishMaxWait = 120000; // 2分钟
|
|
|
const publishStartTime = Date.now();
|
|
|
let aiCheckCounter = 0;
|
|
|
+ let lastProgressCheckTime = 0;
|
|
|
+ const progressCheckInterval = 5000; // 每5秒检测一次发布进度
|
|
|
|
|
|
while (Date.now() - publishStartTime < publishMaxWait) {
|
|
|
await this.page.waitForTimeout(3000);
|
|
|
@@ -965,6 +1092,48 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+ // 检查发布进度条(DOM方式)
|
|
|
+ const publishProgressText = await this.page.locator('[class*="progress"], [class*="loading"]').first().textContent().catch(() => '');
|
|
|
+ if (publishProgressText) {
|
|
|
+ const match = publishProgressText.match(/(\d+)%/);
|
|
|
+ if (match) {
|
|
|
+ const progress = parseInt(match[1]);
|
|
|
+ onProgress?.(90 + Math.floor(progress * 0.1), `发布中: ${progress}%`);
|
|
|
+ logger.info(`[Xiaohongshu Publish] Publish progress: ${progress}%`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // AI检测发布进度(定期检测)
|
|
|
+ if (Date.now() - lastProgressCheckTime > progressCheckInterval) {
|
|
|
+ lastProgressCheckTime = Date.now();
|
|
|
+ try {
|
|
|
+ const screenshot = await this.screenshotBase64();
|
|
|
+ const publishStatus = await aiService.analyzePublishProgress(screenshot, 'xiaohongshu');
|
|
|
+ logger.info(`[Xiaohongshu Publish] AI publish progress status:`, publishStatus);
|
|
|
+
|
|
|
+ if (publishStatus.isComplete) {
|
|
|
+ logger.info('[Xiaohongshu Publish] AI detected publish complete');
|
|
|
+ onProgress?.(100, '发布成功!');
|
|
|
+ await this.closeBrowser();
|
|
|
+ return { success: true, videoUrl: this.page.url() };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (publishStatus.isFailed) {
|
|
|
+ throw new Error(`发布失败: ${publishStatus.statusDescription}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (publishStatus.progress !== null) {
|
|
|
+ onProgress?.(90 + Math.floor(publishStatus.progress * 0.1), `发布中: ${publishStatus.progress}%`);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (publishStatus.isPublishing) {
|
|
|
+ logger.info(`[Xiaohongshu Publish] Still publishing: ${publishStatus.statusDescription}`);
|
|
|
+ }
|
|
|
+ } catch (aiError) {
|
|
|
+ logger.warn('[Xiaohongshu Publish] AI publish progress check failed:', aiError);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 检查错误提示
|
|
|
const errorToast = await this.page.locator('[class*="error"], [class*="fail"]').first().textContent().catch(() => '');
|
|
|
if (errorToast && (errorToast.includes('失败') || errorToast.includes('错误'))) {
|
|
|
@@ -1027,6 +1196,10 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
// AI 建议需要操作
|
|
|
if (aiStatus.status === 'need_action' && aiStatus.nextAction) {
|
|
|
logger.info(`[Xiaohongshu Publish] AI suggests action: ${aiStatus.nextAction.targetDescription}`);
|
|
|
+
|
|
|
+ // 先尝试关闭可能存在的弹窗
|
|
|
+ await this.closeModalDialogs();
|
|
|
+
|
|
|
const guide = await this.aiGetPublishOperationGuide(aiStatus.pageDescription);
|
|
|
if (guide?.hasAction) {
|
|
|
await this.aiExecuteOperation(guide);
|
|
|
@@ -1060,9 +1233,11 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
|
|
|
// 截图调试
|
|
|
try {
|
|
|
- const screenshotPath = `uploads/debug/xhs_publish_timeout_${Date.now()}.png`;
|
|
|
- await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
- logger.info(`[Xiaohongshu Publish] Timeout screenshot saved: ${screenshotPath}`);
|
|
|
+ if (this.page) {
|
|
|
+ const screenshotPath = `uploads/debug/xhs_publish_timeout_${Date.now()}.png`;
|
|
|
+ await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
+ logger.info(`[Xiaohongshu Publish] Timeout screenshot saved: ${screenshotPath}`);
|
|
|
+ }
|
|
|
} catch {}
|
|
|
|
|
|
throw new Error('发布超时,请手动检查是否发布成功');
|
|
|
@@ -1311,9 +1486,11 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
|
|
|
// 截图用于调试
|
|
|
try {
|
|
|
- const screenshotPath = `uploads/debug/xhs_delete_page_${Date.now()}.png`;
|
|
|
- await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
- logger.info(`[Xiaohongshu Delete] Page screenshot: ${screenshotPath}`);
|
|
|
+ if (this.page) {
|
|
|
+ const screenshotPath = `uploads/debug/xhs_delete_page_${Date.now()}.png`;
|
|
|
+ await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
+ logger.info(`[Xiaohongshu Delete] Page screenshot: ${screenshotPath}`);
|
|
|
+ }
|
|
|
} catch {}
|
|
|
|
|
|
// 在笔记管理页面找到对应的笔记行
|
|
|
@@ -1404,9 +1581,11 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
|
|
|
if (!deleteClicked) {
|
|
|
// 截图调试
|
|
|
try {
|
|
|
- const screenshotPath = `uploads/debug/xhs_delete_no_btn_${Date.now()}.png`;
|
|
|
- await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
- logger.info(`[Xiaohongshu Delete] No delete button found, screenshot: ${screenshotPath}`);
|
|
|
+ if (this.page) {
|
|
|
+ const screenshotPath = `uploads/debug/xhs_delete_no_btn_${Date.now()}.png`;
|
|
|
+ await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
|
+ logger.info(`[Xiaohongshu Delete] No delete button found, screenshot: ${screenshotPath}`);
|
|
|
+ }
|
|
|
} catch {}
|
|
|
throw new Error('未找到删除按钮');
|
|
|
}
|