| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797 |
- /// <reference lib="dom" />
- import path from 'path';
- import { BasePlatformAdapter } from './base.js';
- import type {
- AccountProfile,
- PublishParams,
- PublishResult,
- DateRange,
- AnalyticsData,
- CommentData,
- } from './base.js';
- import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
- import { logger } from '../../utils/logger.js';
- import { aiService } from '../../ai/index.js';
- import { getPythonServiceBaseUrl } from '../../services/PythonServiceConfigService.js';
- // 服务器根目录(用于构造绝对路径)
- const SERVER_ROOT = path.resolve(process.cwd());
- /**
- * 百家号平台适配器
- */
- export class BaijiahaoAdapter extends BasePlatformAdapter {
- readonly platform: PlatformType = 'baijiahao';
- readonly loginUrl = 'https://baijiahao.baidu.com/';
- readonly publishUrl = 'https://baijiahao.baidu.com/builder/rc/edit?type=videoV2&is_from_cms=1';
-
- protected getCookieDomain(): string {
- return '.baidu.com';
- }
- /**
- * 检查 Python 发布服务是否可用
- */
- private async checkPythonServiceAvailable(): Promise<boolean> {
- try {
- const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
- const response = await fetch(`${pythonUrl}/health`, {
- method: 'GET',
- signal: AbortSignal.timeout(3000),
- });
- if (response.ok) {
- const data = await response.json();
- return data.status === 'ok' && data.supported_platforms?.includes('baijiahao');
- }
- return false;
- } catch {
- return false;
- }
- }
-
- async getQRCode(): Promise<QRCodeInfo> {
- try {
- await this.initBrowser();
-
- if (!this.page) throw new Error('Page not initialized');
-
- // 访问百家号登录页面
- await this.page.goto('https://baijiahao.baidu.com/', {
- waitUntil: 'domcontentloaded',
- timeout: 30000,
- });
-
- // 等待二维码出现
- await this.page.waitForSelector('img[src*="qrcode"]', { timeout: 15000 });
-
- // 获取二维码图片
- const qrcodeImg = await this.page.$('img[src*="qrcode"]');
- const qrcodeUrl = await qrcodeImg?.getAttribute('src');
-
- if (!qrcodeUrl) {
- throw new Error('Failed to get QR code');
- }
-
- return {
- qrcodeUrl,
- qrcodeKey: `baijiahao_${Date.now()}`,
- expireTime: Date.now() + 300000,
- };
- } catch (error) {
- logger.error('Baijiahao getQRCode error:', error);
- throw error;
- }
- }
-
- async checkQRCodeStatus(qrcodeKey: string): Promise<LoginStatusResult> {
- try {
- if (!this.page) {
- return { status: 'expired', message: '二维码已过期' };
- }
-
- // 检查是否登录成功(URL 变化或出现用户信息)
- const currentUrl = this.page.url();
-
- if (currentUrl.includes('/builder/rc/home') || currentUrl.includes('/builder/rc/content')) {
- const cookies = await this.getCookies();
- await this.closeBrowser();
-
- return {
- status: 'success',
- message: '登录成功',
- cookies,
- };
- }
-
- // 检查是否需要手机验证
- const phoneInput = await this.page.$('input[type="tel"]');
- if (phoneInput) {
- return { status: 'scanned', message: '需要手机验证' };
- }
-
- return { status: 'pending', message: '等待扫码' };
- } catch (error) {
- logger.error('Baijiahao checkQRCodeStatus error:', error);
- return { status: 'expired', message: '检查状态失败' };
- }
- }
-
- async checkLoginStatus(cookies: string): Promise<boolean> {
- try {
- await this.initBrowser({ headless: true });
- await this.setCookies(cookies);
-
- if (!this.page) throw new Error('Page not initialized');
-
- // 访问百家号后台首页
- await this.page.goto('https://baijiahao.baidu.com/builder/rc/home', {
- waitUntil: 'domcontentloaded',
- timeout: 30000,
- });
-
- await this.page.waitForTimeout(2000);
-
- const currentUrl = this.page.url();
- const isLoggedIn = currentUrl.includes('/builder/rc/') && !currentUrl.includes('login');
-
- await this.closeBrowser();
- return isLoggedIn;
- } catch (error) {
- logger.error('Baijiahao checkLoginStatus error:', error);
- await this.closeBrowser();
- return false;
- }
- }
-
- /**
- * 关闭页面上可能存在的弹窗对话框
- */
- private async closeModalDialogs(): Promise<boolean> {
- if (!this.page) return false;
-
- let closedAny = false;
-
- try {
- const modalSelectors = [
- // 百家号常见弹窗关闭按钮
- '.Dialog-close',
- '.modal-close',
- '[class*="dialog"] [class*="close"]',
- '[class*="modal"] [class*="close"]',
- '[role="dialog"] button[aria-label="close"]',
- '.ant-modal-close',
- 'button:has-text("关闭")',
- 'button:has-text("取消")',
- 'button:has-text("我知道了")',
- 'button:has-text("暂不")',
- '.close-btn',
- ];
-
- for (const selector of modalSelectors) {
- try {
- const closeBtn = this.page.locator(selector).first();
- if (await closeBtn.count() > 0 && await closeBtn.isVisible()) {
- logger.info(`[Baijiahao] 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('[class*="dialog"], [class*="modal"], [role="dialog"]').count();
- if (hasModal > 0) {
- logger.info('[Baijiahao] Trying ESC key to close modal...');
- await this.page.keyboard.press('Escape');
- await this.page.waitForTimeout(500);
- closedAny = true;
- }
- }
-
- if (closedAny) {
- logger.info('[Baijiahao] Successfully closed modal dialog');
- }
-
- } catch (error) {
- logger.warn('[Baijiahao] Error closing modal:', error);
- }
-
- return closedAny;
- }
- async getAccountInfo(cookies: string): Promise<AccountProfile> {
- try {
- await this.initBrowser({ headless: true });
- await this.setCookies(cookies);
-
- if (!this.page) throw new Error('Page not initialized');
-
- // 访问设置页面获取账号信息
- await this.page.goto('https://baijiahao.baidu.com/builder/rc/home', {
- waitUntil: 'domcontentloaded',
- timeout: 30000,
- });
-
- await this.page.waitForTimeout(3000);
-
- // 尝试从页面获取账号信息
- const accountInfo = await this.page.evaluate(() => {
- // 尝试获取用户名
- const nameEl = document.querySelector('.user-name, .user-info .name, [class*="author-name"]');
- const name = nameEl?.textContent?.trim() || '';
-
- // 尝试获取头像
- const avatarEl = document.querySelector('.user-avatar img, .author-avatar img, [class*="avatar"] img');
- const avatar = avatarEl?.getAttribute('src') || '';
-
- return { name, avatar };
- });
-
- await this.closeBrowser();
-
- return {
- accountId: `baijiahao_${Date.now()}`,
- accountName: accountInfo.name || '百家号账号',
- avatarUrl: accountInfo.avatar || '',
- fansCount: 0,
- worksCount: 0,
- };
- } catch (error) {
- logger.error('Baijiahao getAccountInfo error:', error);
- await this.closeBrowser();
- return {
- accountId: '',
- accountName: '百家号账号',
- avatarUrl: '',
- fansCount: 0,
- worksCount: 0,
- };
- }
- }
-
- /**
- * 通过 Python 服务发布视频(带 AI 辅助)
- */
- private async publishVideoViaPython(
- cookies: string,
- params: PublishParams,
- onProgress?: (progress: number, message: string) => void
- ): Promise<PublishResult> {
- logger.info('[Baijiahao Python] Starting publish via Python service with AI assist...');
- onProgress?.(5, '正在通过 Python 服务发布...');
- try {
- // 准备 cookie 字符串
- let cookieStr = cookies;
- try {
- const cookieArray = JSON.parse(cookies);
- if (Array.isArray(cookieArray)) {
- cookieStr = cookieArray.map((c: { name: string; value: string }) => `${c.name}=${c.value}`).join('; ');
- }
- } catch {
- // 已经是字符串格式
- }
- // 将相对路径转换为绝对路径
- const absoluteVideoPath = path.isAbsolute(params.videoPath)
- ? params.videoPath
- : path.resolve(SERVER_ROOT, params.videoPath);
- const absoluteCoverPath = params.coverPath
- ? (path.isAbsolute(params.coverPath) ? params.coverPath : path.resolve(SERVER_ROOT, params.coverPath))
- : undefined;
- // 使用 AI 辅助发布接口
- const extra = (params.extra || {}) as Record<string, unknown>;
- const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
- const response = await fetch(`${pythonUrl}/publish/ai-assisted`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- platform: 'baijiahao',
- cookie: cookieStr,
- user_id: (extra as any).userId,
- publish_task_id: (extra as any).publishTaskId,
- publish_account_id: (extra as any).publishAccountId,
- proxy: (extra as any).publishProxy || null,
- title: params.title,
- description: params.description || params.title,
- video_path: absoluteVideoPath,
- cover_path: absoluteCoverPath,
- tags: params.tags || [],
- post_time: params.scheduledTime ? new Date(params.scheduledTime).toISOString().replace('T', ' ').slice(0, 19) : null,
- return_screenshot: true,
- }),
- signal: AbortSignal.timeout(600000), // 10分钟超时
- });
- const result = await response.json();
- logger.info('[Baijiahao Python] Response:', { ...result, screenshot_base64: result.screenshot_base64 ? '[截图已省略]' : undefined });
- // 使用通用的 AI 辅助处理方法
- return await this.aiProcessPythonPublishResult(result, undefined, onProgress);
- } catch (error) {
- logger.error('[Baijiahao Python] Publish failed:', error);
- throw error;
- }
- }
- async publishVideo(
- cookies: string,
- params: PublishParams,
- onProgress?: (progress: number, message: string) => void,
- onCaptchaRequired?: (captchaInfo: { taskId: string; type: 'sms' | 'image'; phone?: string; imageBase64?: string }) => Promise<string>,
- options?: { headless?: boolean }
- ): Promise<PublishResult> {
- // 只使用 Python 服务发布
- const pythonAvailable = await this.checkPythonServiceAvailable();
- if (!pythonAvailable) {
- logger.error('[Baijiahao] Python service not available');
- return {
- success: false,
- errorMessage: 'Python 发布服务不可用,请确保 Python 服务已启动',
- };
- }
- logger.info('[Baijiahao] Using Python service for publishing');
- try {
- const result = await this.publishVideoViaPython(cookies, params, onProgress);
-
- // 检查是否需要验证码
- if (!result.success && result.errorMessage?.includes('验证码')) {
- logger.info('[Baijiahao] Python detected captcha, need headful browser');
- return {
- success: false,
- errorMessage: `CAPTCHA_REQUIRED:${result.errorMessage}`,
- };
- }
-
- return result;
- } catch (pythonError) {
- logger.error('[Baijiahao] Python publish failed:', pythonError);
- return {
- success: false,
- errorMessage: pythonError instanceof Error ? pythonError.message : '发布失败',
- };
- }
- /* ========== Playwright 方式已注释,只使用 Python API ==========
- const useHeadless = options?.headless ?? true;
- try {
- await this.initBrowser({ headless: useHeadless });
- await this.setCookies(cookies);
- if (!this.page) throw new Error('Page not initialized');
- onProgress?.(5, '正在打开发布页面...');
- // 访问发布页面
- await this.page.goto(this.publishUrl, {
- waitUntil: 'domcontentloaded',
- timeout: 60000,
- });
- await this.page.waitForTimeout(3000);
- // 先关闭可能存在的弹窗
- await this.closeModalDialogs();
- // 检查是否需要登录
- const currentUrl = this.page.url();
- if (currentUrl.includes('login') || currentUrl.includes('passport')) {
- await this.closeBrowser();
- return {
- success: false,
- errorMessage: '账号登录已过期,请重新登录',
- };
- }
- // 再次关闭可能的弹窗(登录后可能出现活动弹窗)
- await this.closeModalDialogs();
- onProgress?.(10, '正在上传视频...');
- // 上传视频 - 优先使用 AI 截图分析找到上传入口
- let uploadTriggered = false;
- // 方法1: AI 截图分析找到上传入口
- logger.info('[Baijiahao Publish] Using AI to find upload entry...');
- try {
- const screenshot = await this.screenshotBase64();
- const guide = await aiService.getPageOperationGuide(screenshot, 'baijiahao', '找到视频上传入口并点击上传按钮');
- logger.info(`[Baijiahao Publish] AI analysis result:`, guide);
- if (guide.hasAction && guide.targetSelector) {
- logger.info(`[Baijiahao Publish] AI suggested selector: ${guide.targetSelector}`);
- try {
- const [fileChooser] = await Promise.all([
- this.page.waitForEvent('filechooser', { timeout: 10000 }),
- this.page.click(guide.targetSelector),
- ]);
- await fileChooser.setFiles(params.videoPath);
- uploadTriggered = true;
- logger.info('[Baijiahao Publish] Upload triggered via AI selector');
- } catch (e) {
- logger.warn(`[Baijiahao Publish] AI selector click failed: ${e}`);
- }
- }
- } catch (e) {
- logger.warn(`[Baijiahao Publish] AI analysis failed: ${e}`);
- }
- // 方法2: 尝试点击常见的上传区域触发 file chooser
- if (!uploadTriggered) {
- logger.info('[Baijiahao Publish] Trying common upload selectors...');
- const uploadSelectors = [
- // 百家号常见上传区域选择器 - 虚线框拖拽上传区域
- '[class*="drag"]',
- '[class*="drop"]',
- '[class*="upload-area"]',
- '[class*="upload-zone"]',
- '[class*="upload-wrapper"]',
- '[class*="upload-box"]',
- '[class*="upload-btn"]',
- '[class*="upload-video"]',
- '[class*="video-upload"]',
- '[class*="drag-upload"]',
- '.upload-container',
- '.video-uploader',
- 'div[class*="uploader"]',
- // 匹配包含"点击上传"文字的区域
- 'div:has-text("点击上传")',
- 'div:has-text("拖动入此区域")',
- 'span:has-text("点击上传")',
- // 带虚线边框的容器(通常是拖拽上传区域)
- '[style*="dashed"]',
- '[class*="dashed"]',
- '[class*="border-dashed"]',
- // 其他常见选择器
- 'button:has-text("上传")',
- '[class*="add-btn"]',
- '.bjh-upload',
- '[class*="file-select"]',
- // 通用的上传触发器
- '[class*="trigger"]',
- '[class*="picker"]',
- ];
-
- 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(`[Baijiahao Publish] Trying selector: ${selector}`);
- const [fileChooser] = await Promise.all([
- this.page.waitForEvent('filechooser', { timeout: 5000 }),
- element.click(),
- ]);
- await fileChooser.setFiles(params.videoPath);
- uploadTriggered = true;
- logger.info(`[Baijiahao Publish] Upload triggered via selector: ${selector}`);
- }
- } catch (e) {
- // 继续尝试下一个选择器
- }
- }
- }
- // 方法3: 直接设置 file input
- if (!uploadTriggered) {
- logger.info('[Baijiahao Publish] Trying file input method...');
- const fileInputs = await this.page.$$('input[type="file"]');
- logger.info(`[Baijiahao Publish] Found ${fileInputs.length} file inputs`);
- for (const fileInput of fileInputs) {
- try {
- const accept = await fileInput.getAttribute('accept');
- if (!accept || accept.includes('video') || accept.includes('*')) {
- await fileInput.setInputFiles(params.videoPath);
- uploadTriggered = true;
- logger.info('[Baijiahao Publish] Upload triggered via file input');
- break;
- }
- } catch (e) {
- logger.warn(`[Baijiahao Publish] File input method failed: ${e}`);
- }
- }
- }
- // 方法4: 如果AI给出了坐标,尝试基于坐标点击
- if (!uploadTriggered) {
- logger.info('[Baijiahao Publish] Trying AI position-based click...');
- try {
- const screenshot = await this.screenshotBase64();
- const guide = await aiService.getPageOperationGuide(screenshot, 'baijiahao', '请找到页面中央的虚线框上传区域(有"点击上传或将文件拖动入此区域"文字的区域),返回该区域的中心坐标');
- logger.info(`[Baijiahao Publish] AI position analysis:`, guide);
-
- if (guide.hasAction && guide.targetPosition) {
- const { x, y } = guide.targetPosition;
- logger.info(`[Baijiahao Publish] Clicking at position: ${x}, ${y}`);
- const [fileChooser] = await Promise.all([
- this.page.waitForEvent('filechooser', { timeout: 10000 }),
- this.page.mouse.click(x, y),
- ]);
- await fileChooser.setFiles(params.videoPath);
- uploadTriggered = true;
- logger.info('[Baijiahao Publish] Upload triggered via position click');
- }
- } catch (e) {
- logger.warn(`[Baijiahao Publish] Position-based click failed: ${e}`);
- }
- }
- // 方法5: 点击页面中央区域(百家号上传区域通常在中央)
- if (!uploadTriggered) {
- logger.info('[Baijiahao Publish] Trying center area click...');
- try {
- const viewport = this.page.viewportSize();
- if (viewport) {
- // 百家号的上传区域大约在页面中央偏上的位置
- const centerX = viewport.width / 2;
- const centerY = viewport.height * 0.35; // 上传区域通常在页面上半部分
- logger.info(`[Baijiahao Publish] Clicking center area: ${centerX}, ${centerY}`);
- const [fileChooser] = await Promise.all([
- this.page.waitForEvent('filechooser', { timeout: 10000 }),
- this.page.mouse.click(centerX, centerY),
- ]);
- await fileChooser.setFiles(params.videoPath);
- uploadTriggered = true;
- logger.info('[Baijiahao Publish] Upload triggered via center click');
- }
- } catch (e) {
- logger.warn(`[Baijiahao Publish] Center click failed: ${e}`);
- }
- }
- if (!uploadTriggered) {
- // 截图调试
- try {
- if (this.page) {
- const screenshotPath = `uploads/debug/baijiahao_no_upload_${Date.now()}.png`;
- await this.page.screenshot({ path: screenshotPath, fullPage: true });
- logger.info(`[Baijiahao Publish] Screenshot saved: ${screenshotPath}`);
- }
- } catch {}
- throw new Error('未找到上传入口');
- }
- // 等待上传完成
- onProgress?.(30, '视频上传中...');
- await this.page.waitForTimeout(5000);
- // 等待视频处理
- const maxWaitTime = 300000; // 5分钟
- const startTime = Date.now();
- let lastAiCheckTime = 0;
- const aiCheckInterval = 10000; // 每10秒使用AI检测一次
- while (Date.now() - startTime < maxWaitTime) {
- // 检查上传进度(通过DOM)
- let progressDetected = false;
- const progressText = await this.page.locator('[class*="progress"]').first().textContent().catch(() => '');
- if (progressText) {
- const match = progressText.match(/(\d+)%/);
- if (match) {
- const progress = parseInt(match[1]);
- onProgress?.(30 + Math.floor(progress * 0.3), `视频上传中: ${progress}%`);
- progressDetected = true;
- if (progress >= 100) {
- logger.info('[Baijiahao Publish] Upload progress reached 100%');
- break;
- }
- }
- }
- // 检查是否上传完成
- const uploadSuccess = await this.page.locator('[class*="success"], [class*="complete"]').count();
- if (uploadSuccess > 0) {
- logger.info('[Baijiahao Publish] Upload success indicator found');
- break;
- }
- // 使用AI检测上传进度(每隔一段时间检测一次)
- if (!progressDetected && Date.now() - lastAiCheckTime > aiCheckInterval) {
- lastAiCheckTime = Date.now();
- try {
- const screenshot = await this.screenshotBase64();
- const uploadStatus = await aiService.analyzeUploadProgress(screenshot, 'baijiahao');
- logger.info(`[Baijiahao Publish] AI upload status:`, uploadStatus);
-
- if (uploadStatus.isComplete) {
- logger.info('[Baijiahao Publish] AI detected upload complete');
- break;
- }
-
- if (uploadStatus.isFailed) {
- throw new Error(`视频上传失败: ${uploadStatus.statusDescription}`);
- }
-
- if (uploadStatus.progress !== null) {
- onProgress?.(30 + Math.floor(uploadStatus.progress * 0.3), `视频上传中: ${uploadStatus.progress}%`);
- if (uploadStatus.progress >= 100) {
- logger.info('[Baijiahao Publish] AI detected progress 100%');
- break;
- }
- }
- } catch (aiError) {
- logger.warn('[Baijiahao Publish] AI progress check failed:', aiError);
- }
- }
- await this.page.waitForTimeout(2000);
- }
- onProgress?.(60, '正在填写视频信息...');
- // 填写标题
- const titleInput = this.page.locator('input[placeholder*="标题"], textarea[placeholder*="标题"]').first();
- if (await titleInput.count() > 0) {
- await titleInput.fill(params.title);
- }
- // 填写描述
- if (params.description) {
- const descInput = this.page.locator('textarea[placeholder*="简介"], textarea[placeholder*="描述"]').first();
- if (await descInput.count() > 0) {
- await descInput.fill(params.description);
- }
- }
- onProgress?.(80, '正在发布...');
- // 点击发布按钮
- const publishBtn = this.page.locator('button:has-text("发布"), [class*="publish-btn"]').first();
- if (await publishBtn.count() > 0) {
- await publishBtn.click();
- } else {
- throw new Error('未找到发布按钮');
- }
- // 等待发布结果
- onProgress?.(90, '等待发布完成...');
- const publishMaxWait = 120000; // 2分钟
- const publishStartTime = Date.now();
- let lastProgressCheckTime = 0;
- const progressCheckInterval = 5000; // 每5秒检测一次发布进度
- while (Date.now() - publishStartTime < publishMaxWait) {
- await this.page.waitForTimeout(3000);
-
- // 检查是否跳转到内容管理页面
- const currentUrl = this.page.url();
- if (currentUrl.includes('/content') || currentUrl.includes('/rc/home')) {
- onProgress?.(100, '发布成功!');
- await this.closeBrowser();
- return {
- success: true,
- videoUrl: currentUrl,
- };
- }
- // 检查发布进度条(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(`[Baijiahao 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, 'baijiahao');
- logger.info(`[Baijiahao Publish] AI publish status:`, publishStatus);
-
- if (publishStatus.isComplete) {
- logger.info('[Baijiahao 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.needAction && onCaptchaRequired) {
- logger.info(`[Baijiahao Publish] Need action: ${publishStatus.actionDescription}`);
- const imageBase64 = await this.screenshotBase64();
- try {
- await onCaptchaRequired({
- taskId: `baijiahao_captcha_${Date.now()}`,
- type: 'image',
- imageBase64,
- });
- } catch {
- logger.error('[Baijiahao] Captcha handling failed');
- }
- }
-
- if (publishStatus.isPublishing) {
- logger.info(`[Baijiahao Publish] Still publishing: ${publishStatus.statusDescription}`);
- }
- } catch (aiError) {
- logger.warn('[Baijiahao Publish] AI publish progress check failed:', aiError);
- }
- }
- // 检查错误提示
- const errorHint = await this.page.locator('[class*="error"], [class*="fail"]').first().textContent().catch(() => '');
- if (errorHint && (errorHint.includes('失败') || errorHint.includes('错误'))) {
- throw new Error(`发布失败: ${errorHint}`);
- }
- }
- // 最后再检查一次AI状态
- const aiStatus = await this.aiAnalyzePublishStatus();
- if (aiStatus) {
- if (aiStatus.status === 'success') {
- onProgress?.(100, '发布成功!');
- await this.closeBrowser();
- return {
- success: true,
- videoUrl: this.page.url(),
- };
- }
- if (aiStatus.status === 'failed') {
- throw new Error(aiStatus.errorMessage || '发布失败');
- }
- }
- // 超时
- await this.closeBrowser();
- return {
- success: false,
- errorMessage: '发布超时,请手动检查是否发布成功',
- };
- } catch (error) {
- logger.error('[Baijiahao Publish] Error:', error);
- await this.closeBrowser();
- return {
- success: false,
- errorMessage: error instanceof Error ? error.message : '发布失败',
- };
- }
- ========== Playwright 方式已注释结束 ========== */
- }
-
- async getComments(cookies: string, videoId: string): Promise<CommentData[]> {
- logger.warn('[Baijiahao] getComments not implemented');
- return [];
- }
-
- async replyComment(cookies: string, commentId: string, content: string): Promise<boolean> {
- logger.warn('[Baijiahao] replyComment not implemented');
- return false;
- }
-
- async getAnalytics(cookies: string, dateRange: DateRange): Promise<AnalyticsData> {
- logger.warn('[Baijiahao] getAnalytics not implemented');
- return {
- fansCount: 0,
- fansIncrease: 0,
- viewsCount: 0,
- likesCount: 0,
- commentsCount: 0,
- sharesCount: 0,
- };
- }
- }
|