From 439f79a5aed3165168bd03f48a97f92695e53b0e Mon Sep 17 00:00:00 2001 From: root Date: Thu, 2 Jul 2026 03:13:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=81=AB=E7=AE=AD=E5=B7=A5?= =?UTF-8?q?=E5=8D=95=E8=BF=87=E7=9B=BE=E5=90=8C=E6=AD=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _php_code/Huojian.php | 180 +++++++++++++----- _php_code/index.php | 11 +- .../library/scrm/AntiBotConfigBuilder.php | 138 ++++++++++---- .../common/library/scrm/HuojianUrlHelper.php | 103 ++++++++++ .../library/scrm/spider/HuojianSpider.php | 14 +- .../common/service/SplitPageUrlResolver.php | 136 +++++++++++++ application/extra/site.php | 2 +- .../library/scrm/AntiBotConfigBuilder.php | 138 ++++++++++---- .../common/library/scrm/HuojianUrlHelper.php | 103 ++++++++++ .../library/scrm/spider/HuojianSpider.php | 14 +- .../common/service/SplitPageUrlResolver.php | 136 +++++++++++++ patches/puppeteer-api/lib/huojian-url.js | 103 ++++++++++ patches/puppeteer-api/server.js | 59 +++++- public/assets/js/backend/split/number.js | 4 +- 14 files changed, 1018 insertions(+), 123 deletions(-) create mode 100644 application/common/library/scrm/HuojianUrlHelper.php create mode 100644 application/common/service/SplitPageUrlResolver.php create mode 100644 patches/application/common/library/scrm/HuojianUrlHelper.php create mode 100644 patches/application/common/service/SplitPageUrlResolver.php create mode 100644 patches/puppeteer-api/lib/huojian-url.js diff --git a/_php_code/Huojian.php b/_php_code/Huojian.php index c235e34..e797ebb 100755 --- a/_php_code/Huojian.php +++ b/_php_code/Huojian.php @@ -1,102 +1,194 @@ account = $account; $this->password = $password; $this->pageUrl = $pageUrl; - + $this->landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl); $this->unifiedData = new UnifiedScrmData(); } protected function getSpiderConfig() { + $antiBot = $this->buildAntiBotConfig(); + + $this->logDebug('huojian spider config', [ + 'pageUrl' => $this->pageUrl, + 'landingUrlHint' => $this->landingUrlHint, + 'sessionKey' => $antiBot['sessionKey'] ?? '', + 'businessHostHint' => $antiBot['businessHostHint'] ?? '', + 'cfClearanceRequired' => $antiBot['cfClearanceRequired'] ?? null, + ]); + return [ - 'pageUrl' => $this->pageUrl, - 'apiUrls' => [self::API_LIST], - - // 明确指派角色 - 'listApi' => self::API_LIST, // 必须 - - 'listMethod' => 'POST', + 'pageUrl' => $this->pageUrl, + 'listApi' => self::API_LIST, + 'listMethod' => 'POST', 'paginationMode' => self::MODE_UI, - - 'authActions' => [ - // 1. 填入密码:寻找 class 包含 el-message-box__input 下面的任意 input - // Node.js 会自动扫描所有匹配项,并只把密码强行注入到那个“肉眼可见”的框里 + 'authActions' => [ ['type' => 'vue_fill', 'selector' => '.el-message-box__input input', 'value' => $this->password], - - // 2. 停顿 500ms,让 Vue 绑定的 v-model 彻底反应过来 ['type' => 'wait', 'ms' => 500], - - // 3. 点击确认:寻找 MessageBox 底部的蓝色 primary 确认按钮 - // 同样利用 vue_click 的可见性过滤,无视隐藏的旧弹窗按钮 ['type' => 'vue_click', 'selector' => '.el-message-box__btns .el-button--primary'], - - // 4. 等待弹窗淡出,接口开始请求 - ['type' => 'wait', 'ms' => 2000] - ] + ['type' => 'wait', 'ms' => 2000], + ], + 'antiBot' => $antiBot, ]; } - // 只负责解析 List 的总页数 + /** + * 对齐生产 AntiBotConfigBuilder 火箭策略(长链 host 动态解析) + * + * @return array + */ + private function buildAntiBotConfig() + { + $sessionScope = $this->resolveHuojianSessionScope(); + $businessHostHint = $this->resolveHuojianBusinessHostHint(); + + return [ + 'enabled' => true, + 'profile' => 'real', + 'turnstile' => true, + 'solverFallback' => true, + 'cfClearanceRequired' => true, + 'sessionKey' => 'huojian:' . $sessionScope, + 'challengeTimeoutMs' => 60000, + 'landingUrlHint' => $this->landingUrlHint, + 'businessHostHint' => $businessHostHint, + 'postCfReadyUrlContains' => '/gds', + 'postCfReadySelector' => '.el-message-box__input', + 'postCfReadyTimeoutMs' => 45000, + 'postCfSettleMs' => 500, + 'postCfSpaWaitMs' => 8000, + ]; + } + + private function isHuojianLongLinkUrl($pageUrl) + { + $path = (string) parse_url($pageUrl, PHP_URL_PATH); + if ($path === '' || strpos($path, '/gds') === false) { + return false; + } + $query = []; + parse_str((string) parse_url($pageUrl, PHP_URL_QUERY), $query); + + return trim((string) ($query['link'] ?? '')) !== ''; + } + + private function extractHuojianShareToken($pageUrl) + { + if ($this->isHuojianLongLinkUrl($pageUrl)) { + $query = []; + parse_str((string) parse_url($pageUrl, PHP_URL_QUERY), $query); + + return trim((string) ($query['link'] ?? '')); + } + $path = trim((string) parse_url($pageUrl, PHP_URL_PATH), '/'); + if ($path === '' || strpos($path, '/') !== false) { + return ''; + } + if (preg_match('/^[a-zA-Z0-9]+$/', $path) !== 1) { + return ''; + } + + return $path; + } + + private function resolveHuojianSessionScope() + { + if ($this->isHuojianLongLinkUrl($this->pageUrl)) { + return strtolower((string) parse_url($this->pageUrl, PHP_URL_HOST)); + } + $token = $this->extractHuojianShareToken($this->pageUrl); + if ($token !== '') { + return 'share.' . $token; + } + if ($this->landingUrlHint !== '' && $this->isHuojianLongLinkUrl($this->landingUrlHint)) { + return strtolower((string) parse_url($this->landingUrlHint, PHP_URL_HOST)); + } + + return 'unknown'; + } + + private function resolveHuojianBusinessHostHint() + { + if ($this->isHuojianLongLinkUrl($this->pageUrl)) { + return strtolower((string) parse_url($this->pageUrl, PHP_URL_HOST)); + } + if ($this->landingUrlHint !== '' && $this->isHuojianLongLinkUrl($this->landingUrlHint)) { + return strtolower((string) parse_url($this->landingUrlHint, PHP_URL_HOST)); + } + + return ''; + } + protected function extractListTotalPages($listFirstPageData, $countData = null) { - // 只有一页 return 1; } - // 没有分页返回空数组 protected function buildListPageParams($page) { return []; } - // 提供 List 的下一页按钮信息 protected function getUiPaginationConfig() { return []; } - // 清爽至极的数据清洗:详情是详情,列表是列表 protected function parseToUnifiedData($detailData, $allListPagesData) { $unifiedData = $this->unifiedData; - - $unifiedData->todayNewCount = $allListPagesData[0]['data']['counterWorker']['newTodayFriend']; - - // 2. 循环合并了所有页数的 List 数组 + + $unifiedData->todayNewCount = (int) ($allListPagesData[0]['data']['counterWorker']['newTodayFriend'] ?? 0); + $count = 0; foreach ($allListPagesData as $pageRaw) { - $records = $pageRaw['data']['counterCsAccountVo'] ?? []; + $records = $pageRaw['data']['counterCsAccountVo'] ?? []; foreach ($records as $item) { - if(!empty($item['accountLogin'])) { - $number = $item['accountLogin'] ?? null; - $isOnline = (isset($item['accountStatus']) && $item['accountStatus'] == 1); + if (!empty($item['accountLogin'])) { + $number = $item['accountLogin']; + $isOnline = (isset($item['accountStatus']) && (int) $item['accountStatus'] === 1); $count++; - $unifiedData->addNumber($number, $isOnline, $item['newTodayFriend']); + $unifiedData->addNumber($number, $isOnline, (int) ($item['newTodayFriend'] ?? 0)); } } } - + $unifiedData->total = $count; return $unifiedData; } -} \ No newline at end of file +} diff --git a/_php_code/index.php b/_php_code/index.php index e5d079a..99b9d18 100755 --- a/_php_code/index.php +++ b/_php_code/index.php @@ -128,10 +128,13 @@ try { // try { // echo "🚀 开始执行<火箭工单>抓取任务 (多引擎智能调度)...\n\r"; -// $pageUrl = 'https://s.url99.me/68vfje8m'; // PageUrl 入口授权页 -// $username = ""; // 登录账号 -// $password = "542187"; // 登录密码 -// $spider = new Huojian($pageUrl, $username, $password); +// // 长链(业务域动态,如 v3.url66.me) +// // $pageUrl = 'https://v3.url66.me/gds?link=6a387e66e4b09e9d162cd647'; +// // 短链(与客户后台一致) +// $pageUrl = 'https://s.url99.me/50q3622j'; +// $username = ""; +// $password = "你的分享密码"; +// $spider = (new Huojian($pageUrl, $username, $password))->setVerbose(true); // $finalData = $spider->run(); // echo "✅ 任务完成!统一数据如下:\n\r"; diff --git a/application/common/library/scrm/AntiBotConfigBuilder.php b/application/common/library/scrm/AntiBotConfigBuilder.php index 7d7cdee..23fafad 100644 --- a/application/common/library/scrm/AntiBotConfigBuilder.php +++ b/application/common/library/scrm/AntiBotConfigBuilder.php @@ -16,65 +16,74 @@ class AntiBotConfigBuilder /** * 为指定工单类型与落地页 URL 构建 Node antiBot 配置;未启用类型返回 null * + * @param string $landingUrlHint HTTP 预解析或历史上次落地的长链(可选) * @return array|null */ - public static function build(string $ticketType, string $pageUrl): ?array + public static function build(string $ticketType, string $pageUrl, string $landingUrlHint = ''): ?array { $enabledTypes = self::getEnabledTypes(); if (!in_array($ticketType, $enabledTypes, true)) { return null; } - $sessionHost = self::resolveSessionHost($ticketType, $pageUrl); - if ($sessionHost === '') { + $sessionScope = self::resolveSessionScope($ticketType, $pageUrl, $landingUrlHint); + if ($sessionScope === '') { return null; } - $sessionKey = $ticketType . ':' . $sessionHost; + $sessionKey = $ticketType . ':' . $sessionScope; $ttlMinutes = self::getSessionTtlMinutes(); + $businessHostHint = self::resolveBusinessHostHint($ticketType, $pageUrl, $landingUrlHint); + return [ - 'enabled' => true, - 'profile' => 'real', - 'turnstile' => self::defaultTurnstile($ticketType), - 'solverFallback' => self::defaultSolverFallback($ticketType), - 'cfClearanceRequired'=> self::defaultCfClearanceRequired($ticketType), - 'sessionKey' => $sessionKey, - 'challengeTimeoutMs' => 60000, - 'sessionTtlMinutes' => $ttlMinutes, - 'businessHostHint' => self::defaultBusinessHostHint($ticketType, $sessionHost), + 'enabled' => true, + 'profile' => 'real', + 'turnstile' => self::defaultTurnstile($ticketType), + 'solverFallback' => self::defaultSolverFallback($ticketType), + 'cfClearanceRequired' => self::defaultCfClearanceRequired($ticketType), + 'sessionKey' => $sessionKey, + 'challengeTimeoutMs' => 60000, + 'sessionTtlMinutes' => $ttlMinutes, + 'businessHostHint' => $businessHostHint, + 'landingUrlHint' => $landingUrlHint !== '' ? $landingUrlHint : '', ] + self::postCfReadyExtras($ticketType); } /** - * 会话域:A2C 短链(yyk.ink)仍用业务域 user.a2c.chat 复用 Real Browser 会话 + * 会话 scope:A2C 短链固定业务域;火箭长链用动态 host、短链用 share.{token} + */ + public static function resolveSessionScope(string $ticketType, string $pageUrl, string $landingUrlHint = ''): string + { + if ($ticketType === 'a2c') { + return self::resolveA2cSessionHost($pageUrl, $landingUrlHint); + } + + if ($ticketType === 'huojian') { + return self::resolveHuojianSessionScope($pageUrl, $landingUrlHint); + } + + $host = strtolower(trim((string) parse_url($pageUrl, PHP_URL_HOST))); + + return $host !== '' ? $host : ''; + } + + /** + * @deprecated 兼容旧调用,等价 resolveSessionScope */ public static function resolveSessionHost(string $ticketType, string $pageUrl): string { - $host = strtolower(trim((string) parse_url($pageUrl, PHP_URL_HOST))); - if ($host === '') { - return ''; - } - - if ($ticketType === 'a2c') { - if (strpos($host, 'a2c.chat') !== false) { - return $host; - } - - return 'user.a2c.chat'; - } - - return $host; + return self::resolveSessionScope($ticketType, $pageUrl, ''); } /** * 解析 sessionKey(供 cron 分组调度使用) */ - public static function resolveSessionKey(string $ticketType, string $pageUrl): string + public static function resolveSessionKey(string $ticketType, string $pageUrl, string $landingUrlHint = ''): string { - $host = self::resolveSessionHost($ticketType, $pageUrl); + $scope = self::resolveSessionScope($ticketType, $pageUrl, $landingUrlHint); - return $ticketType . ':' . ($host !== '' ? $host : 'unknown'); + return $ticketType . ':' . ($scope !== '' ? $scope : 'unknown'); } /** @@ -133,6 +142,16 @@ class AntiBotConfigBuilder ]; } + if ($ticketType === 'huojian') { + return [ + 'postCfReadyUrlContains' => HuojianUrlHelper::BUSINESS_PATH_NEEDLE, + 'postCfReadySelector' => '.el-message-box__input', + 'postCfReadyTimeoutMs' => 45000, + 'postCfSettleMs' => 500, + 'postCfSpaWaitMs' => 8000, + ]; + } + return []; } @@ -159,10 +178,63 @@ class AntiBotConfigBuilder return true; } - private static function defaultBusinessHostHint(string $ticketType, string $sessionHost): string + /** + * 火箭/A2C 业务域 hint:优先长链动态 host,其次 landing 预解析 + */ + private static function resolveBusinessHostHint(string $ticketType, string $pageUrl, string $landingUrlHint): string { if ($ticketType === 'a2c') { - return $sessionHost; + return self::resolveA2cSessionHost($pageUrl, $landingUrlHint); + } + + if ($ticketType === 'huojian') { + $host = HuojianUrlHelper::extractBusinessHost($pageUrl); + if ($host !== '') { + return $host; + } + if ($landingUrlHint !== '') { + $hintHost = HuojianUrlHelper::extractBusinessHost($landingUrlHint); + if ($hintHost !== '') { + return $hintHost; + } + } + + return ''; + } + + return ''; + } + + private static function resolveA2cSessionHost(string $pageUrl, string $landingUrlHint = ''): string + { + $candidates = []; + if ($landingUrlHint !== '') { + $candidates[] = (string) parse_url($landingUrlHint, PHP_URL_HOST); + } + $candidates[] = (string) parse_url($pageUrl, PHP_URL_HOST); + + foreach ($candidates as $host) { + $host = strtolower(trim($host)); + if ($host !== '' && strpos($host, 'a2c.chat') !== false) { + return $host; + } + } + + return 'user.a2c.chat'; + } + + private static function resolveHuojianSessionScope(string $pageUrl, string $landingUrlHint = ''): string + { + $scope = HuojianUrlHelper::resolveSessionScope($pageUrl); + if ($scope !== '') { + return $scope; + } + + if ($landingUrlHint !== '') { + $fromLanding = HuojianUrlHelper::resolveSessionScope($landingUrlHint); + if ($fromLanding !== '') { + return $fromLanding; + } } return ''; diff --git a/application/common/library/scrm/HuojianUrlHelper.php b/application/common/library/scrm/HuojianUrlHelper.php new file mode 100644 index 0000000..94b4e88 --- /dev/null +++ b/application/common/library/scrm/HuojianUrlHelper.php @@ -0,0 +1,103 @@ +pageUrl = $pageUrl; $this->account = $account; $this->password = $password; + $this->landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl); $this->unifiedData = new UnifiedScrmData(); } protected function getSpiderConfig(): array { + $antiBot = AntiBotConfigBuilder::build('huojian', $this->pageUrl, $this->landingUrlHint); + return [ 'pageUrl' => $this->pageUrl, 'listApi' => self::API_LIST, @@ -48,6 +58,7 @@ class HuojianSpider extends AbstractScrmSpider ['type' => 'vue_click', 'selector' => '.el-message-box__btns .el-button--primary'], ['type' => 'wait', 'ms' => 2000], ], + 'antiBot' => $antiBot, ]; } @@ -84,6 +95,7 @@ class HuojianSpider extends AbstractScrmSpider } } $unifiedData->total = $count; + return $unifiedData; } } diff --git a/application/common/service/SplitPageUrlResolver.php b/application/common/service/SplitPageUrlResolver.php new file mode 100644 index 0000000..e8b584a --- /dev/null +++ b/application/common/service/SplitPageUrlResolver.php @@ -0,0 +1,136 @@ + $headOnly, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_TIMEOUT => $timeoutSec, + CURLOPT_HEADER => true, + CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; SplitPageUrlResolver/1.0)', + ]); + + $body = curl_exec($ch); + if ($body === false) { + curl_close($ch); + + return null; + } + + $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $redirectUrl = (string) curl_getinfo($ch, CURLINFO_REDIRECT_URL); + curl_close($ch); + + $location = $redirectUrl; + if ($location === '' && is_string($body)) { + $location = self::parseLocationHeader($body); + } + + return ['code' => $code, 'location' => trim($location)]; + } + + private static function parseLocationHeader(string $rawHeaders): string + { + foreach (preg_split('/\r\n|\n|\r/', $rawHeaders) as $line) { + if (stripos($line, 'Location:') === 0) { + return trim(substr($line, 9)); + } + } + + return ''; + } + + private static function resolveRelativeUrl(string $baseUrl, string $location): string + { + if (preg_match('#^https?://#i', $location)) { + return $location; + } + + $base = parse_url($baseUrl); + if (!is_array($base)) { + return $location; + } + + $scheme = (string) ($base['scheme'] ?? 'https'); + $host = (string) ($base['host'] ?? ''); + if ($host === '') { + return $location; + } + + if (strpos($location, '/') === 0) { + return $scheme . '://' . $host . $location; + } + + $path = (string) ($base['path'] ?? '/'); + $dir = rtrim(str_replace(basename($path), '', $path), '/'); + + return $scheme . '://' . $host . ($dir !== '' ? $dir . '/' : '/') . $location; + } +} diff --git a/application/extra/site.php b/application/extra/site.php index c127bd5..b5c6ea4 100755 --- a/application/extra/site.php +++ b/application/extra/site.php @@ -44,7 +44,7 @@ return array ( ), 'split_platform_domain' => 'flowerbells.top', 'split_scrm_node_host' => 'http://127.0.0.1:3001', - 'split_scrm_antibot_types' => 'whatshub,chatknow,a2c', + 'split_scrm_antibot_types' => 'whatshub,chatknow,a2c,huojian', 'split_scrm_session_ttl_minutes' => '120', 'split_sync_interval_a2c' => '3', 'split_sync_interval_haiwang' => '3', diff --git a/patches/application/common/library/scrm/AntiBotConfigBuilder.php b/patches/application/common/library/scrm/AntiBotConfigBuilder.php index 7d7cdee..23fafad 100644 --- a/patches/application/common/library/scrm/AntiBotConfigBuilder.php +++ b/patches/application/common/library/scrm/AntiBotConfigBuilder.php @@ -16,65 +16,74 @@ class AntiBotConfigBuilder /** * 为指定工单类型与落地页 URL 构建 Node antiBot 配置;未启用类型返回 null * + * @param string $landingUrlHint HTTP 预解析或历史上次落地的长链(可选) * @return array|null */ - public static function build(string $ticketType, string $pageUrl): ?array + public static function build(string $ticketType, string $pageUrl, string $landingUrlHint = ''): ?array { $enabledTypes = self::getEnabledTypes(); if (!in_array($ticketType, $enabledTypes, true)) { return null; } - $sessionHost = self::resolveSessionHost($ticketType, $pageUrl); - if ($sessionHost === '') { + $sessionScope = self::resolveSessionScope($ticketType, $pageUrl, $landingUrlHint); + if ($sessionScope === '') { return null; } - $sessionKey = $ticketType . ':' . $sessionHost; + $sessionKey = $ticketType . ':' . $sessionScope; $ttlMinutes = self::getSessionTtlMinutes(); + $businessHostHint = self::resolveBusinessHostHint($ticketType, $pageUrl, $landingUrlHint); + return [ - 'enabled' => true, - 'profile' => 'real', - 'turnstile' => self::defaultTurnstile($ticketType), - 'solverFallback' => self::defaultSolverFallback($ticketType), - 'cfClearanceRequired'=> self::defaultCfClearanceRequired($ticketType), - 'sessionKey' => $sessionKey, - 'challengeTimeoutMs' => 60000, - 'sessionTtlMinutes' => $ttlMinutes, - 'businessHostHint' => self::defaultBusinessHostHint($ticketType, $sessionHost), + 'enabled' => true, + 'profile' => 'real', + 'turnstile' => self::defaultTurnstile($ticketType), + 'solverFallback' => self::defaultSolverFallback($ticketType), + 'cfClearanceRequired' => self::defaultCfClearanceRequired($ticketType), + 'sessionKey' => $sessionKey, + 'challengeTimeoutMs' => 60000, + 'sessionTtlMinutes' => $ttlMinutes, + 'businessHostHint' => $businessHostHint, + 'landingUrlHint' => $landingUrlHint !== '' ? $landingUrlHint : '', ] + self::postCfReadyExtras($ticketType); } /** - * 会话域:A2C 短链(yyk.ink)仍用业务域 user.a2c.chat 复用 Real Browser 会话 + * 会话 scope:A2C 短链固定业务域;火箭长链用动态 host、短链用 share.{token} + */ + public static function resolveSessionScope(string $ticketType, string $pageUrl, string $landingUrlHint = ''): string + { + if ($ticketType === 'a2c') { + return self::resolveA2cSessionHost($pageUrl, $landingUrlHint); + } + + if ($ticketType === 'huojian') { + return self::resolveHuojianSessionScope($pageUrl, $landingUrlHint); + } + + $host = strtolower(trim((string) parse_url($pageUrl, PHP_URL_HOST))); + + return $host !== '' ? $host : ''; + } + + /** + * @deprecated 兼容旧调用,等价 resolveSessionScope */ public static function resolveSessionHost(string $ticketType, string $pageUrl): string { - $host = strtolower(trim((string) parse_url($pageUrl, PHP_URL_HOST))); - if ($host === '') { - return ''; - } - - if ($ticketType === 'a2c') { - if (strpos($host, 'a2c.chat') !== false) { - return $host; - } - - return 'user.a2c.chat'; - } - - return $host; + return self::resolveSessionScope($ticketType, $pageUrl, ''); } /** * 解析 sessionKey(供 cron 分组调度使用) */ - public static function resolveSessionKey(string $ticketType, string $pageUrl): string + public static function resolveSessionKey(string $ticketType, string $pageUrl, string $landingUrlHint = ''): string { - $host = self::resolveSessionHost($ticketType, $pageUrl); + $scope = self::resolveSessionScope($ticketType, $pageUrl, $landingUrlHint); - return $ticketType . ':' . ($host !== '' ? $host : 'unknown'); + return $ticketType . ':' . ($scope !== '' ? $scope : 'unknown'); } /** @@ -133,6 +142,16 @@ class AntiBotConfigBuilder ]; } + if ($ticketType === 'huojian') { + return [ + 'postCfReadyUrlContains' => HuojianUrlHelper::BUSINESS_PATH_NEEDLE, + 'postCfReadySelector' => '.el-message-box__input', + 'postCfReadyTimeoutMs' => 45000, + 'postCfSettleMs' => 500, + 'postCfSpaWaitMs' => 8000, + ]; + } + return []; } @@ -159,10 +178,63 @@ class AntiBotConfigBuilder return true; } - private static function defaultBusinessHostHint(string $ticketType, string $sessionHost): string + /** + * 火箭/A2C 业务域 hint:优先长链动态 host,其次 landing 预解析 + */ + private static function resolveBusinessHostHint(string $ticketType, string $pageUrl, string $landingUrlHint): string { if ($ticketType === 'a2c') { - return $sessionHost; + return self::resolveA2cSessionHost($pageUrl, $landingUrlHint); + } + + if ($ticketType === 'huojian') { + $host = HuojianUrlHelper::extractBusinessHost($pageUrl); + if ($host !== '') { + return $host; + } + if ($landingUrlHint !== '') { + $hintHost = HuojianUrlHelper::extractBusinessHost($landingUrlHint); + if ($hintHost !== '') { + return $hintHost; + } + } + + return ''; + } + + return ''; + } + + private static function resolveA2cSessionHost(string $pageUrl, string $landingUrlHint = ''): string + { + $candidates = []; + if ($landingUrlHint !== '') { + $candidates[] = (string) parse_url($landingUrlHint, PHP_URL_HOST); + } + $candidates[] = (string) parse_url($pageUrl, PHP_URL_HOST); + + foreach ($candidates as $host) { + $host = strtolower(trim($host)); + if ($host !== '' && strpos($host, 'a2c.chat') !== false) { + return $host; + } + } + + return 'user.a2c.chat'; + } + + private static function resolveHuojianSessionScope(string $pageUrl, string $landingUrlHint = ''): string + { + $scope = HuojianUrlHelper::resolveSessionScope($pageUrl); + if ($scope !== '') { + return $scope; + } + + if ($landingUrlHint !== '') { + $fromLanding = HuojianUrlHelper::resolveSessionScope($landingUrlHint); + if ($fromLanding !== '') { + return $fromLanding; + } } return ''; diff --git a/patches/application/common/library/scrm/HuojianUrlHelper.php b/patches/application/common/library/scrm/HuojianUrlHelper.php new file mode 100644 index 0000000..94b4e88 --- /dev/null +++ b/patches/application/common/library/scrm/HuojianUrlHelper.php @@ -0,0 +1,103 @@ +pageUrl = $pageUrl; $this->account = $account; $this->password = $password; + $this->landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl); $this->unifiedData = new UnifiedScrmData(); } protected function getSpiderConfig(): array { + $antiBot = AntiBotConfigBuilder::build('huojian', $this->pageUrl, $this->landingUrlHint); + return [ 'pageUrl' => $this->pageUrl, 'listApi' => self::API_LIST, @@ -48,6 +58,7 @@ class HuojianSpider extends AbstractScrmSpider ['type' => 'vue_click', 'selector' => '.el-message-box__btns .el-button--primary'], ['type' => 'wait', 'ms' => 2000], ], + 'antiBot' => $antiBot, ]; } @@ -84,6 +95,7 @@ class HuojianSpider extends AbstractScrmSpider } } $unifiedData->total = $count; + return $unifiedData; } } diff --git a/patches/application/common/service/SplitPageUrlResolver.php b/patches/application/common/service/SplitPageUrlResolver.php new file mode 100644 index 0000000..e8b584a --- /dev/null +++ b/patches/application/common/service/SplitPageUrlResolver.php @@ -0,0 +1,136 @@ + $headOnly, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_TIMEOUT => $timeoutSec, + CURLOPT_HEADER => true, + CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; SplitPageUrlResolver/1.0)', + ]); + + $body = curl_exec($ch); + if ($body === false) { + curl_close($ch); + + return null; + } + + $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $redirectUrl = (string) curl_getinfo($ch, CURLINFO_REDIRECT_URL); + curl_close($ch); + + $location = $redirectUrl; + if ($location === '' && is_string($body)) { + $location = self::parseLocationHeader($body); + } + + return ['code' => $code, 'location' => trim($location)]; + } + + private static function parseLocationHeader(string $rawHeaders): string + { + foreach (preg_split('/\r\n|\n|\r/', $rawHeaders) as $line) { + if (stripos($line, 'Location:') === 0) { + return trim(substr($line, 9)); + } + } + + return ''; + } + + private static function resolveRelativeUrl(string $baseUrl, string $location): string + { + if (preg_match('#^https?://#i', $location)) { + return $location; + } + + $base = parse_url($baseUrl); + if (!is_array($base)) { + return $location; + } + + $scheme = (string) ($base['scheme'] ?? 'https'); + $host = (string) ($base['host'] ?? ''); + if ($host === '') { + return $location; + } + + if (strpos($location, '/') === 0) { + return $scheme . '://' . $host . $location; + } + + $path = (string) ($base['path'] ?? '/'); + $dir = rtrim(str_replace(basename($path), '', $path), '/'); + + return $scheme . '://' . $host . ($dir !== '' ? $dir . '/' : '/') . $location; + } +} diff --git a/patches/puppeteer-api/lib/huojian-url.js b/patches/puppeteer-api/lib/huojian-url.js new file mode 100644 index 0000000..bbef79b --- /dev/null +++ b/patches/puppeteer-api/lib/huojian-url.js @@ -0,0 +1,103 @@ +/** + * 火箭云控 URL 解析(与 PHP HuojianUrlHelper 对齐) + * 长链域名动态识别,禁止写死 v3.url66.me + */ + +const BUSINESS_PATH_NEEDLE = '/gds'; +const LINK_QUERY_PARAM = 'link'; + +/** + * @param {string} url + */ +function isHuojianLongLinkUrl(url) { + if (!url || typeof url !== 'string') { + return false; + } + try { + const parsed = new URL(url); + if (!parsed.pathname.includes(BUSINESS_PATH_NEEDLE)) { + return false; + } + const link = (parsed.searchParams.get(LINK_QUERY_PARAM) || '').trim(); + return link !== ''; + } catch (_) { + return false; + } +} + +/** + * @param {string} url + * @returns {string} + */ +function extractHuojianBusinessHost(url) { + if (!isHuojianLongLinkUrl(url)) { + return ''; + } + try { + return new URL(url).hostname.toLowerCase(); + } catch (_) { + return ''; + } +} + +/** + * @param {string} url + * @returns {string} + */ +function extractHuojianShareToken(url) { + if (!url || typeof url !== 'string') { + return ''; + } + try { + const parsed = new URL(url); + if (isHuojianLongLinkUrl(url)) { + return (parsed.searchParams.get(LINK_QUERY_PARAM) || '').trim(); + } + const path = parsed.pathname.replace(/^\/+|\/+$/g, ''); + if (path === '' || path.includes('/')) { + return ''; + } + if (/^[a-zA-Z0-9]+$/.test(path)) { + return path; + } + } catch (_) { + /* ignore */ + } + return ''; +} + +/** + * @param {string} url + */ +function isHuojianShortEntryUrl(url) { + if (isHuojianLongLinkUrl(url)) { + return false; + } + return extractHuojianShareToken(url) !== ''; +} + +/** + * 落地后补全 businessHostHint(多长链域名兼容) + * @param {object} antiBot + * @param {string} finalPageUrl + */ +function applyHuojianBusinessHostHint(antiBot, finalPageUrl) { + if (!antiBot) { + return antiBot; + } + const host = extractHuojianBusinessHost(finalPageUrl); + if (host !== '') { + antiBot.businessHostHint = host; + console.log(`[Huojian] 落地业务域=${host} url=${finalPageUrl}`); + } + return antiBot; +} + +module.exports = { + isHuojianLongLinkUrl, + extractHuojianBusinessHost, + extractHuojianShareToken, + isHuojianShortEntryUrl, + applyHuojianBusinessHostHint, + BUSINESS_PATH_NEEDLE, +}; diff --git a/patches/puppeteer-api/server.js b/patches/puppeteer-api/server.js index 0f3cdc6..9a353ed 100755 --- a/patches/puppeteer-api/server.js +++ b/patches/puppeteer-api/server.js @@ -55,7 +55,11 @@ const { } = require('./lib/session-store'); const { resolveSessionContext, touchSessionIfPresent } = require('./lib/session-context'); const { getProfileStats, profileExists } = require('./lib/profile-dir'); -const { runUiPagination } = require('./lib/ui-pagination-runner'); +const { + isHuojianLongLinkUrl, + isHuojianShortEntryUrl, + applyHuojianBusinessHostHint, +} = require('./lib/huojian-url'); const app = express(); app.use(express.json({ limit: '50mb' })); @@ -269,10 +273,50 @@ function applyA2cPostCfDefaults(antiBot, pageUrl) { }; } -/** 按 pageUrl 合并 antiBot(含 Whatshub / A2C 默认 postCf) */ +/** + * 火箭短链 → 动态长链域 /gds?link=:PHP 未传 postCfReady* 时 Node 侧自动补全 + * @param {object} antiBot + * @param {string} pageUrl + */ +function applyHuojianPostCfDefaults(antiBot, pageUrl) { + if (!antiBot || !antiBot.enabled) { + return antiBot; + } + if ((antiBot.postCfReadyUrlContains || '').trim()) { + return antiBot; + } + + let shouldApply = false; + const sessionKey = String(antiBot.sessionKey || '').toLowerCase(); + if (sessionKey.startsWith('huojian:')) { + shouldApply = true; + } + if (isHuojianLongLinkUrl(pageUrl) || isHuojianShortEntryUrl(pageUrl)) { + shouldApply = true; + } + + if (!shouldApply) { + return antiBot; + } + + console.log('[CF] 火箭 自动启用 postCfReady 默认配置'); + return { + ...antiBot, + postCfReadyUrlContains: '/gds', + postCfReadySelector: '.el-message-box__input', + postCfReadyTimeoutMs: antiBot.postCfReadyTimeoutMs || 45000, + postCfSettleMs: antiBot.postCfSettleMs || 500, + postCfSpaWaitMs: antiBot.postCfSpaWaitMs || 8000, + }; +} + +/** 按 pageUrl 合并 antiBot(含 Whatshub / A2C / 火箭 默认 postCf) */ function resolveAntiBotForPage(rawAntiBot, pageUrl) { - return applyA2cPostCfDefaults( - applyWhatshubPostCfDefaults(resolveAntiBot(rawAntiBot), pageUrl), + return applyHuojianPostCfDefaults( + applyA2cPostCfDefaults( + applyWhatshubPostCfDefaults(resolveAntiBot(rawAntiBot), pageUrl), + pageUrl + ), pageUrl ); } @@ -361,6 +405,9 @@ function isOnShortLinkNotBusiness(page, urlNeedle) { return false; } } + if (urlNeedle && urlNeedle.includes('/gds')) { + return !url.includes('/gds'); + } return false; } @@ -1218,6 +1265,7 @@ app.post('/api/auth-and-intercept', async (req, res) => { await navigateToPage(page, pageUrl); let finalPageUrl = await waitForUrlSettled(page); + applyHuojianBusinessHostHint(antiBot, finalPageUrl); if (finalPageUrl !== pageUrl) { console.log(`[URL解析] 浏览器落地: ${pageUrl} -> ${finalPageUrl}`); } @@ -1246,6 +1294,7 @@ app.post('/api/auth-and-intercept', async (req, res) => { } finalPageUrl = page.url(); + applyHuojianBusinessHostHint(antiBot, finalPageUrl); touchSessionIfPresent(antiBot.sessionKey); await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, pageUrl); @@ -1599,6 +1648,7 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => { await prepareWhatshubShortLinkBeforeNav(page, pageUrl, antiBot); await navigateToPage(page, pageUrl); let finalPageUrl = await waitForUrlSettled(page); + applyHuojianBusinessHostHint(antiBot, finalPageUrl); let interceptor; if (antiBot.enabled) { @@ -1623,6 +1673,7 @@ app.post('/api/auth-intercept-and-paginate', async (req, res) => { return; } finalPageUrl = page.url(); + applyHuojianBusinessHostHint(antiBot, finalPageUrl); touchSessionIfPresent(antiBot.sessionKey); await awaitPostCfBusinessReady(page, antiBot, waitForUrlSettled, storedSession, pageUrl); diff --git a/public/assets/js/backend/split/number.js b/public/assets/js/backend/split/number.js index c60647a..e6d771b 100755 --- a/public/assets/js/backend/split/number.js +++ b/public/assets/js/backend/split/number.js @@ -266,7 +266,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin linkOptions.push(''); }); var html = [ - '
', + '
', '
', ' ', ' ', '
', - '
', + '
', ' ', ' ', '
',