check-douyin-account.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. /**
  2. * 检查抖音账号同步问题 - 诊断脚本
  3. * 用法: tsx src/scripts/check-douyin-account.ts <accountId或account_id>
  4. */
  5. import { initDatabase, AppDataSource, PlatformAccount } from '../models/index.js';
  6. import { logger } from '../utils/logger.js';
  7. import { headlessBrowserService } from '../services/HeadlessBrowserService.js';
  8. import { CookieManager } from '../automation/cookie.js';
  9. async function main() {
  10. const accountIdOrAccountId = process.argv[2];
  11. if (!accountIdOrAccountId) {
  12. logger.error('请提供账号ID或account_id');
  13. logger.info('用法: tsx src/scripts/check-douyin-account.ts <accountId或account_id>');
  14. process.exit(1);
  15. }
  16. try {
  17. await initDatabase();
  18. const accountRepository = AppDataSource.getRepository(PlatformAccount);
  19. // 先尝试作为数据库主键ID查询(仅当参数是纯数字时)
  20. const maybeId = Number(accountIdOrAccountId);
  21. let account: PlatformAccount | null = null;
  22. if (!Number.isNaN(maybeId) && Number.isInteger(maybeId) && maybeId > 0) {
  23. account = await accountRepository.findOne({
  24. where: { id: maybeId },
  25. });
  26. }
  27. // 如果没找到,尝试作为account_id(字符串)查询
  28. if (!account) {
  29. account = await accountRepository.findOne({
  30. where: { accountId: accountIdOrAccountId },
  31. });
  32. }
  33. // 如果还是没找到,尝试模糊匹配(dy_开头)
  34. if (!account && accountIdOrAccountId.startsWith('dy_')) {
  35. const accounts = await accountRepository.find({
  36. where: { platform: 'douyin' },
  37. });
  38. account = accounts.find(
  39. (a) =>
  40. a.accountId?.includes(accountIdOrAccountId.replace('dy_', '')) ||
  41. a.accountId === accountIdOrAccountId
  42. );
  43. }
  44. if (!account) {
  45. logger.error(`未找到账号: ${accountIdOrAccountId}`);
  46. logger.info('可用的抖音账号:');
  47. const allAccounts = await accountRepository.find({
  48. where: { platform: 'douyin' },
  49. });
  50. allAccounts.forEach((a) => {
  51. logger.info(
  52. ` ID=${a.id}, accountId=${a.accountId}, name=${a.accountName}, status=${a.status}`
  53. );
  54. });
  55. process.exit(1);
  56. }
  57. logger.info(`找到账号: ID=${account.id}, accountId=${account.accountId}, name=${account.accountName}, status=${account.status}`);
  58. if (!account.cookieData) {
  59. logger.error('账号没有 cookie 数据');
  60. process.exit(1);
  61. }
  62. // 解密 Cookie
  63. let decryptedCookies: string;
  64. try {
  65. decryptedCookies = CookieManager.decrypt(account.cookieData);
  66. logger.info('Cookie 解密成功');
  67. } catch {
  68. decryptedCookies = account.cookieData;
  69. logger.info('使用原始 cookie 数据');
  70. }
  71. // 解析 Cookie(支持 JSON 和 \"name=value; name2=value2\" 两种格式)
  72. let cookieList: { name: string; value: string; domain: string; path: string }[];
  73. try {
  74. cookieList = JSON.parse(decryptedCookies);
  75. logger.info(`从 JSON 解析了 ${cookieList.length} 个 cookie`);
  76. } catch {
  77. // 回退解析字符串格式的 cookie(与 WorkService.parseCookieString 保持一致)
  78. logger.warn('Cookie 不是 JSON,尝试按字符串格式解析...');
  79. const domain = '.douyin.com';
  80. const cookies: { name: string; value: string; domain: string; path: string }[] = [];
  81. const pairs = decryptedCookies.split(';');
  82. for (const pair of pairs) {
  83. const trimmed = pair.trim();
  84. if (!trimmed) continue;
  85. const eqIndex = trimmed.indexOf('=');
  86. if (eqIndex === -1) continue;
  87. const name = trimmed.substring(0, eqIndex).trim();
  88. const value = trimmed.substring(eqIndex + 1).trim();
  89. if (!name || !value) continue;
  90. cookies.push({ name, value, domain, path: '/' });
  91. }
  92. cookieList = cookies;
  93. logger.info(`从字符串解析了 ${cookieList.length} 个 cookie`);
  94. if (cookieList.length === 0) {
  95. logger.error('Cookie 字符串解析失败,未能获取任何 cookie');
  96. logger.info('Cookie 前100字符:', decryptedCookies.substring(0, 100));
  97. process.exit(1);
  98. }
  99. }
  100. // 调用 fetchAccountInfo
  101. logger.info('开始获取账号信息和作品列表...');
  102. logger.info('注意:这可能需要一些时间,请耐心等待...');
  103. let accountInfo;
  104. try {
  105. accountInfo = await headlessBrowserService.fetchAccountInfo('douyin', cookieList, {
  106. onWorksFetchProgress: (info) => {
  107. logger.info(
  108. `[进度] 已获取 ${info.totalSoFar} 个作品,总计: ${info.declaredTotal || '?'}, 当前页: ${info.page}`
  109. );
  110. },
  111. });
  112. } catch (error) {
  113. logger.error('获取账号信息时出错:', error);
  114. throw error;
  115. }
  116. logger.info('\n=== 账号信息 ===');
  117. logger.info(`accountId: ${accountInfo.accountId}`);
  118. logger.info(`accountName: ${accountInfo.accountName}`);
  119. logger.info(`avatarUrl: ${accountInfo.avatarUrl ? '有' : '无'}`);
  120. logger.info(`fansCount: ${accountInfo.fansCount}`);
  121. logger.info(`worksCount: ${accountInfo.worksCount}`);
  122. logger.info(`worksList.length: ${accountInfo.worksList?.length || 0}`);
  123. logger.info(`source: ${accountInfo.source || 'unknown'}`);
  124. logger.info(`pythonAvailable: ${accountInfo.pythonAvailable ? 'yes' : 'no'}`);
  125. if (accountInfo.worksCount > 0 && (!accountInfo.worksList || accountInfo.worksList.length === 0)) {
  126. logger.warn('\n⚠️ 警告:worksCount > 0 但 worksList 为空!');
  127. logger.warn('这可能表示:');
  128. logger.warn('1. API 返回了总数,但实际列表为空');
  129. logger.warn('2. 作品列表在解析过程中丢失');
  130. logger.warn('3. 分页逻辑有问题,没有正确获取所有作品');
  131. }
  132. if (accountInfo.worksList && accountInfo.worksList.length > 0) {
  133. logger.info('\n=== 作品列表 ===');
  134. accountInfo.worksList.forEach((work, idx) => {
  135. logger.info(
  136. `${idx + 1}. ${work.title} (videoId: ${work.videoId}, playCount: ${work.playCount}, likeCount: ${work.likeCount})`
  137. );
  138. });
  139. } else {
  140. logger.warn('\n=== 未获取到作品列表 ===');
  141. logger.warn(`worksCount: ${accountInfo.worksCount}`);
  142. logger.warn(`worksList.length: ${accountInfo.worksList?.length || 0}`);
  143. logger.warn(`source: ${accountInfo.source || 'unknown'}`);
  144. logger.warn(`pythonAvailable: ${accountInfo.pythonAvailable ? 'yes' : 'no'}`);
  145. if (accountInfo.worksCount > 0 && (!accountInfo.worksList || accountInfo.worksList.length === 0)) {
  146. logger.error('\n⚠️ 严重问题:worksCount > 0 但 worksList 为空!');
  147. logger.error('这表示 API 返回了作品总数,但实际列表为空');
  148. logger.error('可能的原因:');
  149. logger.error('1. API 分页逻辑有问题');
  150. logger.error('2. 作品列表在解析过程中丢失');
  151. logger.error('3. API 返回格式变化');
  152. } else if (accountInfo.worksCount === 0) {
  153. logger.warn('\n可能的原因:');
  154. logger.warn('1. API 调用失败或超时(检查上面的错误日志)');
  155. logger.warn('2. 账号确实没有作品');
  156. logger.warn('3. Cookie 已过期(检查是否跳转到登录页)');
  157. logger.warn('4. 账号权限不足(无法访问作品列表)');
  158. logger.warn('5. Python API 返回空列表,Playwright 也失败');
  159. }
  160. }
  161. // 检查账号ID匹配
  162. logger.info('\n=== 账号ID匹配检查 ===');
  163. logger.info(`数据库 accountId: ${account.accountId}`);
  164. logger.info(`API 返回 accountId: ${accountInfo.accountId}`);
  165. if (account.accountId !== accountInfo.accountId) {
  166. logger.warn('⚠️ 账号ID不匹配!这可能导致同步时无法正确匹配账号');
  167. logger.warn('建议: 更新数据库中的 accountId 为 API 返回的值');
  168. } else {
  169. logger.info('✓ 账号ID匹配');
  170. }
  171. process.exit(0);
  172. } catch (e) {
  173. logger.error('执行失败:', e);
  174. process.exit(1);
  175. }
  176. }
  177. void main();