"""网页测试工作台;与当前聊天接口使用相同的本地用户归属约定。""" import asyncio from typing import Any, Literal from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile from fastapi.responses import FileResponse from pydantic import BaseModel, ConfigDict, Field, ValidationError import config from db import test_lab_store from runtime.business_tasks import get_controller from runtime.test_lab import files, service from runtime.test_lab.spreadsheets import preview_workbook from runtime.test_lab.main_fan_simulation import MainFanSimulation def enabled(): if not config.get_bool('TEST_LAB_ENABLED',True): raise HTTPException(404,'测试工作台未启用') router=APIRouter(prefix='/test-lab',tags=['智能体测试'],dependencies=[Depends(enabled)]) class TestRequest(BaseModel): model_config=ConfigDict(extra='forbid') agent_name:str=Field(min_length=1,max_length=64) scenario_id:str=Field(default='custom',max_length=64) label:str=Field(default='自定义测试',min_length=1,max_length=128) input:dict[str,Any] asset_ids:list[str]=Field(default_factory=list,max_length=20) input_asset_id:str|None=None execution_mode:Literal['pipeline','agent']='agent' request_id:str|None=Field(default=None,min_length=1,max_length=128) main_fan_simulation:MainFanSimulation|None=None def bad_request(exc): if isinstance(exc,ValidationError): return HTTPException(422,exc.errors(include_input=False,include_context=False)) return HTTPException(400,str(exc)) @router.get('/catalog') async def catalog(user_name:str=Query('default',min_length=1,max_length=128)): return await asyncio.to_thread(service.catalog,user_name) @router.get('/presets/{agent}/{identifier}') async def preset(agent:str,identifier:str): try: return await asyncio.to_thread(service.get_preset,agent,identifier) except ValueError as exc: raise HTTPException(404,str(exc)) from None @router.post('/assets',status_code=201) async def upload(file:UploadFile=File(...),user_name:str=Query('default',min_length=1,max_length=128)): try: return await files.upload(file,user_name) except (ValueError,UnicodeError) as exc: raise bad_request(exc) from None @router.get('/assets/{identifier}/content') async def asset_content(identifier:str,user_name:str='default',download:bool=False): try: path,description=await asyncio.to_thread(files.owned_file,identifier,user_name) except ValueError as exc: raise HTTPException(404,str(exc)) from None return FileResponse(path,media_type=files.media_type(path),filename=description['filename'], content_disposition_type='attachment' if download else 'inline',headers={'X-Content-Type-Options':'nosniff'}) @router.get('/assets/{identifier}/preview') async def asset_preview(identifier:str,user_name:str='default'): try: path,description=await asyncio.to_thread(files.owned_file,identifier,user_name) if description['kind']=='json': return {'kind':'json','content':await asyncio.to_thread(files.json_value,path)} if description['kind']=='excel': return {'kind':'excel',**await asyncio.to_thread(preview_workbook,path)} return description except ValueError as exc: raise bad_request(exc) from None @router.post('/runs',status_code=202) async def start(payload:TestRequest,user_name:str=Query('default',min_length=1,max_length=128)): try: return await asyncio.to_thread(service.start_run,payload,user_name) except ValueError as exc: raise bad_request(exc) from None except RuntimeError as exc: raise HTTPException(503,str(exc)) from None @router.get('/runs') async def runs(user_name:str='default',limit:int=Query(50,ge=1,le=200)): rows=await asyncio.to_thread(test_lab_store.runs,user_name,limit) return {'items':[{k:v for k,v in r.items() if k!='request_json'} for r in rows]} @router.get('/runs/{identifier}') async def run(identifier:str,user_name:str='default'): try: return await asyncio.to_thread(service.public_run,identifier,user_name) except ValueError as exc: raise HTTPException(404,str(exc)) from None @router.post('/runs/{identifier}/cancel') async def cancel(identifier:str,user_name:str='default'): try: await asyncio.to_thread(test_lab_store.run,identifier,user_name) await get_controller().cancel_job(identifier) return await asyncio.to_thread(service.public_run,identifier,user_name) except ValueError as exc: raise HTTPException(404,str(exc)) from None @router.get('/runs/{identifier}/artifacts/{key}/content') async def artifact_content(identifier:str,key:str,user_name:str='default',download:bool=True): try: item=await asyncio.to_thread(service.artifact,identifier,key,user_name) except ValueError as exc: raise HTTPException(404,str(exc)) from None return FileResponse(item['path'],media_type=files.media_type(item['path']),filename=item['filename'], content_disposition_type='attachment' if download else 'inline',headers={'X-Content-Type-Options':'nosniff'}) @router.get('/runs/{identifier}/artifacts/{key}/preview') async def artifact_preview(identifier:str,key:str,user_name:str='default'): try: item=await asyncio.to_thread(service.artifact,identifier,key,user_name) return {'kind':'excel','origin':item['origin'],**await asyncio.to_thread(preview_workbook,item['path'])} except ValueError as exc: raise bad_request(exc) from None