@@ -503,73 +503,95 @@ async def _on_step(step: int, status: str, desc: str) -> None:
503503 on_step = _on_step ,
504504 )
505505
506- try :
507- await ws_manager .send_log (f"蓝本测试开始: { blueprint .app_name } " )
508- report = await runner .run (blueprint )
506+ # 使用 StreamingResponse + 心跳保活,防止 HTTP 连接在长测试中断开
507+ # 每 20 秒发送一个空格作为心跳,测试完成后发送完整 JSON 结果
508+ import asyncio as _asyncio
509+ from fastapi .responses import StreamingResponse as _StreamingResponse
509510
510- # v1.3:保存记忆(Bug模式提取)
511- if memory_store and report .bugs :
512- try :
513- from src .memory .compressor import MemoryCompressor
514- compressor = MemoryCompressor (memory_store )
515- compressor .extract_from_report (report )
516- except Exception as mem_err :
517- logger .warning ("蓝本测试记忆提取失败: {}" , mem_err )
511+ result_holder : dict = {}
518512
519- from src .api .models import StepDetail , BugDetail
520- stopped = test_controller .was_stopped
521- if stopped :
522- test_controller .reset ()
523- pass_rate = report .passed_steps / report .total_steps * 100 if report .total_steps > 0 else 0
524- response = TestReportResponse (
525- test_name = report .test_name ,
526- url = report .url ,
527- total_steps = report .total_steps ,
528- passed_steps = report .passed_steps ,
529- failed_steps = report .failed_steps ,
530- bug_count = len (report .bugs ),
531- pass_rate = pass_rate ,
532- duration_seconds = report .duration_seconds ,
533- report_markdown = report .report_markdown ,
534- stopped = stopped ,
535- steps = [
536- StepDetail (
537- step = r .step ,
538- action = r .action .value if hasattr (r .action , 'value' ) else str (r .action ),
539- description = r .description ,
540- status = r .status .value if hasattr (r .status , 'value' ) else str (r .status ),
541- duration_seconds = r .duration_seconds ,
542- error_message = r .error_message ,
543- screenshot_path = r .screenshot_path ,
544- )
545- for r in report .step_results
546- ],
547- bugs = [
548- BugDetail (
549- severity = b .severity .value if hasattr (b .severity , 'value' ) else str (b .severity ),
550- title = b .title ,
551- description = b .description ,
552- category = b .category ,
553- location = b .location ,
554- step_number = b .step_number ,
555- screenshot_path = b .screenshot_path ,
556- )
557- for b in report .bugs
558- ],
559- )
560-
561- # 通过 WebSocket 推送完整报告(作为 HTTP response 的后备)
562- # 即使 HTTP 连接中途断开,WebView 也能通过 WS 拿到结果
513+ async def _run_test ():
563514 try :
564- report_dict = response .model_dump ()
565- except AttributeError :
566- report_dict = response .dict ()
567- await ws_manager .send_test_done (pass_rate , len (report .bugs ), full_report = report_dict )
515+ await ws_manager .send_log (f"蓝本测试开始: { blueprint .app_name } " )
516+ report = await runner .run (blueprint )
568517
569- return response
570- except Exception as e :
571- logger .error ("蓝本测试执行失败: {}" , e )
572- raise HTTPException (status_code = 500 , detail = f"蓝本测试执行失败: { e } " )
518+ if memory_store and report .bugs :
519+ try :
520+ from src .memory .compressor import MemoryCompressor
521+ compressor = MemoryCompressor (memory_store )
522+ compressor .extract_from_report (report )
523+ except Exception as mem_err :
524+ logger .warning ("蓝本测试记忆提取失败: {}" , mem_err )
525+
526+ from src .api .models import StepDetail , BugDetail
527+ stopped = test_controller .was_stopped
528+ if stopped :
529+ test_controller .reset ()
530+ pass_rate = report .passed_steps / report .total_steps * 100 if report .total_steps > 0 else 0
531+ response = TestReportResponse (
532+ test_name = report .test_name ,
533+ url = report .url ,
534+ total_steps = report .total_steps ,
535+ passed_steps = report .passed_steps ,
536+ failed_steps = report .failed_steps ,
537+ bug_count = len (report .bugs ),
538+ pass_rate = pass_rate ,
539+ duration_seconds = report .duration_seconds ,
540+ report_markdown = report .report_markdown ,
541+ stopped = stopped ,
542+ steps = [
543+ StepDetail (
544+ step = r .step ,
545+ action = r .action .value if hasattr (r .action , 'value' ) else str (r .action ),
546+ description = r .description ,
547+ status = r .status .value if hasattr (r .status , 'value' ) else str (r .status ),
548+ duration_seconds = r .duration_seconds ,
549+ error_message = r .error_message ,
550+ screenshot_path = r .screenshot_path ,
551+ )
552+ for r in report .step_results
553+ ],
554+ bugs = [
555+ BugDetail (
556+ severity = b .severity .value if hasattr (b .severity , 'value' ) else str (b .severity ),
557+ title = b .title ,
558+ description = b .description ,
559+ category = b .category ,
560+ location = b .location ,
561+ step_number = b .step_number ,
562+ screenshot_path = b .screenshot_path ,
563+ )
564+ for b in report .bugs
565+ ],
566+ )
567+ try :
568+ report_dict = response .model_dump ()
569+ except AttributeError :
570+ report_dict = response .dict ()
571+ await ws_manager .send_test_done (pass_rate , len (report .bugs ), full_report = report_dict )
572+ result_holder ["data" ] = report_dict
573+ except Exception as e :
574+ logger .error ("蓝本测试执行失败: {}" , e )
575+ result_holder ["error" ] = str (e )
576+
577+ async def _stream_with_heartbeat ():
578+ task = _asyncio .create_task (_run_test ())
579+ while not task .done ():
580+ await _asyncio .sleep (20 )
581+ if not task .done ():
582+ yield b" " # 心跳空格,保持HTTP连接活跃
583+ await task # 确保异常被传播
584+ if "error" in result_holder :
585+ import json as _json
586+ yield _json .dumps ({"detail" : result_holder ["error" ]}).encode ("utf-8" )
587+ else :
588+ import json as _json
589+ yield _json .dumps (result_holder .get ("data" , {})).encode ("utf-8" )
590+
591+ return _StreamingResponse (
592+ _stream_with_heartbeat (),
593+ media_type = "application/json" ,
594+ )
573595
574596 @router .post ("/test/mobile-blueprint" , response_model = TestReportResponse , tags = ["测试" ])
575597 async def run_mobile_blueprint_test (req : RunMobileBlueprintRequest ) -> TestReportResponse :
0 commit comments