BaijiahaoLoginService.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. /**
  2. * 百家号登录服务
  3. * @module services/login/BaijiahaoLoginService
  4. *
  5. * 登录流程(按用户要求):
  6. * 1. 打开 https://baijiahao.baidu.com/builder/theme/bjh/login
  7. * 2. 等待用户扫码登录,跳转到 /builder/rc/home 或 AI 识别已登录
  8. * 3. 监听 API /builder/app/appinfo 获取:头像(user.avatar)、昵称(user.name)、百家号ID(user.app_id)
  9. * 4. 监听 API /cms-ui/rights/growth/get_info 获取:粉丝数(total_fans)
  10. * 5. 跳转到 https://baijiahao.baidu.com/builder/rc/content 作品管理页
  11. * 6. 监听 API /pcui/article/lists 获取作品列表,取 list 数量作为作品数
  12. * 7. 完成后发送事件,账号ID使用 bjh_ 前缀
  13. */
  14. import { logger } from '../../utils/logger.js';
  15. import { BaseLoginService, type ApiInterceptConfig } from './BaseLoginService.js';
  16. import type { AccountInfo, LoginSession } from './types.js';
  17. export class BaijiahaoLoginService extends BaseLoginService {
  18. constructor() {
  19. super({
  20. platform: 'baijiahao',
  21. displayName: '百家号',
  22. loginUrl: 'https://baijiahao.baidu.com/builder/theme/bjh/login',
  23. successIndicators: ['/builder/rc/home', 'baijiahao.baidu.com/builder/rc'],
  24. cookieDomain: '.baidu.com',
  25. accountIdPrefix: 'bjh_',
  26. });
  27. }
  28. /**
  29. * API 拦截配置
  30. */
  31. protected override getApiInterceptConfigs(): ApiInterceptConfig[] {
  32. return [
  33. // 应用信息接口 - 获取头像、昵称、app_id
  34. {
  35. urlPattern: '/builder/app/appinfo',
  36. dataKey: 'appInfo',
  37. handler: (data: any) => {
  38. const user = data.data?.user || data.user || {};
  39. return {
  40. appId: user.app_id,
  41. name: user.name,
  42. avatar: user.avatar?.startsWith('http') ? user.avatar : `https:${user.avatar}`,
  43. userId: user.userid,
  44. };
  45. },
  46. },
  47. // 成长信息接口 - 获取粉丝数
  48. {
  49. urlPattern: '/cms-ui/rights/growth/get_info',
  50. dataKey: 'growthInfo',
  51. handler: (data: any) => ({
  52. totalFans: data.data?.total_fans || data.total_fans || 0,
  53. }),
  54. },
  55. // 文章列表接口 - 获取作品数
  56. {
  57. urlPattern: '/pcui/article/lists',
  58. dataKey: 'articleList',
  59. handler: (data: any) => {
  60. // 处理分散的响应格式
  61. const dataList = data.data?.article_list || data.data?.list || data.list || [];
  62. return { list: dataList, count: dataList.length };
  63. },
  64. },
  65. ];
  66. }
  67. /**
  68. * 收集账号信息
  69. */
  70. protected override async collectAccountInfo(session: LoginSession): Promise<AccountInfo | null> {
  71. try {
  72. // 关键步骤:确保在主页面(首页)获取完整Cookie上下文
  73. logger.info('[百家号] 确保在主页面获取完整Cookie...');
  74. const currentUrl = session.page.url();
  75. if (!currentUrl.includes('/builder/rc/home')) {
  76. logger.info(`[百家号] 当前不在主页: ${currentUrl},跳转到主页...`);
  77. await session.page.goto('https://baijiahao.baidu.com/builder/rc/home', {
  78. waitUntil: 'domcontentloaded',
  79. timeout: 30000
  80. });
  81. await new Promise(resolve => setTimeout(resolve, 3000)); // 等待页面完全加载
  82. }
  83. // 步骤3: 等待 appinfo API
  84. logger.info('[百家号] 等待 appinfo API...');
  85. let appInfo = await this.waitForApiData(session, 'appInfo', 10000);
  86. if (!appInfo) {
  87. logger.info('[百家号] 未拿到 appinfo,刷新页面重试...');
  88. await session.page.reload({ waitUntil: 'domcontentloaded' });
  89. await new Promise(resolve => setTimeout(resolve, 3000)); // 增加等待时间
  90. appInfo = await this.waitForApiData(session, 'appInfo', 15000);
  91. }
  92. if (!appInfo?.appId) {
  93. logger.error('[百家号] 无法获取百家号ID');
  94. return null;
  95. }
  96. logger.info('[百家号] 基本信息:', appInfo);
  97. // 步骤4: 等待 growth 信息(可能首页已触发)
  98. logger.info('[百家号] 等待粉丝数据...');
  99. const growthInfo = await this.waitForApiData(session, 'growthInfo', 5000);
  100. const fansCount = growthInfo?.totalFans || 0;
  101. logger.info(`[百家号] 粉丝数: ${fansCount}`);
  102. // 步骤5+6: 跳转到作品管理页,等待文章列表 API
  103. logger.info('[百家号] 跳转到作品管理页...');
  104. const contentUrl = 'https://baijiahao.baidu.com/builder/rc/content';
  105. // 尝试多次获取作品列表(百家号可能有分散认证问题)
  106. let articleData = null;
  107. for (let attempt = 1; attempt <= 3; attempt++) {
  108. try {
  109. logger.info(`[百家号] 获取作品列表(第${attempt}次尝试)...`);
  110. articleData = await this.navigateAndWaitForApi(session, contentUrl, 'articleList', 15000);
  111. if (articleData) {
  112. break;
  113. }
  114. if (attempt < 3) {
  115. logger.info(`[百家号] 第${attempt}次尝试失败,等待${attempt * 2}秒后重试...`);
  116. await new Promise(resolve => setTimeout(resolve, attempt * 2000));
  117. }
  118. } catch (error) {
  119. logger.warn(`[百家号] 第${attempt}次尝试获取作品列表失败:`, error);
  120. if (attempt < 3) {
  121. await new Promise(resolve => setTimeout(resolve, attempt * 2000));
  122. }
  123. }
  124. }
  125. const worksCount = articleData?.count || 0;
  126. logger.info(`[百家号] 作品数: ${worksCount}`);
  127. // 步骤7: 组装账号信息,使用 bjh_ 前缀
  128. return {
  129. accountId: `bjh_${appInfo.appId}`,
  130. accountName: appInfo.name || '百家号用户',
  131. avatarUrl: appInfo.avatar || '',
  132. fansCount,
  133. worksCount,
  134. };
  135. } catch (error) {
  136. logger.error('[百家号] 收集账号信息失败:', error);
  137. return null;
  138. }
  139. }
  140. }
  141. // 导出单例
  142. export const baijiahaoLoginService = new BaijiahaoLoginService();