修复whatshub工单数据同步
This commit is contained in:
@@ -147,12 +147,16 @@ class AntiBotConfigBuilder
|
|||||||
{
|
{
|
||||||
if ($ticketType === 'whatshub') {
|
if ($ticketType === 'whatshub') {
|
||||||
return [
|
return [
|
||||||
'postCfReadyUrlContains' => 'work-order-sharing',
|
'postCfReadyUrlContains' => 'work-order-sharing',
|
||||||
'postCfReadySelector' => '.el-input__inner',
|
// 业务页表格就绪即可;密码框为 vxe-form,勿用泛化 .el-input__inner
|
||||||
'postCfReadyApiUrl' => '/api/whatshub-counter/workShare/open/statistics',
|
'postCfReadySelector' => '.vxe-table, .vxe-grid',
|
||||||
'postCfReadyTimeoutMs' => 60000,
|
'postCfReadySelectorOptional' => true,
|
||||||
'postCfSettleMs' => 500,
|
// statistics 仅在密码验证后触发,禁止 pre-auth 等待(见 Node skipPostCfReadyApi)
|
||||||
'postCfSpaWaitMs' => 4000,
|
'postCfReadyTimeoutMs' => 60000,
|
||||||
|
'postCfSettleMs' => 500,
|
||||||
|
'postCfSpaWaitMs' => 4000,
|
||||||
|
'skipShortLinkCfPurgeOnReuse' => true,
|
||||||
|
'authSkipIfNoPasswordDialog' => true,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,12 +52,13 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 混合同步:cURL 拉号码 + Node 监听 statistics 算完成量;任一失败则完整 Node fallback
|
* 混合同步:cURL 拉号码 + Node 监听 statistics 算完成量
|
||||||
|
* statistics 失败时清 session 重试;仍失败则 fallback 完整 Node 仅取完成量
|
||||||
*/
|
*/
|
||||||
public function run(): UnifiedScrmData
|
public function run(): UnifiedScrmData
|
||||||
{
|
{
|
||||||
$apiResult = $this->tryFetchViaShareCodeApi();
|
$apiResult = $this->tryFetchViaShareCodeApi();
|
||||||
$statsCount = $this->tryFetchStatisticsViaNode();
|
$statsCount = $this->tryFetchStatisticsViaNode(false);
|
||||||
|
|
||||||
if ($apiResult instanceof UnifiedScrmData && $statsCount !== null) {
|
if ($apiResult instanceof UnifiedScrmData && $statsCount !== null) {
|
||||||
$apiResult->todayNewCount = $statsCount;
|
$apiResult->todayNewCount = $statsCount;
|
||||||
@@ -68,26 +69,82 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
return $apiResult;
|
return $apiResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cURL 已成功:清 session 再试 statistics;仍失败则完整 Node 只取完成量
|
||||||
|
if ($apiResult instanceof UnifiedScrmData) {
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub stats retry after purge session', [
|
||||||
|
'numberCount' => count($apiResult->numbers),
|
||||||
|
]);
|
||||||
|
$statsCount = $this->tryFetchStatisticsViaNode(true);
|
||||||
|
if ($statsCount !== null) {
|
||||||
|
$apiResult->todayNewCount = $statsCount;
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub hybrid path ok (retry)', [
|
||||||
|
'numberCount' => count($apiResult->numbers),
|
||||||
|
'todayNewCount' => $statsCount,
|
||||||
|
]);
|
||||||
|
return $apiResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub stats fallback full node for complete count', [
|
||||||
|
'pageUrl' => $this->pageUrl,
|
||||||
|
'numberCount' => count($apiResult->numbers),
|
||||||
|
]);
|
||||||
|
try {
|
||||||
|
$fullResult = parent::run();
|
||||||
|
$apiResult->todayNewCount = $fullResult->todayNewCount;
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub hybrid path ok (full node stats)', [
|
||||||
|
'numberCount' => count($apiResult->numbers),
|
||||||
|
'todayNewCount' => $apiResult->todayNewCount,
|
||||||
|
]);
|
||||||
|
return $apiResult;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub stats fallback full node failed', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \RuntimeException('Whatshub statistics 同步失败(号码已通过 API 获取,无法更新完成量)');
|
||||||
|
}
|
||||||
|
|
||||||
SplitTicketSyncLogger::log('spider', 'whatshub hybrid incomplete, fallback full node', [
|
SplitTicketSyncLogger::log('spider', 'whatshub hybrid incomplete, fallback full node', [
|
||||||
'pageUrl' => $this->pageUrl,
|
'pageUrl' => $this->pageUrl,
|
||||||
'apiOk' => $apiResult instanceof UnifiedScrmData,
|
'apiOk' => false,
|
||||||
'statisticsOk' => $statsCount !== null,
|
'statisticsOk' => $statsCount !== null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return parent::run();
|
return parent::run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whatshub vxe 密码表单:placeholder 含「密码」+ 确认按钮,与业务页搜索框区分 */
|
||||||
|
private const AUTH_PASSWORD_INPUT = '.vxe-form .el-input__inner[placeholder*="密码"]';
|
||||||
|
|
||||||
|
private const AUTH_CONFIRM_BUTTON = '.vxe-form .vxe-button-group button.theme--primary[type="submit"]';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Node 监听 statistics API,返回 dayNewFans + reDayNewFans 总和;失败返回 null
|
* Node 监听 statistics API,返回 dayNewFans + reDayNewFans 总和;失败返回 null
|
||||||
|
*
|
||||||
|
* @param bool $isRetry 是否为清 session 后的二次尝试
|
||||||
*/
|
*/
|
||||||
private function tryFetchStatisticsViaNode(): ?int
|
private function tryFetchStatisticsViaNode(bool $isRetry = false): ?int
|
||||||
{
|
{
|
||||||
$config = $this->getSpiderConfig();
|
$config = $this->getSpiderConfig();
|
||||||
$antiBot = $config['antiBot'] ?? null;
|
$antiBot = $config['antiBot'] ?? null;
|
||||||
|
if (is_array($antiBot)) {
|
||||||
|
// statistics-only:早挂拦截器、识别 vxe 密码框、session 有效时 skip auth
|
||||||
|
$antiBot = array_merge($antiBot, [
|
||||||
|
'skipPostCfReadyApi' => true,
|
||||||
|
'authSkipIfNoPasswordDialog' => true,
|
||||||
|
'authSelectorTimeoutMs' => 30000,
|
||||||
|
'earlyApiIntercept' => true,
|
||||||
|
]);
|
||||||
|
if ($isRetry) {
|
||||||
|
$antiBot['purgeSession'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SplitTicketSyncLogger::log('spider', 'whatshub stats node request', [
|
SplitTicketSyncLogger::log('spider', 'whatshub stats node request', [
|
||||||
'pageUrl' => $this->pageUrl,
|
'pageUrl' => $this->pageUrl,
|
||||||
'api' => self::API_DETAILS,
|
'api' => self::API_DETAILS,
|
||||||
|
'isRetry' => $isRetry,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$result = $this->requestNode('/api/auth-and-intercept', [
|
$result = $this->requestNode('/api/auth-and-intercept', [
|
||||||
@@ -101,6 +158,7 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
SplitTicketSyncLogger::log('spider', 'whatshub stats node failed', [
|
SplitTicketSyncLogger::log('spider', 'whatshub stats node failed', [
|
||||||
'error' => $result['error'] ?? '未知',
|
'error' => $result['error'] ?? '未知',
|
||||||
'code' => $result['code'] ?? null,
|
'code' => $result['code'] ?? null,
|
||||||
|
'page' => $result['page'] ?? null,
|
||||||
]);
|
]);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -109,6 +167,7 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
if (!isset($intercepted[self::API_DETAILS]['data']) || !is_array($intercepted[self::API_DETAILS]['data'])) {
|
if (!isset($intercepted[self::API_DETAILS]['data']) || !is_array($intercepted[self::API_DETAILS]['data'])) {
|
||||||
SplitTicketSyncLogger::log('spider', 'whatshub stats node missing payload', [
|
SplitTicketSyncLogger::log('spider', 'whatshub stats node missing payload', [
|
||||||
'intercepted' => array_keys(is_array($intercepted) ? $intercepted : []),
|
'intercepted' => array_keys(is_array($intercepted) ? $intercepted : []),
|
||||||
|
'page' => $result['page'] ?? null,
|
||||||
]);
|
]);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -287,10 +346,10 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
'listMethod' => 'POST',
|
'listMethod' => 'POST',
|
||||||
'paginationMode' => self::MODE_UI,
|
'paginationMode' => self::MODE_UI,
|
||||||
'authActions' => [
|
'authActions' => [
|
||||||
// Element Plus 密码弹窗:仅注入可见 input
|
// Whatshub 密码 UI 为 vxe-form + Element input,限定在表单内避免误填搜索框
|
||||||
['type' => 'vue_fill', 'selector' => '.el-input__inner', 'value' => $this->password],
|
['type' => 'vue_fill', 'selector' => self::AUTH_PASSWORD_INPUT, 'value' => $this->password],
|
||||||
['type' => 'wait', 'ms' => 500],
|
['type' => 'wait', 'ms' => 500],
|
||||||
['type' => 'vue_click', 'selector' => '.vxe-button-group .theme--primary'],
|
['type' => 'vue_click', 'selector' => self::AUTH_CONFIRM_BUTTON],
|
||||||
['type' => 'wait', 'ms' => 2200],
|
['type' => 'wait', 'ms' => 2200],
|
||||||
],
|
],
|
||||||
'antiBot' => AntiBotConfigBuilder::build('whatshub', $this->pageUrl),
|
'antiBot' => AntiBotConfigBuilder::build('whatshub', $this->pageUrl),
|
||||||
|
|||||||
@@ -147,12 +147,16 @@ class AntiBotConfigBuilder
|
|||||||
{
|
{
|
||||||
if ($ticketType === 'whatshub') {
|
if ($ticketType === 'whatshub') {
|
||||||
return [
|
return [
|
||||||
'postCfReadyUrlContains' => 'work-order-sharing',
|
'postCfReadyUrlContains' => 'work-order-sharing',
|
||||||
'postCfReadySelector' => '.el-input__inner',
|
// 业务页表格就绪即可;密码框为 vxe-form,勿用泛化 .el-input__inner
|
||||||
'postCfReadyApiUrl' => '/api/whatshub-counter/workShare/open/statistics',
|
'postCfReadySelector' => '.vxe-table, .vxe-grid',
|
||||||
'postCfReadyTimeoutMs' => 60000,
|
'postCfReadySelectorOptional' => true,
|
||||||
'postCfSettleMs' => 500,
|
// statistics 仅在密码验证后触发,禁止 pre-auth 等待(见 Node skipPostCfReadyApi)
|
||||||
'postCfSpaWaitMs' => 4000,
|
'postCfReadyTimeoutMs' => 60000,
|
||||||
|
'postCfSettleMs' => 500,
|
||||||
|
'postCfSpaWaitMs' => 4000,
|
||||||
|
'skipShortLinkCfPurgeOnReuse' => true,
|
||||||
|
'authSkipIfNoPasswordDialog' => true,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,12 +52,13 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 混合同步:cURL 拉号码 + Node 监听 statistics 算完成量;任一失败则完整 Node fallback
|
* 混合同步:cURL 拉号码 + Node 监听 statistics 算完成量
|
||||||
|
* statistics 失败时清 session 重试;仍失败则 fallback 完整 Node 仅取完成量
|
||||||
*/
|
*/
|
||||||
public function run(): UnifiedScrmData
|
public function run(): UnifiedScrmData
|
||||||
{
|
{
|
||||||
$apiResult = $this->tryFetchViaShareCodeApi();
|
$apiResult = $this->tryFetchViaShareCodeApi();
|
||||||
$statsCount = $this->tryFetchStatisticsViaNode();
|
$statsCount = $this->tryFetchStatisticsViaNode(false);
|
||||||
|
|
||||||
if ($apiResult instanceof UnifiedScrmData && $statsCount !== null) {
|
if ($apiResult instanceof UnifiedScrmData && $statsCount !== null) {
|
||||||
$apiResult->todayNewCount = $statsCount;
|
$apiResult->todayNewCount = $statsCount;
|
||||||
@@ -68,26 +69,82 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
return $apiResult;
|
return $apiResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cURL 已成功:清 session 再试 statistics;仍失败则完整 Node 只取完成量
|
||||||
|
if ($apiResult instanceof UnifiedScrmData) {
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub stats retry after purge session', [
|
||||||
|
'numberCount' => count($apiResult->numbers),
|
||||||
|
]);
|
||||||
|
$statsCount = $this->tryFetchStatisticsViaNode(true);
|
||||||
|
if ($statsCount !== null) {
|
||||||
|
$apiResult->todayNewCount = $statsCount;
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub hybrid path ok (retry)', [
|
||||||
|
'numberCount' => count($apiResult->numbers),
|
||||||
|
'todayNewCount' => $statsCount,
|
||||||
|
]);
|
||||||
|
return $apiResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub stats fallback full node for complete count', [
|
||||||
|
'pageUrl' => $this->pageUrl,
|
||||||
|
'numberCount' => count($apiResult->numbers),
|
||||||
|
]);
|
||||||
|
try {
|
||||||
|
$fullResult = parent::run();
|
||||||
|
$apiResult->todayNewCount = $fullResult->todayNewCount;
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub hybrid path ok (full node stats)', [
|
||||||
|
'numberCount' => count($apiResult->numbers),
|
||||||
|
'todayNewCount' => $apiResult->todayNewCount,
|
||||||
|
]);
|
||||||
|
return $apiResult;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub stats fallback full node failed', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \RuntimeException('Whatshub statistics 同步失败(号码已通过 API 获取,无法更新完成量)');
|
||||||
|
}
|
||||||
|
|
||||||
SplitTicketSyncLogger::log('spider', 'whatshub hybrid incomplete, fallback full node', [
|
SplitTicketSyncLogger::log('spider', 'whatshub hybrid incomplete, fallback full node', [
|
||||||
'pageUrl' => $this->pageUrl,
|
'pageUrl' => $this->pageUrl,
|
||||||
'apiOk' => $apiResult instanceof UnifiedScrmData,
|
'apiOk' => false,
|
||||||
'statisticsOk' => $statsCount !== null,
|
'statisticsOk' => $statsCount !== null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return parent::run();
|
return parent::run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whatshub vxe 密码表单:placeholder 含「密码」+ 确认按钮,与业务页搜索框区分 */
|
||||||
|
private const AUTH_PASSWORD_INPUT = '.vxe-form .el-input__inner[placeholder*="密码"]';
|
||||||
|
|
||||||
|
private const AUTH_CONFIRM_BUTTON = '.vxe-form .vxe-button-group button.theme--primary[type="submit"]';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Node 监听 statistics API,返回 dayNewFans + reDayNewFans 总和;失败返回 null
|
* Node 监听 statistics API,返回 dayNewFans + reDayNewFans 总和;失败返回 null
|
||||||
|
*
|
||||||
|
* @param bool $isRetry 是否为清 session 后的二次尝试
|
||||||
*/
|
*/
|
||||||
private function tryFetchStatisticsViaNode(): ?int
|
private function tryFetchStatisticsViaNode(bool $isRetry = false): ?int
|
||||||
{
|
{
|
||||||
$config = $this->getSpiderConfig();
|
$config = $this->getSpiderConfig();
|
||||||
$antiBot = $config['antiBot'] ?? null;
|
$antiBot = $config['antiBot'] ?? null;
|
||||||
|
if (is_array($antiBot)) {
|
||||||
|
// statistics-only:早挂拦截器、识别 vxe 密码框、session 有效时 skip auth
|
||||||
|
$antiBot = array_merge($antiBot, [
|
||||||
|
'skipPostCfReadyApi' => true,
|
||||||
|
'authSkipIfNoPasswordDialog' => true,
|
||||||
|
'authSelectorTimeoutMs' => 30000,
|
||||||
|
'earlyApiIntercept' => true,
|
||||||
|
]);
|
||||||
|
if ($isRetry) {
|
||||||
|
$antiBot['purgeSession'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SplitTicketSyncLogger::log('spider', 'whatshub stats node request', [
|
SplitTicketSyncLogger::log('spider', 'whatshub stats node request', [
|
||||||
'pageUrl' => $this->pageUrl,
|
'pageUrl' => $this->pageUrl,
|
||||||
'api' => self::API_DETAILS,
|
'api' => self::API_DETAILS,
|
||||||
|
'isRetry' => $isRetry,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$result = $this->requestNode('/api/auth-and-intercept', [
|
$result = $this->requestNode('/api/auth-and-intercept', [
|
||||||
@@ -101,6 +158,7 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
SplitTicketSyncLogger::log('spider', 'whatshub stats node failed', [
|
SplitTicketSyncLogger::log('spider', 'whatshub stats node failed', [
|
||||||
'error' => $result['error'] ?? '未知',
|
'error' => $result['error'] ?? '未知',
|
||||||
'code' => $result['code'] ?? null,
|
'code' => $result['code'] ?? null,
|
||||||
|
'page' => $result['page'] ?? null,
|
||||||
]);
|
]);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -109,6 +167,7 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
if (!isset($intercepted[self::API_DETAILS]['data']) || !is_array($intercepted[self::API_DETAILS]['data'])) {
|
if (!isset($intercepted[self::API_DETAILS]['data']) || !is_array($intercepted[self::API_DETAILS]['data'])) {
|
||||||
SplitTicketSyncLogger::log('spider', 'whatshub stats node missing payload', [
|
SplitTicketSyncLogger::log('spider', 'whatshub stats node missing payload', [
|
||||||
'intercepted' => array_keys(is_array($intercepted) ? $intercepted : []),
|
'intercepted' => array_keys(is_array($intercepted) ? $intercepted : []),
|
||||||
|
'page' => $result['page'] ?? null,
|
||||||
]);
|
]);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -287,10 +346,10 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
'listMethod' => 'POST',
|
'listMethod' => 'POST',
|
||||||
'paginationMode' => self::MODE_UI,
|
'paginationMode' => self::MODE_UI,
|
||||||
'authActions' => [
|
'authActions' => [
|
||||||
// Element Plus 密码弹窗:仅注入可见 input
|
// Whatshub 密码 UI 为 vxe-form + Element input,限定在表单内避免误填搜索框
|
||||||
['type' => 'vue_fill', 'selector' => '.el-input__inner', 'value' => $this->password],
|
['type' => 'vue_fill', 'selector' => self::AUTH_PASSWORD_INPUT, 'value' => $this->password],
|
||||||
['type' => 'wait', 'ms' => 500],
|
['type' => 'wait', 'ms' => 500],
|
||||||
['type' => 'vue_click', 'selector' => '.vxe-button-group .theme--primary'],
|
['type' => 'vue_click', 'selector' => self::AUTH_CONFIRM_BUTTON],
|
||||||
['type' => 'wait', 'ms' => 2200],
|
['type' => 'wait', 'ms' => 2200],
|
||||||
],
|
],
|
||||||
'antiBot' => AntiBotConfigBuilder::build('whatshub', $this->pageUrl),
|
'antiBot' => AntiBotConfigBuilder::build('whatshub', $this->pageUrl),
|
||||||
|
|||||||
+536
-37
@@ -61,9 +61,11 @@ const {
|
|||||||
getSessionDebugInfo,
|
getSessionDebugInfo,
|
||||||
getSessionStats,
|
getSessionStats,
|
||||||
isSessionLikelyValid,
|
isSessionLikelyValid,
|
||||||
|
loadSession,
|
||||||
} = require('./lib/session-store');
|
} = require('./lib/session-store');
|
||||||
const { resolveSessionContext, touchSessionIfPresent } = require('./lib/session-context');
|
const { resolveSessionContext, touchSessionIfPresent } = require('./lib/session-context');
|
||||||
const { getProfileStats, profileExists } = require('./lib/profile-dir');
|
const { getProfileStats, profileExists } = require('./lib/profile-dir');
|
||||||
|
const { getProfileStats, profileExists } = require('./lib/profile-dir');
|
||||||
const {
|
const {
|
||||||
isHuojianLongLinkUrl,
|
isHuojianLongLinkUrl,
|
||||||
isHuojianShortEntryUrl,
|
isHuojianShortEntryUrl,
|
||||||
@@ -86,6 +88,27 @@ async function runBrowserTask(taskName, handler, profile = 'standard') {
|
|||||||
return runWithProfileLimit(taskName, profile, handler);
|
return runWithProfileLimit(taskName, profile, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** PHP antiBot.purgeSession:清除磁盘 session 文件(不修改 session-context 模块) */
|
||||||
|
function purgeStoredSessionIfRequested(antiBot) {
|
||||||
|
const sessionKey = antiBot?.sessionKey || '';
|
||||||
|
if (!sessionKey || antiBot?.purgeSession !== true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const path = require('path');
|
||||||
|
const hash = crypto.createHash('md5').update(sessionKey).digest('hex');
|
||||||
|
const filePath = path.join(RUNTIME_DIR, 'sessions', `${hash}.json`);
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
console.log(`[Session] purgeSession 已清除 sessionKey=${sessionKey}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (_) { /* ignore */ }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function sendTaskError(res, error, extra = {}) {
|
function sendTaskError(res, error, extra = {}) {
|
||||||
if (error.code === 'QUEUE_TIMEOUT' || error.message === 'POOL_ACQUIRE_TIMEOUT') {
|
if (error.code === 'QUEUE_TIMEOUT' || error.message === 'POOL_ACQUIRE_TIMEOUT') {
|
||||||
return res.status(503).json({
|
return res.status(503).json({
|
||||||
@@ -190,6 +213,30 @@ async function waitForUrlSettled(page, { maxAttempts = 15, stableMs = 500, redir
|
|||||||
return currentUrl;
|
return currentUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** PHP → Node 透传的 Whatshub/auth 扩展字段(normalizeAntiBot 不含这些项) */
|
||||||
|
const WHATSHUB_AUTH_EXTRA_KEYS = [
|
||||||
|
'authSkipIfNoPasswordDialog',
|
||||||
|
'authSelectorTimeoutMs',
|
||||||
|
'earlyApiIntercept',
|
||||||
|
'skipPostCfReadyApi',
|
||||||
|
'purgeSession',
|
||||||
|
'postCfReadySelectorOptional',
|
||||||
|
'skipShortLinkCfPurgeOnReuse',
|
||||||
|
];
|
||||||
|
|
||||||
|
function pickAntiBotExtraFields(rawAntiBot, keys) {
|
||||||
|
const extras = {};
|
||||||
|
if (!rawAntiBot || typeof rawAntiBot !== 'object') {
|
||||||
|
return extras;
|
||||||
|
}
|
||||||
|
for (const key of keys) {
|
||||||
|
if (rawAntiBot[key] !== undefined) {
|
||||||
|
extras[key] = rawAntiBot[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extras;
|
||||||
|
}
|
||||||
|
|
||||||
/** 合并 antiBot 扩展字段(postCfReady* 由 PHP 按工单类型传入,未配置则跳过) */
|
/** 合并 antiBot 扩展字段(postCfReady* 由 PHP 按工单类型传入,未配置则跳过) */
|
||||||
function resolveAntiBot(rawAntiBot) {
|
function resolveAntiBot(rawAntiBot) {
|
||||||
const base = normalizeAntiBot(rawAntiBot);
|
const base = normalizeAntiBot(rawAntiBot);
|
||||||
@@ -198,6 +245,7 @@ function resolveAntiBot(rawAntiBot) {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
|
...pickAntiBotExtraFields(rawAntiBot, WHATSHUB_AUTH_EXTRA_KEYS),
|
||||||
postCfReadyUrlContains: rawAntiBot.postCfReadyUrlContains || '',
|
postCfReadyUrlContains: rawAntiBot.postCfReadyUrlContains || '',
|
||||||
postCfReadySelector: rawAntiBot.postCfReadySelector || '',
|
postCfReadySelector: rawAntiBot.postCfReadySelector || '',
|
||||||
postCfReadyApiUrl: rawAntiBot.postCfReadyApiUrl || '',
|
postCfReadyApiUrl: rawAntiBot.postCfReadyApiUrl || '',
|
||||||
@@ -237,11 +285,13 @@ function applyWhatshubPostCfDefaults(antiBot, pageUrl) {
|
|||||||
return {
|
return {
|
||||||
...antiBot,
|
...antiBot,
|
||||||
postCfReadyUrlContains: 'work-order-sharing',
|
postCfReadyUrlContains: 'work-order-sharing',
|
||||||
postCfReadySelector: '.el-input__inner',
|
postCfReadySelector: '.vxe-table, .vxe-grid',
|
||||||
postCfReadyApiUrl: '/api/whatshub-counter/workShare/open/statistics',
|
postCfReadySelectorOptional: true,
|
||||||
postCfReadyTimeoutMs: antiBot.postCfReadyTimeoutMs || 60000,
|
postCfReadyTimeoutMs: antiBot.postCfReadyTimeoutMs || 60000,
|
||||||
postCfSettleMs: antiBot.postCfSettleMs || 500,
|
postCfSettleMs: antiBot.postCfSettleMs || 500,
|
||||||
postCfSpaWaitMs: antiBot.postCfSpaWaitMs || 4000,
|
postCfSpaWaitMs: antiBot.postCfSpaWaitMs || 4000,
|
||||||
|
skipShortLinkCfPurgeOnReuse: antiBot.skipShortLinkCfPurgeOnReuse !== false,
|
||||||
|
authSkipIfNoPasswordDialog: antiBot.authSkipIfNoPasswordDialog !== false,
|
||||||
};
|
};
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return antiBot;
|
return antiBot;
|
||||||
@@ -392,6 +442,26 @@ async function waitForPostCfSelector(page, selector, timeoutMs) {
|
|||||||
await page.waitForSelector(selector, { timeout: Math.min(15000, timeoutMs), visible: true });
|
await page.waitForSelector(selector, { timeout: Math.min(15000, timeoutMs), visible: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 可选 selector:URL 已到业务页时允许降级继续(Whatshub session 复用) */
|
||||||
|
async function waitForPostCfSelectorOptional(page, selector, timeoutMs, antiBot) {
|
||||||
|
const optional = antiBot?.postCfReadySelectorOptional === true;
|
||||||
|
if (!optional) {
|
||||||
|
await waitForPostCfSelector(page, selector, timeoutMs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const shortTimeout = Math.min(8000, timeoutMs);
|
||||||
|
try {
|
||||||
|
await waitForPostCfSelector(page, selector, shortTimeout);
|
||||||
|
} catch (err) {
|
||||||
|
const urlContains = (antiBot.postCfReadyUrlContains || '').trim();
|
||||||
|
if (urlContains && page.url().includes(urlContains)) {
|
||||||
|
console.log(`[CF] 可选 selector "${selector}" 未就绪但 URL 已到业务页,继续`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Whatshub 分享短链 /m/xxx/1 */
|
/** Whatshub 分享短链 /m/xxx/1 */
|
||||||
function isWhatshubShortLinkPath(url) {
|
function isWhatshubShortLinkPath(url) {
|
||||||
try {
|
try {
|
||||||
@@ -402,6 +472,116 @@ function isWhatshubShortLinkPath(url) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whatshub 域名判定 */
|
||||||
|
function isWhatshubHost(url) {
|
||||||
|
try {
|
||||||
|
return new URL(url || '').hostname.includes('whatshub.cc');
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whatshub 密码弹窗 DOM 选择器(与 PHP WhatshubSpider authActions 保持一致) */
|
||||||
|
const WHATSHUB_PASSWORD_INPUT_SEL = '.vxe-form .el-input__inner[placeholder*="密码"]';
|
||||||
|
const WHATSHUB_PASSWORD_CONFIRM_SEL = '.vxe-form .vxe-button-group button.theme--primary[type="submit"]';
|
||||||
|
|
||||||
|
/** Whatshub 密码 UI 是否可见(必须密码 input + 确认按钮同框,排除搜索区 vxe-form) */
|
||||||
|
async function isWhatshubPasswordDialogVisible(page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const isVisible = (el) => {
|
||||||
|
if (!el) return false;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
if (rect.width <= 0 || rect.height <= 0) return false;
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
return style.display !== 'none' && style.visibility !== 'hidden';
|
||||||
|
};
|
||||||
|
const isPasswordInput = (input) => {
|
||||||
|
if (!input || !isVisible(input)) return false;
|
||||||
|
const placeholder = (input.getAttribute('placeholder') || '').trim();
|
||||||
|
if (placeholder.includes('密码')) return true;
|
||||||
|
return input.getAttribute('maxlength') === '8';
|
||||||
|
};
|
||||||
|
const rootHasPasswordForm = (root) => {
|
||||||
|
if (!root || !isVisible(root)) return false;
|
||||||
|
const pwd = root.querySelector('.el-input__inner[placeholder*="密码"], .el-input__inner[maxlength="8"]');
|
||||||
|
if (!isPasswordInput(pwd)) return false;
|
||||||
|
const confirm = root.querySelector(
|
||||||
|
'.vxe-button-group button.theme--primary[type="submit"], button.theme--primary[type="submit"]'
|
||||||
|
);
|
||||||
|
return !!(confirm && isVisible(confirm));
|
||||||
|
};
|
||||||
|
const roots = document.querySelectorAll(
|
||||||
|
'form.vxe-form, .vxe-modal--wrapper, .vxe-modal, '
|
||||||
|
+ '.el-message-box, .el-overlay-message-box, .el-dialog, .el-overlay-dialog'
|
||||||
|
);
|
||||||
|
for (const root of roots) {
|
||||||
|
if (rootHasPasswordForm(root)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 业务页表格已就绪且页面上无密码确认按钮 → 视为 session 已登录 */
|
||||||
|
async function isWhatshubBusinessLoggedIn(page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const isVisible = (el) => {
|
||||||
|
if (!el) return false;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
if (rect.width <= 0 || rect.height <= 0) return false;
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
return style.display !== 'none' && style.visibility !== 'hidden';
|
||||||
|
};
|
||||||
|
const tableReady = !!document.querySelector('.vxe-table, .vxe-grid');
|
||||||
|
const pwdDialog = document.querySelector('.vxe-form .el-input__inner[placeholder*="密码"]');
|
||||||
|
const confirmBtn = document.querySelector(
|
||||||
|
'.vxe-form .vxe-button-group button.theme--primary[type="submit"]'
|
||||||
|
);
|
||||||
|
const hasPasswordUi = (pwdDialog && isVisible(pwdDialog))
|
||||||
|
&& (confirmBtn && isVisible(confirmBtn));
|
||||||
|
return tableReady && !hasPasswordUi;
|
||||||
|
}).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** statistics-only 等场景:导航后立即挂 API 拦截器,避免 session 复用时响应早于监听 */
|
||||||
|
function shouldUseEarlyApiIntercept(antiBot, apiUrls) {
|
||||||
|
if (antiBot?.earlyApiIntercept === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(apiUrls) || apiUrls.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return apiUrls.every((u) => String(u).includes('/open/statistics'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** statistics-only 拦截时不应在密码前等待业务 API */
|
||||||
|
function shouldSkipPostCfReadyApiWait(antiBot, apiUrls = null) {
|
||||||
|
if (antiBot?.skipPostCfReadyApi) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Array.isArray(apiUrls) && apiUrls.length > 0) {
|
||||||
|
return apiUrls.every((u) => String(u).includes('/open/statistics'));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whatshub 使用 PHP 配置的 postCfSpaWaitMs;A2C 等仍保底 8s */
|
||||||
|
function resolveSpaWaitMs(antiBot, onA2cDirect, pageUrl = '') {
|
||||||
|
const configured = antiBot?.postCfSpaWaitMs || 0;
|
||||||
|
if (onA2cDirect) {
|
||||||
|
return Math.max(8000, configured || 8000);
|
||||||
|
}
|
||||||
|
if (isWhatshubShortLinkPath(pageUrl) || isWhatshubHost(pageUrl)) {
|
||||||
|
return Math.max(2000, configured || 4000);
|
||||||
|
}
|
||||||
|
return Math.max(8000, configured || 8000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** authActions 选择器超时(Whatshub session 复用时可放宽) */
|
||||||
|
function resolveAuthSelectorTimeoutMs(antiBot) {
|
||||||
|
const ms = antiBot?.authSelectorTimeoutMs;
|
||||||
|
return typeof ms === 'number' && ms > 0 ? ms : 15000;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whatshub 短链 + Real Browser:不覆盖 UA、不预注入磁盘 cf_clearance
|
* Whatshub 短链 + Real Browser:不覆盖 UA、不预注入磁盘 cf_clearance
|
||||||
* @param {string} pageUrl
|
* @param {string} pageUrl
|
||||||
@@ -766,10 +946,8 @@ async function waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettledFn,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const spaWaitMs = onA2cDirect
|
const spaWaitMs = resolveSpaWaitMs(antiBot, onA2cDirect, targetUrl);
|
||||||
? Math.max(8000, antiBot.postCfSpaWaitMs || 8000)
|
const isWhatshub = isWhatshubShortLinkPath(targetUrl) || isWhatshubHost(targetUrl);
|
||||||
: Math.max(8000, antiBot.postCfSpaWaitMs || 8000);
|
|
||||||
const isWhatshub = isWhatshubShortLinkPath(targetUrl);
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[CF] CF 已完成,等待 SPA ${onA2cDirect ? '直链挂载' : '跳转'} settle=${spaWaitMs}ms current=${page.url()}`
|
`[CF] CF 已完成,等待 SPA ${onA2cDirect ? '直链挂载' : '跳转'} settle=${spaWaitMs}ms current=${page.url()}`
|
||||||
@@ -1132,11 +1310,22 @@ async function purgeCfClearanceFully(page, pageUrl) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Whatshub 短链首访:清除 Chrome Profile 内陈旧 cf_clearance,确保 Turnstile+SPA 在同一 goto 生命周期
|
* Whatshub 短链首访:清除 Chrome Profile 内陈旧 cf_clearance,确保 Turnstile+SPA 在同一 goto 生命周期
|
||||||
|
* session 复用时跳过 purge,保留 clearance 加速二次同步
|
||||||
*/
|
*/
|
||||||
async function prepareWhatshubShortLinkBeforeNav(page, pageUrl, antiBot) {
|
async function prepareWhatshubShortLinkBeforeNav(page, pageUrl, antiBot, storedSession = null) {
|
||||||
if (!antiBot?.enabled || antiBot.profile !== 'real' || !isWhatshubShortLinkPath(pageUrl)) {
|
if (!antiBot?.enabled || antiBot.profile !== 'real' || !isWhatshubShortLinkPath(pageUrl)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (antiBot.skipShortLinkCfPurgeOnReuse) {
|
||||||
|
let session = storedSession;
|
||||||
|
if (!session && antiBot.sessionKey) {
|
||||||
|
session = loadSession(antiBot.sessionKey);
|
||||||
|
}
|
||||||
|
if (session && isSessionLikelyValid(session)) {
|
||||||
|
console.log('[Whatshub] 有效 session 复用,跳过短链 cf_clearance 清除');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
const cookies = await page.cookies();
|
const cookies = await page.cookies();
|
||||||
if (!cookies.some((c) => c.name === 'cf_clearance')) {
|
if (!cookies.some((c) => c.name === 'cf_clearance')) {
|
||||||
return;
|
return;
|
||||||
@@ -1260,14 +1449,18 @@ async function collectPageDiagnostics(page) {
|
|||||||
title: document.title || '',
|
title: document.title || '',
|
||||||
bodyText: (document.body && document.body.innerText ? document.body.innerText : '').slice(0, 500),
|
bodyText: (document.body && document.body.innerText ? document.body.innerText : '').slice(0, 500),
|
||||||
elInputInner: document.querySelectorAll('.el-input__inner').length,
|
elInputInner: document.querySelectorAll('.el-input__inner').length,
|
||||||
|
vxeForm: document.querySelectorAll('form.vxe-form').length,
|
||||||
|
vxePasswordInput: document.querySelectorAll('.vxe-form .el-input__inner[placeholder*="密码"]').length,
|
||||||
|
vxeConfirmBtn: document.querySelectorAll('.vxe-form .vxe-button-group button.theme--primary[type="submit"]').length,
|
||||||
elTable: document.querySelectorAll('.el-table').length,
|
elTable: document.querySelectorAll('.el-table').length,
|
||||||
|
vxeTable: document.querySelectorAll('.vxe-table, .vxe-grid').length,
|
||||||
btnNext: document.querySelectorAll('.btn-next').length,
|
btnNext: document.querySelectorAll('.btn-next').length,
|
||||||
elPagination: document.querySelectorAll('.el-pagination').length,
|
elPagination: document.querySelectorAll('.el-pagination').length,
|
||||||
})).catch(() => null);
|
})).catch(() => null);
|
||||||
return snapshot || { url, title: '', bodyText: '', elInputInner: 0 };
|
return snapshot || { url, title: '', bodyText: '', elInputInner: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, storedSession = null, pageUrl = '') {
|
async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, storedSession = null, pageUrl = '', apiUrls = null) {
|
||||||
if (!antiBot || !antiBot.enabled) {
|
if (!antiBot || !antiBot.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1278,10 +1471,12 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor
|
|||||||
}
|
}
|
||||||
|
|
||||||
const navUrl = pageUrl || page.url();
|
const navUrl = pageUrl || page.url();
|
||||||
|
const skipApiWait = shouldSkipPostCfReadyApiWait(antiBot, apiUrls);
|
||||||
|
|
||||||
await ensureCfCompleteBeforeBusinessReady(page, antiBot, waitForUrlSettledFn, storedSession);
|
await ensureCfCompleteBeforeBusinessReady(page, antiBot, waitForUrlSettledFn, storedSession);
|
||||||
|
|
||||||
const onA2cDirect = isA2cDirectSharePage(page, urlContains);
|
const onA2cDirect = isA2cDirectSharePage(page, urlContains);
|
||||||
|
const whatshubHost = isWhatshubHost(navUrl) || isWhatshubHost(page.url());
|
||||||
let skipSpaWait = false;
|
let skipSpaWait = false;
|
||||||
if (onA2cDirect && await isCfTrulyComplete(page, antiBot)) {
|
if (onA2cDirect && await isCfTrulyComplete(page, antiBot)) {
|
||||||
const domReady = selector
|
const domReady = selector
|
||||||
@@ -1292,6 +1487,17 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor
|
|||||||
skipSpaWait = true;
|
skipSpaWait = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Whatshub session 复用:已在 work-order-sharing 且无密码弹窗时跳过 SPA 固定等待
|
||||||
|
if (!skipSpaWait && whatshubHost && urlContains && page.url().includes(urlContains)) {
|
||||||
|
if (await isCfTrulyComplete(page, antiBot)) {
|
||||||
|
const pwdVisible = await isWhatshubPasswordDialogVisible(page);
|
||||||
|
const tableReady = await page.$('.vxe-table, .vxe-grid').catch(() => null);
|
||||||
|
if (!pwdVisible && (tableReady || (storedSession && isSessionLikelyValid(storedSession)))) {
|
||||||
|
console.log('[CF] Whatshub 业务页已就绪(session 复用),跳过 SPA 等待');
|
||||||
|
skipSpaWait = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!skipSpaWait) {
|
if (!skipSpaWait) {
|
||||||
await waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettledFn, navUrl);
|
await waitForSpaBusinessNavigation(page, antiBot, waitForUrlSettledFn, navUrl);
|
||||||
}
|
}
|
||||||
@@ -1299,7 +1505,7 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor
|
|||||||
const timeoutMs = Math.max(5000, antiBot.postCfReadyTimeoutMs || 30000);
|
const timeoutMs = Math.max(5000, antiBot.postCfReadyTimeoutMs || 30000);
|
||||||
console.log(
|
console.log(
|
||||||
`[CF] 等待业务页就绪 current=${page.url()} urlContains="${urlContains}" `
|
`[CF] 等待业务页就绪 current=${page.url()} urlContains="${urlContains}" `
|
||||||
+ `selector="${selector}" timeout=${timeoutMs}ms`
|
+ `selector="${selector}" timeout=${timeoutMs}ms skipApiWait=${skipApiWait}`
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1310,9 +1516,18 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor
|
|||||||
urlContains
|
urlContains
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await waitForPostCfListApi(page, antiBot, timeoutMs);
|
if (!skipApiWait) {
|
||||||
|
await waitForPostCfListApi(page, antiBot, timeoutMs);
|
||||||
|
} else if (antiBot?.authSkipIfNoPasswordDialog && isStatisticsOnlyApiUrls(apiUrls)) {
|
||||||
|
console.log(
|
||||||
|
'[CF] 跳过 pre-auth statistics 等待'
|
||||||
|
+ '(sid 直进业务页:auth 跳过后 reload 补捕获;需填密码:auth 提交后 waitForApis)'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('[CF] 跳过 pre-auth 业务 API 等待(statistics 在密码验证后触发)');
|
||||||
|
}
|
||||||
if (selector !== '') {
|
if (selector !== '') {
|
||||||
await waitForPostCfSelector(page, selector, timeoutMs);
|
await waitForPostCfSelectorOptional(page, selector, timeoutMs, antiBot);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const retryMs = Math.min(30000, timeoutMs);
|
const retryMs = Math.min(30000, timeoutMs);
|
||||||
@@ -1331,7 +1546,7 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (selector !== '') {
|
if (selector !== '') {
|
||||||
await waitForPostCfSelector(page, selector, retryMs);
|
await waitForPostCfSelectorOptional(page, selector, retryMs, antiBot);
|
||||||
}
|
}
|
||||||
} catch (retryErr) {
|
} catch (retryErr) {
|
||||||
retryErr.pageDiagnostics = await collectPageDiagnostics(page);
|
retryErr.pageDiagnostics = await collectPageDiagnostics(page);
|
||||||
@@ -1351,9 +1566,11 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor
|
|||||||
urlContains
|
urlContains
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await waitForPostCfListApi(page, antiBot, retryMs);
|
if (!skipApiWait) {
|
||||||
|
await waitForPostCfListApi(page, antiBot, retryMs);
|
||||||
|
}
|
||||||
if (selector !== '') {
|
if (selector !== '') {
|
||||||
await waitForPostCfSelector(page, selector, retryMs);
|
await waitForPostCfSelectorOptional(page, selector, retryMs, antiBot);
|
||||||
}
|
}
|
||||||
} catch (retryErr) {
|
} catch (retryErr) {
|
||||||
retryErr.pageDiagnostics = await collectPageDiagnostics(page);
|
retryErr.pageDiagnostics = await collectPageDiagnostics(page);
|
||||||
@@ -1365,9 +1582,11 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor
|
|||||||
try {
|
try {
|
||||||
await page.waitForNetworkIdle({ idleTime: 500, timeout: 20000 }).catch(() => {});
|
await page.waitForNetworkIdle({ idleTime: 500, timeout: 20000 }).catch(() => {});
|
||||||
await softReloadForSpa(page, waitForUrlSettledFn, antiBot, navUrl);
|
await softReloadForSpa(page, waitForUrlSettledFn, antiBot, navUrl);
|
||||||
await waitForPostCfListApi(page, antiBot, retryMs);
|
if (!skipApiWait) {
|
||||||
|
await waitForPostCfListApi(page, antiBot, retryMs);
|
||||||
|
}
|
||||||
if (selector !== '') {
|
if (selector !== '') {
|
||||||
await waitForPostCfSelector(page, selector, retryMs);
|
await waitForPostCfSelectorOptional(page, selector, retryMs, antiBot);
|
||||||
}
|
}
|
||||||
} catch (retryErr) {
|
} catch (retryErr) {
|
||||||
retryErr.pageDiagnostics = await collectPageDiagnostics(page);
|
retryErr.pageDiagnostics = await collectPageDiagnostics(page);
|
||||||
@@ -1386,24 +1605,191 @@ async function awaitPostCfBusinessReady(page, antiBot, waitForUrlSettledFn, stor
|
|||||||
console.log(`[CF] 业务页就绪 url=${page.url()}`);
|
console.log(`[CF] 业务页就绪 url=${page.url()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeAuthActionsWithDiagnostics(page, authActions) {
|
async function executeAuthActionsWithDiagnostics(page, authActions, antiBot = null) {
|
||||||
try {
|
try {
|
||||||
await executeAuthActions(page, authActions);
|
return await executeAuthActions(page, authActions, antiBot);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
err.pageDiagnostics = await collectPageDiagnostics(page);
|
err.pageDiagnostics = await collectPageDiagnostics(page);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeAuthActions(page, authActions) {
|
/** 采集 Whatshub auth 决策所需的 DOM 计数(与 collectPageDiagnostics 字段对齐) */
|
||||||
if (!Array.isArray(authActions)) return;
|
async function collectWhatshubAuthDomCounts(page) {
|
||||||
|
return page.evaluate(() => ({
|
||||||
|
vxeTable: document.querySelectorAll('.vxe-table, .vxe-grid').length,
|
||||||
|
vxePasswordInput: document.querySelectorAll(
|
||||||
|
'.vxe-form .el-input__inner[placeholder*="密码"]'
|
||||||
|
).length,
|
||||||
|
vxeConfirmBtn: document.querySelectorAll(
|
||||||
|
'.vxe-form .vxe-button-group button.theme--primary[type="submit"]'
|
||||||
|
).length,
|
||||||
|
})).catch(() => ({ vxeTable: 0, vxePasswordInput: 0, vxeConfirmBtn: 0 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whatshub skip-auth 决策:URL 仅确认路由,登录/业务态靠 DOM
|
||||||
|
* @returns {Promise<{skip: boolean, reason: string, pwdVisible?: boolean, businessLoggedIn?: boolean, dom?: object, url?: string}>}
|
||||||
|
*/
|
||||||
|
async function resolveWhatshubAuthSkipDecision(page, antiBot) {
|
||||||
|
if (!antiBot?.authSkipIfNoPasswordDialog) {
|
||||||
|
return { skip: false, reason: 'authSkipIfNoPasswordDialog_disabled' };
|
||||||
|
}
|
||||||
|
const urlContains = (antiBot.postCfReadyUrlContains || 'work-order-sharing').trim();
|
||||||
|
let currentUrl = '';
|
||||||
|
try {
|
||||||
|
currentUrl = page.url();
|
||||||
|
} catch (_) {
|
||||||
|
return { skip: false, reason: 'page_url_unavailable' };
|
||||||
|
}
|
||||||
|
const onBusinessRoute = !urlContains || currentUrl.includes(urlContains);
|
||||||
|
if (!onBusinessRoute) {
|
||||||
|
return { skip: false, reason: 'not_on_business_route', url: currentUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
const pwdVisible = await isWhatshubPasswordDialogVisible(page);
|
||||||
|
const businessLoggedIn = await isWhatshubBusinessLoggedIn(page);
|
||||||
|
const dom = await collectWhatshubAuthDomCounts(page);
|
||||||
|
|
||||||
|
if (pwdVisible) {
|
||||||
|
return {
|
||||||
|
skip: false,
|
||||||
|
reason: 'password_dialog_visible',
|
||||||
|
pwdVisible,
|
||||||
|
businessLoggedIn,
|
||||||
|
dom,
|
||||||
|
url: currentUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const tableReadyNoPassword = dom.vxeTable > 0
|
||||||
|
&& dom.vxePasswordInput === 0
|
||||||
|
&& dom.vxeConfirmBtn === 0;
|
||||||
|
if (businessLoggedIn || tableReadyNoPassword) {
|
||||||
|
return {
|
||||||
|
skip: true,
|
||||||
|
reason: businessLoggedIn ? 'business_logged_in' : 'table_ready_no_password_ui',
|
||||||
|
pwdVisible,
|
||||||
|
businessLoggedIn,
|
||||||
|
dom,
|
||||||
|
url: currentUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
skip: true,
|
||||||
|
reason: 'no_password_dialog_on_business_route',
|
||||||
|
pwdVisible,
|
||||||
|
businessLoggedIn,
|
||||||
|
dom,
|
||||||
|
url: currentUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function logWhatshubAuthSkipDecision(decision) {
|
||||||
|
const dom = decision.dom || {};
|
||||||
|
console.log(
|
||||||
|
'[Whatshub] auth skip 决策'
|
||||||
|
+ ` skip=${decision.skip}`
|
||||||
|
+ ` reason=${decision.reason}`
|
||||||
|
+ ` pwdVisible=${decision.pwdVisible}`
|
||||||
|
+ ` businessLoggedIn=${decision.businessLoggedIn}`
|
||||||
|
+ ` vxeTable=${dom.vxeTable ?? '?'}`
|
||||||
|
+ ` vxePasswordInput=${dom.vxePasswordInput ?? '?'}`
|
||||||
|
+ ` vxeConfirmBtn=${dom.vxeConfirmBtn ?? '?'}`
|
||||||
|
+ ` url=${decision.url || ''}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shouldSkipWhatshubAuthActions(page, antiBot) {
|
||||||
|
const decision = await resolveWhatshubAuthSkipDecision(page, antiBot);
|
||||||
|
logWhatshubAuthSkipDecision(decision);
|
||||||
|
return decision.skip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading 结束后才会出现密码框或业务表格;先等二者之一再决定是否需要填密码
|
||||||
|
* @returns {Promise<boolean>} true=应跳过 authActions
|
||||||
|
*/
|
||||||
|
async function waitForWhatshubPasswordFormIfNeeded(page, antiBot) {
|
||||||
|
if (!antiBot || !(isWhatshubHost(page.url()) || page.url().includes('work-order-sharing'))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const initial = await resolveWhatshubAuthSkipDecision(page, antiBot);
|
||||||
|
logWhatshubAuthSkipDecision({ ...initial, reason: `pre_wait_${initial.reason}` });
|
||||||
|
if (initial.skip) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = resolveAuthSelectorTimeoutMs(antiBot);
|
||||||
|
const surfaceTimeout = Math.min(timeout, 10000);
|
||||||
|
console.log('[Whatshub] 等待密码框或业务表格渲染(Loading 结束后)...');
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => {
|
||||||
|
const isVisible = (el) => {
|
||||||
|
if (!el) return false;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
if (rect.width <= 0 || rect.height <= 0) return false;
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
return style.display !== 'none' && style.visibility !== 'hidden';
|
||||||
|
};
|
||||||
|
if (document.querySelector('.vxe-table, .vxe-grid')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const roots = document.querySelectorAll(
|
||||||
|
'form.vxe-form, .vxe-modal--wrapper, .vxe-modal, .el-dialog, .el-overlay-dialog'
|
||||||
|
);
|
||||||
|
for (const root of roots) {
|
||||||
|
const pwd = root.querySelector('.el-input__inner[placeholder*="密码"], .el-input__inner[maxlength="8"]');
|
||||||
|
const confirm = root.querySelector(
|
||||||
|
'.vxe-button-group button.theme--primary[type="submit"], button.theme--primary[type="submit"]'
|
||||||
|
);
|
||||||
|
if (isVisible(pwd) && isVisible(confirm)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
{ timeout: surfaceTimeout, polling: 200 }
|
||||||
|
).catch(() => null);
|
||||||
|
|
||||||
|
const afterSurface = await resolveWhatshubAuthSkipDecision(page, antiBot);
|
||||||
|
logWhatshubAuthSkipDecision({ ...afterSurface, reason: `after_surface_${afterSurface.reason}` });
|
||||||
|
if (afterSurface.skip) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Whatshub] 等待 vxe 密码表单出现(password input + confirm)...');
|
||||||
|
await page.waitForSelector(WHATSHUB_PASSWORD_INPUT_SEL, { visible: true, timeout }).catch(() => null);
|
||||||
|
await page.waitForSelector(WHATSHUB_PASSWORD_CONFIRM_SEL, { visible: true, timeout }).catch(() => null);
|
||||||
|
|
||||||
|
const afterWait = await resolveWhatshubAuthSkipDecision(page, antiBot);
|
||||||
|
logWhatshubAuthSkipDecision({ ...afterWait, reason: `after_password_wait_${afterWait.reason}` });
|
||||||
|
return afterWait.skip;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeAuthActions(page, authActions, antiBot = null) {
|
||||||
|
if (!Array.isArray(authActions)) {
|
||||||
|
return { skipped: false, ran: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await shouldSkipWhatshubAuthActions(page, antiBot)) {
|
||||||
|
console.log('[Whatshub] Session 已登录且无密码弹窗,跳过 authActions');
|
||||||
|
return { skipped: true, ran: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const skipAfterWait = await waitForWhatshubPasswordFormIfNeeded(page, antiBot);
|
||||||
|
if (skipAfterWait) {
|
||||||
|
console.log('[Whatshub] 密码等待后复检:业务页已就绪,跳过 authActions');
|
||||||
|
return { skipped: true, ran: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectorTimeout = resolveAuthSelectorTimeoutMs(antiBot);
|
||||||
|
|
||||||
for (const action of authActions) {
|
for (const action of authActions) {
|
||||||
if (action.type === 'type') {
|
if (action.type === 'type') {
|
||||||
await page.waitForSelector(action.selector, { timeout: 15000 });
|
await page.waitForSelector(action.selector, { timeout: selectorTimeout });
|
||||||
await page.type(action.selector, action.value, { delay: 100 });
|
await page.type(action.selector, action.value, { delay: 100 });
|
||||||
} else if (action.type === 'click') {
|
} else if (action.type === 'click') {
|
||||||
await page.waitForSelector(action.selector, { timeout: 15000 });
|
await page.waitForSelector(action.selector, { timeout: selectorTimeout });
|
||||||
await page.evaluate((sel) => { const el = document.querySelector(sel); if (el) el.click(); }, action.selector);
|
await page.evaluate((sel) => { const el = document.querySelector(sel); if (el) el.click(); }, action.selector);
|
||||||
} else if (action.type === 'press') {
|
} else if (action.type === 'press') {
|
||||||
await new Promise((r) => setTimeout(r, 200));
|
await new Promise((r) => setTimeout(r, 200));
|
||||||
@@ -1411,7 +1797,7 @@ async function executeAuthActions(page, authActions) {
|
|||||||
} else if (action.type === 'wait') {
|
} else if (action.type === 'wait') {
|
||||||
await new Promise((r) => setTimeout(r, action.ms));
|
await new Promise((r) => setTimeout(r, action.ms));
|
||||||
} else if (action.type === 'vue_fill') {
|
} else if (action.type === 'vue_fill') {
|
||||||
await page.waitForSelector(action.selector, { timeout: 15000 });
|
await page.waitForSelector(action.selector, { timeout: selectorTimeout });
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
(sel) => {
|
(sel) => {
|
||||||
const el = document.querySelector(sel);
|
const el = document.querySelector(sel);
|
||||||
@@ -1436,7 +1822,7 @@ async function executeAuthActions(page, authActions) {
|
|||||||
}
|
}
|
||||||
}, action.selector, action.value);
|
}, action.selector, action.value);
|
||||||
} else if (action.type === 'vue_click') {
|
} else if (action.type === 'vue_click') {
|
||||||
await page.waitForSelector(action.selector, { timeout: 15000 });
|
await page.waitForSelector(action.selector, { timeout: selectorTimeout });
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
(sel) => {
|
(sel) => {
|
||||||
const el = document.querySelector(sel);
|
const el = document.querySelector(sel);
|
||||||
@@ -1473,6 +1859,78 @@ async function executeAuthActions(page, authActions) {
|
|||||||
}, action.selector, action.text);
|
}, action.selector, action.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return { skipped: false, ran: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否 statistics-only 拦截(Whatshub 完成量) */
|
||||||
|
function isStatisticsOnlyApiUrls(apiUrls) {
|
||||||
|
if (!Array.isArray(apiUrls) || apiUrls.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return apiUrls.every((u) => String(u).includes('/open/statistics'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function areAllApisIntercepted(interceptor, apiUrls) {
|
||||||
|
if (!interceptor || !Array.isArray(apiUrls)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return apiUrls.every((api) => !!interceptor.interceptedApis[api]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createApiInterceptorHandle() {
|
||||||
|
return { interceptor: null, boundPage: null, cleanup: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachApiInterceptorToPage(handle, page, apiUrls, timeoutMs, label = '') {
|
||||||
|
if (handle.interceptor && handle.boundPage === page) {
|
||||||
|
return handle.interceptor;
|
||||||
|
}
|
||||||
|
if (handle.cleanup) {
|
||||||
|
handle.cleanup();
|
||||||
|
handle.cleanup = null;
|
||||||
|
}
|
||||||
|
handle.interceptor = createApiInterceptor(page, apiUrls, timeoutMs);
|
||||||
|
handle.boundPage = page;
|
||||||
|
handle.cleanup = handle.interceptor.cleanup;
|
||||||
|
const tag = label ? ` (${label})` : '';
|
||||||
|
console.log(`[API] 拦截器已挂载${tag} apis=${apiUrls.join(',')} url=${page.url()}`);
|
||||||
|
return handle.interceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureWhatshubStatisticsAfterAuthSkip(page, interceptor, apiUrls, authSkipped, interceptTimeout) {
|
||||||
|
if (!authSkipped || !isStatisticsOnlyApiUrls(apiUrls)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (areAllApisIntercepted(interceptor, apiUrls)) {
|
||||||
|
console.log('[Whatshub] statistics 已在页面加载时捕获');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'[Whatshub] auth 已跳过但 statistics 未捕获'
|
||||||
|
+ '(可能 CF 换页后漏包或 sid 直进业务页时 pre-auth 未等待),reload 补触发'
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||||
|
if (areAllApisIntercepted(interceptor, apiUrls)) {
|
||||||
|
console.log('[Whatshub] statistics 延迟响应已捕获');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reloadTimeout = Math.min(60000, interceptTimeout);
|
||||||
|
try {
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded', timeout: reloadTimeout });
|
||||||
|
await page.waitForSelector('.vxe-table, .vxe-grid', { timeout: 30000 }).catch(() => null);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[Whatshub] reload 触发 statistics 失败: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areAllApisIntercepted(interceptor, apiUrls)) {
|
||||||
|
console.log('[Whatshub] reload 后 statistics 已捕获');
|
||||||
|
} else {
|
||||||
|
console.warn('[Whatshub] reload 后 statistics 仍未捕获,继续 waitForApis');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createApiInterceptor(page, apiUrls, defaultTimeoutMs = API_INTERCEPT_TIMEOUT_MS) {
|
function createApiInterceptor(page, apiUrls, defaultTimeoutMs = API_INTERCEPT_TIMEOUT_MS) {
|
||||||
@@ -1702,6 +2160,7 @@ app.post('/api/auth-and-intercept', async (req, res) => {
|
|||||||
tagBrowserSession(browserSession, profile, antiBot.sessionKey || '', ['--mute-audio']);
|
tagBrowserSession(browserSession, profile, antiBot.sessionKey || '', ['--mute-audio']);
|
||||||
cfLog.browserReused = !!browserSession.reused;
|
cfLog.browserReused = !!browserSession.reused;
|
||||||
|
|
||||||
|
purgeStoredSessionIfRequested(antiBot);
|
||||||
const { storedSession, mergedCookies } = resolveSessionContext(antiBot, pageUrl);
|
const { storedSession, mergedCookies } = resolveSessionContext(antiBot, pageUrl);
|
||||||
const navigationUrl = resolveNavigationUrl(pageUrl, antiBot);
|
const navigationUrl = resolveNavigationUrl(pageUrl, antiBot);
|
||||||
|
|
||||||
@@ -1727,7 +2186,17 @@ app.post('/api/auth-and-intercept', async (req, res) => {
|
|||||||
await seedShareTokenCookie(page, httpResolvedUrl);
|
await seedShareTokenCookie(page, httpResolvedUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot);
|
await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot, storedSession);
|
||||||
|
|
||||||
|
const apiHandle = createApiInterceptorHandle();
|
||||||
|
let interceptor = null;
|
||||||
|
const useEarlyIntercept = shouldUseEarlyApiIntercept(antiBot, apiUrls);
|
||||||
|
if (useEarlyIntercept) {
|
||||||
|
interceptor = attachApiInterceptorToPage(
|
||||||
|
apiHandle, page, apiUrls, interceptTimeout, 'pre-nav'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await navigateToPage(page, navigationUrl);
|
await navigateToPage(page, navigationUrl);
|
||||||
|
|
||||||
let finalPageUrl = await waitForUrlSettled(page);
|
let finalPageUrl = await waitForUrlSettled(page);
|
||||||
@@ -1753,7 +2222,12 @@ app.post('/api/auth-and-intercept', async (req, res) => {
|
|||||||
browserSession = browserCtx.browserSession;
|
browserSession = browserCtx.browserSession;
|
||||||
page = browserCtx.page;
|
page = browserCtx.page;
|
||||||
|
|
||||||
let interceptor;
|
if (useEarlyIntercept) {
|
||||||
|
interceptor = attachApiInterceptorToPage(
|
||||||
|
apiHandle, page, apiUrls, interceptTimeout, 'post-cf-proxy'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (antiBot.enabled) {
|
if (antiBot.enabled) {
|
||||||
const cfResult = await runCloudflareWithBusinessGuard(
|
const cfResult = await runCloudflareWithBusinessGuard(
|
||||||
page, antiBot, waitForUrlSettled, storedSession, pageUrl, browserCtx
|
page, antiBot, waitForUrlSettled, storedSession, pageUrl, browserCtx
|
||||||
@@ -1782,7 +2256,7 @@ app.post('/api/auth-and-intercept', async (req, res) => {
|
|||||||
applyHuojianBusinessHostHint(antiBot, finalPageUrl);
|
applyHuojianBusinessHostHint(antiBot, finalPageUrl);
|
||||||
touchSessionIfPresent(antiBot.sessionKey);
|
touchSessionIfPresent(antiBot.sessionKey);
|
||||||
|
|
||||||
await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, pageUrl);
|
await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, pageUrl, apiUrls);
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalShareToken = resolveShareToken(finalPageUrl);
|
const finalShareToken = resolveShareToken(finalPageUrl);
|
||||||
@@ -1791,12 +2265,37 @@ app.post('/api/auth-and-intercept', async (req, res) => {
|
|||||||
await seedShareTokenCookie(page, finalPageUrl);
|
await seedShareTokenCookie(page, finalPageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
interceptor = createApiInterceptor(page, apiUrls, interceptTimeout);
|
if (useEarlyIntercept) {
|
||||||
interceptorCleanup = interceptor.cleanup;
|
interceptor = attachApiInterceptorToPage(
|
||||||
|
apiHandle, page, apiUrls, interceptTimeout, 'post-cf-ready'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
interceptor = attachApiInterceptorToPage(
|
||||||
|
apiHandle, page, apiUrls, interceptTimeout, 'pre-auth'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
interceptorCleanup = apiHandle.cleanup;
|
||||||
|
|
||||||
await attachHttpIpRewriteInterceptor(page, 'auth-and-intercept', { shareToken });
|
await attachHttpIpRewriteInterceptor(page, 'auth-and-intercept', { shareToken });
|
||||||
|
|
||||||
await executeAuthActionsWithDiagnostics(page, authActions);
|
const authResult = await executeAuthActionsWithDiagnostics(page, authActions, antiBot);
|
||||||
|
await ensureWhatshubStatisticsAfterAuthSkip(
|
||||||
|
page,
|
||||||
|
interceptor,
|
||||||
|
apiUrls,
|
||||||
|
!!authResult?.skipped,
|
||||||
|
interceptTimeout
|
||||||
|
);
|
||||||
|
|
||||||
|
const capturedBeforeWait = Object.keys(interceptor.interceptedApis).join(',') || 'none';
|
||||||
|
console.log(
|
||||||
|
`[API] 等待 intercept apis=${apiUrls.join(',')} timeout=${interceptTimeout}ms`
|
||||||
|
+ ` already=${capturedBeforeWait}`
|
||||||
|
);
|
||||||
await interceptor.waitForApis(interceptTimeout);
|
await interceptor.waitForApis(interceptTimeout);
|
||||||
|
console.log(
|
||||||
|
`[API] intercept 完成 captured=${Object.keys(interceptor.interceptedApis).join(',') || 'none'}`
|
||||||
|
);
|
||||||
|
|
||||||
const rawCookies = await page.cookies();
|
const rawCookies = await page.cookies();
|
||||||
const cookiePayload = rawCookies.map((c) => ({
|
const cookiePayload = rawCookies.map((c) => ({
|
||||||
@@ -2017,7 +2516,7 @@ app.post('/api/ui-pagination', async (req, res) => {
|
|||||||
const shareToken = resolveShareToken(finalPageUrl, pageUrl);
|
const shareToken = resolveShareToken(finalPageUrl, pageUrl);
|
||||||
await seedShareTokenCookie(page, navigationUrl);
|
await seedShareTokenCookie(page, navigationUrl);
|
||||||
|
|
||||||
await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot);
|
await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot, storedSession);
|
||||||
await navigateToPage(page, navigationUrl);
|
await navigateToPage(page, navigationUrl);
|
||||||
|
|
||||||
const settledPageUrl = await waitForUrlSettled(page);
|
const settledPageUrl = await waitForUrlSettled(page);
|
||||||
@@ -2062,7 +2561,7 @@ app.post('/api/ui-pagination', async (req, res) => {
|
|||||||
}
|
}
|
||||||
touchSessionIfPresent(antiBot.sessionKey);
|
touchSessionIfPresent(antiBot.sessionKey);
|
||||||
|
|
||||||
await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, navigationUrl);
|
await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, navigationUrl, apiUrl ? [apiUrl] : null);
|
||||||
await attachHttpIpRewriteInterceptor(page, 'ui-pagination', { shareToken });
|
await attachHttpIpRewriteInterceptor(page, 'ui-pagination', { shareToken });
|
||||||
} else {
|
} else {
|
||||||
await attachHttpIpRewriteInterceptor(page, 'ui-pagination', { shareToken });
|
await attachHttpIpRewriteInterceptor(page, 'ui-pagination', { shareToken });
|
||||||
@@ -2075,7 +2574,7 @@ app.post('/api/ui-pagination', async (req, res) => {
|
|||||||
waitMs,
|
waitMs,
|
||||||
firstPageData,
|
firstPageData,
|
||||||
authActions,
|
authActions,
|
||||||
executeAuthActions,
|
executeAuthActions: (p, actions) => executeAuthActions(p, actions, antiBot),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -2173,7 +2672,7 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => {
|
|||||||
let shareToken = resolveShareToken(pageUrl, httpResolvedUrl);
|
let shareToken = resolveShareToken(pageUrl, httpResolvedUrl);
|
||||||
await seedShareTokenCookie(page, navigationUrl);
|
await seedShareTokenCookie(page, navigationUrl);
|
||||||
|
|
||||||
await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot);
|
await prepareWhatshubShortLinkBeforeNav(page, navigationUrl, antiBot, storedSession);
|
||||||
await navigateToPage(page, navigationUrl);
|
await navigateToPage(page, navigationUrl);
|
||||||
let finalPageUrl = await waitForUrlSettled(page);
|
let finalPageUrl = await waitForUrlSettled(page);
|
||||||
applyHuojianBusinessHostHint(antiBot, finalPageUrl);
|
applyHuojianBusinessHostHint(antiBot, finalPageUrl);
|
||||||
@@ -2223,14 +2722,14 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => {
|
|||||||
applyHuojianBusinessHostHint(antiBot, finalPageUrl);
|
applyHuojianBusinessHostHint(antiBot, finalPageUrl);
|
||||||
touchSessionIfPresent(antiBot.sessionKey);
|
touchSessionIfPresent(antiBot.sessionKey);
|
||||||
|
|
||||||
await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, navigationUrl);
|
await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, navigationUrl, apiUrls);
|
||||||
}
|
}
|
||||||
|
|
||||||
interceptor = createApiInterceptor(page, apiUrls, interceptTimeout);
|
interceptor = createApiInterceptor(page, apiUrls, interceptTimeout);
|
||||||
interceptorCleanup = interceptor.cleanup;
|
interceptorCleanup = interceptor.cleanup;
|
||||||
await attachHttpIpRewriteInterceptor(page, 'auth-intercept-and-paginate', { shareToken });
|
await attachHttpIpRewriteInterceptor(page, 'auth-intercept-and-paginate', { shareToken });
|
||||||
|
|
||||||
await executeAuthActionsWithDiagnostics(page, authActions);
|
await executeAuthActionsWithDiagnostics(page, authActions, antiBot);
|
||||||
await interceptor.waitForApis(interceptTimeout);
|
await interceptor.waitForApis(interceptTimeout);
|
||||||
|
|
||||||
const rawCookies = await page.cookies();
|
const rawCookies = await page.cookies();
|
||||||
@@ -2269,7 +2768,7 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => {
|
|||||||
waitMs,
|
waitMs,
|
||||||
firstPageData: effectiveFirstPageData,
|
firstPageData: effectiveFirstPageData,
|
||||||
authActions,
|
authActions,
|
||||||
executeAuthActions,
|
executeAuthActions: (p, actions) => executeAuthActions(p, actions, antiBot),
|
||||||
});
|
});
|
||||||
extraPages = paginationResult.data;
|
extraPages = paginationResult.data;
|
||||||
paginationDebug = paginationResult.debugLogs;
|
paginationDebug = paginationResult.debugLogs;
|
||||||
|
|||||||
Reference in New Issue
Block a user