diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 345fef046..e4516c616 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -792,6 +792,8 @@ const CANVAS_AGENT_SECTION_TITLES = new Set([ "需求理解", CANVAS_IMAGE_ANALYSIS_TITLE, "执行计划", + "执行结果", + "后续操作", "优化后提示词", "输出设置", ]); @@ -2303,6 +2305,7 @@ export function CanvasChatPanel({ const [analysisCollapseState, setAnalysisCollapseState] = useState>({}); const [typedAssistantSections, setTypedAssistantSections] = useState>({}); const [activeTypingSections, setActiveTypingSections] = useState>({}); + const [agentTypewriterMessageIds, setAgentTypewriterMessageIds] = useState>({}); const [clawdTypewriterMessageIds, setClawdTypewriterMessageIds] = useState>({}); const [insufficientPointsState, setInsufficientPointsState] = useState<{ balance: number; @@ -6466,6 +6469,18 @@ export function CanvasChatPanel({ const normalizedAttachments = Array.isArray(messageData.attachments) ? messageData.attachments : []; + const appendedMessageId = String(messageData.id || "").trim(); + const appendedAssistantText = normalizedParts + .filter( + (part: any) => + part?.type === "text" && typeof part?.text === "string" + ) + .map((part: any) => String(part.text || "")) + .join("\n\n"); + const appendedAssistantVisibleText = normalizeCanvasAgentText( + stripCanvasModelRoutingLines(stripMediaMarkup(appendedAssistantText)) + .replace(//g, "") + ); const emptyNearestUserImageKeys = new Set(); const incomingImageUrls = normalizedParts.flatMap((part: any) => part?.type === "text" && typeof part?.text === "string" @@ -6525,6 +6540,18 @@ export function CanvasChatPanel({ if (!isEventForActiveChat) { return; } + + if ( + messageData.role === "assistant" && + appendedMessageId && + appendedAssistantVisibleText + ) { + setAgentTypewriterMessageIds((previousIds) => + previousIds[appendedMessageId] + ? previousIds + : { ...previousIds, [appendedMessageId]: true } + ); + } setMessages((prevMessages) => { const existingIndex = prevMessages.findIndex( @@ -6993,11 +7020,20 @@ export function CanvasChatPanel({ currentModelIdRef.current = selectedModelId; inFlightChatIdRef.current = activeChatId; inFlightRawChatIdRef.current = rawChatId; + const launchUserMessageId = generateUUID(); + const launchCreatedAt = new Date(); sendMessage({ + id: launchUserMessageId, role: "user", parts, - }); + createdAt: launchCreatedAt, + metadata: { + createdAt: launchCreatedAt.toISOString(), + composerMode: activeComposerModeRef.current || activeComposerMode, + modelId: selectedModelId, + }, + } as any); hasHydratedHistoryRef.current = true; pendingLaunchRef.current = null; @@ -7010,7 +7046,7 @@ export function CanvasChatPanel({ if (editableDiv) { editableDiv.innerHTML = ""; } - }, [activeChatId, rawChatId, selectedModelId, status, sendMessage, clearSelection, launchAutoSendTick]); + }, [activeChatId, rawChatId, selectedModelId, status, sendMessage, clearSelection, launchAutoSendTick, activeComposerMode]); // 当历史消息加载完成后,更新消息列表(支持同一会话的增量刷新) const loadedHistorySignatureRef = useRef(""); @@ -10821,6 +10857,20 @@ export function CanvasChatPanel({ !shouldTreatAsCanvasAgentControlIntent && effectiveComposerMode === "image" && Boolean(requestModel && isCanvasImageModel(requestModel)); + const userMessageId = generateUUID(); + const latestMessageTimestamp = messages.reduce( + (latest, message) => Math.max(latest, getMessageTimestamp(message) ?? 0), + 0 + ); + const optimisticCreatedAt = new Date( + Math.max(Date.now(), latestMessageTimestamp + 1) + ); + const optimisticUserMetadata = { + createdAt: optimisticCreatedAt.toISOString(), + composerMode: effectiveComposerMode, + agentEntryMode: effectiveAgentEntryMode, + modelId: effectiveModelId, + }; // 🆕 隐藏提示词(不在 UI 中展示) const hiddenSegments: string[] = []; @@ -10933,9 +10983,12 @@ export function CanvasChatPanel({ // 使用与 multimodal-input 相同的格式发送消息 try { sendMessage({ + id: userMessageId, role: "user", parts: parts, - }); + createdAt: optimisticCreatedAt, + metadata: optimisticUserMetadata, + } as any); } catch (error) { releaseSendGuard(activeChatId); if (shouldShowOptimisticImageGeneration) { @@ -11019,6 +11072,7 @@ export function CanvasChatPanel({ markSessionClawdSkillTagPromptApplied, buildHistoryMessagesUrl, workflowContext, + messages, ]); const handleOpenClawQuickAction = useCallback( @@ -12953,6 +13007,11 @@ export function CanvasChatPanel({ const shouldTypeClawAssistantMessage = message.role === "assistant" && Boolean(clawdTypewriterMessageIds[message.id]); + const shouldTypeAgentAssistantMessage = + message.role === "assistant" && + Boolean(agentTypewriterMessageIds[message.id]); + const shouldForceTypeAssistantMessage = + shouldTypeClawAssistantMessage || shouldTypeAgentAssistantMessage; const isClawAssistantMessage = message.role === "assistant" && (isClawdMode || isClawdChatMessage(message)); @@ -13161,16 +13220,16 @@ export function CanvasChatPanel({ !typedAssistantSections[analysisTypingKey] && ((shouldTypeAssistantMessage && !hasCanvasRenderableMarkdown(analysisSection?.analysis || "")) || - shouldTypeClawAssistantMessage); + shouldForceTypeAssistantMessage); const shouldAnimatePlainText = !analysisSection && Boolean(plainText) && !typedAssistantSections[plainTextTypingKey] && ((shouldTypeAssistantMessage && !hasCanvasRenderableMarkdown(plainText)) || - shouldTypeClawAssistantMessage); + shouldForceTypeAssistantMessage); const shouldDeferAssistantMedia = - isClawAssistantMessage && + (isClawAssistantMessage || shouldTypeAgentAssistantMessage) && (shouldAnimateAnalysis || shouldAnimatePlainText); const visibleImageUrls = shouldDeferAssistantMedia ? [] @@ -13182,6 +13241,7 @@ export function CanvasChatPanel({ !isDetailGenerationMode && !isClawAssistantMessage && visibleImageUrls.length > 0 && + !plainText.trim() && !plainText.includes(CANVAS_IMAGE_DONE_SUMMARY) && !isLatestAssistantDuringGeneration; const renderPlainText = (text: string, key: string) => ( diff --git a/lib/ai/runtime/direct-agent-response-builders.ts b/lib/ai/runtime/direct-agent-response-builders.ts index c1b349c86..0576f56d1 100644 --- a/lib/ai/runtime/direct-agent-response-builders.ts +++ b/lib/ai/runtime/direct-agent-response-builders.ts @@ -10,6 +10,56 @@ import { type DirectAgentToolCapability, } from "@/lib/ai/runtime/direct-agent-execution"; +function normalizeDirectAgentResultSentence(value: string) { + const normalized = String(value || "") + .replace(/结果如下[::]?\s*$/u, "") + .replace(/[,,。::]\s*$/u, "") + .trim(); + return normalized || "已完成处理"; +} + +function buildDirectAgentResultCard(params: { + result: string; + mediaMarkdown?: string | null; + followUp: string; +}) { + return [ + "### 执行结果", + `- ${normalizeDirectAgentResultSentence(params.result)},结果如下。`, + params.mediaMarkdown, + "### 后续操作", + `- ${params.followUp}`, + ] + .filter(Boolean) + .join("\n\n"); +} + +function getDirectAgentFollowUpMessage(capability: DirectAgentToolCapability) { + switch (capability) { + case "remove_background": + return "如需微调边缘、替换背景或继续合成,可以直接告诉我。"; + case "remove_watermark": + return "如需继续修补局部痕迹或统一画面质感,可以直接告诉我。"; + case "enhance": + return "如需继续调整清晰度、色彩或细节强度,可以直接告诉我。"; + case "resize": + return "如需继续改尺寸、比例或输出规格,可以直接告诉我。"; + case "extend": + return "如需继续扩展画面、调整构图或补充边缘内容,可以直接告诉我。"; + case "angle_variant": + return "如需继续换角度、换机位或统一系列视角,可以直接告诉我。"; + case "local_edit": + case "box_cutout": + return "如需继续修改局部、替换元素或细化边缘,可以直接告诉我。"; + case "generate_video": + return "如需继续调整镜头、节奏、比例或时长,可以直接告诉我。"; + case "generate_image": + case "generic_edit": + default: + return "如需继续调整风格、构图、主体细节或尺寸,可以直接告诉我。"; + } +} + export function buildDirectAgentAnalysisAssistantText(params: { summary: string; answer?: string | null; @@ -32,12 +82,11 @@ export function buildDirectAgentImageGenerationAssistantText(params: { modelDisplayName?: string | null; }) { return prependImageModelBadge( - [ - getDirectAgentToolSuccessMessage("generate_image"), - params.imageMarkdown, - ] - .filter(Boolean) - .join("\n\n"), + buildDirectAgentResultCard({ + result: getDirectAgentToolSuccessMessage("generate_image"), + mediaMarkdown: params.imageMarkdown, + followUp: getDirectAgentFollowUpMessage("generate_image"), + }), params.modelDisplayName, ); } @@ -47,12 +96,11 @@ export function buildDirectAgentVideoGenerationAssistantText(params: { modelDisplayName?: string | null; }) { return prependImageModelBadge( - [ - getDirectAgentToolSuccessMessage("generate_video"), - buildAgentToolVideoMarkdown(params.videoUrl), - ] - .filter(Boolean) - .join("\n\n"), + buildDirectAgentResultCard({ + result: getDirectAgentToolSuccessMessage("generate_video"), + mediaMarkdown: buildAgentToolVideoMarkdown(params.videoUrl), + followUp: getDirectAgentFollowUpMessage("generate_video"), + }), params.modelDisplayName, ); } @@ -64,16 +112,15 @@ export function buildDirectAgentImageEditAssistantText(params: { imageSize?: string; }) { return prependImageModelBadge( - [ - getDirectAgentToolSuccessMessage(params.capability), - buildAgentToolImageMarkdown({ + buildDirectAgentResultCard({ + result: getDirectAgentToolSuccessMessage(params.capability), + mediaMarkdown: buildAgentToolImageMarkdown({ originalUrl: params.originalUrl, thumbnailUrl: params.thumbnailUrl, imageSize: params.imageSize, }), - ] - .filter(Boolean) - .join("\n\n"), + followUp: getDirectAgentFollowUpMessage(params.capability), + }), getDirectAgentToolLabel(params.capability), ); } @@ -87,12 +134,11 @@ export function buildDirectAgentBatchImageEditAssistantText(params: { }>; }) { return prependImageModelBadge( - [ - getDirectAgentToolSuccessMessage(params.capability), - buildAgentToolImagesMarkdown(params.results), - ] - .filter(Boolean) - .join("\n\n"), + buildDirectAgentResultCard({ + result: getDirectAgentToolSuccessMessage(params.capability), + mediaMarkdown: buildAgentToolImagesMarkdown(params.results), + followUp: getDirectAgentFollowUpMessage(params.capability), + }), getDirectAgentToolLabel(params.capability), ); } diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index 6edd96b14..c4d257b53 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -131,6 +131,10 @@ import { resolveDirectAgentToolExecution, shouldRunDirectAgentBatchImageEdit, } from "@/lib/ai/runtime/direct-agent-execution"; +import { + buildDirectAgentImageEditAssistantText, + buildDirectAgentImageGenerationAssistantText, +} from "@/lib/ai/runtime/direct-agent-response-builders"; import { inferAngleEditOptionsFromText } from "@/lib/ai/runtime/angle-edit-options"; import { hasDirectAgentLocalEditIntent, @@ -1179,6 +1183,28 @@ test.describe("canvas chat pure helpers", () => { ); }); + test("formats direct agent image results as structured markdown cards", () => { + const generatedText = buildDirectAgentImageGenerationAssistantText({ + imageMarkdown: "![生成结果](https://cdn.example.com/generated.png)", + modelDisplayName: "Test Image Model", + }); + + expect(generatedText).toContain("### 执行结果"); + expect(generatedText).toContain("- 已完成图片生成,结果如下。"); + expect(generatedText).toContain("![生成结果](https://cdn.example.com/generated.png)"); + expect(generatedText).toContain("### 后续操作"); + + const editText = buildDirectAgentImageEditAssistantText({ + capability: "remove_background", + originalUrl: "https://cdn.example.com/cutout.png", + }); + + expect(editText).toContain("### 执行结果"); + expect(editText).toContain("- 已完成抠图处理,结果如下。"); + expect(editText).toContain("https://cdn.example.com/cutout.png"); + expect(editText).toContain("替换背景"); + }); + test("only runs direct image edit batches for explicit safe batch requests", () => { expect( shouldRunDirectAgentBatchImageEdit({