fix: stabilize canvas agent chat feedback

This commit is contained in:
134 2026-07-07 14:54:58 +08:00
parent 2d6289f0fa
commit 7e1fd15ebd
3 changed files with 162 additions and 30 deletions

View File

@ -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<Record<string, boolean>>({});
const [typedAssistantSections, setTypedAssistantSections] = useState<Record<string, boolean>>({});
const [activeTypingSections, setActiveTypingSections] = useState<Record<string, boolean>>({});
const [agentTypewriterMessageIds, setAgentTypewriterMessageIds] = useState<Record<string, boolean>>({});
const [clawdTypewriterMessageIds, setClawdTypewriterMessageIds] = useState<Record<string, boolean>>({});
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(/<!--\s*IMAGE_GENERATION_(?:START|END)\s*-->/g, "")
);
const emptyNearestUserImageKeys = new Set<string>();
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<string>("");
@ -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) => (

View File

@ -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),
);
}

View File

@ -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({