diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 6bded23fd..04b5154fa 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -1120,6 +1120,19 @@ type CanvasTypewriterTextProps = { onComplete?: (typingKey: string) => void; }; +type OpenClawQuickAction = { + label: string; + message: string; + intent?: string; + description?: string; + danger?: boolean; +}; + +type OpenClawActionsBlock = { + title?: string; + actions: OpenClawQuickAction[]; +}; + type CanvasComposerUploadProgress = { title: string; message: string; @@ -1128,6 +1141,87 @@ type CanvasComposerUploadProgress = { fileName?: string; }; +const OPENCLAW_ACTIONS_CODE_BLOCK_REGEX = + /```openclaw-actions\s*([\s\S]*?)```/gi; + +function normalizeOpenClawActionText(value: unknown, maxLength: number) { + return String(value || "") + .replace(/\s+/g, " ") + .trim() + .slice(0, maxLength); +} + +function parseOpenClawActionsPayload(payload: unknown): OpenClawActionsBlock | null { + if (!payload || typeof payload !== "object" || Array.isArray(payload)) { + return null; + } + + const row = payload as Record; + if ( + row.openclaw_actions !== true && + row.type !== "quick_replies" && + !Array.isArray(row.actions) + ) { + return null; + } + + const actions = Array.isArray(row.actions) + ? row.actions + .map((item): OpenClawQuickAction | null => { + if (!item || typeof item !== "object" || Array.isArray(item)) { + return null; + } + const action = item as Record; + const label = normalizeOpenClawActionText(action.label, 24); + const message = normalizeOpenClawActionText(action.message, 300); + if (!label || !message) return null; + + const intent = normalizeOpenClawActionText(action.intent, 80); + const description = normalizeOpenClawActionText(action.description, 120); + return { + label, + message, + intent: intent || undefined, + description: description || undefined, + danger: action.danger === true, + }; + }) + .filter((item): item is OpenClawQuickAction => Boolean(item)) + .slice(0, 10) + : []; + + if (actions.length === 0) return null; + + return { + title: normalizeOpenClawActionText(row.title, 40) || undefined, + actions, + }; +} + +function extractOpenClawActionsFromMarkdown(content: string) { + const blocks: OpenClawActionsBlock[] = []; + const markdown = String(content || "").replace( + OPENCLAW_ACTIONS_CODE_BLOCK_REGEX, + (_match, rawJson) => { + try { + const parsed = JSON.parse(String(rawJson || "").trim()); + const block = parseOpenClawActionsPayload(parsed); + if (block) { + blocks.push(block); + } + } catch { + // Invalid action JSON should not break normal Markdown rendering. + } + return "\n"; + } + ); + + return { + markdown: markdown.replace(/\n{3,}/g, "\n\n").trim(), + blocks, + }; +} + type MarkdownTableBlockRange = { start: number; end: number; @@ -1324,6 +1418,58 @@ function CanvasTypewriterText({ ); } +function OpenClawQuickActions({ + blocks, + disabled, + onAction, +}: { + blocks: OpenClawActionsBlock[]; + disabled: boolean; + onAction: (message: string) => void; +}) { + if (blocks.length === 0) return null; + + return ( +
+ {blocks.map((block, blockIndex) => ( +
+ {block.title ? ( +
+ {block.title} +
+ ) : null} +
+ {block.actions.map((action, actionIndex) => ( + + ))} +
+
+ ))} +
+ ); +} + function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) { return (
@@ -9937,11 +10083,13 @@ export function CanvasChatPanel({ ]); // 处理发送(图片已在上传时添加到画布,这里只发送消息) - const handleSend = useCallback(async () => { + const handleSend = useCallback(async (overrideInput?: string) => { const currentSendGuard = getSendGuard(activeChatId); if (currentSendGuard.inFlight || status === "streaming" || status === "submitted") { return; } + const hasOverrideInput = typeof overrideInput === "string"; + const composerInput = hasOverrideInput ? String(overrideInput || "") : input; if (isDetailGenerationMode && canUseDetailGeneration) { clearLatestGeneratedImageHighlights(); @@ -9959,11 +10107,15 @@ export function CanvasChatPanel({ } if (isClawdMode) { - const textContent = normalizeClawdComposerText(input); - const tableFilesSnapshot = [...clawdTableFiles]; + const isProgrammaticClawdQuickSend = hasOverrideInput; + const textContent = normalizeClawdComposerText(composerInput); + const tableFilesSnapshot = isProgrammaticClawdQuickSend ? [] : [...clawdTableFiles]; const hasTableFiles = tableFilesSnapshot.length > 0; - const clawdSkillTagForRequest = selectedSkillTag || activeClawdSkillTag; + const clawdSkillTagForRequest = isProgrammaticClawdQuickSend + ? activeClawdSkillTag + : selectedSkillTag || activeClawdSkillTag; const shouldIncludeClawdSkillTagPrompt = Boolean( + !isProgrammaticClawdQuickSend && clawdSkillTagForRequest && (selectedSkillTag || !activeClawdSkillTagPromptApplied) ); @@ -9971,7 +10123,7 @@ export function CanvasChatPanel({ ? buildClawdSkillTagPrompt(clawdSkillTagForRequest) : ""; const hasPendingClawdSkillTagPrompt = Boolean(clawdSkillTagPrompt); - const attachmentsSnapshot = [...attachments]; + const attachmentsSnapshot = isProgrammaticClawdQuickSend ? [] : [...attachments]; const imageAttachmentsSnapshot = attachmentsSnapshot .map((attachment) => sanitizeChatAttachment(attachment)) .filter((attachment): attachment is PickedObjectAttachment => Boolean(attachment)); @@ -10103,12 +10255,14 @@ export function CanvasChatPanel({ optimisticUserMessage, ]) ); - clearComposerEditor(); - scheduleClearRestoredClawdComposerText(); - setAttachments([]); - setClawdTableFiles([]); - setSelectedSkillTagId(null); - clearSelection(); + if (!isProgrammaticClawdQuickSend) { + clearComposerEditor(); + scheduleClearRestoredClawdComposerText(); + setAttachments([]); + setClawdTableFiles([]); + setSelectedSkillTagId(null); + clearSelection(); + } try { const formData = new FormData(); @@ -10191,30 +10345,32 @@ export function CanvasChatPanel({ previousMessages.filter((message) => message.id !== userMessageId) ); toast.error(CLAW_UPGRADE_RETRY_MESSAGE); - setClawdTableFiles(tableFilesSnapshot); - setAttachments(attachmentsSnapshot); - if (clawdSkillTagForRequest && hasPendingClawdSkillTagPrompt) { - setSelectedSkillTagId(clawdSkillTagForRequest.id); - } - const editableDiv = textareaRef.current as HTMLDivElement | null; - const currentEditorText = (editableDiv?.textContent || "") - .replace(/\u200b/g, " ") - .replace(/\s+/g, " ") - .trim(); - if (!currentEditorText) { - setInput(textContent); - if (editableDiv) { - editableDiv.textContent = textContent; - window.setTimeout(() => { - attachmentsSnapshot.forEach((attachment, index) => { - if (!attachment.url) return; - insertImageTagInEditor( - attachment.url, - normalizeChatAttachmentName(attachment.name), - index - ); - }); - }, 0); + if (!isProgrammaticClawdQuickSend) { + setClawdTableFiles(tableFilesSnapshot); + setAttachments(attachmentsSnapshot); + if (clawdSkillTagForRequest && hasPendingClawdSkillTagPrompt) { + setSelectedSkillTagId(clawdSkillTagForRequest.id); + } + const editableDiv = textareaRef.current as HTMLDivElement | null; + const currentEditorText = (editableDiv?.textContent || "") + .replace(/\u200b/g, " ") + .replace(/\s+/g, " ") + .trim(); + if (!currentEditorText) { + setInput(textContent); + if (editableDiv) { + editableDiv.textContent = textContent; + window.setTimeout(() => { + attachmentsSnapshot.forEach((attachment, index) => { + if (!attachment.url) return; + insertImageTagInEditor( + attachment.url, + normalizeChatAttachmentName(attachment.name), + index + ); + }); + }, 0); + } } } } finally { @@ -10234,7 +10390,7 @@ export function CanvasChatPanel({ return; } - if (!input.trim() && attachments.length === 0 && !selectedSkillTag && !hasWorkflowContext) { + if (!composerInput.trim() && attachments.length === 0 && !selectedSkillTag && !hasWorkflowContext) { return; } const effectiveModelId = currentModelIdRef.current || selectedModelId; @@ -10324,7 +10480,7 @@ export function CanvasChatPanel({ }); // 添加文本部分(用户可见) - let textContent = input + let textContent = composerInput .replace(/\u200b/g, " ") .replace(/\s+/g, " ") .trim(); @@ -10574,6 +10730,22 @@ export function CanvasChatPanel({ workflowContext, ]); + const handleOpenClawQuickAction = useCallback( + (message: string) => { + const quickMessage = normalizeOpenClawActionText(message, 300); + if (!quickMessage) { + toast.error("快捷标签内容为空"); + return; + } + if (!isClawdMode) { + toast.error(`请先切换到 ${CLAW_MODE_LABEL} 对话后再使用快捷标签`); + return; + } + void handleSend(quickMessage); + }, + [handleSend, isClawdMode] + ); + const maybeAutoSetCanvasTitle = useCallback(async (firstImageUrl: string | null) => { if (!firstImageUrl || hasSetCanvasTitle.current) { return; @@ -12630,8 +12802,13 @@ export function CanvasChatPanel({ const textWithoutImages = stripCanvasModelRoutingLines( stripMediaMarkup(rawText) ); - const analysisSection = extractCanvasImageAnalysisSection(textWithoutImages); - const plainText = normalizeCanvasAgentText(textWithoutImages); + const openClawActionsResult = isClawAssistantMessage + ? extractOpenClawActionsFromMarkdown(textWithoutImages) + : { markdown: textWithoutImages, blocks: [] }; + const textWithoutActions = openClawActionsResult.markdown; + const openClawActionBlocks = openClawActionsResult.blocks; + const analysisSection = extractCanvasImageAnalysisSection(textWithoutActions); + const plainText = normalizeCanvasAgentText(textWithoutActions); const analysisToggleKey = `${message.id}-${i}-analysis`; const analysisOpen = analysisCollapseState[analysisToggleKey] ?? true; const analysisTypingKey = `${message.id}-${i}-analysis-typing`; @@ -12870,6 +13047,20 @@ export function CanvasChatPanel({ ))}
)} + {openClawActionBlocks.length > 0 ? ( + + ) : null} ); } @@ -14832,7 +15023,7 @@ export function CanvasChatPanel({ ? "bg-[#eef0f3] text-[#20242a] shadow-sm ring-1 ring-inset ring-[#d5dae2] hover:bg-[#e4e7ec] dark:bg-white dark:text-zinc-950 dark:ring-white/15 dark:hover:bg-zinc-100" : "bg-[#f1f1f2] text-[#b6bac2] hover:bg-[#ececee] dark:bg-[#242529] dark:text-[#777d88] dark:hover:bg-[#2c2d33]" )} - onClick={handleSend} + onClick={() => void handleSend()} disabled={isSubmitting || !canSend} title={ isWorkspacePresentation || isDetailGenerationMode