argument('identifier'); $type = $this->option('type'); $timeout = (int)$this->option('timeout'); $noclearLogs = $this->option('no-clear-logs'); $dataSource = $this->option('data-source'); // 如果开启了清空日志选项,则清空日志文件 if ($noclearLogs) { } else { $this->clearLogFiles(); } Log::info("正在使用 debug:reproduce-error 命令重放请求,当前执行命令: 'php artisan debug:reproduce-error {$identifier}' "); $this->info("开始查找请求记录..."); $this->info("标识符: {$identifier}"); $this->info("类型: {$type}"); $this->info("数据源: {$dataSource}"); // 查找请求记录 $requestLog = $this->findRequestLog($identifier, $type); if (!$requestLog) { $this->error("未找到匹配的请求记录"); return 1; } $this->info("找到请求记录:"); $this->line(" ID: {$requestLog->id}"); $this->line(" Request UNID: {$requestLog->request_unid}"); $this->line(" Run UNID: {$requestLog->run_unid}"); $this->line(" UserId : {$requestLog->user_id}"); $this->line(" 路径: {$requestLog->path}"); $this->line(" 方法: {$requestLog->method}"); $this->line(" 创建时间: {$requestLog->created_at}"); // 解析并显示请求头信息 $headers = $this->parseHeaders($requestLog->headers); $this->displayRequestHeaders($headers); // 提取关键信息 $contentType = $this->extractContentType($headers); $token = $this->extractToken($requestLog->headers); $headerAnalysis = $this->analyzeRequestHeaders($headers); // 设置用户会话 if ($requestLog->user_id) { SessionHelper::sessionLogin($token, $requestLog->user_id); $this->info("token 设置用户: {$requestLog->user_id} "); } // 准备请求数据(使用头部分析结果) $requestData = $this->prepareRequestData($requestLog, $dataSource, $contentType, $headerAnalysis); if ($requestData === null) { return 1; } $this->displayRequestDataInfo($requestData); // 初始化 HTTP 客户端 $this->initializeHttpClient($timeout); // 发起请求 $this->info("开始发起请求..."); $originalHeaders = $this->parseHeaders($requestLog->headers); $response = $this->makeRequest($requestData['data'], $token, $requestData['type'], $originalHeaders); if ($response === null) { return 1; } // 输出结果 $this->line("响应内容:"); $this->displayResponse($response); return 0; } /** * 查找请求记录 * * @param string $identifier 标识符 * @param string $type 类型 * @return RequestLog|null */ protected function findRequestLog(string $identifier, string $type): ?RequestLog { $query = RequestLog::query(); if ($type === 'auto') { // 自动检测类型 if (is_numeric($identifier)) { // 纯数字,优先按 ID 查找 $requestLog = $query->where('id', $identifier)->first(); if ($requestLog) { return $requestLog; } } // 按 request_unid 查找 $requestLog = RequestLog::query()->where('request_unid', $identifier)->first(); if ($requestLog) { return $requestLog; } // 按 run_unid 查找 $requestLog = RequestLog::query()->where('run_unid', $identifier)->first(); if ($requestLog) { return $requestLog; } return null; } // 指定类型查找 switch ($type) { case 'id': return $query->where('id', $identifier)->first(); case 'request_unid': return $query->where('request_unid', $identifier)->first(); case 'run_unid': return $query->where('run_unid', $identifier)->first(); default: $this->error("不支持的类型: {$type}"); return null; } } /** * 显示请求头信息 * * @param array $headers 解析后的请求头数组 */ protected function displayRequestHeaders(array $headers): void { if (empty($headers)) { $this->warn("未找到请求头信息"); return; } $this->info("请求头信息:"); foreach ($headers as $name => $values) { $value = is_array($values) ? implode(', ', $values) : $values; // 对敏感信息进行脱敏处理 if (in_array(strtolower($name), ['token', 'authorization', 'cookie'])) { if (strlen($value) > 10) { $value = substr($value, 0, 10) . '...(已脱敏)'; } } $this->line(" {$name}: {$value}"); } } /** * 分析请求头信息 * * @param array $headers 解析后的请求头数组 * @return array 分析结果 */ protected function analyzeRequestHeaders(array $headers): array { $analysis = [ 'content_type' => null, 'content_length' => null, 'user_agent' => null, 'accept' => null, 'encoding' => null, 'auth_type' => null, 'is_ajax' => false, 'is_mobile' => false, 'has_custom_headers' => false, 'security_headers' => [], 'custom_headers' => [] ]; foreach ($headers as $name => $values) { $value = is_array($values) ? ($values[0] ?? '') : $values; $lowerName = strtolower($name); switch ($lowerName) { case 'content-type': $analysis['content_type'] = $value; if (str_contains($value, 'charset=')) { $analysis['encoding'] = trim(explode('charset=', $value)[1]); } break; case 'content-length': $analysis['content_length'] = (int)$value; break; case 'user-agent': $analysis['user_agent'] = $value; $analysis['is_mobile'] = $this->isMobileUserAgent($value); break; case 'accept': $analysis['accept'] = $value; break; case 'authorization': $analysis['auth_type'] = 'Bearer/Basic'; break; case 'token': $analysis['auth_type'] = 'Token'; break; case 'x-requested-with': if (strtolower($value) === 'xmlhttprequest') { $analysis['is_ajax'] = true; } break; default: // 检测自定义头部 if (str_starts_with($lowerName, 'x-') || !in_array($lowerName, ['host', 'connection', 'cache-control', 'pragma', 'accept-encoding', 'accept-language'])) { $analysis['has_custom_headers'] = true; $analysis['custom_headers'][$name] = $value; } // 检测安全相关头部 if (in_array($lowerName, ['x-csrf-token', 'x-xsrf-token', 'x-frame-options', 'x-content-type-options'])) { $analysis['security_headers'][$name] = $value; } break; } } $this->displayHeaderAnalysis($analysis); return $analysis; } /** * 显示请求头分析结果 * * @param array $analysis 分析结果 */ protected function displayHeaderAnalysis(array $analysis): void { $this->info("请求头分析:"); if ($analysis['content_type']) { $this->line(" Content-Type: {$analysis['content_type']}"); } if ($analysis['content_length']) { $this->line(" Content-Length: {$analysis['content_length']} 字节"); } if ($analysis['auth_type']) { $this->line(" 认证方式: {$analysis['auth_type']}"); } if ($analysis['is_ajax']) { $this->line(" 请求类型: AJAX请求"); } if ($analysis['is_mobile']) { $this->line(" 客户端: 移动设备"); } if ($analysis['has_custom_headers']) { $this->line(" 自定义头部: " . count($analysis['custom_headers']) . " 个"); } if (!empty($analysis['security_headers'])) { $this->line(" 安全头部: " . implode(', ', array_keys($analysis['security_headers']))); } } /** * 检测是否为移动设备User-Agent * * @param string $userAgent * @return bool */ protected function isMobileUserAgent(string $userAgent): bool { $mobileKeywords = ['Mobile', 'Android', 'iPhone', 'iPad', 'Windows Phone', 'BlackBerry']; foreach ($mobileKeywords as $keyword) { if (str_contains($userAgent, $keyword)) { return true; } } return false; } /** * 解析 headers JSON 字符串 * * @param string|null $headersJson * @return array */ protected function parseHeaders(?string $headersJson): array { if (empty($headersJson)) { return []; } try { $headers = json_decode($headersJson, true); return is_array($headers) ? $headers : []; } catch (\Exception $e) { $this->warn("解析 headers 失败: " . $e->getMessage()); return []; } } /** * 从 headers 中提取 Content-Type * * @param array $headers * @return string|null */ protected function extractContentType(array $headers): ?string { $contentTypeKeys = ['content-type', 'Content-Type', 'CONTENT-TYPE']; foreach ($contentTypeKeys as $key) { if (isset($headers[$key])) { $contentType = $headers[$key]; // headers 中的值可能是数组 if (is_array($contentType)) { return $contentType[0] ?? null; } return $contentType; } } return null; } /** * 从 headers JSON 中提取 token * * @param string|null $headersJson * @return string|null */ protected function extractToken(?string $headersJson): ?string { $headers = $this->parseHeaders($headersJson); // 查找 token 字段(可能在不同的键名下) $tokenKeys = [ 'token', 'Token', 'authorization', 'Authorization' ]; foreach ($tokenKeys as $key) { if (isset($headers[$key])) { $tokenValue = $headers[$key]; // headers 中的值可能是数组 if (is_array($tokenValue)) { return $tokenValue[0] ?? null; } return $tokenValue; } } return null; } /** * 准备请求数据 * * @param RequestLog $requestLog * @param string $dataSource * @param string|null $contentType * @param array $headerAnalysis * @return array|null */ protected function prepareRequestData(RequestLog $requestLog, string $dataSource, ?string $contentType, array $headerAnalysis = []): ?array { // 根据数据源选择策略,结合头部分析结果 if ($dataSource === 'auto') { // 智能选择:根据头部分析结果决定最佳数据源 $bestSource = $this->determineBestDataSource($requestLog, $headerAnalysis); $this->info("根据请求头分析,推荐使用数据源: {$bestSource}"); if ($bestSource === 'post' && !empty($requestLog->post)) { return $this->preparePostData($requestLog->post, $contentType, $headerAnalysis); } elseif ($bestSource === 'protobuf_json' && !empty($requestLog->protobuf_json)) { return $this->prepareProtobufJsonData($requestLog->protobuf_json, $headerAnalysis); } else { // 回退到原有逻辑 if (!empty($requestLog->post)) { return $this->preparePostData($requestLog->post, $contentType, $headerAnalysis); } elseif (!empty($requestLog->protobuf_json)) { return $this->prepareProtobufJsonData($requestLog->protobuf_json, $headerAnalysis); } else { $this->error("请求记录中既没有 post 数据也没有 protobuf_json 数据"); return null; } } } elseif ($dataSource === 'post') { if (empty($requestLog->post)) { $this->error("请求记录中缺少 post 数据"); return null; } return $this->preparePostData($requestLog->post, $contentType, $headerAnalysis); } elseif ($dataSource === 'protobuf_json') { if (empty($requestLog->protobuf_json)) { $this->error("请求记录中缺少 protobuf_json 数据"); return null; } return $this->prepareProtobufJsonData($requestLog->protobuf_json, $headerAnalysis); } else { $this->error("不支持的数据源: {$dataSource}"); return null; } } /** * 根据请求头分析确定最佳数据源 * * @param RequestLog $requestLog * @param array $headerAnalysis * @return string */ protected function determineBestDataSource(RequestLog $requestLog, array $headerAnalysis): string { // 如果Content-Type明确指示JSON,优先使用protobuf_json if ($headerAnalysis['content_type'] && str_contains(strtolower($headerAnalysis['content_type']), 'json')) { return 'protobuf_json'; } // 如果Content-Type指示protobuf或二进制,优先使用post if ($headerAnalysis['content_type'] && (str_contains(strtolower($headerAnalysis['content_type']), 'protobuf') || str_contains(strtolower($headerAnalysis['content_type']), 'octet-stream'))) { return 'post'; } // 如果是AJAX请求,通常是JSON格式 if ($headerAnalysis['is_ajax']) { return 'protobuf_json'; } // 默认优先使用post数据 return 'post'; } /** * 准备 post 数据 * * @param string $postData 可能是base64编码的原始数据或JSON格式的数据 * @param string|null $contentType * @param array $headerAnalysis * @return array */ protected function preparePostData(string $postData, ?string $contentType, array $headerAnalysis = []): array { // 首先尝试解析为JSON(新的存储格式) $jsonData = json_decode($postData, true); if ($jsonData !== null && is_array($jsonData)) { // 如果是JSON格式,根据Content-Type转换为相应格式 if ($contentType && stripos($contentType, 'application/x-www-form-urlencoded') !== false) { // 转换为表单格式 $formData = http_build_query($jsonData); $dataType = 'form'; $rawData = $formData; } else { // 保持JSON格式 $dataType = 'json'; $rawData = $postData; } $result = [ 'source' => 'post', 'type' => $dataType, 'data' => $rawData, 'analysis' => [ 'original_size' => strlen($postData), 'processed_size' => strlen($rawData), 'content_type' => $contentType, 'detected_type' => $dataType, 'storage_format' => 'json' ] ]; } else { // 尝试作为base64编码的原始数据处理(旧的存储格式) $rawData = base64_decode($postData); // 如果base64解码失败,直接使用原始数据 if ($rawData === false) { $rawData = $postData; } // 根据请求头分析和 Content-Type 判断数据类型 $dataType = $this->determineDataType($contentType, $headerAnalysis, $rawData); $result = [ 'source' => 'post', 'type' => $dataType, 'data' => $rawData, 'analysis' => [ 'original_size' => strlen($postData), 'decoded_size' => strlen($rawData), 'content_type' => $contentType, 'detected_type' => $dataType, 'storage_format' => 'base64' ] ]; } // 如果有头部分析信息,添加到结果中 if (!empty($headerAnalysis)) { $result['header_context'] = [ 'is_ajax' => $headerAnalysis['is_ajax'] ?? false, 'is_mobile' => $headerAnalysis['is_mobile'] ?? false, 'auth_type' => $headerAnalysis['auth_type'] ?? null, 'content_length' => $headerAnalysis['content_length'] ?? null ]; } return $result; } /** * 确定数据类型 * * @param string|null $contentType * @param array $headerAnalysis * @param string $rawData * @return string */ protected function determineDataType(?string $contentType, array $headerAnalysis, string $rawData): string { // 优先根据Content-Type判断 if ($contentType) { if (stripos($contentType, 'json') !== false) { return 'json'; } if (stripos($contentType, 'protobuf') !== false || stripos($contentType, 'octet-stream') !== false) { return 'protobuf'; } // 处理表单数据 if (stripos($contentType, 'application/x-www-form-urlencoded') !== false) { return 'form'; } } // 根据请求头分析判断 if (!empty($headerAnalysis['is_ajax'])) { return 'json'; } // 尝试解析数据内容判断 if ($this->looksLikeJson($rawData)) { return 'json'; } // 检查是否看起来像表单数据 if ($this->looksLikeFormData($rawData)) { return 'form'; } // 默认为protobuf return 'protobuf'; } /** * 检查数据是否看起来像JSON * * @param string $data * @return bool */ protected function looksLikeJson(string $data): bool { // 检查是否以JSON常见字符开始 $trimmed = trim($data); if (empty($trimmed)) { return false; } $firstChar = $trimmed[0]; if (in_array($firstChar, ['{', '[', '"'])) { // 尝试解析JSON json_decode($trimmed); return json_last_error() === JSON_ERROR_NONE; } return false; } /** * 检查数据是否看起来像表单数据 * * @param string $data * @return bool */ protected function looksLikeFormData(string $data): bool { // 检查是否包含表单数据的特征:key=value&key2=value2 $trimmed = trim($data); if (empty($trimmed)) { return false; } // 检查是否包含等号和可能的&符号 if (strpos($trimmed, '=') !== false) { // 尝试解析为表单数据 parse_str($trimmed, $parsed); // 如果解析成功且不为空,则认为是表单数据 return !empty($parsed); } return false; } /** * 准备 protobuf_json 数据 * * @param string $protobufJson * @param array $headerAnalysis * @return array */ protected function prepareProtobufJsonData(string $protobufJson, array $headerAnalysis = []): array { $result = [ 'source' => 'protobuf_json', 'type' => 'json', 'data' => $protobufJson, 'analysis' => [ 'data_size' => strlen($protobufJson), 'detected_type' => 'json' ] ]; // 如果有头部分析信息,添加到结果中 if (!empty($headerAnalysis)) { $result['header_context'] = [ 'is_ajax' => $headerAnalysis['is_ajax'] ?? false, 'is_mobile' => $headerAnalysis['is_mobile'] ?? false, 'auth_type' => $headerAnalysis['auth_type'] ?? null ]; } return $result; } /** * 显示请求数据信息 * * @param array $requestData */ protected function displayRequestDataInfo(array $requestData): void { $this->info("请求数据信息:"); $this->line(" 数据源: {$requestData['source']}"); $this->line(" 数据类型: {$requestData['type']}"); // 显示分析信息 if (isset($requestData['analysis'])) { $analysis = $requestData['analysis']; $this->line(" 数据分析:"); if (isset($analysis['decoded_size'])) { $this->line(" 原始大小: {$analysis['original_size']} 字节"); $this->line(" 解码后大小: {$analysis['decoded_size']} 字节"); } elseif (isset($analysis['data_size'])) { $this->line(" 数据大小: {$analysis['data_size']} 字节"); } if (isset($analysis['content_type'])) { $this->line(" Content-Type: {$analysis['content_type']}"); } if (isset($analysis['detected_type'])) { $this->line(" 检测类型: {$analysis['detected_type']}"); } } // 显示头部上下文信息 if (isset($requestData['header_context'])) { $context = $requestData['header_context']; $contextInfo = []; if ($context['is_ajax']) { $contextInfo[] = 'AJAX请求'; } if ($context['is_mobile']) { $contextInfo[] = '移动设备'; } if ($context['auth_type']) { $contextInfo[] = "认证: {$context['auth_type']}"; } if (!empty($contextInfo)) { $this->line(" 请求上下文: " . implode(', ', $contextInfo)); } } // 显示数据预览 $this->info("请求数据预览:"); if ($requestData['type'] === 'json') { $jsonData = json_decode($requestData['data'], true); if ($jsonData !== null) { dump($jsonData); } else { $this->warn("数据类型标记为JSON但解析失败"); $this->line("原始数据: " . substr($requestData['data'], 0, 200) . "..."); } } elseif ($requestData['type'] === 'form') { // 表单数据预览 parse_str($requestData['data'], $formData); if (!empty($formData)) { $this->line(" 表单数据:"); foreach ($formData as $key => $value) { $this->line(" {$key}: {$value}"); } } else { $this->warn("数据类型标记为表单但解析失败"); $this->line("原始数据: " . substr($requestData['data'], 0, 200) . "..."); } } else { $this->line(" 二进制数据长度: " . strlen($requestData['data']) . " 字节"); $this->line(" Base64预览: " . substr(base64_encode($requestData['data']), 0, 100) . "..."); // 尝试检测是否包含可读文本 if (mb_check_encoding($requestData['data'], 'UTF-8')) { $preview = substr($requestData['data'], 0, 100); if (ctype_print($preview)) { $this->line(" 文本预览: {$preview}..."); } } } } /** * 初始化 HTTP 客户端 * * @param int $timeout */ protected function initializeHttpClient(int $timeout): void { $this->baseUrl = env('UNITTEST_URL', 'http://localhost:8000'); $this->info("目标地址: {$this->baseUrl}"); $this->client = new Client([ 'base_uri' => $this->baseUrl, 'timeout' => $timeout, 'http_errors' => false, 'verify' => false, // 禁用 SSL 验证 ]); } /** * 发起请求 * * @param string $requestData 请求数据 * @param string|null $token 认证token * @param string $dataType 数据类型 (json|protobuf|form) * @param array $originalHeaders 原始请求头 * @return array|null */ protected function makeRequest(string $requestData, ?string $token, string $dataType, array $originalHeaders = []): ?array { try { // 根据数据类型设置不同的 headers 和请求选项 $requestOptions = []; if ($dataType === 'json') { $headers = [ 'Content-Type' => 'application/json', 'Accept' => 'application/json' ]; $requestOptions['body'] = $requestData; } elseif ($dataType === 'form') { $headers = [ 'Content-Type' => 'application/x-www-form-urlencoded', 'Accept' => 'application/json' ]; // 对于表单数据,使用form_params而不是body parse_str($requestData, $formData); $requestOptions['form_params'] = $formData; } else { // protobuf 二进制格式 $headers = [ 'Content-Type' => 'application/x-protobuf', 'Accept' => 'application/x-protobuf', 'Force-Json'=>'1' ]; $requestOptions['body'] = $requestData; } // 复制原始请求中的重要头部信息 $importantHeaders = ['x-signature', 'x-timestamp', 'x-nonce', 'user-agent']; foreach ($importantHeaders as $headerName) { $value = $this->findHeaderValue($originalHeaders, $headerName); if ($value !== null) { $headers[$headerName] = $value; } } // 设置正确的host头(使用目标服务器的host) $parsedUrl = parse_url($this->baseUrl); if (isset($parsedUrl['host'])) { $headers['host'] = $parsedUrl['host']; } if ($token) { $headers['token'] = $token; } $requestOptions['headers'] = $headers; // 根据数据类型确定目标URL $targetUrl = $dataType === 'form' ? '/thirdParty/webhook/urs/deposit' : '/gameapi'; Log::info('复现请求开始', [ 'url' => $this->baseUrl . $targetUrl, 'headers' => $headers, 'body_length' => strlen($requestData), 'data_type' => $dataType, 'form_data' => $dataType === 'form' ? $formData ?? [] : null ]); $response = $this->client->post($targetUrl, $requestOptions); $statusCode = $response->getStatusCode(); $responseHeaders = $response->getHeaders(); $responseBody = $response->getBody()->getContents(); Log::info('复现请求完成', [ 'status_code' => $statusCode, 'response_length' => strlen($responseBody), 'data_type' => $dataType ]); return [ 'status_code' => $statusCode, 'headers' => $responseHeaders, 'body' => $responseBody ]; } catch (\Exception $e) { $this->error("请求失败: " . $e->getMessage()); Log::error('复现请求失败', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'data_type' => $dataType ]); return null; } } /** * 显示响应内容 * * @param array $response 响应数据(包含status_code、headers、body) */ protected function displayResponse(array $response): void { $this->line("状态码: " . $response['status_code']); // 显示响应头信息 $this->line("响应头:"); foreach ($response['headers'] as $name => $values) { $value = is_array($values) ? implode(', ', $values) : $values; $this->line(" {$name}: {$value}"); } // 从响应头中提取Content-Type $responseContentType = $this->extractResponseContentType($response['headers']); $this->line("检测到响应Content-Type: " . ($responseContentType ?: '未知')); // 根据响应Content-Type决定显示方式 $responseBody = $response['body']; $bodyLength = strlen($responseBody); $this->line("响应体长度: {$bodyLength} 字节"); if ($this->isJsonContentType($responseContentType)) { // JSON 响应 $this->line("按JSON格式解析响应:"); $jsonData = json_decode($responseBody, true); if ($jsonData !== null) { dump($jsonData); } else { $this->warn("响应Content-Type为JSON但解析失败"); $this->displayRawContent($responseBody); } } elseif ($this->isProtobufContentType($responseContentType)) { // Protobuf 二进制响应 $this->line("按Protobuf格式处理响应:"); $this->line("Base64 编码: " . substr(base64_encode($responseBody), 0, 200) . "..."); // 尝试解析为 JSON(某些情况下服务器可能返回 JSON) $jsonData = json_decode($responseBody, true); if ($jsonData !== null) { $this->line("响应可解析为 JSON:"); dump($jsonData); } else { $this->line("响应为二进制 Protobuf 数据,无法直接显示"); } } elseif ($this->isHtmlContentType($responseContentType)) { // HTML 响应 $this->line("按HTML格式处理响应:"); $this->displayHtmlContent($responseBody); } elseif ($this->isTextContentType($responseContentType)) { // 纯文本响应 $this->line("按文本格式显示响应:"); $this->line($responseBody); } else { // 未知类型,尝试智能检测 $this->line("未知Content-Type,尝试智能检测:"); $this->smartDisplayContent($responseBody); } } /** * 从响应头中提取Content-Type * * @param array $headers 响应头数组 * @return string|null */ protected function extractResponseContentType(array $headers): ?string { // 尝试不同的Content-Type键名(大小写不敏感) $contentTypeKeys = ['Content-Type', 'content-type', 'Content-type', 'CONTENT-TYPE']; foreach ($contentTypeKeys as $key) { if (isset($headers[$key])) { $value = $headers[$key]; // 如果是数组,取第一个值 if (is_array($value)) { $value = $value[0] ?? ''; } // 只取分号前的部分(去掉charset等参数) return trim(explode(';', $value)[0]); } } return null; } /** * 判断是否为JSON内容类型 * * @param string|null $contentType * @return bool */ protected function isJsonContentType(?string $contentType): bool { if (!$contentType) { return false; } $jsonTypes = [ 'application/json', 'text/json', 'application/ld+json' ]; return in_array(strtolower($contentType), $jsonTypes); } /** * 判断是否为Protobuf内容类型 * * @param string|null $contentType * @return bool */ protected function isProtobufContentType(?string $contentType): bool { if (!$contentType) { return false; } $protobufTypes = [ 'application/x-protobuf', 'application/protobuf', 'application/octet-stream' ]; return in_array(strtolower($contentType), $protobufTypes); } /** * 判断是否为HTML内容类型 * * @param string|null $contentType * @return bool */ protected function isHtmlContentType(?string $contentType): bool { if (!$contentType) { return false; } $htmlTypes = [ 'text/html', 'application/xhtml+xml' ]; return in_array(strtolower($contentType), $htmlTypes); } /** * 判断是否为文本内容类型 * * @param string|null $contentType * @return bool */ protected function isTextContentType(?string $contentType): bool { if (!$contentType) { return false; } return str_starts_with(strtolower($contentType), 'text/'); } /** * 显示HTML内容 * * @param string $content */ protected function displayHtmlContent(string $content): void { // 检查是否包含错误页面标识 if (str_contains($content, '') && str_contains($content, '')) { preg_match('/(.*?)<\/title>/i', $content, $matches); $title = $matches[1] ?? '未知'; $this->line("HTML页面标题: {$title}"); } // 显示部分内容 $preview = substr(strip_tags($content), 0, 500); $this->line("HTML内容预览: " . $preview . "..."); // 检查是否为错误页面 if (str_contains($content, 'error') || str_contains($content, 'Error') || str_contains($content, 'exception')) { $this->warn("检测到可能的错误页面"); } } /** * 显示原始内容 * * @param string $content */ protected function displayRawContent(string $content): void { $this->line("原始响应内容:"); if (strlen($content) > 1000) { $this->line(substr($content, 0, 1000) . "..."); $this->line("(内容过长,已截断显示)"); } else { $this->line($content); } } /** * 智能检测并显示内容 * * @param string $content */ protected function smartDisplayContent(string $content): void { // 尝试JSON解析 $jsonData = json_decode($content, true); if ($jsonData !== null) { $this->line("内容可解析为JSON:"); dump($jsonData); return; } // 检查是否为HTML if (str_contains($content, '<html') || str_contains($content, '<!DOCTYPE')) { $this->line("检测到HTML内容:"); $this->displayHtmlContent($content); return; } // 检查是否为二进制数据 if (!mb_check_encoding($content, 'UTF-8')) { $this->line("检测到二进制数据:"); $this->line("数据长度: " . strlen($content) . " 字节"); $this->line("Base64 编码: " . substr(base64_encode($content), 0, 200) . "..."); return; } // 默认按文本显示 $this->line("按文本格式显示:"); $this->displayRawContent($content); } /** * 在头部数组中查找指定头部的值(不区分大小写) * * @param array $headers 头部数组 * @param string $headerName 要查找的头部名称 * @return string|null */ protected function findHeaderValue(array $headers, string $headerName): ?string { $lowerHeaderName = strtolower($headerName); foreach ($headers as $name => $values) { if (strtolower($name) === $lowerHeaderName) { // 如果值是数组,取第一个值 if (is_array($values)) { return $values[0] ?? null; } return $values; } } return null; } /** * 清空当前日志文件 */ protected function clearLogFiles(): void { Logger::clear_log(); Logger::debug('旧的日志已经清理'); } }