|
|
@@ -6,6 +6,7 @@ import { logger } from '../utils/logger.js';
|
|
|
import { WorkDayStatisticsService } from './WorkDayStatisticsService.js';
|
|
|
import type { ProxyConfig } from '@media-manager/shared';
|
|
|
import { AccountService } from './AccountService.js';
|
|
|
+import { getPythonServiceBaseUrl } from './PythonServiceConfigService.js';
|
|
|
|
|
|
type PlaywrightCookie = {
|
|
|
name: string;
|
|
|
@@ -419,6 +420,132 @@ export class BaijiahaoWorkDailyStatisticsImportService {
|
|
|
return msg.includes('未登录') || msg.includes('not logged in');
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 通过 Python 服务调用百家号 articleListStatistic 接口
|
|
|
+ * 复用 Python 端对 Cookie 的处理和登录逻辑
|
|
|
+ */
|
|
|
+ private async fetchArticleListStatisticViaPython(
|
|
|
+ account: PlatformAccount,
|
|
|
+ params: {
|
|
|
+ startDay: string; // YYYYMMDD
|
|
|
+ endDay: string; // YYYYMMDD
|
|
|
+ type: BjhListType;
|
|
|
+ num: number;
|
|
|
+ count: number;
|
|
|
+ }
|
|
|
+ ): Promise<ArticleListStatisticResponse> {
|
|
|
+ const base = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
|
|
|
+ const url = `${base}/baijiahao/article_stats`;
|
|
|
+
|
|
|
+ const cookie = String(account.cookieData || '').trim();
|
|
|
+ if (!cookie) {
|
|
|
+ throw new Error('百家号账号 cookie 为空,无法调用 Python article_stats');
|
|
|
+ }
|
|
|
+
|
|
|
+ const controller = new AbortController();
|
|
|
+ const timeoutId = setTimeout(() => controller.abort(), 30_000);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(url, {
|
|
|
+ method: 'POST',
|
|
|
+ signal: controller.signal,
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ cookie,
|
|
|
+ start_day: params.startDay,
|
|
|
+ end_day: params.endDay,
|
|
|
+ type: params.type,
|
|
|
+ num: params.num,
|
|
|
+ count: params.count,
|
|
|
+ }),
|
|
|
+ });
|
|
|
+
|
|
|
+ const text = await res.text();
|
|
|
+ let data: any = {};
|
|
|
+ try {
|
|
|
+ data = text ? JSON.parse(text) : {};
|
|
|
+ } catch {
|
|
|
+ throw new Error(`Python article_stats 返回非 JSON 响应: http=${res.status}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!res.ok) {
|
|
|
+ const msg = String(data?.errmsg || data?.error || '').trim() || `HTTP ${res.status}`;
|
|
|
+ throw new Error(`Python article_stats 调用失败: ${msg}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const errno = typeof data?.errno === 'number' ? data.errno : Number(data?.errno ?? 0);
|
|
|
+ const errmsg = String(data?.errmsg || data?.error || '').trim() || undefined;
|
|
|
+ const payload: ArticleListStatisticResponse = {
|
|
|
+ errno,
|
|
|
+ errmsg,
|
|
|
+ data: data?.data,
|
|
|
+ };
|
|
|
+ return payload;
|
|
|
+ } finally {
|
|
|
+ clearTimeout(timeoutId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通过 Python 服务调用百家号 gettrenddata 接口
|
|
|
+ * 复用 Python 端对 Cookie 的处理和登录逻辑
|
|
|
+ */
|
|
|
+ private async fetchTrendDataViaPython(
|
|
|
+ account: PlatformAccount,
|
|
|
+ nid: string
|
|
|
+ ): Promise<GetTrendDataResponse> {
|
|
|
+ const base = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
|
|
|
+ const url = `${base}/baijiahao/trend_data`;
|
|
|
+
|
|
|
+ const cookie = String(account.cookieData || '').trim();
|
|
|
+ if (!cookie) {
|
|
|
+ throw new Error('百家号账号 cookie 为空,无法调用 Python trend_data');
|
|
|
+ }
|
|
|
+
|
|
|
+ const controller = new AbortController();
|
|
|
+ const timeoutId = setTimeout(() => controller.abort(), 30_000);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(url, {
|
|
|
+ method: 'POST',
|
|
|
+ signal: controller.signal,
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ cookie,
|
|
|
+ nid,
|
|
|
+ }),
|
|
|
+ });
|
|
|
+
|
|
|
+ const text = await res.text();
|
|
|
+ let data: any = {};
|
|
|
+ try {
|
|
|
+ data = text ? JSON.parse(text) : {};
|
|
|
+ } catch {
|
|
|
+ throw new Error(`Python trend_data 返回非 JSON 响应: http=${res.status}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!res.ok) {
|
|
|
+ const msg = String(data?.errmsg || data?.error || '').trim() || `HTTP ${res.status}`;
|
|
|
+ throw new Error(`Python trend_data 调用失败: ${msg}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const errno = typeof data?.errno === 'number' ? data.errno : Number(data?.errno ?? 0);
|
|
|
+ const errmsg = String(data?.errmsg || data?.error || '').trim() || undefined;
|
|
|
+ const payload: GetTrendDataResponse = {
|
|
|
+ errno,
|
|
|
+ errmsg,
|
|
|
+ data: data?.data,
|
|
|
+ };
|
|
|
+ return payload;
|
|
|
+ } finally {
|
|
|
+ clearTimeout(timeoutId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private async importAccountWorkDaily(account: PlatformAccount, isRetry = false): Promise<void> {
|
|
|
const cookies = parseCookiesFromAccount(account.cookieData);
|
|
|
if (!cookies.length) {
|
|
|
@@ -444,32 +571,14 @@ export class BaijiahaoWorkDailyStatisticsImportService {
|
|
|
if (k) idMap.set(k, w.id);
|
|
|
}
|
|
|
|
|
|
- let context: BrowserContext | null = null;
|
|
|
- let browser: Browser | null = null;
|
|
|
- let shouldClose = false;
|
|
|
- let token = '';
|
|
|
-
|
|
|
try {
|
|
|
- const created = await this.createContext(account, cookies);
|
|
|
- context = created.context;
|
|
|
- browser = created.browser;
|
|
|
- shouldClose = created.shouldClose;
|
|
|
- token = created.token;
|
|
|
-
|
|
|
- if (!token) {
|
|
|
- throw Object.assign(
|
|
|
- new Error('未能提取百家号 token(可能未登录)'),
|
|
|
- { code: 'BJH_NOT_LOGGED_IN' }
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- // 默认取昨天(中国时区)
|
|
|
+ // 默认取近 30 天(中国时区):昨天作为 end_day,往前推 29 天作为 start_day
|
|
|
const now = new Date();
|
|
|
const chinaNow = new Date(now.getTime() + 8 * 60 * 60 * 1000);
|
|
|
const chinaYesterday = new Date(chinaNow.getTime() - 24 * 60 * 60 * 1000);
|
|
|
const endDay = toYmd(chinaYesterday);
|
|
|
const startDayDate = new Date(chinaYesterday);
|
|
|
- startDayDate.setDate(startDayDate.getDate() - 6);
|
|
|
+ startDayDate.setDate(startDayDate.getDate() - 29);
|
|
|
const startDay = toYmd(startDayDate);
|
|
|
|
|
|
const types: BjhListType[] = ['small_video_v2', 'video', 'news'];
|
|
|
@@ -483,7 +592,7 @@ export class BaijiahaoWorkDailyStatisticsImportService {
|
|
|
let num = 1;
|
|
|
let total = 0;
|
|
|
while (true) {
|
|
|
- const body = await this.fetchArticleListStatisticPage(context!, token, {
|
|
|
+ const body = await this.fetchArticleListStatisticViaPython(account, {
|
|
|
startDay,
|
|
|
endDay,
|
|
|
type: t,
|
|
|
@@ -538,7 +647,7 @@ export class BaijiahaoWorkDailyStatisticsImportService {
|
|
|
const workId = idMap.get(articleId);
|
|
|
if (!workId) continue;
|
|
|
|
|
|
- const trend = await this.fetchTrendData(context!, token, articleId);
|
|
|
+ const trend = await this.fetchTrendDataViaPython(account, articleId);
|
|
|
if (this.isNotLoggedInErrno(trend.errno)) {
|
|
|
const err = new Error(
|
|
|
`gettrenddata errno=${trend.errno} 未登录/会话失效`
|
|
|
@@ -634,11 +743,6 @@ export class BaijiahaoWorkDailyStatisticsImportService {
|
|
|
}
|
|
|
|
|
|
throw e;
|
|
|
- } finally {
|
|
|
- await context?.close().catch(() => undefined);
|
|
|
- if (shouldClose && browser) {
|
|
|
- await browser.close().catch(() => undefined);
|
|
|
- }
|
|
|
}
|
|
|
}
|
|
|
}
|