TechnicalRetryDetails.tsx 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. import type { TechnicalRetryDetail } from "@/lib/flow-ledger/types";
  2. export function TechnicalRetryDetails({ detail, compact = false }: { detail?: TechnicalRetryDetail | null; compact?: boolean }) {
  3. if (!detail) return null;
  4. const rows = [
  5. ["接口/阶段", detail.stageLabel],
  6. ["简短原因", detail.briefReason],
  7. ["错误类型", [detail.failureLabel, detail.failureType].filter(Boolean).join(" / ")],
  8. ["异常", detail.exceptionType || "无"],
  9. ["HTTP", detail.httpStatusCode ? String(detail.httpStatusCode) : "无"],
  10. ["原始错误", detail.errorMessage || "无"],
  11. ["视频源", detail.videoSource || "未进入视频处理"],
  12. ["下载", formatSeconds(detail.timings.download_seconds)],
  13. ["ffmpeg", formatSeconds(detail.timings.ffmpeg_seconds)],
  14. ["Gemini", formatSeconds(detail.timings.gemini_seconds)],
  15. ["原视频", formatMb(detail.sizes.downloaded_mb)],
  16. ["压缩后", formatMb(detail.sizes.compressed_mb)],
  17. ["OSS", ossText(detail)]
  18. ].filter(([, value]) => value && value !== "无");
  19. return (
  20. <details className={`technical-detail ${compact ? "compact" : ""}`}>
  21. <summary>技术详情</summary>
  22. <div className="technical-detail-body">
  23. {rows.map(([label, value]) => (
  24. <div className="technical-detail-row" key={label}>
  25. <span>{label}</span>
  26. <b>{value}</b>
  27. </div>
  28. ))}
  29. {detail.attempts.length ? (
  30. <div className="technical-attempts">
  31. <span>Gemini attempts</span>
  32. {detail.attempts.map((attempt, index) => (
  33. <div className="technical-attempt" key={`${attempt.attempt || index}-${index}`}>
  34. #{attempt.attempt || index + 1} · {attempt.status || "failed"} · {formatSeconds(attempt.durationSeconds)}
  35. {attempt.failureType ? ` · ${attempt.failureType}` : ""}
  36. {attempt.exceptionType ? ` · ${attempt.exceptionType}` : ""}
  37. {attempt.httpStatusCode ? ` · HTTP ${attempt.httpStatusCode}` : ""}
  38. </div>
  39. ))}
  40. </div>
  41. ) : null}
  42. </div>
  43. </details>
  44. );
  45. }
  46. function formatSeconds(value?: number | null): string {
  47. if (value === null || value === undefined || Number.isNaN(value)) return "无";
  48. return `${value}s`;
  49. }
  50. function formatMb(value?: number | null): string {
  51. if (value === null || value === undefined || Number.isNaN(value)) return "无";
  52. return `${value}MB`;
  53. }
  54. function ossText(detail: TechnicalRetryDetail): string {
  55. const oss = detail.oss || {};
  56. const status = typeof oss.status === "string" ? oss.status : "";
  57. const error = typeof oss.last_error === "string" ? oss.last_error : "";
  58. const duration = typeof oss.duration_seconds === "number" ? ` · ${oss.duration_seconds}s` : "";
  59. return [status, error].filter(Boolean).join(" / ") + duration;
  60. }