|
|
@@ -531,6 +531,23 @@ class HeadlessBrowserService {
|
|
|
let isLoggedIn = false;
|
|
|
let checkCompleted = false;
|
|
|
let isRiskControl = false;
|
|
|
+ let page: import('playwright').Page | null = null;
|
|
|
+
|
|
|
+ // 监听 check/user 接口响应
|
|
|
+ const checkUserHandler = async (response: import('playwright').Response) => {
|
|
|
+ const url = response.url();
|
|
|
+ if (url.includes(DOUYIN_API.CHECK_USER)) {
|
|
|
+ try {
|
|
|
+ const data = await response.json();
|
|
|
+ // result: true 表示已登录
|
|
|
+ isLoggedIn = data?.result === true && data?.status_code === 0;
|
|
|
+ checkCompleted = true;
|
|
|
+ logger.info(`[Douyin] check/user API response: result=${data?.result}, status_code=${data?.status_code}, isLoggedIn=${isLoggedIn}`);
|
|
|
+ } catch {
|
|
|
+ // 忽略解析错误
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
try {
|
|
|
const context = await browser.newContext({
|
|
|
@@ -541,23 +558,10 @@ class HeadlessBrowserService {
|
|
|
});
|
|
|
|
|
|
await context.addCookies(cookies);
|
|
|
- const page = await context.newPage();
|
|
|
+ page = await context.newPage();
|
|
|
|
|
|
- // 监听 check/user 接口响应
|
|
|
- page.on('response', async (response) => {
|
|
|
- const url = response.url();
|
|
|
- if (url.includes(DOUYIN_API.CHECK_USER)) {
|
|
|
- try {
|
|
|
- const data = await response.json();
|
|
|
- // result: true 表示已登录
|
|
|
- isLoggedIn = data?.result === true && data?.status_code === 0;
|
|
|
- checkCompleted = true;
|
|
|
- logger.info(`[Douyin] check/user API response: result=${data?.result}, status_code=${data?.status_code}, isLoggedIn=${isLoggedIn}`);
|
|
|
- } catch {
|
|
|
- // 忽略解析错误
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
+ // 绑定监听器
|
|
|
+ page.on('response', checkUserHandler);
|
|
|
|
|
|
// 访问创作者首页,触发 check/user 接口
|
|
|
await page.goto(DOUYIN_API.CREATOR_HOME, {
|
|
|
@@ -617,6 +621,11 @@ class HeadlessBrowserService {
|
|
|
source: 'browser',
|
|
|
message: error instanceof Error ? error.message : 'Douyin check error',
|
|
|
};
|
|
|
+ } finally {
|
|
|
+ // 移除监听器防止内存泄漏
|
|
|
+ if (page) {
|
|
|
+ page.off('response', checkUserHandler);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1271,124 +1280,127 @@ class HeadlessBrowserService {
|
|
|
total?: number;
|
|
|
} = {};
|
|
|
|
|
|
- try {
|
|
|
- // 从 Cookie 获取用户 ID
|
|
|
- const uidCookie = cookies.find(c =>
|
|
|
- ['passport_uid', 'uid', 'ssid'].includes(c.name)
|
|
|
- );
|
|
|
- if (uidCookie?.value) {
|
|
|
- accountId = `douyin_${uidCookie.value}`;
|
|
|
- }
|
|
|
-
|
|
|
- // 设置 API 响应监听器
|
|
|
- page.on('response', async (response) => {
|
|
|
- const url = response.url();
|
|
|
- try {
|
|
|
- // 监听 check/user 接口 - 验证登录状态
|
|
|
- if (url.includes(DOUYIN_API.CHECK_USER)) {
|
|
|
- const data = await response.json();
|
|
|
- isLoggedIn = data?.result === true && data?.status_code === 0;
|
|
|
- logger.info(`[Douyin API] check/user: isLoggedIn=${isLoggedIn}`);
|
|
|
- }
|
|
|
+ // 设置 API 响应监听器
|
|
|
+ const responseHandler = async (response: import('playwright').Response) => {
|
|
|
+ const url = response.url();
|
|
|
+ try {
|
|
|
+ // 监听 check/user 接口 - 验证登录状态
|
|
|
+ if (url.includes(DOUYIN_API.CHECK_USER)) {
|
|
|
+ const data = await response.json();
|
|
|
+ isLoggedIn = data?.result === true && data?.status_code === 0;
|
|
|
+ logger.info(`[Douyin API] check/user: isLoggedIn=${isLoggedIn}`);
|
|
|
+ }
|
|
|
|
|
|
- // 监听 work_list 接口 - 获取作品列表
|
|
|
- if (url.includes('/work_list') || url.includes('/janus/douyin/creator/pc/work_list')) {
|
|
|
- const data = await response.json();
|
|
|
- if (data?.aweme_list && data.aweme_list.length > 0) {
|
|
|
- // 优先从 author.aweme_count 获取真实的作品数(最准确)
|
|
|
- const firstAweme = data.aweme_list[0];
|
|
|
- const authorAwemeCount = firstAweme?.author?.aweme_count;
|
|
|
- if (authorAwemeCount !== undefined && authorAwemeCount > 0) {
|
|
|
- capturedData.total = authorAwemeCount;
|
|
|
- logger.info(`[Douyin API] Using author.aweme_count as works count: ${authorAwemeCount}`);
|
|
|
+ // 监听 work_list 接口 - 获取作品列表
|
|
|
+ if (url.includes('/work_list') || url.includes('/janus/douyin/creator/pc/work_list')) {
|
|
|
+ const data = await response.json();
|
|
|
+ if (data?.aweme_list && data.aweme_list.length > 0) {
|
|
|
+ // 优先从 author.aweme_count 获取真实的作品数(最准确)
|
|
|
+ const firstAweme = data.aweme_list[0];
|
|
|
+ const authorAwemeCount = firstAweme?.author?.aweme_count;
|
|
|
+ if (authorAwemeCount !== undefined && authorAwemeCount > 0) {
|
|
|
+ capturedData.total = authorAwemeCount;
|
|
|
+ logger.info(`[Douyin API] Using author.aweme_count as works count: ${authorAwemeCount}`);
|
|
|
+ } else {
|
|
|
+ // 备用方案:使用 items 数组长度
|
|
|
+ const itemsCount = data?.items?.length || 0;
|
|
|
+ if (itemsCount > 0) {
|
|
|
+ capturedData.total = (capturedData.total || 0) + itemsCount;
|
|
|
} else {
|
|
|
- // 备用方案:使用 items 数组长度
|
|
|
- const itemsCount = data?.items?.length || 0;
|
|
|
- if (itemsCount > 0) {
|
|
|
- capturedData.total = (capturedData.total || 0) + itemsCount;
|
|
|
- } else {
|
|
|
- // 如果没有 items,使用 aweme_list 长度
|
|
|
- capturedData.total = (capturedData.total || 0) + data.aweme_list.length;
|
|
|
- }
|
|
|
+ // 如果没有 items,使用 aweme_list 长度
|
|
|
+ capturedData.total = (capturedData.total || 0) + data.aweme_list.length;
|
|
|
}
|
|
|
- // 解析作品列表;video_url 使用 video.play_addr.url_list 的第一项
|
|
|
- capturedData.worksList = data.aweme_list.map((aweme: Record<string, unknown>) => {
|
|
|
- const statistics = aweme.statistics as Record<string, unknown> || {};
|
|
|
- const cover = aweme.Cover as { url_list?: string[] } || aweme.video as { cover?: { url_list?: string[] } };
|
|
|
- const coverUrl = cover?.url_list?.[0] || (cover as { cover?: { url_list?: string[] } })?.cover?.url_list?.[0] || '';
|
|
|
- const video = aweme.video as { play_addr?: { url_list?: string[] } } | undefined;
|
|
|
- const videoUrl = video?.play_addr?.url_list?.[0] || '';
|
|
|
-
|
|
|
- return {
|
|
|
- awemeId: String(aweme.aweme_id || ''),
|
|
|
- title: String(aweme.item_title || aweme.desc || '').split('\n')[0].slice(0, 50) || '无标题',
|
|
|
- coverUrl,
|
|
|
- videoUrl,
|
|
|
- duration: Number(aweme.duration || 0),
|
|
|
- createTime: Number(aweme.create_time || 0),
|
|
|
- statistics: {
|
|
|
- play_count: Number(statistics.play_count || 0),
|
|
|
- digg_count: Number(statistics.digg_count || 0),
|
|
|
- comment_count: Number(statistics.comment_count || 0),
|
|
|
- share_count: Number(statistics.share_count || 0),
|
|
|
- collect_count: Number((statistics as any).collect_count || 0),
|
|
|
- },
|
|
|
- };
|
|
|
- });
|
|
|
- logger.info(`[Douyin API] work_list: itemsCount=${capturedData.total}, aweme_list_length=${capturedData.worksList?.length}`);
|
|
|
}
|
|
|
+ // 解析作品列表;video_url 使用 video.play_addr.url_list 的第一项
|
|
|
+ capturedData.worksList = data.aweme_list.map((aweme: Record<string, unknown>) => {
|
|
|
+ const statistics = aweme.statistics as Record<string, unknown> || {};
|
|
|
+ const cover = aweme.Cover as { url_list?: string[] } || aweme.video as { cover?: { url_list?: string[] } };
|
|
|
+ const coverUrl = cover?.url_list?.[0] || (cover as { cover?: { url_list?: string[] } })?.cover?.url_list?.[0] || '';
|
|
|
+ const video = aweme.video as { play_addr?: { url_list?: string[] } } | undefined;
|
|
|
+ const videoUrl = video?.play_addr?.url_list?.[0] || '';
|
|
|
+
|
|
|
+ return {
|
|
|
+ awemeId: String(aweme.aweme_id || ''),
|
|
|
+ title: String(aweme.item_title || aweme.desc || '').split('\n')[0].slice(0, 50) || '无标题',
|
|
|
+ coverUrl,
|
|
|
+ videoUrl,
|
|
|
+ duration: Number(aweme.duration || 0),
|
|
|
+ createTime: Number(aweme.create_time || 0),
|
|
|
+ statistics: {
|
|
|
+ play_count: Number(statistics.play_count || 0),
|
|
|
+ digg_count: Number(statistics.digg_count || 0),
|
|
|
+ comment_count: Number(statistics.comment_count || 0),
|
|
|
+ share_count: Number(statistics.share_count || 0),
|
|
|
+ collect_count: Number((statistics as any).collect_count || 0),
|
|
|
+ },
|
|
|
+ };
|
|
|
+ });
|
|
|
+ logger.info(`[Douyin API] work_list: itemsCount=${capturedData.total}, aweme_list_length=${capturedData.worksList?.length}`);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // 监听账号信息接口 - 增加更多可能的接口
|
|
|
- if (url.includes('/account_base_info') ||
|
|
|
- url.includes('/user/info') ||
|
|
|
- url.includes('/creator/user') ||
|
|
|
- url.includes('/data/overview') ||
|
|
|
- url.includes('/creator-micro/data') ||
|
|
|
- url.includes('/home_data')) {
|
|
|
- const data = await response.json();
|
|
|
- logger.info(`[Douyin API] Captured response from: ${url.split('?')[0]}`);
|
|
|
-
|
|
|
- // 处理 data/overview API - 获取总作品数
|
|
|
- if (url.includes('/data/overview') || url.includes('/creator-micro/data')) {
|
|
|
- if (data?.data) {
|
|
|
- capturedData.dataOverview = {
|
|
|
- fans_count: data.data.fans_count || data.data.follower_count,
|
|
|
- total_works: data.data.total_item_cnt || data.data.works_count || data.data.video_count,
|
|
|
- total_play: data.data.total_play_cnt,
|
|
|
- };
|
|
|
- logger.info(`[Douyin API] Captured data overview: total_works=${capturedData.dataOverview.total_works}, fans_count=${capturedData.dataOverview.fans_count}`);
|
|
|
- }
|
|
|
+ // 监听账号信息接口 - 增加更多可能的接口
|
|
|
+ if (url.includes('/account_base_info') ||
|
|
|
+ url.includes('/user/info') ||
|
|
|
+ url.includes('/creator/user') ||
|
|
|
+ url.includes('/data/overview') ||
|
|
|
+ url.includes('/creator-micro/data') ||
|
|
|
+ url.includes('/home_data')) {
|
|
|
+ const data = await response.json();
|
|
|
+ logger.info(`[Douyin API] Captured response from: ${url.split('?')[0]}`);
|
|
|
+
|
|
|
+ // 处理 data/overview API - 获取总作品数
|
|
|
+ if (url.includes('/data/overview') || url.includes('/creator-micro/data')) {
|
|
|
+ if (data?.data) {
|
|
|
+ capturedData.dataOverview = {
|
|
|
+ fans_count: data.data.fans_count || data.data.follower_count,
|
|
|
+ total_works: data.data.total_item_cnt || data.data.works_count || data.data.video_count,
|
|
|
+ total_play: data.data.total_play_cnt,
|
|
|
+ };
|
|
|
+ logger.info(`[Douyin API] Captured data overview: total_works=${capturedData.dataOverview.total_works}, fans_count=${capturedData.dataOverview.fans_count}`);
|
|
|
}
|
|
|
-
|
|
|
- // 尝试多种数据结构
|
|
|
- const user = data?.user || data?.data?.user || data?.data || data;
|
|
|
- if (user) {
|
|
|
- const nickname = user.nickname || user.name || user.nick_name || user.user_name;
|
|
|
- const avatar = user.avatar_url || user.avatar_thumb?.url_list?.[0] || user.avatar || user.avatar_larger?.url_list?.[0];
|
|
|
- const uid = user.uid || user.user_id || user.id;
|
|
|
- const fans = user.follower_count || user.fans_count || user.mplatform_followers_count;
|
|
|
- // 获取抖音号(unique_id 或 short_id)
|
|
|
- const uniqueId = user.unique_id || user.short_id || user.douyin_id;
|
|
|
-
|
|
|
- if (nickname || uid || uniqueId) {
|
|
|
- capturedData.userInfo = {
|
|
|
- nickname: nickname,
|
|
|
- avatar: avatar,
|
|
|
- uid: uid,
|
|
|
- sec_uid: user.sec_uid,
|
|
|
- unique_id: uniqueId,
|
|
|
- short_id: user.short_id,
|
|
|
- follower_count: fans,
|
|
|
- };
|
|
|
- logger.info(`[Douyin API] user info captured: nickname=${capturedData.userInfo.nickname}, uid=${capturedData.userInfo.uid}, unique_id=${capturedData.userInfo.unique_id}`);
|
|
|
- }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 尝试多种数据结构
|
|
|
+ const user = data?.user || data?.data?.user || data?.data || data;
|
|
|
+ if (user) {
|
|
|
+ const nickname = user.nickname || user.name || user.nick_name || user.user_name;
|
|
|
+ const avatar = user.avatar_url || user.avatar_thumb?.url_list?.[0] || user.avatar || user.avatar_larger?.url_list?.[0];
|
|
|
+ const uid = user.uid || user.user_id || user.id;
|
|
|
+ const fans = user.follower_count || user.fans_count || user.mplatform_followers_count;
|
|
|
+ // 获取抖音号(unique_id 或 short_id)
|
|
|
+ const uniqueId = user.unique_id || user.short_id || user.douyin_id;
|
|
|
+
|
|
|
+ if (nickname || uid || uniqueId) {
|
|
|
+ capturedData.userInfo = {
|
|
|
+ nickname: nickname,
|
|
|
+ avatar: avatar,
|
|
|
+ uid: uid,
|
|
|
+ sec_uid: user.sec_uid,
|
|
|
+ unique_id: uniqueId,
|
|
|
+ short_id: user.short_id,
|
|
|
+ follower_count: fans,
|
|
|
+ };
|
|
|
+ logger.info(`[Douyin API] user info captured: nickname=${capturedData.userInfo.nickname}, uid=${capturedData.userInfo.uid}, unique_id=${capturedData.userInfo.unique_id}`);
|
|
|
}
|
|
|
}
|
|
|
- } catch (e) {
|
|
|
- // 忽略非 JSON 响应
|
|
|
}
|
|
|
- });
|
|
|
+ } catch (e) {
|
|
|
+ // 忽略非 JSON 响应
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 从 Cookie 获取用户 ID
|
|
|
+ const uidCookie = cookies.find(c =>
|
|
|
+ ['passport_uid', 'uid', 'ssid'].includes(c.name)
|
|
|
+ );
|
|
|
+ if (uidCookie?.value) {
|
|
|
+ accountId = `douyin_${uidCookie.value}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绑定监听器
|
|
|
+ page.on('response', responseHandler);
|
|
|
|
|
|
// 访问主页获取基本信息并触发 check/user 接口
|
|
|
logger.info('[Douyin] Navigating to creator home...');
|
|
|
@@ -1701,6 +1713,9 @@ class HeadlessBrowserService {
|
|
|
} catch (error) {
|
|
|
logger.error('Failed to fetch Douyin account info:', error);
|
|
|
logger.error('Error details:', error instanceof Error ? error.stack : String(error));
|
|
|
+ } finally {
|
|
|
+ // 移除监听器防止内存泄漏
|
|
|
+ page.off('response', responseHandler);
|
|
|
}
|
|
|
|
|
|
return { accountId, accountName, avatarUrl, fansCount, worksCount, worksList };
|