|null */ public static function fetchStats(): ?array { $url = SplitSyncConfigService::getNodeHost() . '/api/stats'; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 2); curl_setopt($ch, CURLOPT_TIMEOUT, 3); $response = curl_exec($ch); if (curl_errno($ch)) { curl_close($ch); return null; } $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200) { return null; } $decoded = json_decode((string) $response, true); return is_array($decoded) ? $decoded : null; } /** * 当前是否适合向 Node 提交新 Browser 任务 * * A2C 等 antiBot 任务走 real profile,应参考 queueReal 而非 standard 队列。 */ public static function canAcceptWork(): bool { $stats = self::fetchStats(); if ($stats === null) { return true; } if (!empty($stats['shuttingDown'])) { return false; } $snapshot = self::resolveQueueSnapshot($stats); $threshold = SplitSyncConfigService::getNodeQueueSkipThreshold(); if ($snapshot['queued'] >= $threshold) { return false; } return $snapshot['active'] < $snapshot['max']; } /** * @return array{queued:int,active:int,max:int,profile:string} */ public static function getQueueSnapshot(): array { $stats = self::fetchStats(); if ($stats === null) { return ['queued' => 0, 'active' => 0, 'max' => 0, 'profile' => 'real']; } $snapshot = self::resolveQueueSnapshot($stats); return [ 'queued' => $snapshot['queued'], 'active' => $snapshot['active'], 'max' => $snapshot['max'], 'profile' => $snapshot['profile'], ]; } /** * 优先使用 real 队列(antiBot / A2C),无数据时回退 standard * * @param array $stats * @return array{queued:int,active:int,max:int,profile:string} */ private static function resolveQueueSnapshot(array $stats): array { $config = is_array($stats['config'] ?? null) ? $stats['config'] : []; $queueReal = is_array($stats['queueReal'] ?? null) ? $stats['queueReal'] : []; if ($queueReal !== []) { return [ 'queued' => (int) ($queueReal['queued'] ?? 0), 'active' => (int) ($queueReal['active'] ?? 0), 'max' => max(1, (int) ($config['maxConcurrentBrowsersReal'] ?? ($queueReal['max'] ?? 2))), 'profile' => 'real', ]; } $queue = is_array($stats['queue'] ?? null) ? $stats['queue'] : []; return [ 'queued' => (int) ($queue['queued'] ?? 0), 'active' => (int) ($queue['active'] ?? 0), 'max' => max(1, (int) ($config['maxConcurrentBrowsers'] ?? ($queue['max'] ?? 4))), 'profile' => 'standard', ]; } /** * Node 侧 Real Browser / Captcha 是否就绪(供同步失败日志可读性) * * @return array{realBrowserReady:bool,captchaConfigured:bool} */ public static function getAntiBotSnapshot(): array { $stats = self::fetchStats(); if ($stats === null) { return ['realBrowserReady' => false, 'captchaConfigured' => false]; } return [ 'realBrowserReady' => !empty($stats['realBrowserReady']), 'captchaConfigured' => !empty($stats['captchaConfigured']), ]; } }