Files
links/patches/application/common/library/scrm/AntiBotConfigBuilder.php
T

399 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\common\library\scrm;
use app\common\service\SplitPageUrlResolver;
use think\Config;
/**
* 云控蜘蛛 antiBot 配置构建器
*
* 约定:sessionKey = "{ticketType}:{host}"profile=real 当 ticket_type 在 site 配置列表中
*/
class AntiBotConfigBuilder
{
/**
* 为指定工单类型与落地页 URL 构建 Node antiBot 配置;未启用类型返回 null
*
* @param string $landingUrlHint HTTP 预解析或历史上次落地的长链(可选)
* @return array<string, mixed>|null
*/
public static function build(string $ticketType, string $pageUrl, string $landingUrlHint = ''): ?array
{
$enabledTypes = self::getEnabledTypes();
if (!in_array($ticketType, $enabledTypes, true)) {
return null;
}
if ($ticketType === 'a2c' && $landingUrlHint === '') {
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
}
if ($ticketType === 'whatshub' && $landingUrlHint === '') {
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
}
$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);
}
/**
* 会话 scopeA2C 业务域+分享 idWhatshub host+shareCode/workId;火箭长链用动态 host
*/
public static function resolveSessionScope(string $ticketType, string $pageUrl, string $landingUrlHint = ''): string
{
if ($ticketType === 'a2c') {
return self::resolveA2cSessionScope($pageUrl, $landingUrlHint);
}
if ($ticketType === 'whatshub') {
return self::resolveWhatshubSessionScope($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
{
if ($ticketType === 'a2c' && $landingUrlHint === '') {
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
}
if ($ticketType === 'whatshub' && $landingUrlHint === '') {
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
}
$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<string>
*/
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<string, mixed>
*/
private static function postCfReadyExtras(string $ticketType): array
{
if ($ticketType === 'whatshub') {
return [
'postCfReadyUrlContains' => 'work-order-sharing',
'postCfReadySelector' => '.el-input__inner',
'postCfReadyApiUrl' => '/api/whatshub-counter/workShare/open/statistics',
'postCfReadyTimeoutMs' => 60000,
'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' || $ticketType === 'whatshub') {
return false;
}
return true;
}
/**
* A2C / Whatshub 等为 CF Managed Challenge5s 盾),非独立 Turnstile 挂件
*/
private static function defaultCfChallengeMode(string $ticketType): string
{
if ($ticketType === 'a2c' || $ticketType === 'whatshub') {
return 'managed';
}
return '';
}
/**
* sitekey 缺失时是否允许 CapSolver AntiCloudflareTask(需 Node 侧 CAPTCHA_PROXY
*/
private static function defaultCfCloudflareSolver(string $ticketType): bool
{
return $ticketType === 'a2c' || $ticketType === 'whatshub';
}
/**
* 火箭/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 '';
}
/**
* Whatshub 会话 scopehost + shareCode 或 workId,避免多工单共用温池 Browser
*/
private static function resolveWhatshubSessionScope(string $pageUrl, string $landingUrlHint = ''): string
{
$host = strtolower(trim((string) parse_url($pageUrl, PHP_URL_HOST)));
if ($host === '' && $landingUrlHint !== '') {
$host = strtolower(trim((string) parse_url($landingUrlHint, PHP_URL_HOST)));
}
if ($host === '') {
return '';
}
$shareCode = self::extractWhatshubShareCode($pageUrl);
if ($shareCode === '' && $landingUrlHint !== '') {
$shareCode = self::extractWhatshubShareCode($landingUrlHint);
}
if ($shareCode !== '') {
return $host . ':' . $shareCode;
}
$workId = self::extractWhatshubWorkId($pageUrl);
if ($workId === '' && $landingUrlHint !== '') {
$workId = self::extractWhatshubWorkId($landingUrlHint);
}
if ($workId !== '') {
return $host . ':' . $workId;
}
return $host;
}
/**
* 从 Whatshub 短链 /m/{shareCode}/ 提取 shareCode
*/
private static function extractWhatshubShareCode(string $url): string
{
$path = (string) parse_url($url, PHP_URL_PATH);
if ($path === '') {
return '';
}
if (preg_match('#/m/([^/]+)/#', $path, $matches)) {
return trim($matches[1]);
}
return '';
}
/**
* 从 Whatshub 长链 work-order-sharing?workId= 提取 workId
*/
private static function extractWhatshubWorkId(string $url): string
{
$query = (string) parse_url($url, PHP_URL_QUERY);
if ($query === '') {
return '';
}
parse_str($query, $params);
$workId = isset($params['workId']) ? trim((string) $params['workId']) : '';
return $workId !== '' ? $workId : '';
}
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 互相干扰)
*
* 短链先 HTTP 预解析再提取 ?id=,确保不同分享页不会落到同一 sessionKey。
*/
private static function resolveA2cSessionScope(string $pageUrl, string $landingUrlHint = ''): string
{
$resolvedUrl = self::resolveA2cResolvedUrl($pageUrl, $landingUrlHint);
$host = self::resolveA2cSessionHost($pageUrl, $resolvedUrl);
$shareId = self::extractA2cShareId($pageUrl);
if ($shareId === '') {
$shareId = self::extractA2cShareId($resolvedUrl);
}
return $shareId !== '' ? $host . ':' . $shareId : $host;
}
/**
* A2C 短链 HTTP 预解析:优先已有 landingUrlHint,否则跟随重定向得到长链
*/
private static function resolveA2cResolvedUrl(string $pageUrl, string $landingUrlHint = ''): string
{
if ($landingUrlHint !== '' && self::extractA2cShareId($landingUrlHint) !== '') {
return $landingUrlHint;
}
if (self::extractA2cShareId($pageUrl) !== '') {
return $pageUrl;
}
$resolved = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
if ($resolved !== '') {
return $resolved;
}
return $landingUrlHint !== '' ? $landingUrlHint : $pageUrl;
}
/**
* 从 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 '';
}
}