|
@@ -306,15 +306,60 @@ export class WeixinVideoWorkStatisticsImportService {
|
|
|
|
|
|
|
|
const postListData = { list: [] as Array<{ exportId?: string; objectId?: string; [k: string]: any }> };
|
|
const postListData = { list: [] as Array<{ exportId?: string; objectId?: string; [k: string]: any }> };
|
|
|
|
|
|
|
|
|
|
+ // 只采纳 _pageUrl 为该值的 post_list 响应(统计-作品页)。请求里可能是编码或解码形式,统一解码后比较
|
|
|
|
|
+ const POST_LIST_PAGE_URL_DECODED = 'https://channels.weixin.qq.com/micro/statistic/post';
|
|
|
|
|
+ const normalizePageUrl = (raw: string): string => {
|
|
|
|
|
+ if (!raw || !raw.trim()) return '';
|
|
|
|
|
+ try {
|
|
|
|
|
+ return decodeURIComponent(raw.trim());
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return raw.trim();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ const getPageUrl = (req: { url: () => string; postData: () => string | undefined }): string => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const fromUrl = new URL(req.url()).searchParams.get('_pageUrl') ?? '';
|
|
|
|
|
+ if (fromUrl) return normalizePageUrl(fromUrl);
|
|
|
|
|
+ const postData = req.postData();
|
|
|
|
|
+ if (typeof postData === 'string') {
|
|
|
|
|
+ const p = JSON.parse(postData);
|
|
|
|
|
+ const raw = (p?._pageUrl ?? p?._page_url ?? '') as string;
|
|
|
|
|
+ return normalizePageUrl(raw);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // ignore
|
|
|
|
|
+ }
|
|
|
|
|
+ return '';
|
|
|
|
|
+ };
|
|
|
page.on('response', async (response) => {
|
|
page.on('response', async (response) => {
|
|
|
- const url = response.url();
|
|
|
|
|
try {
|
|
try {
|
|
|
- if (url.includes('statistic/post_list') && response.request().method() === 'POST') {
|
|
|
|
|
- const body = await response.json().catch(() => ({}));
|
|
|
|
|
- if (body?.errCode === 0 && body?.data?.list) {
|
|
|
|
|
- postListData.list = body.data.list;
|
|
|
|
|
|
|
+ const url = response.request().url();
|
|
|
|
|
+ if (!url.includes('statistic/post_list')) return;
|
|
|
|
|
+ const req = response.request();
|
|
|
|
|
+ const pageUrl = getPageUrl(req);
|
|
|
|
|
+ const reqUrl = new URL(url);
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ `[WX WorkStats] post_list 请求: _pageUrl=${reqUrl.searchParams.get('_pageUrl') ?? '(query无)'} getPageUrl=${pageUrl || '(空)'}`
|
|
|
|
|
+ );
|
|
|
|
|
+ const postData = req.postData();
|
|
|
|
|
+ if (typeof postData === 'string') {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const payload = JSON.parse(postData);
|
|
|
|
|
+ const { _aid, _rid, _pageUrl: _, ...rest } = payload;
|
|
|
|
|
+ logger.info(`[WX WorkStats] post_list 请求体(不含 aid/rid): ${JSON.stringify(rest)}`);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ logger.info(`[WX WorkStats] post_list 请求体(原始): ${postData.slice(0, 500)}`);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ if (pageUrl !== POST_LIST_PAGE_URL_DECODED) return;
|
|
|
|
|
+ const body = await response.json().catch(() => ({}));
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ `[WX WorkStats] post_list 返回: errCode=${body?.errCode} errMsg=${body?.errMsg} totalCount=${body?.data?.totalCount} listLength=${body?.data?.list?.length ?? 0}`
|
|
|
|
|
+ );
|
|
|
|
|
+ logger.info(`[WX WorkStats] post_list 返回完整 body: ${JSON.stringify(body, null, 2)}`);
|
|
|
|
|
+ if (body?.errCode === 0 && body?.data?.list) {
|
|
|
|
|
+ postListData.list = body.data.list;
|
|
|
|
|
+ }
|
|
|
} catch {
|
|
} catch {
|
|
|
// ignore
|
|
// ignore
|
|
|
}
|
|
}
|
|
@@ -327,23 +372,45 @@ export class WeixinVideoWorkStatisticsImportService {
|
|
|
throw new Error('Cookie 已过期,请重新登录');
|
|
throw new Error('Cookie 已过期,请重新登录');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ let singleVideoClicked = false;
|
|
|
for (const sel of TAB_SINGLE_VIDEO_SELECTORS) {
|
|
for (const sel of TAB_SINGLE_VIDEO_SELECTORS) {
|
|
|
const baseLoc = page.locator(sel);
|
|
const baseLoc = page.locator(sel);
|
|
|
if ((await baseLoc.count()) > 0) {
|
|
if ((await baseLoc.count()) > 0) {
|
|
|
- await baseLoc.nth(0).click().catch(() => undefined);
|
|
|
|
|
- break;
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ await baseLoc.nth(0).waitFor({ state: 'visible', timeout: 3000 });
|
|
|
|
|
+ await baseLoc.nth(0).click({ timeout: 3000 });
|
|
|
|
|
+ singleVideoClicked = true;
|
|
|
|
|
+ logger.info(`[WX WorkStats] accountId=${account.id} 已点击「单篇视频」tab, selector=${sel}`);
|
|
|
|
|
+ break;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ logger.warn(`[WX WorkStats] accountId=${account.id} 点击「单篇视频」失败, selector=${sel}`, e);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ if (!singleVideoClicked) {
|
|
|
|
|
+ logger.warn(`[WX WorkStats] accountId=${account.id} 未找到或未点击成功「单篇视频」tab,可能影响 post_list 数据`);
|
|
|
|
|
+ }
|
|
|
await page.waitForTimeout(2000);
|
|
await page.waitForTimeout(2000);
|
|
|
|
|
|
|
|
postListData.list = [];
|
|
postListData.list = [];
|
|
|
|
|
+ let near30Clicked = false;
|
|
|
for (const sel of NEAR_30_DAYS_SELECTORS) {
|
|
for (const sel of NEAR_30_DAYS_SELECTORS) {
|
|
|
const baseLoc = page.locator(sel);
|
|
const baseLoc = page.locator(sel);
|
|
|
if ((await baseLoc.count()) > 0) {
|
|
if ((await baseLoc.count()) > 0) {
|
|
|
- await baseLoc.nth(0).click().catch(() => undefined);
|
|
|
|
|
- break;
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ await baseLoc.nth(0).waitFor({ state: 'visible', timeout: 3000 });
|
|
|
|
|
+ await baseLoc.nth(0).click({ timeout: 3000 });
|
|
|
|
|
+ near30Clicked = true;
|
|
|
|
|
+ logger.info(`[WX WorkStats] accountId=${account.id} 已点击「近30天」, selector=${sel}`);
|
|
|
|
|
+ break;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ logger.warn(`[WX WorkStats] accountId=${account.id} 点击「近30天」失败, selector=${sel}`, e);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ if (!near30Clicked) {
|
|
|
|
|
+ logger.warn(`[WX WorkStats] accountId=${account.id} 未找到或未点击成功「近30天」,post_list 可能无数据`);
|
|
|
|
|
+ }
|
|
|
await page.waitForTimeout(5000);
|
|
await page.waitForTimeout(5000);
|
|
|
|
|
|
|
|
const items = postListData.list;
|
|
const items = postListData.list;
|
|
@@ -421,7 +488,8 @@ export class WeixinVideoWorkStatisticsImportService {
|
|
|
for (const sel of TAB_SINGLE_VIDEO_SELECTORS) {
|
|
for (const sel of TAB_SINGLE_VIDEO_SELECTORS) {
|
|
|
const baseLoc = page.locator(sel);
|
|
const baseLoc = page.locator(sel);
|
|
|
if ((await baseLoc.count()) > 0) {
|
|
if ((await baseLoc.count()) > 0) {
|
|
|
- await baseLoc.nth(0).click().catch(() => undefined);
|
|
|
|
|
|
|
+ await baseLoc.nth(0).waitFor({ state: 'visible', timeout: 3000 }).catch(() => undefined);
|
|
|
|
|
+ await baseLoc.nth(0).click({ timeout: 3000 }).catch(() => undefined);
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -429,7 +497,8 @@ export class WeixinVideoWorkStatisticsImportService {
|
|
|
for (const sel of NEAR_30_DAYS_SELECTORS) {
|
|
for (const sel of NEAR_30_DAYS_SELECTORS) {
|
|
|
const baseLoc = page.locator(sel);
|
|
const baseLoc = page.locator(sel);
|
|
|
if ((await baseLoc.count()) > 0) {
|
|
if ((await baseLoc.count()) > 0) {
|
|
|
- await baseLoc.nth(0).click().catch(() => undefined);
|
|
|
|
|
|
|
+ await baseLoc.nth(0).waitFor({ state: 'visible', timeout: 3000 }).catch(() => undefined);
|
|
|
|
|
+ await baseLoc.nth(0).click({ timeout: 3000 }).catch(() => undefined);
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -438,8 +507,8 @@ export class WeixinVideoWorkStatisticsImportService {
|
|
|
|
|
|
|
|
for (let idx = 0; idx < items.length; idx++) {
|
|
for (let idx = 0; idx < items.length; idx++) {
|
|
|
const item = items[idx]!;
|
|
const item = items[idx]!;
|
|
|
- const eid = (item.exportId ?? '').trim();
|
|
|
|
|
- const oid = (item.objectId ?? '').trim();
|
|
|
|
|
|
|
+ const eid = (item.exportId ?? item.export_id ?? '').trim();
|
|
|
|
|
+ const oid = (item.objectId ?? item.object_id ?? '').trim();
|
|
|
if (!oid) continue;
|
|
if (!oid) continue;
|
|
|
if (processedExportIds.has(eid)) continue;
|
|
if (processedExportIds.has(eid)) continue;
|
|
|
|
|
|