import { create } from "zustand"; import type { CanvasAgentOp } from "@/lib/canvas/canvas-agent-ops"; export type AgentChatRole = "user" | "assistant" | "system" | "tool" | "error"; export type AgentAttachment = { id: string; name: string; type: string; size: number; url: string; dataUrl: string }; export type AgentChatItem = { id: string; role: AgentChatRole; title?: string; text: string; meta?: string; detail?: unknown; attachments?: AgentAttachment[]; streamId?: string }; export type AgentEventLog = { id: string; time: string; title: string; text: string; raw?: unknown }; export type AgentPendingToolCall = { requestId: string; name: string; input?: { ops?: CanvasAgentOp[] } }; export type AgentThreadSummary = { id: string; preview: string; name?: string | null; cwd?: string; status?: string; source?: unknown; createdAt?: number; updatedAt?: number }; export type AgentPanelTab = "chat" | "setup" | "history" | "log"; const CONNECT_TIMEOUT_MS = 6000; let agentSource: EventSource | null = null; let connectTimer: ReturnType | null = null; type CanvasAgentStore = { width: number; url: string; token: string; connected: boolean; enabled: boolean; prompt: string; attachments: AgentAttachment[]; sending: boolean; waiting: boolean; messages: AgentChatItem[]; eventLogs: AgentEventLog[]; threads: AgentThreadSummary[]; activeThreadId: string; workspacePath: string; loadingThreads: boolean; activeTab: AgentPanelTab; confirmTools: boolean; activity: string; connectError: string; pendingTool: AgentPendingToolCall | null; setAgentState: (patch: Partial>) => void; connectAgent: () => void; disconnectAgent: (patch?: Partial>) => void; addMessage: (item: AgentChatItem) => void; addEventLog: (item: AgentEventLog) => void; clearEventLogs: () => void; }; export const useCanvasAgentStore = create((set, get) => ({ width: typeof window === "undefined" ? 440 : Number(localStorage.getItem("canvas-agent-panel-width")) || 440, url: typeof window === "undefined" ? "http://127.0.0.1:17371" : localStorage.getItem("canvas-agent-url") || "http://127.0.0.1:17371", token: typeof window === "undefined" ? "" : localStorage.getItem("canvas-agent-token") || "", connected: false, enabled: false, prompt: "", attachments: [], sending: false, waiting: false, messages: [], eventLogs: [], threads: [], activeThreadId: "", workspacePath: "", loadingThreads: false, activeTab: "setup", confirmTools: true, activity: "就绪", connectError: "", pendingTool: null, setAgentState: (patch) => set(patch), connectAgent: () => { const endpoint = get().url.trim().replace(/\/$/, ""); const token = get().token.trim(); if (!endpoint || !token) return set({ connectError: "请填写 Local URL 和 Connect token" }); try { const parsed = new URL(endpoint); if (!["http:", "https:"].includes(parsed.protocol)) throw new Error(); } catch { return set({ connectError: "Local URL 格式不正确" }); } get().disconnectAgent({ url: endpoint, token, enabled: true, activity: "连接中", connectError: "" }); localStorage.setItem("canvas-agent-url", endpoint); localStorage.setItem("canvas-agent-token", token); const clientId = typeof crypto === "undefined" ? `${Date.now()}` : crypto.randomUUID(); const source = new EventSource(`${endpoint}/events?token=${encodeURIComponent(token)}&clientId=${encodeURIComponent(clientId)}&role=status`); agentSource = source; connectTimer = setTimeout(() => { if (agentSource !== source) return; get().disconnectAgent({ activity: "连接超时", connectError: "连接超时:请确认 Canvas Agent 正在运行,并使用当前启动输出的 Connect token" }); }, CONNECT_TIMEOUT_MS); source.addEventListener("hello", () => { if (connectTimer) clearTimeout(connectTimer); connectTimer = null; set({ enabled: true, connected: true, activity: "已连接", connectError: "" }); }); source.onerror = () => { if (agentSource === source) get().disconnectAgent({ activity: "连接失败", connectError: "连接失败:请检查 Local URL、Connect token 或已绑定的网页 Origin" }); }; }, disconnectAgent: (patch = {}) => { agentSource?.close(); agentSource = null; if (connectTimer) clearTimeout(connectTimer); connectTimer = null; set({ enabled: false, connected: false, activity: "离线", ...patch }); }, addMessage: (item) => set((state) => ({ messages: [...state.messages.slice(-120), item] })), addEventLog: (item) => set((state) => ({ eventLogs: [...state.eventLogs.slice(-160), item] })), clearEventLogs: () => set({ eventLogs: [] }), }));