// Logs slice: per-node-run log fetch + display state. // toggleLog owns the open/closed toggle and lazily loads content from the // signed URL on first open. import type { StateCreator } from "zustand"; import { requireApi } from "./helpers"; import type { LogState } from "./types"; import type { SchedulesStore } from "./useSchedulesStore"; // ---- State ---- export type LogsSliceState = { openLogNodeRunIds: Set; logStates: Record; }; // ---- Actions ---- export type LogsSliceActions = { // pure setters setOpenLogNodeRunIds: ( value: Set | ((current: Set) => Set), ) => void; setLogStates: ( updater: (current: Record) => Record, ) => void; // mutations toggleLog: (runId: string, nodeRunId: string) => Promise; }; // `SchedulesStore` is the combined state+actions of all slices, so `get()` // returns a fully typed snapshot — no `any` propagation. // eslint-disable-next-line @typescript-eslint/no-explicit-any export const createLogsSlice: StateCreator = (set, get) => ({ // ---- initial state ---- openLogNodeRunIds: new Set(), logStates: {}, // ---- pure setters ---- setOpenLogNodeRunIds: (value) => set((s: any) => ({ openLogNodeRunIds: typeof value === "function" ? (value as (current: Set) => Set)( s.openLogNodeRunIds, ) : value, })), setLogStates: (updater) => set((s: any) => ({ logStates: updater(s.logStates) })), // ---- mutations ---- toggleLog: async (runId, nodeRunId) => { const api = requireApi(); const state = get(); const open = state.openLogNodeRunIds; if (open.has(nodeRunId)) { const next = new Set(open); next.delete(nodeRunId); set({ openLogNodeRunIds: next }); return; } const next = new Set(open); next.add(nodeRunId); set({ openLogNodeRunIds: next }); if (state.logStates[nodeRunId]?.content) return; set((s: any) => ({ logStates: { ...s.logStates, [nodeRunId]: { loading: true, content: null, fileName: null, error: null, }, }, })); try { const artifacts = await api.getScheduleNodeRunArtifacts(runId, nodeRunId); if (!artifacts.log) throw new Error("本次节点执行没有可用日志"); const response = await fetch(artifacts.log.url, { credentials: "same-origin", cache: "no-store", }); if (!response.ok) { throw new Error(`日志读取失败(HTTP ${response.status})`); } const content = await response.text(); set((s: any) => ({ logStates: { ...s.logStates, [nodeRunId]: { loading: false, content, fileName: artifacts.log?.file_name ?? null, error: null, }, }, })); } catch (error) { set((s: any) => ({ logStates: { ...s.logStates, [nodeRunId]: { loading: false, content: null, fileName: null, error: error instanceof Error ? error.message : "日志读取失败", }, }, })); } }, });