/** * 检查抖音账号同步问题 - 诊断脚本 * 用法: tsx src/scripts/check-douyin-account.ts */ import { initDatabase, AppDataSource, PlatformAccount } from '../models/index.js'; import { logger } from '../utils/logger.js'; import { headlessBrowserService } from '../services/HeadlessBrowserService.js'; import { CookieManager } from '../automation/cookie.js'; async function main() { const accountIdOrAccountId = process.argv[2]; if (!accountIdOrAccountId) { logger.error('请提供账号ID或account_id'); logger.info('用法: tsx src/scripts/check-douyin-account.ts '); process.exit(1); } try { await initDatabase(); const accountRepository = AppDataSource.getRepository(PlatformAccount); // 先尝试作为数据库主键ID查询(仅当参数是纯数字时) const maybeId = Number(accountIdOrAccountId); let account: PlatformAccount | null = null; if (!Number.isNaN(maybeId) && Number.isInteger(maybeId) && maybeId > 0) { account = await accountRepository.findOne({ where: { id: maybeId }, }); } // 如果没找到,尝试作为account_id(字符串)查询 if (!account) { account = await accountRepository.findOne({ where: { accountId: accountIdOrAccountId }, }); } // 如果还是没找到,尝试模糊匹配(dy_开头) if (!account && accountIdOrAccountId.startsWith('dy_')) { const accounts = await accountRepository.find({ where: { platform: 'douyin' }, }); account = accounts.find( (a) => a.accountId?.includes(accountIdOrAccountId.replace('dy_', '')) || a.accountId === accountIdOrAccountId ); } if (!account) { logger.error(`未找到账号: ${accountIdOrAccountId}`); logger.info('可用的抖音账号:'); const allAccounts = await accountRepository.find({ where: { platform: 'douyin' }, }); allAccounts.forEach((a) => { logger.info( ` ID=${a.id}, accountId=${a.accountId}, name=${a.accountName}, status=${a.status}` ); }); process.exit(1); } logger.info(`找到账号: ID=${account.id}, accountId=${account.accountId}, name=${account.accountName}, status=${account.status}`); if (!account.cookieData) { logger.error('账号没有 cookie 数据'); process.exit(1); } // 解密 Cookie let decryptedCookies: string; try { decryptedCookies = CookieManager.decrypt(account.cookieData); logger.info('Cookie 解密成功'); } catch { decryptedCookies = account.cookieData; logger.info('使用原始 cookie 数据'); } // 解析 Cookie(支持 JSON 和 \"name=value; name2=value2\" 两种格式) let cookieList: { name: string; value: string; domain: string; path: string }[]; try { cookieList = JSON.parse(decryptedCookies); logger.info(`从 JSON 解析了 ${cookieList.length} 个 cookie`); } catch { // 回退解析字符串格式的 cookie(与 WorkService.parseCookieString 保持一致) logger.warn('Cookie 不是 JSON,尝试按字符串格式解析...'); const domain = '.douyin.com'; const cookies: { name: string; value: string; domain: string; path: string }[] = []; const pairs = decryptedCookies.split(';'); for (const pair of pairs) { const trimmed = pair.trim(); if (!trimmed) continue; const eqIndex = trimmed.indexOf('='); if (eqIndex === -1) continue; const name = trimmed.substring(0, eqIndex).trim(); const value = trimmed.substring(eqIndex + 1).trim(); if (!name || !value) continue; cookies.push({ name, value, domain, path: '/' }); } cookieList = cookies; logger.info(`从字符串解析了 ${cookieList.length} 个 cookie`); if (cookieList.length === 0) { logger.error('Cookie 字符串解析失败,未能获取任何 cookie'); logger.info('Cookie 前100字符:', decryptedCookies.substring(0, 100)); process.exit(1); } } // 调用 fetchAccountInfo logger.info('开始获取账号信息和作品列表...'); logger.info('注意:这可能需要一些时间,请耐心等待...'); let accountInfo; try { accountInfo = await headlessBrowserService.fetchAccountInfo('douyin', cookieList, { onWorksFetchProgress: (info) => { logger.info( `[进度] 已获取 ${info.totalSoFar} 个作品,总计: ${info.declaredTotal || '?'}, 当前页: ${info.page}` ); }, }); } catch (error) { logger.error('获取账号信息时出错:', error); throw error; } logger.info('\n=== 账号信息 ==='); logger.info(`accountId: ${accountInfo.accountId}`); logger.info(`accountName: ${accountInfo.accountName}`); logger.info(`avatarUrl: ${accountInfo.avatarUrl ? '有' : '无'}`); logger.info(`fansCount: ${accountInfo.fansCount}`); logger.info(`worksCount: ${accountInfo.worksCount}`); logger.info(`worksList.length: ${accountInfo.worksList?.length || 0}`); logger.info(`source: ${accountInfo.source || 'unknown'}`); logger.info(`pythonAvailable: ${accountInfo.pythonAvailable ? 'yes' : 'no'}`); if (accountInfo.worksCount > 0 && (!accountInfo.worksList || accountInfo.worksList.length === 0)) { logger.warn('\n⚠️ 警告:worksCount > 0 但 worksList 为空!'); logger.warn('这可能表示:'); logger.warn('1. API 返回了总数,但实际列表为空'); logger.warn('2. 作品列表在解析过程中丢失'); logger.warn('3. 分页逻辑有问题,没有正确获取所有作品'); } if (accountInfo.worksList && accountInfo.worksList.length > 0) { logger.info('\n=== 作品列表 ==='); accountInfo.worksList.forEach((work, idx) => { logger.info( `${idx + 1}. ${work.title} (videoId: ${work.videoId}, playCount: ${work.playCount}, likeCount: ${work.likeCount})` ); }); } else { logger.warn('\n=== 未获取到作品列表 ==='); logger.warn(`worksCount: ${accountInfo.worksCount}`); logger.warn(`worksList.length: ${accountInfo.worksList?.length || 0}`); logger.warn(`source: ${accountInfo.source || 'unknown'}`); logger.warn(`pythonAvailable: ${accountInfo.pythonAvailable ? 'yes' : 'no'}`); if (accountInfo.worksCount > 0 && (!accountInfo.worksList || accountInfo.worksList.length === 0)) { logger.error('\n⚠️ 严重问题:worksCount > 0 但 worksList 为空!'); logger.error('这表示 API 返回了作品总数,但实际列表为空'); logger.error('可能的原因:'); logger.error('1. API 分页逻辑有问题'); logger.error('2. 作品列表在解析过程中丢失'); logger.error('3. API 返回格式变化'); } else if (accountInfo.worksCount === 0) { logger.warn('\n可能的原因:'); logger.warn('1. API 调用失败或超时(检查上面的错误日志)'); logger.warn('2. 账号确实没有作品'); logger.warn('3. Cookie 已过期(检查是否跳转到登录页)'); logger.warn('4. 账号权限不足(无法访问作品列表)'); logger.warn('5. Python API 返回空列表,Playwright 也失败'); } } // 检查账号ID匹配 logger.info('\n=== 账号ID匹配检查 ==='); logger.info(`数据库 accountId: ${account.accountId}`); logger.info(`API 返回 accountId: ${accountInfo.accountId}`); if (account.accountId !== accountInfo.accountId) { logger.warn('⚠️ 账号ID不匹配!这可能导致同步时无法正确匹配账号'); logger.warn('建议: 更新数据库中的 accountId 为 API 返回的值'); } else { logger.info('✓ 账号ID匹配'); } process.exit(0); } catch (e) { logger.error('执行失败:', e); process.exit(1); } } void main();