"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import useSWR from "swr"; import { toast } from "sonner"; import type { AgentRunView } from "@/lib/ai/canvas-agent-runtime/protocol"; export type CanvasAgentRunOptions = { enabled: boolean; chatId: string; projectId: string; busy: boolean; onResults: (projectId: string, nodeIds: string[]) => void; onRunChange?: (run: AgentRunView | null) => void; }; export function useCanvasAgentRun(props: CanvasAgentRunOptions) { const [sending, setSending] = useState(false); const [stopping, setStopping] = useState(false); const sendingRef = useRef(false); const scope = `${props.chatId}:${props.projectId}`; const latest = useRef({ props, scope }); latest.current = { props, scope }; const resultSignature = useRef(""); const key = props.enabled ? `/api/canvas/agent-runs?${new URLSearchParams({ chatId: props.chatId, projectId: props.projectId })}` : null; const { data, mutate } = useSWR<{ run: AgentRunView | null }>(key, async (url: string) => { const response = await fetch(url, { cache: "no-store" }); if (!response.ok) return { run: null }; return response.json(); }, { refreshInterval: latest => latest?.run?.status === "running" || props.busy ? 2000 : 0, revalidateOnFocus: true, shouldRetryOnError: false }); const run = props.enabled && data?.run?.chatId === props.chatId && data.run.projectId === props.projectId ? data.run : null; useEffect(() => { latest.current.props.onRunChange?.(run); }, [run]); useEffect(() => { setSending(false); sendingRef.current = false; setStopping(false); resultSignature.current = ""; }, [scope]); useEffect(() => { if (!props.enabled) return; const handler = (event: Event) => { const update = (event as CustomEvent).detail; if (update?.chatId === props.chatId && update?.projectId === props.projectId) void mutate({ run: update }, false); }; window.addEventListener("canvas-agent-run:update", handler); return () => window.removeEventListener("canvas-agent-run:update", handler); }, [props.enabled, props.chatId, props.projectId, mutate]); useEffect(() => { if (!run?.resultNodeIds.length) return; const signature = `${run.id}:${run.resultNodeIds.join(",")}:${run.status}`; if (signature === resultSignature.current) return; resultSignature.current = signature; latest.current.props.onResults(run.projectId, run.resultNodeIds); }, [run]); const control = useCallback(async (action: "steer" | "cancel", text?: string) => { if (!run || sendingRef.current) return false; sendingRef.current = true; const startedScope = scope; setSending(true); try { const response = await fetch("/api/canvas/agent-runs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, chatId: props.chatId, projectId: props.projectId, runId: run.id, ...(action === "steer" ? { inputId: crypto.randomUUID(), text: text?.trim() } : {}) }) }); const result = await response.json(); if (!response.ok) throw new Error(result.error || "操作失败,请重试"); if (startedScope !== latest.current.scope) return false; await mutate(result, false); if (action === "cancel") setStopping(true); toast.message(action === "steer" ? "补充要求已收到,将在下一步应用" : "正在停止后续步骤,已完成的结果会保留"); return true; } catch (error) { toast.error(error instanceof Error ? error.message : "操作失败"); return false; } finally { if (startedScope === latest.current.scope) { sendingRef.current = false; setSending(false); } } }, [run, sending, scope, props.chatId, props.projectId, mutate]); return { run, sending, stopPending: stopping || Boolean(run?.cancelRequested), control }; }