|
|
@@ -27,30 +27,30 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
readonly platform: PlatformType = 'weixin_video';
|
|
|
readonly loginUrl = 'https://channels.weixin.qq.com/platform';
|
|
|
readonly publishUrl = 'https://channels.weixin.qq.com/platform/post/create';
|
|
|
-
|
|
|
+
|
|
|
protected getCookieDomain(): string {
|
|
|
return '.weixin.qq.com';
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
async getQRCode(): Promise<QRCodeInfo> {
|
|
|
try {
|
|
|
await this.initBrowser();
|
|
|
-
|
|
|
+
|
|
|
if (!this.page) throw new Error('Page not initialized');
|
|
|
-
|
|
|
+
|
|
|
// 访问登录页面
|
|
|
await this.page.goto('https://channels.weixin.qq.com/platform/login-for-iframe?dark_mode=true&host_type=1');
|
|
|
-
|
|
|
+
|
|
|
// 点击二维码切换
|
|
|
await this.page.locator('.qrcode').click();
|
|
|
-
|
|
|
+
|
|
|
// 获取二维码
|
|
|
const qrcodeImg = await this.page.locator('img.qrcode').getAttribute('src');
|
|
|
-
|
|
|
+
|
|
|
if (!qrcodeImg) {
|
|
|
throw new Error('Failed to get QR code');
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return {
|
|
|
qrcodeUrl: qrcodeImg,
|
|
|
qrcodeKey: `weixin_${Date.now()}`,
|
|
|
@@ -61,49 +61,49 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
async checkQRCodeStatus(qrcodeKey: string): Promise<LoginStatusResult> {
|
|
|
try {
|
|
|
if (!this.page) {
|
|
|
return { status: 'expired', message: '二维码已过期' };
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 检查是否扫码成功
|
|
|
const maskDiv = this.page.locator('.mask').first();
|
|
|
const className = await maskDiv.getAttribute('class');
|
|
|
-
|
|
|
+
|
|
|
if (className && className.includes('show')) {
|
|
|
// 等待登录完成
|
|
|
await this.page.waitForTimeout(3000);
|
|
|
-
|
|
|
+
|
|
|
const cookies = await this.getCookies();
|
|
|
if (cookies && cookies.length > 10) {
|
|
|
await this.closeBrowser();
|
|
|
return { status: 'success', message: '登录成功', cookies };
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return { status: 'waiting', message: '等待扫码' };
|
|
|
} catch (error) {
|
|
|
logger.error('Weixin checkQRCodeStatus error:', error);
|
|
|
return { status: 'error', message: '检查状态失败' };
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
async checkLoginStatus(cookies: string): Promise<boolean> {
|
|
|
try {
|
|
|
await this.initBrowser();
|
|
|
await this.setCookies(cookies);
|
|
|
-
|
|
|
+
|
|
|
if (!this.page) throw new Error('Page not initialized');
|
|
|
-
|
|
|
+
|
|
|
await this.page.goto(this.publishUrl);
|
|
|
await this.page.waitForLoadState('networkidle');
|
|
|
-
|
|
|
+
|
|
|
// 检查是否需要登录
|
|
|
const needLogin = await this.page.$('div.title-name:has-text("视频号小店")');
|
|
|
await this.closeBrowser();
|
|
|
-
|
|
|
+
|
|
|
return !needLogin;
|
|
|
} catch (error) {
|
|
|
logger.error('Weixin checkLoginStatus error:', error);
|
|
|
@@ -111,15 +111,15 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 关闭页面上可能存在的弹窗对话框
|
|
|
*/
|
|
|
private async closeModalDialogs(): Promise<boolean> {
|
|
|
if (!this.page) return false;
|
|
|
-
|
|
|
+
|
|
|
let closedAny = false;
|
|
|
-
|
|
|
+
|
|
|
try {
|
|
|
const modalSelectors = [
|
|
|
// 微信视频号常见弹窗关闭按钮
|
|
|
@@ -136,7 +136,7 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
'.close-btn',
|
|
|
'.icon-close',
|
|
|
];
|
|
|
-
|
|
|
+
|
|
|
for (const selector of modalSelectors) {
|
|
|
try {
|
|
|
const closeBtn = this.page.locator(selector).first();
|
|
|
@@ -150,7 +150,7 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
// 忽略错误,继续尝试下一个选择器
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 尝试按 ESC 键关闭弹窗
|
|
|
if (!closedAny) {
|
|
|
const hasModal = await this.page.locator('[class*="dialog"], [class*="modal"], [role="dialog"]').count();
|
|
|
@@ -161,15 +161,15 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
closedAny = true;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
if (closedAny) {
|
|
|
logger.info('[Weixin] Successfully closed modal dialog');
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
} catch (error) {
|
|
|
logger.warn('[Weixin] Error closing modal:', error);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return closedAny;
|
|
|
}
|
|
|
|
|
|
@@ -177,18 +177,18 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
try {
|
|
|
await this.initBrowser();
|
|
|
await this.setCookies(cookies);
|
|
|
-
|
|
|
+
|
|
|
if (!this.page) throw new Error('Page not initialized');
|
|
|
-
|
|
|
+
|
|
|
// 访问视频号创作者平台首页
|
|
|
await this.page.goto('https://channels.weixin.qq.com/platform/home');
|
|
|
await this.page.waitForLoadState('networkidle');
|
|
|
await this.page.waitForTimeout(2000);
|
|
|
-
|
|
|
+
|
|
|
// 从页面提取账号信息
|
|
|
const accountData = await this.page.evaluate(() => {
|
|
|
const result: { accountId?: string; accountName?: string; avatarUrl?: string; fansCount?: number; worksCount?: number } = {};
|
|
|
-
|
|
|
+
|
|
|
try {
|
|
|
// ===== 1. 优先使用精确选择器获取视频号 ID =====
|
|
|
// 方法1: 通过 #finder-uid-copy 的 data-clipboard-text 属性获取
|
|
|
@@ -206,7 +206,7 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 方法2: 通过 .finder-uniq-id 选择器获取
|
|
|
if (!result.accountId) {
|
|
|
const finderUniqIdEl = document.querySelector('.finder-uniq-id');
|
|
|
@@ -224,7 +224,7 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 方法3: 从页面文本中正则匹配
|
|
|
if (!result.accountId) {
|
|
|
const bodyText = document.body.innerText || '';
|
|
|
@@ -241,10 +241,10 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// ===== 2. 获取账号名称 =====
|
|
|
- const nicknameEl = document.querySelector('h2.finder-nickname') ||
|
|
|
- document.querySelector('.finder-nickname');
|
|
|
+ const nicknameEl = document.querySelector('h2.finder-nickname') ||
|
|
|
+ document.querySelector('.finder-nickname');
|
|
|
if (nicknameEl) {
|
|
|
const text = nicknameEl.textContent?.trim();
|
|
|
if (text && text.length >= 2 && text.length <= 30) {
|
|
|
@@ -252,7 +252,7 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
console.log('[WeixinVideo] Found name:', result.accountName);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// ===== 3. 获取头像 =====
|
|
|
const avatarEl = document.querySelector('img.avatar') as HTMLImageElement;
|
|
|
if (avatarEl?.src && avatarEl.src.startsWith('http')) {
|
|
|
@@ -263,7 +263,7 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
result.avatarUrl = altAvatarEl.src;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// ===== 4. 获取视频数和关注者数 =====
|
|
|
const contentInfo = document.querySelector('.finder-content-info');
|
|
|
if (contentInfo) {
|
|
|
@@ -281,7 +281,7 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 备选:从页面整体文本中匹配
|
|
|
if (result.fansCount === undefined || result.worksCount === undefined) {
|
|
|
const bodyText = document.body.innerText || '';
|
|
|
@@ -305,22 +305,22 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
} catch (e) {
|
|
|
console.error('[WeixinVideo] Extract error:', e);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return result;
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
logger.info('[Weixin] Extracted account data:', accountData);
|
|
|
-
|
|
|
+
|
|
|
// 如果首页没有获取到视频号 ID,尝试访问账号设置页面
|
|
|
let finalAccountId = accountData.accountId;
|
|
|
if (!finalAccountId || finalAccountId.length < 10) {
|
|
|
logger.info('[Weixin] Finder ID not found on home page, trying account settings page...');
|
|
|
-
|
|
|
+
|
|
|
try {
|
|
|
await this.page.goto('https://channels.weixin.qq.com/platform/account');
|
|
|
await this.page.waitForLoadState('networkidle');
|
|
|
await this.page.waitForTimeout(2000);
|
|
|
-
|
|
|
+
|
|
|
const settingsId = await this.page.evaluate(() => {
|
|
|
const bodyText = document.body.innerText || '';
|
|
|
const patterns = [
|
|
|
@@ -336,7 +336,7 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
}
|
|
|
return null;
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
if (settingsId) {
|
|
|
finalAccountId = settingsId;
|
|
|
logger.info('[Weixin] Found finder ID from settings page:', finalAccountId);
|
|
|
@@ -345,9 +345,9 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
logger.warn('[Weixin] Failed to fetch from settings page:', e);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
await this.closeBrowser();
|
|
|
-
|
|
|
+
|
|
|
return {
|
|
|
accountId: finalAccountId || `weixin_video_${Date.now()}`,
|
|
|
accountName: accountData.accountName || '视频号账号',
|
|
|
@@ -367,7 +367,7 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 检查 Python 发布服务是否可用
|
|
|
*/
|
|
|
@@ -409,6 +409,7 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
: undefined;
|
|
|
|
|
|
// 使用 AI 辅助发布接口
|
|
|
+ const extra = (params.extra || {}) as Record<string, unknown>;
|
|
|
const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish/ai-assisted`, {
|
|
|
method: 'POST',
|
|
|
headers: {
|
|
|
@@ -417,6 +418,10 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
body: JSON.stringify({
|
|
|
platform: 'weixin',
|
|
|
cookie: cookies,
|
|
|
+ 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,
|
|
|
@@ -439,7 +444,7 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
async publishVideo(
|
|
|
cookies: string,
|
|
|
params: PublishParams,
|
|
|
@@ -460,7 +465,7 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
logger.info('[Weixin] Using Python service for publishing');
|
|
|
try {
|
|
|
const result = await this.publishVideoViaPython(cookies, params, onProgress);
|
|
|
-
|
|
|
+
|
|
|
// 检查是否需要验证码
|
|
|
if (!result.success && result.errorMessage?.includes('验证码')) {
|
|
|
logger.info('[Weixin] Python detected captcha, need headful browser');
|
|
|
@@ -469,7 +474,7 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
errorMessage: `CAPTCHA_REQUIRED:${result.errorMessage}`,
|
|
|
};
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return result;
|
|
|
} catch (pythonError) {
|
|
|
logger.error('[Weixin] Python publish failed:', pythonError);
|
|
|
@@ -1005,13 +1010,13 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
}
|
|
|
========== Playwright 方式已注释结束 ========== */
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 通过 Python API 获取评论
|
|
|
*/
|
|
|
private async getCommentsViaPython(cookies: string, videoId: string): Promise<CommentData[]> {
|
|
|
logger.info('[Weixin] Getting comments via Python API...');
|
|
|
-
|
|
|
+
|
|
|
const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/comments`, {
|
|
|
method: 'POST',
|
|
|
headers: {
|
|
|
@@ -1023,17 +1028,17 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
work_id: videoId,
|
|
|
}),
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
if (!response.ok) {
|
|
|
throw new Error(`Python API returned ${response.status}`);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
const result = await response.json();
|
|
|
-
|
|
|
+
|
|
|
if (!result.success) {
|
|
|
throw new Error(result.error || 'Failed to get comments');
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 转换数据格式
|
|
|
return (result.comments || []).map((comment: {
|
|
|
comment_id: string;
|
|
|
@@ -1067,16 +1072,16 @@ export class WeixinAdapter extends BasePlatformAdapter {
|
|
|
logger.warn('[Weixin] Python API getComments failed:', pythonError);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
logger.warn('Weixin getComments - Python API not available');
|
|
|
return [];
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
async replyComment(cookies: string, videoId: string, commentId: string, content: string): Promise<boolean> {
|
|
|
logger.warn('Weixin replyComment not implemented');
|
|
|
return false;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
async getAnalytics(cookies: string, dateRange?: DateRange): Promise<AnalyticsData> {
|
|
|
logger.warn('Weixin getAnalytics not implemented');
|
|
|
return {
|