router.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. from __future__ import annotations
  2. import json
  3. from concurrent.futures import ThreadPoolExecutor, as_completed
  4. from pathlib import Path
  5. from fastapi import APIRouter, HTTPException
  6. from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
  7. from ..repositories import BuildNotFound
  8. from ..sanitizer import sanitize
  9. from .service import ModuleAuditService, ModuleNotFound
  10. router = APIRouter(tags=["module-audit"])
  11. service = ModuleAuditService()
  12. _STATIC = Path(__file__).resolve().parent / "static"
  13. _ASSETS = {
  14. "app.js": ("application/javascript; charset=utf-8", "app.js"),
  15. "styles.css": ("text/css; charset=utf-8", "styles.css"),
  16. }
  17. @router.get("/audit/{script_build_id}", response_class=HTMLResponse)
  18. def audit_page(script_build_id: int):
  19. template = (_STATIC / "index.html").read_text(encoding="utf-8")
  20. return HTMLResponse(template.replace("__RUN_ID__", str(script_build_id)))
  21. @router.get("/audit-assets/{asset_name}")
  22. def audit_asset(asset_name: str):
  23. asset = _ASSETS.get(asset_name)
  24. if asset is None:
  25. raise HTTPException(status_code=404, detail="未找到审计页面资源")
  26. media_type, filename = asset
  27. return FileResponse(_STATIC / filename, media_type=media_type)
  28. @router.get("/api/script-builds/{script_build_id}/module-audit")
  29. def module_audit_index(script_build_id: int):
  30. try:
  31. return service.index(script_build_id)
  32. except BuildNotFound as exc:
  33. raise HTTPException(status_code=404, detail=str(exc)) from exc
  34. except Exception as exc:
  35. raise HTTPException(
  36. status_code=503,
  37. detail=f"读取模块数据对照失败:{type(exc).__name__}: {sanitize(str(exc))}",
  38. ) from exc
  39. @router.get("/api/script-builds/{script_build_id}/module-audit-stream")
  40. def module_audit_stream(script_build_id: int):
  41. """Stream every module automatically; this is full loading, not click-to-load."""
  42. try:
  43. index = service.index(script_build_id)
  44. except BuildNotFound as exc:
  45. raise HTTPException(status_code=404, detail=str(exc)) from exc
  46. except Exception as exc:
  47. raise HTTPException(
  48. status_code=503,
  49. detail=f"读取模块数据对照失败:{type(exc).__name__}: {sanitize(str(exc))}",
  50. ) from exc
  51. def rows():
  52. yield _ndjson({"type": "index", "data": index})
  53. modules = index.get("modules") or []
  54. with ThreadPoolExecutor(max_workers=4, thread_name_prefix="module-audit") as pool:
  55. futures = {
  56. pool.submit(service.detail, script_build_id, str(module["id"])): str(module["id"])
  57. for module in modules
  58. }
  59. for future in as_completed(futures):
  60. module_id = futures[future]
  61. try:
  62. yield _ndjson(
  63. {
  64. "type": "detail",
  65. "moduleId": module_id,
  66. "data": future.result(),
  67. }
  68. )
  69. except Exception as exc: # One bad module must not block the full audit.
  70. yield _ndjson(
  71. {
  72. "type": "error",
  73. "moduleId": module_id,
  74. "message": f"{type(exc).__name__}: {sanitize(str(exc))}",
  75. }
  76. )
  77. yield _ndjson({"type": "complete", "moduleCount": len(modules)})
  78. return StreamingResponse(
  79. rows(),
  80. media_type="application/x-ndjson; charset=utf-8",
  81. headers={
  82. "Cache-Control": "no-store",
  83. "X-Content-Type-Options": "nosniff",
  84. },
  85. )
  86. @router.get("/api/script-builds/{script_build_id}/module-audit/{module_id:path}")
  87. def module_audit_detail(script_build_id: int, module_id: str):
  88. try:
  89. return service.detail(script_build_id, module_id)
  90. except BuildNotFound as exc:
  91. raise HTTPException(status_code=404, detail=str(exc)) from exc
  92. except ModuleNotFound as exc:
  93. raise HTTPException(status_code=404, detail=f"未找到模块 {module_id}") from exc
  94. except Exception as exc:
  95. raise HTTPException(
  96. status_code=503,
  97. detail=f"读取模块完整数据失败:{type(exc).__name__}: {sanitize(str(exc))}",
  98. ) from exc
  99. def _ndjson(value: dict) -> bytes:
  100. return (json.dumps(value, ensure_ascii=False, separators=(",", ":")) + "\n").encode(
  101. "utf-8"
  102. )