|null */ public static function build(string $ticketType, string $pageUrl, string $landingUrlHint = ''): ?array { $enabledTypes = self::getEnabledTypes(); if (!in_array($ticketType, $enabledTypes, true)) { return null; } $sessionScope = self::resolveSessionScope($ticketType, $pageUrl, $landingUrlHint); if ($sessionScope === '') { return null; } $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' => $businessHostHint, 'landingUrlHint' => $landingUrlHint !== '' ? $landingUrlHint : '', 'cfCloudflareSolver' => self::defaultCfCloudflareSolver($ticketType), 'cfChallengeMode' => self::defaultCfChallengeMode($ticketType), ] + self::postCfReadyExtras($ticketType); } /** * 会话 scope:A2C 业务域+分享 id;火箭长链用动态 host、短链用 share.{token} */ public static function resolveSessionScope(string $ticketType, string $pageUrl, string $landingUrlHint = ''): string { if ($ticketType === 'a2c') { return self::resolveA2cSessionScope($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 { return self::resolveSessionScope($ticketType, $pageUrl, ''); } /** * 解析 sessionKey(供 cron 分组调度使用) */ public static function resolveSessionKey(string $ticketType, string $pageUrl, string $landingUrlHint = ''): string { $scope = self::resolveSessionScope($ticketType, $pageUrl, $landingUrlHint); return $ticketType . ':' . ($scope !== '' ? $scope : 'unknown'); } /** * 是否对该工单类型启用 antiBot */ public static function isEnabledForType(string $ticketType): bool { return in_array($ticketType, self::getEnabledTypes(), true); } /** * @return list */ public static function getEnabledTypes(): array { $raw = (string) Config::get('site.split_scrm_antibot_types'); if ($raw === '') { return ['whatshub', 'chatknow']; } $parts = array_map('trim', explode(',', $raw)); return array_values(array_filter($parts, static function (string $v): bool { return $v !== ''; })); } public static function getSessionTtlMinutes(): int { $minutes = (int) Config::get('site.split_scrm_session_ttl_minutes'); return $minutes > 0 ? $minutes : 120; } /** * 按工单类型附加 CF 过盾后的业务页就绪等待 * * @return array */ private static function postCfReadyExtras(string $ticketType): array { if ($ticketType === 'whatshub') { return [ 'postCfReadyUrlContains' => 'work-order-sharing', 'postCfReadySelector' => '.el-input__inner', 'postCfReadyTimeoutMs' => 30000, 'postCfSettleMs' => 500, 'postCfSpaWaitMs' => 4000, ]; } if ($ticketType === 'a2c') { return [ 'postCfReadyUrlContains' => '/visitors/counter/share', 'postCfReadySelector' => '.el-table', 'postCfReadyApiUrl' => '/api/talk/counter/share/record/list', 'postCfReadyTimeoutMs' => 60000, 'postCfSettleMs' => 500, 'postCfSpaWaitMs' => 8000, ]; } if ($ticketType === 'huojian') { return [ 'postCfReadyUrlContains' => HuojianUrlHelper::BUSINESS_PATH_NEEDLE, 'postCfReadySelector' => '.el-message-box__input', 'postCfReadyTimeoutMs' => 45000, 'postCfSettleMs' => 500, 'postCfSpaWaitMs' => 8000, ]; } return []; } private static function defaultTurnstile(string $ticketType): bool { return true; } private static function defaultSolverFallback(string $ticketType): bool { return true; } private static function defaultCfClearanceRequired(string $ticketType): bool { if ($ticketType === 'a2c') { return false; } return true; } /** * A2C 等为 CF Managed Challenge(5s 盾),非独立 Turnstile 挂件 */ private static function defaultCfChallengeMode(string $ticketType): string { if ($ticketType === 'a2c') { return 'managed'; } return ''; } /** * sitekey 缺失时是否允许 CapSolver AntiCloudflareTask(需 Node 侧 CAPTCHA_PROXY) */ private static function defaultCfCloudflareSolver(string $ticketType): bool { return $ticketType === 'a2c'; } /** * 火箭/A2C 业务域 hint:优先长链动态 host,其次 landing 预解析 */ private static function resolveBusinessHostHint(string $ticketType, string $pageUrl, string $landingUrlHint): string { if ($ticketType === 'a2c') { 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'; } /** * A2C 会话 scope:业务域 + 分享 id(避免多工单共用温池 Browser 互相干扰) */ private static function resolveA2cSessionScope(string $pageUrl, string $landingUrlHint = ''): string { $host = self::resolveA2cSessionHost($pageUrl, $landingUrlHint); $shareId = self::extractA2cShareId($pageUrl); if ($shareId === '' && $landingUrlHint !== '') { $shareId = self::extractA2cShareId($landingUrlHint); } return $shareId !== '' ? $host . ':' . $shareId : $host; } /** * 从 A2C 分享 URL 提取 id 查询参数 */ private static function extractA2cShareId(string $url): string { $query = (string) parse_url($url, PHP_URL_QUERY); if ($query === '') { return ''; } parse_str($query, $params); $id = isset($params['id']) ? trim((string) $params['id']) : ''; return $id !== '' ? $id : ''; } 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 ''; } }