"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import copyToClipboard from "copy-to-clipboard"; import { Bot, Copy, Cpu, History, PanelRightClose, Plus, Settings2, Trash2, X } from "lucide-react"; import { Button, Modal, Segmented, Switch, Tooltip } from "antd"; import { motion } from "motion/react"; import { modelOptionName, resolveModelChannel, selectableModelsByCapability, useConfigStore, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store"; import { canvasThemes } from "@/lib/canvas-theme"; import { nanoid } from "nanoid"; import { requestToolResponse, type ResponseFunctionTool, type ResponseInputMessage, type ResponseToolCall } from "@/services/api/image"; import { imageToDataUrl } from "@/services/image-storage"; import { useAssetStore } from "@/stores/use-asset-store"; import { useThemeStore } from "@/stores/use-theme-store"; import { useUserStore } from "@/stores/use-user-store"; import { imageReferenceLabel } from "@/lib/image-reference-prompt"; import { DiaTextReveal } from "@/components/ui/dia-text-reveal"; import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select"; import { CanvasPromptLibrary } from "./canvas-prompt-library"; import { AgentChatComposer, AgentChatMessage, AgentModeSwitch, AgentPanelTabs, AgentWorkingMessage, type CanvasAgentChatMessage, type CanvasAgentMode } from "./canvas-agent-chat-ui"; import { CanvasLocalAgentPanel } from "./canvas-local-agent-panel"; import { NODE_DEFAULT_SIZE } from "../constants"; import { CanvasNodeType, type CanvasAssistantMessage, type CanvasAssistantReference, type CanvasAssistantSession, type CanvasNodeData } from "../types"; import { useCanvasAgentStore } from "../stores/use-canvas-agent-store"; import { summarizeCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "../utils/canvas-agent-ops"; export const CANVAS_AGENT_PANEL_MOTION_MS = 500; const PANEL_MOTION_SECONDS = CANVAS_AGENT_PANEL_MOTION_MS / 1000; const ONLINE_AGENT_MAX_STEPS = 4; const ONLINE_AGENT_PROMPT = "你是 Infinite Canvas 网页内置在线画布助手。当前画布 JSON 会随用户消息提供。首轮必须调用工具:只读问题调用 canvas_get_state,需要改动画布时调用和本地 Agent 一致的 infinite-canvas 工具。需要生成内容时直接调用 canvas_generate_text、canvas_generate_image、canvas_generate_video、canvas_generate_audio 或 canvas_create_generation_flow;需要精确批量操作时调用 canvas_apply_ops。不要输出 JSON ops,不要编造执行结果。工具参数涉及已有节点时必须使用当前画布 JSON 中真实存在的 id;缺少必要 id 或用户意图不明确时直接说明需要用户明确选择或说明,不要猜测。工具返回结果后,再根据真实结果回答用户。"; const JSON_RECORD_SCHEMA = { type: "object", additionalProperties: true }; const POSITION_SCHEMA = { type: "object", properties: { x: { type: "number" }, y: { type: "number" } }, required: ["x", "y"], additionalProperties: false }; const VIEWPORT_SCHEMA = { type: "object", properties: { x: { type: "number" }, y: { type: "number" }, k: { type: "number" } }, required: ["x", "y", "k"], additionalProperties: false }; const NODE_TYPE_SCHEMA = { type: "string", enum: ["image", "text", "config", "video", "audio"] }; const GENERATION_MODE_SCHEMA = { type: "string", enum: ["text", "image", "video", "audio"] }; const GENERATION_OPTION_PROPERTIES = { model: { type: "string" }, size: { type: "string" }, quality: { type: "string" }, count: { type: "number" }, seconds: { type: "string" }, vquality: { type: "string" }, generateAudio: { type: "string" }, watermark: { type: "string" }, audioVoice: { type: "string" }, audioFormat: { type: "string" }, audioSpeed: { type: "string" }, audioInstructions: { type: "string" }, }; const CANVAS_OP_SCHEMA = { type: "object", properties: { type: { type: "string", enum: ["add_node", "update_node", "delete_node", "delete_connections", "connect_nodes", "set_viewport", "select_nodes", "run_generation"] }, id: { type: "string" }, ids: { type: "array", items: { type: "string" } }, nodeType: NODE_TYPE_SCHEMA, title: { type: "string" }, x: { type: "number" }, y: { type: "number" }, width: { type: "number" }, height: { type: "number" }, position: POSITION_SCHEMA, metadata: JSON_RECORD_SCHEMA, patch: JSON_RECORD_SCHEMA, all: { type: "boolean" }, fromNodeId: { type: "string" }, toNodeId: { type: "string" }, viewport: VIEWPORT_SCHEMA, nodeId: { type: "string" }, mode: GENERATION_MODE_SCHEMA, prompt: { type: "string" }, }, required: ["type"], additionalProperties: false, }; const ONLINE_READ_TOOLS = new Set(["canvas_get_state", "canvas_get_selection", "canvas_export_snapshot"]); function toolDefinition(name: string, description: string, properties: Record, required: string[] = [], strict = false): ResponseFunctionTool { return { type: "function", function: { name, description, parameters: { type: "object", properties, required, additionalProperties: false }, strict } }; } function generationToolDefinition(name: string, description: string, mode?: "text" | "image" | "video" | "audio") { return toolDefinition( name, description, { prompt: { type: "string" }, title: { type: "string" }, x: { type: "number" }, y: { type: "number" }, referenceNodeIds: { type: "array", items: { type: "string" } }, ...(mode ? {} : { mode: GENERATION_MODE_SCHEMA }), autoRun: { type: "boolean" }, ...GENERATION_OPTION_PROPERTIES }, ["prompt"], ); } const ONLINE_AGENT_TOOLS: ResponseFunctionTool[] = [ toolDefinition("canvas_get_state", "读取当前网页画布的节点、连线、选区和视口。", {}), toolDefinition("canvas_get_selection", "读取当前网页画布选中的节点。", {}), toolDefinition("canvas_export_snapshot", "导出当前画布快照,用于理解布局。", {}), toolDefinition("canvas_apply_ops", "批量操作当前网页画布。ops 支持 add_node、update_node、delete_node、delete_connections、connect_nodes、set_viewport、select_nodes、run_generation。", { ops: { type: "array", items: CANVAS_OP_SCHEMA } }, ["ops"], false), toolDefinition("canvas_create_node", "创建任意类型节点:text、image、config、video、audio。适合创建占位图、媒体占位、配置节点或自定义 metadata 节点。", { nodeType: NODE_TYPE_SCHEMA, title: { type: "string" }, x: { type: "number" }, y: { type: "number" }, width: { type: "number" }, height: { type: "number" }, metadata: JSON_RECORD_SCHEMA }, ["nodeType"]), toolDefinition("canvas_create_text_node", "在当前画布创建单个文本节点。", { text: { type: "string" }, x: { type: "number" }, y: { type: "number" }, title: { type: "string" }, width: { type: "number" }, height: { type: "number" } }), toolDefinition("canvas_create_text_nodes", "批量创建文本节点,适合生成标题、段落、脚本、说明等内容块。", { items: { type: "array", minItems: 1, items: { type: "object", properties: { text: { type: "string" }, title: { type: "string" }, x: { type: "number" }, y: { type: "number" }, width: { type: "number" }, height: { type: "number" } }, required: ["text"], additionalProperties: false } }, x: { type: "number" }, y: { type: "number" }, gap: { type: "number" }, direction: { type: "string", enum: ["row", "column"] } }, ["items"]), toolDefinition("canvas_create_config_node", "创建生成配置节点,可指定 text/image/video/audio 模式和生成参数,可选择立即触发生成。", { prompt: { type: "string" }, mode: GENERATION_MODE_SCHEMA, title: { type: "string" }, x: { type: "number" }, y: { type: "number" }, width: { type: "number" }, height: { type: "number" }, autoRun: { type: "boolean" }, ...GENERATION_OPTION_PROPERTIES }), toolDefinition("canvas_create_image_prompt_flow", "创建提示词文本节点和图片生成配置节点,并自动连线,可选择立即触发生图。", { prompt: { type: "string" }, x: { type: "number" }, y: { type: "number" }, autoRun: { type: "boolean" }, ...GENERATION_OPTION_PROPERTIES }, ["prompt"]), generationToolDefinition("canvas_create_generation_flow", "创建通用生成流程:提示词文本节点、生成配置节点、参考节点连线,可用于文案、生图、视频或音频。"), generationToolDefinition("canvas_generate_text", "创建通用文本生成流程并立即触发生成。", "text"), generationToolDefinition("canvas_generate_image", "创建通用图片生成流程并立即触发生成。", "image"), generationToolDefinition("canvas_generate_video", "创建通用视频生成流程并立即触发生成。", "video"), generationToolDefinition("canvas_generate_audio", "创建通用音频生成流程并立即触发生成。", "audio"), toolDefinition("canvas_update_node", "更新节点基础字段或 metadata。", { id: { type: "string" }, patch: JSON_RECORD_SCHEMA, metadata: JSON_RECORD_SCHEMA }, ["id"]), toolDefinition("canvas_update_node_text", "更新文本节点内容和标题。", { id: { type: "string" }, text: { type: "string" }, title: { type: "string" } }, ["id", "text"]), toolDefinition("canvas_move_nodes", "移动一个或多个节点,支持绝对坐标或 dx/dy 偏移。", { items: { type: "array", minItems: 1, items: { type: "object", properties: { id: { type: "string" }, x: { type: "number" }, y: { type: "number" }, dx: { type: "number" }, dy: { type: "number" } }, required: ["id"], additionalProperties: false } } }, ["items"]), toolDefinition("canvas_resize_node", "调整节点尺寸。", { id: { type: "string" }, width: { type: "number" }, height: { type: "number" }, freeResize: { type: "boolean" } }, ["id", "width", "height"]), toolDefinition("canvas_delete_nodes", "删除指定节点及相关连线。", { ids: { type: "array", items: { type: "string" }, minItems: 1 } }, ["ids"]), toolDefinition("canvas_connect_nodes", "批量连接节点。", { connections: { type: "array", minItems: 1, items: { type: "object", properties: { fromNodeId: { type: "string" }, toNodeId: { type: "string" } }, required: ["fromNodeId", "toNodeId"], additionalProperties: false } } }, ["connections"]), toolDefinition("canvas_select_nodes", "设置当前选中节点。", { ids: { type: "array", items: { type: "string" } } }, ["ids"]), toolDefinition("canvas_set_viewport", "调整画布视口。", { viewport: VIEWPORT_SCHEMA }, ["viewport"]), toolDefinition("canvas_run_generation", "触发指定节点生成,通常用于配置节点或文本/图片/视频/音频节点。", { nodeId: { type: "string" }, mode: GENERATION_MODE_SCHEMA, prompt: { type: "string" } }, ["nodeId"]), ]; type OnlineAgentTab = "setup" | "chat" | "history" | "log"; type OnlineAgentLog = { id: string; time: string; title: string; data?: unknown }; type OnlineAgentLogContext = { model: string; running: boolean; confirmTools: boolean; messages: number; nodes: number; connections: number }; type OnlineLoopContext = { step: number }; type OnlineToolResult = { ok: true; message: string; data?: unknown } | { ok: false; message: string }; type OnlineExecutedToolCall = { toolCallId: string; name: string; result: OnlineToolResult }; type PendingOnlineToolContext = { messages: ResponseInputMessage[]; toolCalls: ResponseToolCall[]; assistantId: string; step: number }; type CanvasAssistantPanelProps = { nodes: CanvasNodeData[]; selectedNodeIds: Set; snapshot: CanvasAgentSnapshot; sessions: CanvasAssistantSession[]; activeSessionId: string | null; onSelectNodeIds: (ids: Set) => void; onSessionsChange: (sessions: CanvasAssistantSession[], activeSessionId: string | null) => void; onApplyOps: (ops?: CanvasAgentOp[]) => CanvasAgentSnapshot; canUndoOps: boolean; onUndoOps: () => CanvasAgentSnapshot | null; onPasteImage: (file: File) => void; agentMode: CanvasAgentMode; onAgentModeChange: (mode: CanvasAgentMode) => void; closing: boolean; onCollapse: () => void; }; export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, sessions, activeSessionId, onSelectNodeIds, onSessionsChange, onApplyOps, canUndoOps, onUndoOps, onPasteImage, agentMode, onAgentModeChange, closing, onCollapse }: CanvasAssistantPanelProps) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const user = useUserStore((state) => state.user); const effectiveConfig = useEffectiveConfig(); const cleanupImages = useAssetStore((state) => state.cleanupImages); const isAiConfigReady = useConfigStore((state) => state.isAiConfigReady); const openConfigDialog = useConfigStore((state) => state.openConfigDialog); const updateConfig = useConfigStore((state) => state.updateConfig); const confirmTools = useCanvasAgentStore((state) => state.confirmTools); const setAgentState = useCanvasAgentStore((state) => state.setAgentState); const [width, setWidth] = useState(520); const [view, setView] = useState("chat"); const [prompt, setPrompt] = useState(""); const [isRunning, setIsRunning] = useState(false); const [deleteChatIds, setDeleteChatIds] = useState([]); const [onlineLogs, setOnlineLogs] = useState([]); const [resizing, setResizing] = useState(false); const [removedReferenceIds, setRemovedReferenceIds] = useState>(new Set()); const [localSessions, setLocalSessions] = useState(() => (sessions.length ? sessions : [createSession()])); const [localActiveSessionId, setLocalActiveSessionId] = useState(activeSessionId); const snapshotRef = useRef(snapshot); const pendingToolContextRef = useRef(new Map()); useEffect(() => { if (!sessions.length) return; setLocalSessions(sessions); setLocalActiveSessionId(activeSessionId); }, [activeSessionId, sessions]); useEffect(() => { snapshotRef.current = snapshot; }, [snapshot]); useEffect(() => { onSessionsChange(localSessions, localActiveSessionId); }, [localActiveSessionId, localSessions, onSessionsChange]); const safeSessions = localSessions.length ? localSessions : [createSession()]; const activeSession = useMemo(() => safeSessions.find((session) => session.id === localActiveSessionId) || safeSessions[0] || null, [localActiveSessionId, safeSessions]); const historySessions = safeSessions.filter((session) => session.messages.length > 0); const messages = activeSession?.messages || []; const hasMessages = messages.length > 0; const activeModel = effectiveConfig.textModel || effectiveConfig.model; const selectedNodeKey = useMemo(() => Array.from(selectedNodeIds).sort().join(","), [selectedNodeIds]); const allSelectedReferences = useMemo(() => buildAssistantReferences(nodes, selectedNodeIds), [nodes, selectedNodeIds]); const selectedReferences = useMemo(() => allSelectedReferences.filter((item) => !removedReferenceIds.has(item.id)), [allSelectedReferences, removedReferenceIds]); const iconButtonStyle = { color: theme.node.muted }; useEffect(() => { setRemovedReferenceIds(new Set()); }, [selectedNodeKey]); const updateSession = (sessionId: string, updater: (session: CanvasAssistantSession) => CanvasAssistantSession) => { setLocalSessions((prev) => prev.map((session) => (session.id === sessionId ? updater(session) : session))); }; const appendMessage = (sessionId: string, message: CanvasAssistantMessage) => { updateSession(sessionId, (session) => ({ ...session, title: session.messages.length ? session.title : message.text.slice(0, 18) || "新对话", messages: [...session.messages, message], updatedAt: new Date().toISOString(), })); }; const addOnlineLog = (title: string, data?: unknown) => setOnlineLogs((prev) => [{ id: nanoid(), time: new Date().toLocaleTimeString(), title, data }, ...prev].slice(0, 80)); const upsertMessage = (sessionId: string, message: CanvasAssistantMessage) => { updateSession(sessionId, (session) => { const exists = session.messages.some((item) => item.id === message.id); return { ...session, title: session.messages.length ? session.title : message.text.slice(0, 18) || "新对话", messages: exists ? session.messages.map((item) => (item.id === message.id ? { ...item, ...message } : item)) : [...session.messages, message], updatedAt: new Date().toISOString(), }; }); }; const startChatSession = () => { if (activeSession && activeSession.messages.length === 0) { setLocalActiveSessionId(activeSession.id); return; } const session = createSession(); setLocalSessions((prev) => [session, ...prev]); setLocalActiveSessionId(session.id); }; const removeSessions = (ids: string[]) => { const next = safeSessions.filter((session) => !ids.includes(session.id)); if (!next.length) { const session = createSession(); setLocalSessions([session]); setLocalActiveSessionId(session.id); } else { setLocalSessions(next); setLocalActiveSessionId(localActiveSessionId && ids.includes(localActiveSessionId) ? next[0].id : localActiveSessionId); } cleanupImages({ sessions: next }); }; const clearSessions = () => { const session = createSession(); setLocalSessions([session]); setLocalActiveSessionId(session.id); cleanupImages({ sessions: [session] }); }; const sendMessage = async (text: string, history: CanvasAssistantMessage[], savedReferences?: CanvasAssistantReference[]) => { const requestConfig = { ...effectiveConfig, model: effectiveConfig.textModel || effectiveConfig.model }; if (!isAiConfigReady(requestConfig, requestConfig.model)) { openConfigDialog(true); return; } const session = activeSession || createSession(); if (!activeSession) { setLocalSessions([session]); setLocalActiveSessionId(session.id); } const refs = savedReferences || selectedReferences; const userMessage: CanvasAssistantMessage = { id: nanoid(), role: "user", text, references: refs }; const assistantId = nanoid(); appendMessage(session.id, userMessage); addOnlineLog("发送请求", { text, selectedNodeIds: snapshotRef.current.selectedNodeIds, nodeCount: snapshotRef.current.nodes.length, connectionCount: snapshotRef.current.connections.length }); setPrompt(""); setIsRunning(true); void runOnlineAgentStep(session.id, assistantId, history, userMessage, { step: 1 }); }; const runOnlineAgentStep = async (sessionId: string, assistantId: string, history: CanvasAssistantMessage[], userMessage: CanvasAssistantMessage, loop: OnlineLoopContext) => { const requestConfig = { ...effectiveConfig, model: effectiveConfig.textModel || effectiveConfig.model }; try { setIsRunning(true); const messages = await buildToolAgentMessages(snapshotRef.current, history, userMessage); addOnlineLog(`Agent Tool Loop ${loop.step} 开始`, { toolChoice: "required" }); let streamed = ""; const result = await requestToolResponse({ ...requestConfig, systemPrompt: "" }, messages, ONLINE_AGENT_TOOLS, "required", (text) => { streamed = text; if (text.trim()) upsertMessage(sessionId, { id: assistantId, role: "assistant", text }); }); addOnlineLog("模型工具回复", result); if (result.toolCalls.length) { const writableCalls = result.toolCalls.filter(isWritableToolCall); if (confirmTools && writableCalls.length) { upsertMessage(sessionId, { id: assistantId, role: "assistant", text: result.content || streamed || "准备执行工具,等待确认。" }); const toolMessageId = nanoid(); pendingToolContextRef.current.set(toolMessageId, { messages, toolCalls: result.toolCalls, assistantId, step: loop.step }); const toolMessage: CanvasAssistantMessage = { id: toolMessageId, role: "tool", title: "确认工具调用", text: summarizeToolCalls(result.toolCalls), detail: { status: "pending", step: loop.step, toolCalls: result.toolCalls } }; appendMessage(sessionId, toolMessage); addOnlineLog("等待用户确认", result.toolCalls); return; } await continueOnlineToolLoop(sessionId, assistantId, messages, result, loop.step); } else { if (!result.content.trim()) throw new Error("模型没有返回工具调用,画布操作未执行。"); upsertMessage(sessionId, { id: assistantId, role: "assistant", text: result.content || streamed || "没有返回内容。" }); addOnlineLog(`Agent Tool Loop ${loop.step} 结束`, { reply: result.content }); } } catch (error) { addOnlineLog("请求失败", error instanceof Error ? error.message : error); appendMessage(sessionId, { id: nanoid(), role: "error", title: "操作失败", text: error instanceof Error ? error.message : "操作失败" }); } finally { setIsRunning(false); } }; const continueOnlineToolLoop = async (sessionId: string, assistantId: string, messages: ResponseInputMessage[], result: { content: string; toolCalls: ResponseToolCall[] }, step: number) => { const toolResults = executeOnlineToolCalls(result.toolCalls); addOnlineLog("工具执行结果", toolResults); appendMessage(sessionId, { id: nanoid(), role: "tool", title: "工具自动执行完成", text: toolResults.map((item) => toolResultText(item.result)).join("\n"), detail: { status: "completed", step, toolCalls: result.toolCalls, results: toolResults }, }); await continueOnlineToolLoopAfterResults(sessionId, assistantId, messages, result.toolCalls, toolResults, step); }; const continueOnlineToolLoopAfterResults = async (sessionId: string, assistantId: string, messages: ResponseInputMessage[], toolCalls: ResponseToolCall[], toolResults: OnlineExecutedToolCall[], step: number) => { const nextMessages: ResponseInputMessage[] = [ ...messages, ...toolCalls.map(toolCallToResponseInput), ...toolResults.map((item) => ({ role: "tool" as const, tool_call_id: item.toolCallId, content: JSON.stringify(item.result) })), ]; if (step >= ONLINE_AGENT_MAX_STEPS) { upsertMessage(sessionId, { id: assistantId, role: "assistant", text: toolResults.map((item) => toolResultText(item.result)).join("\n") || "工具已执行。" }); addOnlineLog("Agent Tool Loop 达到步数上限", { maxSteps: ONLINE_AGENT_MAX_STEPS }); return; } const requestConfig = { ...effectiveConfig, model: effectiveConfig.textModel || effectiveConfig.model }; let streamed = ""; const next = await requestToolResponse({ ...requestConfig, systemPrompt: "" }, nextMessages, ONLINE_AGENT_TOOLS, "auto", (text) => { streamed = text; if (text.trim()) upsertMessage(sessionId, { id: assistantId, role: "assistant", text }); }); addOnlineLog(`Agent Tool Loop ${step + 1} 回复`, next); if (next.toolCalls.length) { const writableCalls = next.toolCalls.filter(isWritableToolCall); if (confirmTools && writableCalls.length) { upsertMessage(sessionId, { id: assistantId, role: "assistant", text: next.content || streamed || "准备执行工具,等待确认。" }); const toolMessageId = nanoid(); pendingToolContextRef.current.set(toolMessageId, { messages: nextMessages, toolCalls: next.toolCalls, assistantId, step: step + 1 }); appendMessage(sessionId, { id: toolMessageId, role: "tool", title: "确认工具调用", text: summarizeToolCalls(next.toolCalls), detail: { status: "pending", step: step + 1, toolCalls: next.toolCalls } }); addOnlineLog("等待用户确认", next.toolCalls); return; } await continueOnlineToolLoop(sessionId, assistantId, nextMessages, next, step + 1); return; } upsertMessage(sessionId, { id: assistantId, role: "assistant", text: next.content || streamed || toolResults.map((item) => toolResultText(item.result)).join("\n") || "工具已执行。" }); }; const executeOps = (ops: CanvasAgentOp[]) => { const beforeSnapshot = snapshotRef.current; const before = snapshotSignature(beforeSnapshot); const next = onApplyOps(ops); snapshotRef.current = next; const ranGeneration = ops.some((op) => op.type === "run_generation" && Boolean(op.nodeId)); const changed = before !== snapshotSignature(next) || ranGeneration; const noopReason = changed ? "" : explainNoop(ops, beforeSnapshot); return { changed, ops, ranGeneration, noopReason, before: JSON.parse(before), after: JSON.parse(snapshotSignature(next)) }; }; const executeOnlineTool = (name: string, args: Record): OnlineToolResult => { const current = snapshotRef.current; try { if (name === "canvas_get_state") return { ok: true, message: describeCanvasSnapshot(current), data: compactSnapshot(current) }; if (name === "canvas_export_snapshot") return { ok: true, message: describeCanvasSnapshot(current), data: compactSnapshot(current) }; if (name === "canvas_get_selection") { const ids = new Set(current.selectedNodeIds || []); return { ok: true, message: `当前选中 ${ids.size} 个节点。`, data: { nodes: compactSnapshot({ ...current, nodes: current.nodes.filter((node) => ids.has(node.id)) }).nodes } }; } const ops = onlineToolToOps(name, args, current, effectiveConfig); const result = executeOps(ops); return { ok: result.changed, message: result.changed ? summarizeCanvasAgentOps(ops) || "画布操作已执行。" : result.noopReason, data: result }; } catch (error) { return { ok: false, message: error instanceof Error ? error.message : "工具执行失败" }; } }; const executeOnlineToolCall = (toolCall: ResponseToolCall): OnlineExecutedToolCall => { try { const result = executeOnlineTool(toolCall.function.name, parseToolArguments(toolCall.function.arguments)); return { toolCallId: toolCall.id, name: toolCall.function.name, result }; } catch (error) { return { toolCallId: toolCall.id, name: toolCall.function.name, result: { ok: false, message: error instanceof Error ? error.message : "工具参数错误" } }; } }; const executeOnlineToolCalls = (toolCalls: ResponseToolCall[]) => { const results: OnlineExecutedToolCall[] = []; let stopped = false; toolCalls.forEach((toolCall) => { if (stopped) { results.push({ toolCallId: toolCall.id, name: toolCall.function.name, result: { ok: false, message: "前一个工具调用失败,未继续执行。" } }); return; } const result = executeOnlineToolCall(toolCall); results.push(result); if (!result.result.ok) stopped = true; }); return results; }; const approveOnlineTool = async (messageId: string) => { const message = safeSessions.flatMap((session) => session.messages).find((item) => item.id === messageId); const detail = objectDetail(message?.detail); const pendingContext = pendingToolContextRef.current.get(messageId); const toolCalls = pendingContext?.toolCalls || toolCallsFromDetail(detail); const previousMessages = pendingContext?.messages || []; const session = safeSessions.find((session) => session.messages.some((item) => item.id === messageId)); addOnlineLog("批准工具", { messageId, toolCalls }); const assistantId = pendingContext?.assistantId || ""; if (!session) return; if (!toolCalls.length || !previousMessages.length || !assistantId) { upsertMessage(session.id, { id: messageId, role: "tool", title: "工具执行失败", text: "工具上下文不完整,无法执行。", detail: { ...detail, status: "failed" } }); return; } try { setIsRunning(true); const results = executeOnlineToolCalls(toolCalls); addOnlineLog("工具执行结果", results); upsertMessage(session.id, { id: messageId, role: "tool", title: "工具执行完成", text: results.map((item) => toolResultText(item.result)).join("\n"), detail: { ...detail, results, status: "completed" } }); pendingToolContextRef.current.delete(messageId); await continueOnlineToolLoopAfterResults(session.id, assistantId, previousMessages, toolCalls, results, pendingContext?.step || Number(detail.step) || 1); } catch (error) { addOnlineLog("工具续跑失败", error instanceof Error ? error.message : error); appendMessage(session.id, { id: nanoid(), role: "error", title: "操作失败", text: error instanceof Error ? error.message : "操作失败" }); } finally { setIsRunning(false); } }; const rejectOnlineTool = (messageId: string) => { const session = safeSessions.find((session) => session.messages.some((item) => item.id === messageId)); addOnlineLog("拒绝工具", { messageId }); pendingToolContextRef.current.delete(messageId); if (session) upsertMessage(session.id, { id: messageId, role: "tool", title: "已拒绝执行", text: "工具调用已取消", detail: { ...objectDetail(session.messages.find((item) => item.id === messageId)?.detail), status: "rejected" } }); }; const submit = async () => { const text = prompt.trim(); if (!text || isRunning) return; await sendMessage(text, messages); }; const addImagesToCanvas = (files: FileList | File[] | null) => { const file = Array.from(files || []).find((item) => item.type.startsWith("image/")); if (file) onPasteImage(file); }; const startResize = () => { const move = (event: MouseEvent) => setWidth(Math.min(760, Math.max(320, window.innerWidth - event.clientX))); const stop = () => { setResizing(false); document.body.style.cursor = ""; document.body.style.userSelect = ""; document.removeEventListener("mousemove", move); document.removeEventListener("mouseup", stop); }; setResizing(true); document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; document.addEventListener("mousemove", move); document.addEventListener("mouseup", stop); }; const collapse = () => { onCollapse(); }; const onlineContent = ( <> }, { value: "chat", label: "对话" }, { value: "history", label: "历史", icon: , count: historySessions.length }, { value: "log", label: "日志", count: onlineLogs.length }, ]} onChange={setView} right={ <> {view === "history" ? ( } >

将删除 {deleteChatIds.length} 条对话记录,此操作不可撤销。

); return ( ); } function OnlineAgentLogView({ logs, theme, context, onClear }: { logs: OnlineAgentLog[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; context: OnlineAgentLogContext; onClear: () => void }) { const [mode, setMode] = useState<"text" | "json">("text"); const textareaRef = useRef(null); const content = mode === "text" ? formatOnlineLogText(logs, context) : formatOnlineLogJson(logs, context); const lastError = [...logs].reverse().find((item) => /错误|失败|error/i.test(`${item.title}\n${stringifyLog(item.data)}`)); const copy = async (value = content) => { if (copyToClipboard(value)) return; textareaRef.current?.focus(); textareaRef.current?.select(); }; return (
setMode(value as "text" | "json")} options={[{ label: "排查日志", value: "text" }, { label: "原始 JSON", value: "json" }]} />
{logs.length} 条