useExecutionView.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. "use client";
  2. import { useCallback, useEffect, useRef, useState } from "react";
  3. import { errorMessage, getExecutionView, listScriptBuilds } from "@/lib/api";
  4. import { isTerminalStatus, type BuildSummary, type ExecutionViewV8 } from "@/lib/types";
  5. const RUN_QUERY_KEY = "run";
  6. const LAST_RUN_STORAGE_KEY = "script-build:last-run";
  7. export function initialBuildId(builds: BuildSummary[], search: string, storedId?: string | null) {
  8. const queryId = new URLSearchParams(search).get(RUN_QUERY_KEY)?.trim();
  9. if (queryId) return queryId;
  10. const remembered = storedId?.trim();
  11. if (remembered && builds.some((build) => build.id === remembered)) return remembered;
  12. return builds[0]?.id || "";
  13. }
  14. export function useExecutionView() {
  15. const [builds, setBuilds] = useState<BuildSummary[]>([]);
  16. const [selectedId, setSelectedId] = useState("");
  17. const [view, setView] = useState<ExecutionViewV8 | null>(null);
  18. const [loading, setLoading] = useState(true);
  19. const [refreshing, setRefreshing] = useState(false);
  20. const [error, setError] = useState<string | null>(null);
  21. const generation = useRef(0);
  22. const loadView = useCallback(async (id: string, signal?: AbortSignal, quiet = false) => {
  23. const requestId = ++generation.current;
  24. quiet ? setRefreshing(true) : setLoading(true);
  25. try {
  26. if (!id) throw new Error("请输入真实 Run ID");
  27. const data = await getExecutionView(id, signal);
  28. if (requestId !== generation.current) return;
  29. setView(data);
  30. setError(null);
  31. const loadedId = String(data.header.id);
  32. setSelectedId(loadedId);
  33. rememberBuildId(loadedId);
  34. } catch (requestError) {
  35. if (signal?.aborted || requestId !== generation.current) return;
  36. if (quiet && view) {
  37. setError(`刷新失败,保留 ${view.capturedAt} 的快照:${errorMessage(requestError)}`);
  38. } else {
  39. setView(null);
  40. setError(`无法读取构建 #${id}:${errorMessage(requestError)}`);
  41. }
  42. } finally {
  43. if (requestId === generation.current) { setLoading(false); setRefreshing(false); }
  44. }
  45. }, [view]);
  46. useEffect(() => {
  47. const controller = new AbortController();
  48. void listScriptBuilds(controller.signal)
  49. .then((items) => {
  50. setBuilds(items);
  51. const preferred = initialBuildId(items, window.location.search, readRememberedBuildId());
  52. setSelectedId(preferred);
  53. if (!preferred) throw new Error("当前没有可读取的真实构建记录");
  54. return loadView(preferred, controller.signal);
  55. })
  56. .catch((listError) => { if (!controller.signal.aborted) { setView(null); setLoading(false); setError(`无法读取真实构建列表:${errorMessage(listError)}`); } });
  57. return () => controller.abort();
  58. }, []);
  59. useEffect(() => {
  60. if (!view || isTerminalStatus(view.header.status)) return;
  61. const timer = window.setTimeout(() => void loadView(String(view.header.id), undefined, true), 3000);
  62. return () => window.clearTimeout(timer);
  63. }, [view?.capturedAt, view?.header.id, view?.header.status, loadView]);
  64. const selectBuild = useCallback((id: string) => {
  65. const normalized = id.trim();
  66. if (!normalized) return;
  67. setSelectedId(normalized); setView(null); setError(null);
  68. void loadView(normalized);
  69. }, [loadView]);
  70. return { builds, selectedId, view, loading, refreshing, error, selectBuild };
  71. }
  72. function readRememberedBuildId() {
  73. try {
  74. return window.localStorage.getItem(LAST_RUN_STORAGE_KEY);
  75. } catch {
  76. return null;
  77. }
  78. }
  79. function rememberBuildId(id: string) {
  80. try {
  81. window.localStorage.setItem(LAST_RUN_STORAGE_KEY, id);
  82. } catch {
  83. // URL persistence still works when storage is unavailable.
  84. }
  85. const url = new URL(window.location.href);
  86. if (url.searchParams.get(RUN_QUERY_KEY) === id) return;
  87. url.searchParams.set(RUN_QUERY_KEY, id);
  88. window.history.replaceState(window.history.state, "", url);
  89. }