ReproduceErrorCommand.php 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055
  1. <?php
  2. namespace App\Console\Commands;
  3. use App\Module\AppGame\SessionApp;
  4. use App\Module\AppGame\SessionHelper;
  5. use GuzzleHttp\Client;
  6. use Illuminate\Console\Command;
  7. use UCore\Model\RequestLog;
  8. use Illuminate\Support\Facades\Log;
  9. use UCore\Helper\Logger;
  10. /**
  11. * 错误复现命令
  12. *
  13. * 通过 sys_request_logs 表的记录来复现请求,用于调试和错误排查
  14. * 默认使用 post 字段的原始请求数据,根据 headers 中的 Content-Type 自动检测数据类型
  15. * 根据响应的 Content-Type 头智能显示响应内容(JSON、HTML、Protobuf、文本等)
  16. *
  17. * 使用示例:
  18. * php artisan debug:reproduce-error 68981433 # 使用ID查找,自动选择数据源
  19. * php artisan debug:reproduce-error request_1749626545371 # 使用request_unid查找
  20. * php artisan debug:reproduce-error 68973982 --type=request_unid # 明确指定查找类型
  21. * php artisan debug:reproduce-error 68973982 --no-clear-logs # 不清空日志文件
  22. * php artisan debug:reproduce-error 68973982 --data-source=post # 强制使用post数据
  23. * php artisan debug:reproduce-error 68973982 --data-source=protobuf_json # 强制使用protobuf_json数据
  24. *
  25. */
  26. class ReproduceErrorCommand extends Command
  27. {
  28. /**
  29. * 命令签名
  30. *
  31. * @var string
  32. */
  33. protected $signature = 'debug:reproduce-error {identifier : 请求标识符(可以是id、request_unid或run_unid)} {--type=auto : 标识符类型(id|request_unid|run_unid|auto),auto为自动检测} {--timeout=30 : 请求超时时间(秒)} {--no-clear-logs : 运行时不前清空当前日志文件} {--data-source=auto : 数据源(post|protobuf_json|auto),auto为自动选择,优先使用post}';
  34. /**
  35. * 命令描述
  36. *
  37. * @var string
  38. */
  39. protected $description = '通过请求日志记录复现错误请求,用于调试和问题排查。支持运行前清空日志文件,默认使用post字段数据并根据headers自动检测数据类型,根据响应Content-Type智能显示内容';
  40. /**
  41. * HTTP 客户端
  42. *
  43. * @var Client
  44. */
  45. protected Client $client;
  46. /**
  47. * 基础URL
  48. *
  49. * @var string
  50. */
  51. protected string $baseUrl;
  52. /**
  53. * 执行命令
  54. */
  55. public function handle()
  56. {
  57. $identifier = $this->argument('identifier');
  58. $type = $this->option('type');
  59. $timeout = (int)$this->option('timeout');
  60. $noclearLogs = $this->option('no-clear-logs');
  61. $dataSource = $this->option('data-source');
  62. // 如果开启了清空日志选项,则清空日志文件
  63. if ($noclearLogs) {
  64. } else {
  65. $this->clearLogFiles();
  66. }
  67. Log::info("正在使用 debug:reproduce-error 命令重放请求,当前执行命令: 'php artisan debug:reproduce-error {$identifier}' ");
  68. $this->info("开始查找请求记录...");
  69. $this->info("标识符: {$identifier}");
  70. $this->info("类型: {$type}");
  71. $this->info("数据源: {$dataSource}");
  72. // 查找请求记录
  73. $requestLog = $this->findRequestLog($identifier, $type);
  74. if (!$requestLog) {
  75. $this->error("未找到匹配的请求记录");
  76. return 1;
  77. }
  78. $this->info("找到请求记录:");
  79. $this->line(" ID: {$requestLog->id}");
  80. $this->line(" Request UNID: {$requestLog->request_unid}");
  81. $this->line(" Run UNID: {$requestLog->run_unid}");
  82. $this->line(" UserId : {$requestLog->user_id}");
  83. $this->line(" 路径: {$requestLog->path}");
  84. $this->line(" 方法: {$requestLog->method}");
  85. $this->line(" 创建时间: {$requestLog->created_at}");
  86. // 解析并显示请求头信息
  87. $headers = $this->parseHeaders($requestLog->headers);
  88. $this->displayRequestHeaders($headers);
  89. // 提取关键信息
  90. $contentType = $this->extractContentType($headers);
  91. $token = $this->extractToken($requestLog->headers);
  92. $headerAnalysis = $this->analyzeRequestHeaders($headers);
  93. // 设置用户会话
  94. if ($requestLog->user_id) {
  95. SessionHelper::sessionLogin($token, $requestLog->user_id);
  96. $this->info("token 设置用户: {$requestLog->user_id} ");
  97. }
  98. // 准备请求数据(使用头部分析结果)
  99. $requestData = $this->prepareRequestData($requestLog, $dataSource, $contentType, $headerAnalysis);
  100. if ($requestData === null) {
  101. return 1;
  102. }
  103. $this->displayRequestDataInfo($requestData);
  104. // 初始化 HTTP 客户端
  105. $this->initializeHttpClient($timeout);
  106. // 发起请求
  107. $this->info("开始发起请求...");
  108. $response = $this->makeRequest($requestData['data'], $token, $requestData['type']);
  109. if ($response === null) {
  110. return 1;
  111. }
  112. // 输出结果
  113. $this->line("响应内容:");
  114. $this->displayResponse($response);
  115. return 0;
  116. }
  117. /**
  118. * 查找请求记录
  119. *
  120. * @param string $identifier 标识符
  121. * @param string $type 类型
  122. * @return RequestLog|null
  123. */
  124. protected function findRequestLog(string $identifier, string $type): ?RequestLog
  125. {
  126. $query = RequestLog::query();
  127. if ($type === 'auto') {
  128. // 自动检测类型
  129. if (is_numeric($identifier)) {
  130. // 纯数字,优先按 ID 查找
  131. $requestLog = $query->where('id', $identifier)->first();
  132. if ($requestLog) {
  133. return $requestLog;
  134. }
  135. }
  136. // 按 request_unid 查找
  137. $requestLog = RequestLog::query()->where('request_unid', $identifier)->first();
  138. if ($requestLog) {
  139. return $requestLog;
  140. }
  141. // 按 run_unid 查找
  142. $requestLog = RequestLog::query()->where('run_unid', $identifier)->first();
  143. if ($requestLog) {
  144. return $requestLog;
  145. }
  146. return null;
  147. }
  148. // 指定类型查找
  149. switch ($type) {
  150. case 'id':
  151. return $query->where('id', $identifier)->first();
  152. case 'request_unid':
  153. return $query->where('request_unid', $identifier)->first();
  154. case 'run_unid':
  155. return $query->where('run_unid', $identifier)->first();
  156. default:
  157. $this->error("不支持的类型: {$type}");
  158. return null;
  159. }
  160. }
  161. /**
  162. * 显示请求头信息
  163. *
  164. * @param array $headers 解析后的请求头数组
  165. */
  166. protected function displayRequestHeaders(array $headers): void
  167. {
  168. if (empty($headers)) {
  169. $this->warn("未找到请求头信息");
  170. return;
  171. }
  172. $this->info("请求头信息:");
  173. foreach ($headers as $name => $values) {
  174. $value = is_array($values) ? implode(', ', $values) : $values;
  175. // 对敏感信息进行脱敏处理
  176. if (in_array(strtolower($name), ['token', 'authorization', 'cookie'])) {
  177. if (strlen($value) > 10) {
  178. $value = substr($value, 0, 10) . '...(已脱敏)';
  179. }
  180. }
  181. $this->line(" {$name}: {$value}");
  182. }
  183. }
  184. /**
  185. * 分析请求头信息
  186. *
  187. * @param array $headers 解析后的请求头数组
  188. * @return array 分析结果
  189. */
  190. protected function analyzeRequestHeaders(array $headers): array
  191. {
  192. $analysis = [
  193. 'content_type' => null,
  194. 'content_length' => null,
  195. 'user_agent' => null,
  196. 'accept' => null,
  197. 'encoding' => null,
  198. 'auth_type' => null,
  199. 'is_ajax' => false,
  200. 'is_mobile' => false,
  201. 'has_custom_headers' => false,
  202. 'security_headers' => [],
  203. 'custom_headers' => []
  204. ];
  205. foreach ($headers as $name => $values) {
  206. $value = is_array($values) ? ($values[0] ?? '') : $values;
  207. $lowerName = strtolower($name);
  208. switch ($lowerName) {
  209. case 'content-type':
  210. $analysis['content_type'] = $value;
  211. if (str_contains($value, 'charset=')) {
  212. $analysis['encoding'] = trim(explode('charset=', $value)[1]);
  213. }
  214. break;
  215. case 'content-length':
  216. $analysis['content_length'] = (int)$value;
  217. break;
  218. case 'user-agent':
  219. $analysis['user_agent'] = $value;
  220. $analysis['is_mobile'] = $this->isMobileUserAgent($value);
  221. break;
  222. case 'accept':
  223. $analysis['accept'] = $value;
  224. break;
  225. case 'authorization':
  226. $analysis['auth_type'] = 'Bearer/Basic';
  227. break;
  228. case 'token':
  229. $analysis['auth_type'] = 'Token';
  230. break;
  231. case 'x-requested-with':
  232. if (strtolower($value) === 'xmlhttprequest') {
  233. $analysis['is_ajax'] = true;
  234. }
  235. break;
  236. default:
  237. // 检测自定义头部
  238. if (str_starts_with($lowerName, 'x-') ||
  239. !in_array($lowerName, ['host', 'connection', 'cache-control', 'pragma', 'accept-encoding', 'accept-language'])) {
  240. $analysis['has_custom_headers'] = true;
  241. $analysis['custom_headers'][$name] = $value;
  242. }
  243. // 检测安全相关头部
  244. if (in_array($lowerName, ['x-csrf-token', 'x-xsrf-token', 'x-frame-options', 'x-content-type-options'])) {
  245. $analysis['security_headers'][$name] = $value;
  246. }
  247. break;
  248. }
  249. }
  250. $this->displayHeaderAnalysis($analysis);
  251. return $analysis;
  252. }
  253. /**
  254. * 显示请求头分析结果
  255. *
  256. * @param array $analysis 分析结果
  257. */
  258. protected function displayHeaderAnalysis(array $analysis): void
  259. {
  260. $this->info("请求头分析:");
  261. if ($analysis['content_type']) {
  262. $this->line(" Content-Type: {$analysis['content_type']}");
  263. }
  264. if ($analysis['content_length']) {
  265. $this->line(" Content-Length: {$analysis['content_length']} 字节");
  266. }
  267. if ($analysis['auth_type']) {
  268. $this->line(" 认证方式: {$analysis['auth_type']}");
  269. }
  270. if ($analysis['is_ajax']) {
  271. $this->line(" 请求类型: AJAX请求");
  272. }
  273. if ($analysis['is_mobile']) {
  274. $this->line(" 客户端: 移动设备");
  275. }
  276. if ($analysis['has_custom_headers']) {
  277. $this->line(" 自定义头部: " . count($analysis['custom_headers']) . " 个");
  278. }
  279. if (!empty($analysis['security_headers'])) {
  280. $this->line(" 安全头部: " . implode(', ', array_keys($analysis['security_headers'])));
  281. }
  282. }
  283. /**
  284. * 检测是否为移动设备User-Agent
  285. *
  286. * @param string $userAgent
  287. * @return bool
  288. */
  289. protected function isMobileUserAgent(string $userAgent): bool
  290. {
  291. $mobileKeywords = ['Mobile', 'Android', 'iPhone', 'iPad', 'Windows Phone', 'BlackBerry'];
  292. foreach ($mobileKeywords as $keyword) {
  293. if (str_contains($userAgent, $keyword)) {
  294. return true;
  295. }
  296. }
  297. return false;
  298. }
  299. /**
  300. * 解析 headers JSON 字符串
  301. *
  302. * @param string|null $headersJson
  303. * @return array
  304. */
  305. protected function parseHeaders(?string $headersJson): array
  306. {
  307. if (empty($headersJson)) {
  308. return [];
  309. }
  310. try {
  311. $headers = json_decode($headersJson, true);
  312. return is_array($headers) ? $headers : [];
  313. } catch (\Exception $e) {
  314. $this->warn("解析 headers 失败: " . $e->getMessage());
  315. return [];
  316. }
  317. }
  318. /**
  319. * 从 headers 中提取 Content-Type
  320. *
  321. * @param array $headers
  322. * @return string|null
  323. */
  324. protected function extractContentType(array $headers): ?string
  325. {
  326. $contentTypeKeys = ['content-type', 'Content-Type', 'CONTENT-TYPE'];
  327. foreach ($contentTypeKeys as $key) {
  328. if (isset($headers[$key])) {
  329. $contentType = $headers[$key];
  330. // headers 中的值可能是数组
  331. if (is_array($contentType)) {
  332. return $contentType[0] ?? null;
  333. }
  334. return $contentType;
  335. }
  336. }
  337. return null;
  338. }
  339. /**
  340. * 从 headers JSON 中提取 token
  341. *
  342. * @param string|null $headersJson
  343. * @return string|null
  344. */
  345. protected function extractToken(?string $headersJson): ?string
  346. {
  347. $headers = $this->parseHeaders($headersJson);
  348. // 查找 token 字段(可能在不同的键名下)
  349. $tokenKeys = [ 'token', 'Token', 'authorization', 'Authorization' ];
  350. foreach ($tokenKeys as $key) {
  351. if (isset($headers[$key])) {
  352. $tokenValue = $headers[$key];
  353. // headers 中的值可能是数组
  354. if (is_array($tokenValue)) {
  355. return $tokenValue[0] ?? null;
  356. }
  357. return $tokenValue;
  358. }
  359. }
  360. return null;
  361. }
  362. /**
  363. * 准备请求数据
  364. *
  365. * @param RequestLog $requestLog
  366. * @param string $dataSource
  367. * @param string|null $contentType
  368. * @param array $headerAnalysis
  369. * @return array|null
  370. */
  371. protected function prepareRequestData(RequestLog $requestLog, string $dataSource, ?string $contentType, array $headerAnalysis = []): ?array
  372. {
  373. // 根据数据源选择策略,结合头部分析结果
  374. if ($dataSource === 'auto') {
  375. // 智能选择:根据头部分析结果决定最佳数据源
  376. $bestSource = $this->determineBestDataSource($requestLog, $headerAnalysis);
  377. $this->info("根据请求头分析,推荐使用数据源: {$bestSource}");
  378. if ($bestSource === 'post' && !empty($requestLog->post)) {
  379. return $this->preparePostData($requestLog->post, $contentType, $headerAnalysis);
  380. } elseif ($bestSource === 'protobuf_json' && !empty($requestLog->protobuf_json)) {
  381. return $this->prepareProtobufJsonData($requestLog->protobuf_json, $headerAnalysis);
  382. } else {
  383. // 回退到原有逻辑
  384. if (!empty($requestLog->post)) {
  385. return $this->preparePostData($requestLog->post, $contentType, $headerAnalysis);
  386. } elseif (!empty($requestLog->protobuf_json)) {
  387. return $this->prepareProtobufJsonData($requestLog->protobuf_json, $headerAnalysis);
  388. } else {
  389. $this->error("请求记录中既没有 post 数据也没有 protobuf_json 数据");
  390. return null;
  391. }
  392. }
  393. } elseif ($dataSource === 'post') {
  394. if (empty($requestLog->post)) {
  395. $this->error("请求记录中缺少 post 数据");
  396. return null;
  397. }
  398. return $this->preparePostData($requestLog->post, $contentType, $headerAnalysis);
  399. } elseif ($dataSource === 'protobuf_json') {
  400. if (empty($requestLog->protobuf_json)) {
  401. $this->error("请求记录中缺少 protobuf_json 数据");
  402. return null;
  403. }
  404. return $this->prepareProtobufJsonData($requestLog->protobuf_json, $headerAnalysis);
  405. } else {
  406. $this->error("不支持的数据源: {$dataSource}");
  407. return null;
  408. }
  409. }
  410. /**
  411. * 根据请求头分析确定最佳数据源
  412. *
  413. * @param RequestLog $requestLog
  414. * @param array $headerAnalysis
  415. * @return string
  416. */
  417. protected function determineBestDataSource(RequestLog $requestLog, array $headerAnalysis): string
  418. {
  419. // 如果Content-Type明确指示JSON,优先使用protobuf_json
  420. if ($headerAnalysis['content_type'] &&
  421. str_contains(strtolower($headerAnalysis['content_type']), 'json')) {
  422. return 'protobuf_json';
  423. }
  424. // 如果Content-Type指示protobuf或二进制,优先使用post
  425. if ($headerAnalysis['content_type'] &&
  426. (str_contains(strtolower($headerAnalysis['content_type']), 'protobuf') ||
  427. str_contains(strtolower($headerAnalysis['content_type']), 'octet-stream'))) {
  428. return 'post';
  429. }
  430. // 如果是AJAX请求,通常是JSON格式
  431. if ($headerAnalysis['is_ajax']) {
  432. return 'protobuf_json';
  433. }
  434. // 默认优先使用post数据
  435. return 'post';
  436. }
  437. /**
  438. * 准备 post 数据
  439. *
  440. * @param string $postData base64 编码的原始请求数据
  441. * @param string|null $contentType
  442. * @param array $headerAnalysis
  443. * @return array
  444. */
  445. protected function preparePostData(string $postData, ?string $contentType, array $headerAnalysis = []): array
  446. {
  447. // 解码 base64 数据
  448. $rawData = base64_decode($postData);
  449. // 根据请求头分析和 Content-Type 判断数据类型
  450. $dataType = $this->determineDataType($contentType, $headerAnalysis, $rawData);
  451. $result = [
  452. 'source' => 'post',
  453. 'type' => $dataType,
  454. 'data' => $rawData,
  455. 'analysis' => [
  456. 'original_size' => strlen($postData),
  457. 'decoded_size' => strlen($rawData),
  458. 'content_type' => $contentType,
  459. 'detected_type' => $dataType
  460. ]
  461. ];
  462. // 如果有头部分析信息,添加到结果中
  463. if (!empty($headerAnalysis)) {
  464. $result['header_context'] = [
  465. 'is_ajax' => $headerAnalysis['is_ajax'] ?? false,
  466. 'is_mobile' => $headerAnalysis['is_mobile'] ?? false,
  467. 'auth_type' => $headerAnalysis['auth_type'] ?? null,
  468. 'content_length' => $headerAnalysis['content_length'] ?? null
  469. ];
  470. }
  471. return $result;
  472. }
  473. /**
  474. * 确定数据类型
  475. *
  476. * @param string|null $contentType
  477. * @param array $headerAnalysis
  478. * @param string $rawData
  479. * @return string
  480. */
  481. protected function determineDataType(?string $contentType, array $headerAnalysis, string $rawData): string
  482. {
  483. // 优先根据Content-Type判断
  484. if ($contentType) {
  485. if (stripos($contentType, 'json') !== false) {
  486. return 'json';
  487. }
  488. if (stripos($contentType, 'protobuf') !== false ||
  489. stripos($contentType, 'octet-stream') !== false) {
  490. return 'protobuf';
  491. }
  492. }
  493. // 根据请求头分析判断
  494. if (!empty($headerAnalysis['is_ajax'])) {
  495. return 'json';
  496. }
  497. // 尝试解析数据内容判断
  498. if ($this->looksLikeJson($rawData)) {
  499. return 'json';
  500. }
  501. // 默认为protobuf
  502. return 'protobuf';
  503. }
  504. /**
  505. * 检查数据是否看起来像JSON
  506. *
  507. * @param string $data
  508. * @return bool
  509. */
  510. protected function looksLikeJson(string $data): bool
  511. {
  512. // 检查是否以JSON常见字符开始
  513. $trimmed = trim($data);
  514. if (empty($trimmed)) {
  515. return false;
  516. }
  517. $firstChar = $trimmed[0];
  518. if (in_array($firstChar, ['{', '[', '"'])) {
  519. // 尝试解析JSON
  520. json_decode($trimmed);
  521. return json_last_error() === JSON_ERROR_NONE;
  522. }
  523. return false;
  524. }
  525. /**
  526. * 准备 protobuf_json 数据
  527. *
  528. * @param string $protobufJson
  529. * @param array $headerAnalysis
  530. * @return array
  531. */
  532. protected function prepareProtobufJsonData(string $protobufJson, array $headerAnalysis = []): array
  533. {
  534. $result = [
  535. 'source' => 'protobuf_json',
  536. 'type' => 'json',
  537. 'data' => $protobufJson,
  538. 'analysis' => [
  539. 'data_size' => strlen($protobufJson),
  540. 'detected_type' => 'json'
  541. ]
  542. ];
  543. // 如果有头部分析信息,添加到结果中
  544. if (!empty($headerAnalysis)) {
  545. $result['header_context'] = [
  546. 'is_ajax' => $headerAnalysis['is_ajax'] ?? false,
  547. 'is_mobile' => $headerAnalysis['is_mobile'] ?? false,
  548. 'auth_type' => $headerAnalysis['auth_type'] ?? null
  549. ];
  550. }
  551. return $result;
  552. }
  553. /**
  554. * 显示请求数据信息
  555. *
  556. * @param array $requestData
  557. */
  558. protected function displayRequestDataInfo(array $requestData): void
  559. {
  560. $this->info("请求数据信息:");
  561. $this->line(" 数据源: {$requestData['source']}");
  562. $this->line(" 数据类型: {$requestData['type']}");
  563. // 显示分析信息
  564. if (isset($requestData['analysis'])) {
  565. $analysis = $requestData['analysis'];
  566. $this->line(" 数据分析:");
  567. if (isset($analysis['decoded_size'])) {
  568. $this->line(" 原始大小: {$analysis['original_size']} 字节");
  569. $this->line(" 解码后大小: {$analysis['decoded_size']} 字节");
  570. } elseif (isset($analysis['data_size'])) {
  571. $this->line(" 数据大小: {$analysis['data_size']} 字节");
  572. }
  573. if (isset($analysis['content_type'])) {
  574. $this->line(" Content-Type: {$analysis['content_type']}");
  575. }
  576. if (isset($analysis['detected_type'])) {
  577. $this->line(" 检测类型: {$analysis['detected_type']}");
  578. }
  579. }
  580. // 显示头部上下文信息
  581. if (isset($requestData['header_context'])) {
  582. $context = $requestData['header_context'];
  583. $contextInfo = [];
  584. if ($context['is_ajax']) {
  585. $contextInfo[] = 'AJAX请求';
  586. }
  587. if ($context['is_mobile']) {
  588. $contextInfo[] = '移动设备';
  589. }
  590. if ($context['auth_type']) {
  591. $contextInfo[] = "认证: {$context['auth_type']}";
  592. }
  593. if (!empty($contextInfo)) {
  594. $this->line(" 请求上下文: " . implode(', ', $contextInfo));
  595. }
  596. }
  597. // 显示数据预览
  598. $this->info("请求数据预览:");
  599. if ($requestData['type'] === 'json') {
  600. $jsonData = json_decode($requestData['data'], true);
  601. if ($jsonData !== null) {
  602. dump($jsonData);
  603. } else {
  604. $this->warn("数据类型标记为JSON但解析失败");
  605. $this->line("原始数据: " . substr($requestData['data'], 0, 200) . "...");
  606. }
  607. } else {
  608. $this->line(" 二进制数据长度: " . strlen($requestData['data']) . " 字节");
  609. $this->line(" Base64预览: " . substr(base64_encode($requestData['data']), 0, 100) . "...");
  610. // 尝试检测是否包含可读文本
  611. if (mb_check_encoding($requestData['data'], 'UTF-8')) {
  612. $preview = substr($requestData['data'], 0, 100);
  613. if (ctype_print($preview)) {
  614. $this->line(" 文本预览: {$preview}...");
  615. }
  616. }
  617. }
  618. }
  619. /**
  620. * 初始化 HTTP 客户端
  621. *
  622. * @param int $timeout
  623. */
  624. protected function initializeHttpClient(int $timeout): void
  625. {
  626. $this->baseUrl = env('UNITTEST_URL', 'http://localhost:8000');
  627. $this->info("目标地址: {$this->baseUrl}");
  628. $this->client = new Client([
  629. 'base_uri' => $this->baseUrl,
  630. 'timeout' => $timeout,
  631. 'http_errors' => false,
  632. 'verify' => false, // 禁用 SSL 验证
  633. ]);
  634. }
  635. /**
  636. * 发起请求
  637. *
  638. * @param string $requestData 请求数据
  639. * @param string|null $token 认证token
  640. * @param string $dataType 数据类型 (json|protobuf)
  641. * @return array|null
  642. */
  643. protected function makeRequest(string $requestData, ?string $token, string $dataType): ?array
  644. {
  645. try {
  646. // 根据数据类型设置不同的 headers
  647. if ($dataType === 'json') {
  648. $headers = [
  649. 'Content-Type' => 'application/json',
  650. 'Accept' => 'application/json'
  651. ];
  652. } else {
  653. // protobuf 二进制格式
  654. $headers = [
  655. 'Content-Type' => 'application/x-protobuf',
  656. 'Accept' => 'application/x-protobuf',
  657. 'Force-Json'=>'1'
  658. ];
  659. }
  660. if ($token) {
  661. $headers['token'] = $token;
  662. }
  663. Log::info('复现请求开始', [
  664. 'url' => $this->baseUrl . '/gameapi',
  665. 'headers' => $headers,
  666. 'body_length' => strlen($requestData),
  667. 'data_type' => $dataType
  668. ]);
  669. $response = $this->client->post('/gameapi', [
  670. 'body' => $requestData,
  671. 'headers' => $headers
  672. ]);
  673. $statusCode = $response->getStatusCode();
  674. $responseHeaders = $response->getHeaders();
  675. $responseBody = $response->getBody()->getContents();
  676. Log::info('复现请求完成', [
  677. 'status_code' => $statusCode,
  678. 'response_length' => strlen($responseBody),
  679. 'data_type' => $dataType
  680. ]);
  681. return [
  682. 'status_code' => $statusCode,
  683. 'headers' => $responseHeaders,
  684. 'body' => $responseBody
  685. ];
  686. } catch (\Exception $e) {
  687. $this->error("请求失败: " . $e->getMessage());
  688. Log::error('复现请求失败', [
  689. 'error' => $e->getMessage(),
  690. 'trace' => $e->getTraceAsString(),
  691. 'data_type' => $dataType
  692. ]);
  693. return null;
  694. }
  695. }
  696. /**
  697. * 显示响应内容
  698. *
  699. * @param array $response 响应数据(包含status_code、headers、body)
  700. */
  701. protected function displayResponse(array $response): void
  702. {
  703. $this->line("状态码: " . $response['status_code']);
  704. // 显示响应头信息
  705. $this->line("响应头:");
  706. foreach ($response['headers'] as $name => $values) {
  707. $value = is_array($values) ? implode(', ', $values) : $values;
  708. $this->line(" {$name}: {$value}");
  709. }
  710. // 从响应头中提取Content-Type
  711. $responseContentType = $this->extractResponseContentType($response['headers']);
  712. $this->line("检测到响应Content-Type: " . ($responseContentType ?: '未知'));
  713. // 根据响应Content-Type决定显示方式
  714. $responseBody = $response['body'];
  715. $bodyLength = strlen($responseBody);
  716. $this->line("响应体长度: {$bodyLength} 字节");
  717. if ($this->isJsonContentType($responseContentType)) {
  718. // JSON 响应
  719. $this->line("按JSON格式解析响应:");
  720. $jsonData = json_decode($responseBody, true);
  721. if ($jsonData !== null) {
  722. dump($jsonData);
  723. } else {
  724. $this->warn("响应Content-Type为JSON但解析失败");
  725. $this->displayRawContent($responseBody);
  726. }
  727. } elseif ($this->isProtobufContentType($responseContentType)) {
  728. // Protobuf 二进制响应
  729. $this->line("按Protobuf格式处理响应:");
  730. $this->line("Base64 编码: " . substr(base64_encode($responseBody), 0, 200) . "...");
  731. // 尝试解析为 JSON(某些情况下服务器可能返回 JSON)
  732. $jsonData = json_decode($responseBody, true);
  733. if ($jsonData !== null) {
  734. $this->line("响应可解析为 JSON:");
  735. dump($jsonData);
  736. } else {
  737. $this->line("响应为二进制 Protobuf 数据,无法直接显示");
  738. }
  739. } elseif ($this->isHtmlContentType($responseContentType)) {
  740. // HTML 响应
  741. $this->line("按HTML格式处理响应:");
  742. $this->displayHtmlContent($responseBody);
  743. } elseif ($this->isTextContentType($responseContentType)) {
  744. // 纯文本响应
  745. $this->line("按文本格式显示响应:");
  746. $this->line($responseBody);
  747. } else {
  748. // 未知类型,尝试智能检测
  749. $this->line("未知Content-Type,尝试智能检测:");
  750. $this->smartDisplayContent($responseBody);
  751. }
  752. }
  753. /**
  754. * 从响应头中提取Content-Type
  755. *
  756. * @param array $headers 响应头数组
  757. * @return string|null
  758. */
  759. protected function extractResponseContentType(array $headers): ?string
  760. {
  761. // 尝试不同的Content-Type键名(大小写不敏感)
  762. $contentTypeKeys = ['Content-Type', 'content-type', 'Content-type', 'CONTENT-TYPE'];
  763. foreach ($contentTypeKeys as $key) {
  764. if (isset($headers[$key])) {
  765. $value = $headers[$key];
  766. // 如果是数组,取第一个值
  767. if (is_array($value)) {
  768. $value = $value[0] ?? '';
  769. }
  770. // 只取分号前的部分(去掉charset等参数)
  771. return trim(explode(';', $value)[0]);
  772. }
  773. }
  774. return null;
  775. }
  776. /**
  777. * 判断是否为JSON内容类型
  778. *
  779. * @param string|null $contentType
  780. * @return bool
  781. */
  782. protected function isJsonContentType(?string $contentType): bool
  783. {
  784. if (!$contentType) {
  785. return false;
  786. }
  787. $jsonTypes = [
  788. 'application/json',
  789. 'text/json',
  790. 'application/ld+json'
  791. ];
  792. return in_array(strtolower($contentType), $jsonTypes);
  793. }
  794. /**
  795. * 判断是否为Protobuf内容类型
  796. *
  797. * @param string|null $contentType
  798. * @return bool
  799. */
  800. protected function isProtobufContentType(?string $contentType): bool
  801. {
  802. if (!$contentType) {
  803. return false;
  804. }
  805. $protobufTypes = [
  806. 'application/x-protobuf',
  807. 'application/protobuf',
  808. 'application/octet-stream'
  809. ];
  810. return in_array(strtolower($contentType), $protobufTypes);
  811. }
  812. /**
  813. * 判断是否为HTML内容类型
  814. *
  815. * @param string|null $contentType
  816. * @return bool
  817. */
  818. protected function isHtmlContentType(?string $contentType): bool
  819. {
  820. if (!$contentType) {
  821. return false;
  822. }
  823. $htmlTypes = [
  824. 'text/html',
  825. 'application/xhtml+xml'
  826. ];
  827. return in_array(strtolower($contentType), $htmlTypes);
  828. }
  829. /**
  830. * 判断是否为文本内容类型
  831. *
  832. * @param string|null $contentType
  833. * @return bool
  834. */
  835. protected function isTextContentType(?string $contentType): bool
  836. {
  837. if (!$contentType) {
  838. return false;
  839. }
  840. return str_starts_with(strtolower($contentType), 'text/');
  841. }
  842. /**
  843. * 显示HTML内容
  844. *
  845. * @param string $content
  846. */
  847. protected function displayHtmlContent(string $content): void
  848. {
  849. // 检查是否包含错误页面标识
  850. if (str_contains($content, '<title>') && str_contains($content, '</title>')) {
  851. preg_match('/<title>(.*?)<\/title>/i', $content, $matches);
  852. $title = $matches[1] ?? '未知';
  853. $this->line("HTML页面标题: {$title}");
  854. }
  855. // 显示部分内容
  856. $preview = substr(strip_tags($content), 0, 500);
  857. $this->line("HTML内容预览: " . $preview . "...");
  858. // 检查是否为错误页面
  859. if (str_contains($content, 'error') || str_contains($content, 'Error') || str_contains($content, 'exception')) {
  860. $this->warn("检测到可能的错误页面");
  861. }
  862. }
  863. /**
  864. * 显示原始内容
  865. *
  866. * @param string $content
  867. */
  868. protected function displayRawContent(string $content): void
  869. {
  870. $this->line("原始响应内容:");
  871. if (strlen($content) > 1000) {
  872. $this->line(substr($content, 0, 1000) . "...");
  873. $this->line("(内容过长,已截断显示)");
  874. } else {
  875. $this->line($content);
  876. }
  877. }
  878. /**
  879. * 智能检测并显示内容
  880. *
  881. * @param string $content
  882. */
  883. protected function smartDisplayContent(string $content): void
  884. {
  885. // 尝试JSON解析
  886. $jsonData = json_decode($content, true);
  887. if ($jsonData !== null) {
  888. $this->line("内容可解析为JSON:");
  889. dump($jsonData);
  890. return;
  891. }
  892. // 检查是否为HTML
  893. if (str_contains($content, '<html') || str_contains($content, '<!DOCTYPE')) {
  894. $this->line("检测到HTML内容:");
  895. $this->displayHtmlContent($content);
  896. return;
  897. }
  898. // 检查是否为二进制数据
  899. if (!mb_check_encoding($content, 'UTF-8')) {
  900. $this->line("检测到二进制数据:");
  901. $this->line("数据长度: " . strlen($content) . " 字节");
  902. $this->line("Base64 编码: " . substr(base64_encode($content), 0, 200) . "...");
  903. return;
  904. }
  905. // 默认按文本显示
  906. $this->line("按文本格式显示:");
  907. $this->displayRawContent($content);
  908. }
  909. /**
  910. * 清空当前日志文件
  911. */
  912. protected function clearLogFiles(): void
  913. {
  914. Logger::clear_log();
  915. Logger::debug('旧的日志已经清理');
  916. }
  917. }