Sfoglia il codice sorgente

feat: enhance BaijiahaoAdapter with dialog text handling and captcha detection

ethanfly 3 settimane fa
parent
commit
a97f6717eb

+ 10 - 2
client/src/views/Publish/index.vue

@@ -103,7 +103,7 @@
           </template>
         </el-table-column>
         
-        <el-table-column label="操作" width="136">
+        <el-table-column label="操作" width="176" class-name="operation-column">
           <template #default="{ row }">
             <div class="action-links">
               <el-button type="primary" link size="small" @click="viewDetail(row)">
@@ -1259,7 +1259,7 @@ watch(showCreateDialog, (visible) => {
 .action-links {
   display: flex;
   align-items: center;
-  gap: 10px;
+  gap: 12px;
   white-space: nowrap;
 }
 
@@ -1331,6 +1331,14 @@ watch(showCreateDialog, (visible) => {
   overflow: hidden;
 }
 
+:deep(.operation-column .cell) {
+  overflow: visible;
+}
+
+:deep(.operation-column .el-button + .el-button) {
+  margin-left: 0;
+}
+
 :deep(.el-table__row td) {
   padding-top: 12px;
   padding-bottom: 12px;

+ 193 - 13
server/src/automation/platforms/baijiahao.ts

@@ -221,6 +221,165 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
     return closedAny;
   }
 
+  private cleanPromptText(text: string, maxLength = 180): string {
+    const normalized = text.replace(/\s+/g, ' ').trim();
+    return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
+  }
+
+  private async getVisibleDialogTexts(): Promise<string[]> {
+    if (!this.page) return [];
+
+    return this.page.evaluate(() => {
+      const selectors = [
+        '[role="dialog"]',
+        '[class*="dialog"]',
+        '[class*="Dialog"]',
+        '[class*="modal"]',
+        '[class*="Modal"]',
+        '[class*="popup"]',
+        '[class*="Popup"]',
+        '[class*="toast"]',
+        '[class*="Toast"]',
+        '[class*="message"]',
+        '[class*="Message"]',
+      ];
+
+      const isVisible = (element: Element): boolean => {
+        const htmlElement = element as HTMLElement;
+        const rect = htmlElement.getBoundingClientRect();
+        const style = window.getComputedStyle(htmlElement);
+        return rect.width > 0
+          && rect.height > 0
+          && style.display !== 'none'
+          && style.visibility !== 'hidden'
+          && style.opacity !== '0';
+      };
+
+      return Array.from(document.querySelectorAll(selectors.join(',')))
+        .filter(isVisible)
+        .map((element) => ((element as HTMLElement).innerText || element.textContent || '').replace(/\s+/g, ' ').trim())
+        .filter((text) => text.length > 0)
+        .slice(0, 8);
+    }).catch(() => []);
+  }
+
+  private async getVisiblePageText(): Promise<string> {
+    if (!this.page) return '';
+
+    return this.page.evaluate(() => (document.body?.innerText || '').replace(/\s+/g, ' ').trim())
+      .catch(() => '');
+  }
+
+  private async detectCaptchaPrompt(): Promise<{ detected: boolean; type: 'sms' | 'image'; description: string }> {
+    if (!this.page) return { detected: false, type: 'image', description: '' };
+
+    const dialogText = (await this.getVisibleDialogTexts()).join(' ');
+    const pageText = await this.getVisiblePageText();
+    const visibleText = `${dialogText} ${pageText}`;
+
+    const hasCaptchaTextInDialog = /验证码|图形验证|图片验证|短信验证|安全验证|身份验证|滑块验证|拖动滑块|按住滑块|captcha/i.test(dialogText);
+    const hasCaptchaTextOnPage = /验证码|图形验证|图片验证|短信验证|安全验证|身份验证|滑块验证|拖动滑块|按住滑块|captcha/i.test(pageText);
+    const domSignal = await this.page.evaluate(() => {
+      const isVisible = (element: Element): boolean => {
+        const htmlElement = element as HTMLElement;
+        const rect = htmlElement.getBoundingClientRect();
+        const style = window.getComputedStyle(htmlElement);
+        return rect.width > 0
+          && rect.height > 0
+          && style.display !== 'none'
+          && style.visibility !== 'hidden'
+          && style.opacity !== '0';
+      };
+
+      const elements = Array.from(document.querySelectorAll('input, textarea, button, [class], [id]'));
+      return elements.some((element) => {
+        if (!isVisible(element)) return false;
+        const htmlElement = element as HTMLElement;
+        const input = element as HTMLInputElement;
+        const attrs = [
+          htmlElement.innerText,
+          element.textContent,
+          element.getAttribute('class'),
+          element.getAttribute('id'),
+          element.getAttribute('name'),
+          input.placeholder,
+          element.getAttribute('aria-label'),
+        ]
+          .filter(Boolean)
+          .join(' ')
+          .toLowerCase();
+
+        if (/验证码|获取验证码|发送验证码|换一张/.test(attrs)) return true;
+        return /captcha|geetest|yidun|verifycode|verify-code|vcode|slider/.test(attrs);
+      });
+    }).catch(() => false);
+
+    if (!hasCaptchaTextInDialog && !(hasCaptchaTextOnPage && domSignal) && !domSignal) {
+      return { detected: false, type: 'image', description: '' };
+    }
+
+    const type: 'sms' | 'image' = /短信|手机|获取验证码|发送验证码/.test(visibleText) ? 'sms' : 'image';
+    const description = this.cleanPromptText(dialogText || visibleText || '检测到验证码');
+    return { detected: true, type, description };
+  }
+
+  private async clickFirstVisible(selectors: string[]): Promise<string | null> {
+    if (!this.page) return null;
+
+    for (const selector of selectors) {
+      try {
+        const target = this.page.locator(selector).first();
+        if (await target.count() > 0 && await target.isVisible().catch(() => false)) {
+          await target.click({ timeout: 3000 });
+          await this.page.waitForTimeout(1000);
+          return selector;
+        }
+      } catch {
+        // Continue trying less specific prompt buttons.
+      }
+    }
+
+    return null;
+  }
+
+  private async handlePostPublishPrompt(): Promise<{
+    status: 'none' | 'continued' | 'success' | 'failed';
+    message?: string;
+  }> {
+    const dialogTexts = await this.getVisibleDialogTexts();
+    const promptText = this.cleanPromptText(dialogTexts.join(' '));
+    if (!promptText) return { status: 'none' };
+
+    if (/验证码|图形验证|图片验证|短信验证|安全验证|身份验证|滑块验证|拖动滑块|captcha/i.test(promptText)) {
+      return { status: 'none' };
+    }
+
+    if (/发布成功|提交成功|上传成功/.test(promptText)) {
+      return { status: 'success', message: promptText };
+    }
+
+    if (/失败|错误|异常|不可发布|不支持|禁止|违规|未通过|不能为空|请填写|请上传|请添加|超过|低于|格式不符|审核未通过/.test(promptText)) {
+      return { status: 'failed', message: promptText };
+    }
+
+    const confirmSelector = await this.clickFirstVisible([
+      'button:has-text("确认发布")',
+      'button:has-text("继续发布")',
+      'button:has-text("确定发布")',
+      'button:has-text("立即发布")',
+      'button:has-text("确认")',
+      'button:has-text("确定")',
+      'button:has-text("我知道了")',
+    ]);
+
+    if (confirmSelector) {
+      logger.info(`[Baijiahao Publish] Confirmed post-publish prompt via ${confirmSelector}: ${promptText}`);
+      return { status: 'continued', message: promptText };
+    }
+
+    return { status: 'failed', message: `发布页面提示需要手动处理:${promptText}` };
+  }
+
   async getAccountInfo(cookies: string): Promise<AccountProfile> {
     try {
       await this.initBrowser({ headless: true });
@@ -617,6 +776,37 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
           };
         }
 
+        const promptResult = await this.handlePostPublishPrompt();
+        if (promptResult.status === 'success') {
+          logger.info(`[Baijiahao Publish] Success prompt found: ${promptResult.message}`);
+          onProgress?.(100, '发布成功!');
+          return await this.finishPublishSuccess();
+        }
+
+        if (promptResult.status === 'failed') {
+          throw new Error(`发布失败: ${promptResult.message}`);
+        }
+
+        if (promptResult.status === 'continued') {
+          onProgress?.(90, '已确认发布提示,继续等待发布完成...');
+        }
+
+        const captchaPrompt = await this.detectCaptchaPrompt();
+        if (captchaPrompt.detected) {
+          logger.warn(`[Baijiahao Publish] Captcha prompt detected by DOM: ${captchaPrompt.description}`);
+          if (onCaptchaRequired) {
+            const imageBase64 = captchaPrompt.type === 'image' ? await this.screenshotBase64() : undefined;
+            await onCaptchaRequired({
+              taskId: `baijiahao_captcha_${Date.now()}`,
+              type: captchaPrompt.type,
+              imageBase64,
+            });
+            continue;
+          }
+
+          throw new Error(`CAPTCHA_REQUIRED:${captchaPrompt.description || '检测到验证码'}`);
+        }
+
         // 检查发布进度条(DOM方式)
         const publishProgressText = await this.page.locator('[class*="progress"], [class*="loading"]').first().textContent().catch(() => '');
         if (publishProgressText) {
@@ -650,19 +840,9 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
               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');
-              }
+            // AI 的 needAction 可能只是普通发布确认弹框,验证码必须经过 DOM 二次确认。
+            if (publishStatus.needAction) {
+              logger.info(`[Baijiahao Publish] AI reported needAction but DOM did not confirm captcha: ${publishStatus.actionDescription || publishStatus.statusDescription}`);
             }
             
             if (publishStatus.isPublishing) {