diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts
index 5b901905b..67e892f9b 100644
--- a/app/(chat)/api/chat/route.ts
+++ b/app/(chat)/api/chat/route.ts
@@ -257,6 +257,7 @@ import {
type AgentModelSlots,
} from "@/lib/ai/runtime/agent-model-slots";
import { isPureGreetingTurnRequest } from "@/lib/ai/runtime/task-boundary-classifier";
+import { isCapabilityQuestionTurnRequest } from "@/lib/ai/runtime/task-boundary-text";
import {
getProviderHealthSnapshot,
recordProviderHealthFailure,
@@ -846,6 +847,19 @@ const DEFAULT_STREAM_KEEPALIVE_INTERVAL_MS = 12_000;
const CHAT_CREATION_LOOKUP_RETRY_DELAYS_MS = [60, 180, 360] as const;
const DETAIL_REPLACEMENT_PROMPT_PACK_REQUIRED_MESSAGE =
"请先在「风格化提示词配置」里选择提示词组合,再执行详情场景替换;缺少提示词组时系统不会处理。";
+const CANVAS_AGENT_GREETING_MESSAGE =
+ "你好,我在。你可以直接告诉我想做什么:生图、修图、生成视频、分析图片都可以。";
+const CANVAS_AGENT_CAPABILITY_MESSAGE = [
+ "我主要帮你完成画布里的视觉创作任务:",
+ "",
+ "- 生图:按文字、参考图或画布选中内容生成图片。",
+ "- 修图:换背景、去元素、局部重绘、扩图、增强、抠图、改尺寸。",
+ "- 视频:文生视频、图生视频、分镜/镜头视频需求整理与生成。",
+ "- 图片分析:识别主体、风格、构图、材质、光线,并整理可执行提示词。",
+ "- 画布协作:读取当前画布选择、槽位、最近结果,继续生成、修改或写回画布。",
+ "",
+ "你可以直接说:把这张图换成白底、参考左边图片生成 3 张主图、让这张图动起来。",
+].join("\n");
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -1234,9 +1248,20 @@ export async function POST(request: Request) {
const hasCurrentMessageMediaInput = message.parts.some((part: any) =>
part?.type === "file" || part?.type === "image" || part?.type === "image_url"
);
+ const isGreetingTurnRequest = isPureGreetingTurnRequest(requestTextContent);
+ const isCapabilityQuestionRequest =
+ isCapabilityQuestionTurnRequest(requestTextContent);
+ const canvasFastPathMessage = isCapabilityQuestionRequest
+ ? CANVAS_AGENT_CAPABILITY_MESSAGE
+ : isGreetingTurnRequest
+ ? CANVAS_AGENT_GREETING_MESSAGE
+ : "";
if (
- shouldRunAgentConversation &&
- isPureGreetingTurnRequest(requestTextContent) &&
+ (shouldRunAgentConversation ||
+ isCanvasPageRequest ||
+ contextPayload?.source === "canvas" ||
+ Boolean(requestCanvasAgentContext)) &&
+ canvasFastPathMessage &&
!hasCurrentMessageMediaInput
) {
const session = await auth();
@@ -1245,7 +1270,6 @@ export async function POST(request: Request) {
return new ChatSDKError("unauthorized:chat").toResponse();
}
- const greetingMessage = "你好,我在。你可以直接告诉我想做什么:生图、修图、生成视频、分析图片都可以。";
const fastPathTitle = requestTextContent.trim().slice(0, 80) || "新对话";
await saveChat({
id,
@@ -1266,7 +1290,12 @@ export async function POST(request: Request) {
const greetingStream = createUIMessageStream({
execute: async ({ writer }) => {
const messageId = generateUUID();
- writeAssistantTextChunk(writer, greetingMessage, "greeting-fast-path", messageId);
+ writeAssistantTextChunk(
+ writer,
+ canvasFastPathMessage,
+ isCapabilityQuestionRequest ? "capability-fast-path" : "greeting-fast-path",
+ messageId
+ );
await saveMessages({
messages: [
@@ -1287,7 +1316,7 @@ export async function POST(request: Request) {
id: messageId,
chatId: id,
role: "assistant",
- parts: [{ type: "text", text: greetingMessage }],
+ parts: [{ type: "text", text: canvasFastPathMessage }],
attachments: [],
createdAt: new Date(),
videoStorageStatus: null,
@@ -8625,10 +8654,16 @@ export async function POST(request: Request) {
selectedChatModel: finalModelId,
requestHints,
});
- const agentIdentityInstruction = shouldRunAgentConversation
+ const shouldUseProductIdentityInstruction =
+ shouldRunAgentConversation ||
+ isCanvasPageRequest ||
+ contextPayload?.source === "canvas" ||
+ Boolean(requestCanvasAgentContext);
+ const agentIdentityInstruction = shouldUseProductIdentityInstruction
? await buildAgentRoleIdentityInstruction({
allowVideoGeneration: allowAgentVideoGeneration,
hasCanvasContext: Boolean(
+ isCanvasPageRequest ||
effectiveContextPayload?.source === "canvas" ||
effectiveContextPayload?.selectedNodeIds?.length ||
effectiveContextPayload?.canvasImageNodes?.length,
diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx
index 19e17793d..7a894a236 100644
--- a/components/canvas/CanvasChatPanel.tsx
+++ b/components/canvas/CanvasChatPanel.tsx
@@ -1595,21 +1595,15 @@ function OpenClawQuickActions({
);
}
-function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) {
+function CanvasAssistantTypingPlaceholder({
+ hint,
+ label = "分析中",
+}: {
+ hint?: string | null;
+ label?: string;
+}) {
return (
-
{hint ? (
@@ -1617,19 +1611,11 @@ function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) {
) : null}
- {[0, 1, 2].map((index) => (
-
- ))}
+
+ {label}
@@ -11910,9 +11896,11 @@ export function CanvasChatPanel({
);
const isLoading = status === 'streaming' || status === 'submitted';
+ const activeChatSendGuard = getSendGuard(activeChatId);
+ const isActiveChatPending = isLoading || activeChatSendGuard.inFlight;
const isComposerUploading = Boolean(composerUploadProgress);
const isSubmitting =
- isLoading || isDetailGenerating || isClawdSubmitting || isComposerUploading;
+ isActiveChatPending || isDetailGenerating || isClawdSubmitting || isComposerUploading;
const hasInputOrAttachments = Boolean(
input.trim() || attachments.length > 0 || (!isDetailGenerationMode && selectedSkillTag)
);
@@ -12287,15 +12275,23 @@ export function CanvasChatPanel({
const isDetailEmptyState =
isDetailGenerationMode &&
renderableVisibleMessages.length === 0 &&
- !isLoading &&
+ !isActiveChatPending &&
!shouldShowImageLoading;
const isPlainCanvasEmptyState =
!isWorkspacePresentation &&
!isDetailGenerationMode &&
!isVideoWorkspace &&
renderableVisibleMessages.length === 0 &&
- !isLoading &&
+ !isActiveChatPending &&
!shouldShowImageLoading;
+ const latestRenderableMessage = renderableVisibleMessages.at(-1);
+ const shouldRenderAssistantTypingPlaceholder =
+ !isClawdMode &&
+ !isDetailGenerationMode &&
+ !shouldRenderImageLoading &&
+ !hasActiveTypewriter &&
+ isActiveChatPending &&
+ latestRenderableMessage?.role === "user";
const workspaceEmptyTitle =
isClawdMode
? `开始 ${CLAW_MODE_LABEL} 对话`
@@ -13490,9 +13486,7 @@ export function CanvasChatPanel({
shouldAnimateAnalysis ||
shouldAnimatePlainText ||
isClawdSubmitting ||
- getSendGuard(activeChatId).inFlight ||
- status === "streaming" ||
- status === "submitted"
+ isActiveChatPending
}
onAction={handleOpenClawQuickAction}
/>
@@ -13564,9 +13558,13 @@ export function CanvasChatPanel({
estimatedWaitMs={loadingEstimatedWaitMs}
/>
)}
+
+ {shouldRenderAssistantTypingPlaceholder ? (
+
+ ) : null}
{isClawdMode && isClawdSubmitting ? (
-
+
) : null}
diff --git a/lib/ai/runtime/task-boundary-classifier.ts b/lib/ai/runtime/task-boundary-classifier.ts
index a4dddf4a2..fe6991ed2 100644
--- a/lib/ai/runtime/task-boundary-classifier.ts
+++ b/lib/ai/runtime/task-boundary-classifier.ts
@@ -5,6 +5,7 @@ import type {
PendingGridConfirmation,
} from "@/lib/ai/conversation-action-resolver";
import { isExplicitPromptOptimizationRequest } from "@/lib/ai/smart-analyzer";
+import { isCapabilityQuestionTurnRequest } from "@/lib/ai/runtime/task-boundary-text";
import { detectStoryboardCommandIntent } from "@/lib/video-agent/storyboard-command-interpreter";
export type TaskBoundaryType =
@@ -88,9 +89,7 @@ function isLikelyChatOnlyRequest(text: string): boolean {
const normalized = normalizeCompactText(text);
if (!normalized) return false;
- return /你能做什么|你会什么|你可以做什么|你能帮我做什么|你能干什么|你可以帮我做什么|介绍一下你自己|介绍一下自己|说说你的能力|你有哪些功能|你有什么功能|怎么用|怎么玩|如何使用|帮助说明|帮助手册|功能介绍/.test(
- normalized,
- );
+ return isCapabilityQuestionTurnRequest(normalized);
}
function isLikelyAnalysisOnlyRequest(text: string, hasImageContext: boolean): boolean {
diff --git a/lib/ai/runtime/task-boundary-text.ts b/lib/ai/runtime/task-boundary-text.ts
new file mode 100644
index 000000000..dad761f7f
--- /dev/null
+++ b/lib/ai/runtime/task-boundary-text.ts
@@ -0,0 +1,12 @@
+export function normalizeCompactTaskBoundaryText(text: string) {
+ return String(text || "").toLowerCase().replace(/\s+/g, "").trim();
+}
+
+export function isCapabilityQuestionTurnRequest(text: string): boolean {
+ const normalized = normalizeCompactTaskBoundaryText(text);
+ if (!normalized) return false;
+
+ return /你.*(?:能|会|可以|可|嫩)?.*(?:为我做什么|帮我做什么|帮我什么|做什么|干什么)|你.*(?:有哪些|有什么).*(?:功能|能力)|(?:介绍|说说).*(?:功能|能力|你自己|自己)|怎么用|怎么玩|如何使用|帮助说明|帮助手册|功能介绍/.test(
+ normalized,
+ );
+}
diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts
index 6b0027ab4..bb5d878e7 100644
--- a/tests/routes/canvas-chat-helpers.test.ts
+++ b/tests/routes/canvas-chat-helpers.test.ts
@@ -56,6 +56,7 @@ import {
buildRecentDialogueMemory,
isRecentDialogueRecallRequest,
} from "@/lib/ai/runtime/recent-dialogue-memory";
+import { isCapabilityQuestionTurnRequest } from "@/lib/ai/runtime/task-boundary-text";
import {
isAgentModeScopedChatId,
isScopedEmptyHistoryRequest,
@@ -326,6 +327,13 @@ test.describe("canvas chat pure helpers", () => {
expect(upstream).toContain("| SKU | 销量 |");
});
+ test("recognizes canvas product capability questions with typo tolerance", () => {
+ expect(isCapabilityQuestionTurnRequest("你能为我做什么?")).toBe(true);
+ expect(isCapabilityQuestionTurnRequest("你嫩为我做什么?")).toBe(true);
+ expect(isCapabilityQuestionTurnRequest("你有哪些功能")).toBe(true);
+ expect(isCapabilityQuestionTurnRequest("帮我把这张图换成白底")).toBe(false);
+ });
+
test("builds bounded recent dialogue memory for user recall requests", () => {
const memory = buildRecentDialogueMemory({
messages: [