account = $account; $this->password = $password; $this->pageUrl = $pageUrl; $this->landingUrl = SplitPageUrlResolver::resolveFinalUrl($pageUrl); $this->unifiedData = new UnifiedScrmData(); } protected function getSpiderConfig() { $antiBot = $this->buildAntiBotConfig(); $this->logDebug('a2c spider config', [ 'pageUrl' => $this->pageUrl, 'landingUrl' => $this->landingUrl, 'sessionKey' => $antiBot['sessionKey'] ?? '', 'cfClearanceRequired' => $antiBot['cfClearanceRequired'] ?? null, ]); return [ 'pageUrl' => $this->pageUrl, 'listApi' => self::API_LIST, 'detailApi' => self::API_DETAILS, 'listMethod' => 'POST', 'paginationMode' => self::MODE_UI, 'authActions' => [ ['type' => 'wait', 'ms' => 2000], ], 'antiBot' => $antiBot, ]; } /** * A2C:业务域 + 分享 id 会话;短链仍作 pageUrl,landingUrlHint 供 Node 直达长链 * * @return array */ private function buildAntiBotConfig() { $sessionScope = $this->resolveA2cSessionScope(); return [ 'enabled' => true, 'profile' => 'real', 'turnstile' => true, 'solverFallback' => true, 'cfClearanceRequired' => false, 'sessionKey' => 'a2c:' . $sessionScope, 'challengeTimeoutMs' => 60000, 'landingUrlHint' => $this->landingUrl, 'cfCloudflareSolver' => true, 'cfChallengeMode' => 'managed', 'businessHostHint' => $this->resolveA2cSessionHost(), 'postCfReadyUrlContains' => '/visitors/counter/share', 'postCfReadySelector' => '.el-table', 'postCfReadyApiUrl' => '/api/talk/counter/share/record/list', 'postCfReadyTimeoutMs' => 60000, 'postCfSettleMs' => 500, 'postCfSpaWaitMs' => 8000, ]; } /** * A2C 业务域 host */ private function resolveA2cSessionHost() { $candidates = []; if ($this->landingUrl !== '') { $candidates[] = (string) parse_url($this->landingUrl, PHP_URL_HOST); } $candidates[] = (string) parse_url($this->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'; } /** * A2C 会话 scope:host + share id */ private function resolveA2cSessionScope() { $host = $this->resolveA2cSessionHost(); $shareId = $this->extractA2cShareId($this->pageUrl); if ($shareId === '' && $this->landingUrl !== '') { $shareId = $this->extractA2cShareId($this->landingUrl); } return $shareId !== '' ? $host . ':' . $shareId : $host; } private function extractA2cShareId($url) { $query = (string) parse_url($url, PHP_URL_QUERY); if ($query === '') { return ''; } parse_str($query, $params); return isset($params['id']) ? trim((string) $params['id']) : ''; } protected function extractListTotalPages($listFirstPageData, $countData = null) { $default_per_page_count = self::DEFAULT_PER_PAGE_COUNT; $this->unifiedData->total = $listFirstPageData['data']['total']; if ($listFirstPageData['data']['total'] <= $default_per_page_count) { return 1; } return (int) ceil($listFirstPageData['data']['total'] / $default_per_page_count); } protected function buildListPageParams($page) { return ['page' => $page, 'pageSize' => self::DEFAULT_PER_PAGE_COUNT]; } protected function getUiPaginationConfig() { return [ 'nextBtnSelector' => '.btn-next', 'waitMs' => 2000, ]; } protected function parseToUnifiedData($detailData, $allListPagesData) { $unifiedData = $this->unifiedData; if ($detailData) { $unifiedData->todayNewCount = (int) ($detailData['data']['newFollowersToday'] ?? 0); } foreach ($allListPagesData as $pageRaw) { $records = $pageRaw['data']['rows'] ?? []; foreach ($records as $item) { if (!empty($item['account'])) { $number = $item['account']; $isOnline = (isset($item['numberStatus']) && (int) $item['numberStatus'] === 1); $unifiedData->addNumber($number, $isOnline, (int) ($item['newFollowersToday'] ?? 0)); } } } return $unifiedData; } }