|
@@ -221,6 +221,165 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
|
|
|
return closedAny;
|
|
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> {
|
|
async getAccountInfo(cookies: string): Promise<AccountProfile> {
|
|
|
try {
|
|
try {
|
|
|
await this.initBrowser({ headless: true });
|
|
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方式)
|
|
// 检查发布进度条(DOM方式)
|
|
|
const publishProgressText = await this.page.locator('[class*="progress"], [class*="loading"]').first().textContent().catch(() => '');
|
|
const publishProgressText = await this.page.locator('[class*="progress"], [class*="loading"]').first().textContent().catch(() => '');
|
|
|
if (publishProgressText) {
|
|
if (publishProgressText) {
|
|
@@ -650,19 +840,9 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
|
|
|
onProgress?.(90 + Math.floor(publishStatus.progress * 0.1), `发布中: ${publishStatus.progress}%`);
|
|
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) {
|
|
if (publishStatus.isPublishing) {
|