fix: restore canvas pending feedback and scoped capabilities
This commit is contained in:
parent
f7676716bb
commit
654829c4fd
@ -257,6 +257,7 @@ import {
|
|||||||
type AgentModelSlots,
|
type AgentModelSlots,
|
||||||
} from "@/lib/ai/runtime/agent-model-slots";
|
} from "@/lib/ai/runtime/agent-model-slots";
|
||||||
import { isPureGreetingTurnRequest } from "@/lib/ai/runtime/task-boundary-classifier";
|
import { isPureGreetingTurnRequest } from "@/lib/ai/runtime/task-boundary-classifier";
|
||||||
|
import { isCapabilityQuestionTurnRequest } from "@/lib/ai/runtime/task-boundary-text";
|
||||||
import {
|
import {
|
||||||
getProviderHealthSnapshot,
|
getProviderHealthSnapshot,
|
||||||
recordProviderHealthFailure,
|
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 CHAT_CREATION_LOOKUP_RETRY_DELAYS_MS = [60, 180, 360] as const;
|
||||||
const DETAIL_REPLACEMENT_PROMPT_PACK_REQUIRED_MESSAGE =
|
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));
|
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) =>
|
const hasCurrentMessageMediaInput = message.parts.some((part: any) =>
|
||||||
part?.type === "file" || part?.type === "image" || part?.type === "image_url"
|
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 (
|
if (
|
||||||
shouldRunAgentConversation &&
|
(shouldRunAgentConversation ||
|
||||||
isPureGreetingTurnRequest(requestTextContent) &&
|
isCanvasPageRequest ||
|
||||||
|
contextPayload?.source === "canvas" ||
|
||||||
|
Boolean(requestCanvasAgentContext)) &&
|
||||||
|
canvasFastPathMessage &&
|
||||||
!hasCurrentMessageMediaInput
|
!hasCurrentMessageMediaInput
|
||||||
) {
|
) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
@ -1245,7 +1270,6 @@ export async function POST(request: Request) {
|
|||||||
return new ChatSDKError("unauthorized:chat").toResponse();
|
return new ChatSDKError("unauthorized:chat").toResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
const greetingMessage = "你好,我在。你可以直接告诉我想做什么:生图、修图、生成视频、分析图片都可以。";
|
|
||||||
const fastPathTitle = requestTextContent.trim().slice(0, 80) || "新对话";
|
const fastPathTitle = requestTextContent.trim().slice(0, 80) || "新对话";
|
||||||
await saveChat({
|
await saveChat({
|
||||||
id,
|
id,
|
||||||
@ -1266,7 +1290,12 @@ export async function POST(request: Request) {
|
|||||||
const greetingStream = createUIMessageStream({
|
const greetingStream = createUIMessageStream({
|
||||||
execute: async ({ writer }) => {
|
execute: async ({ writer }) => {
|
||||||
const messageId = generateUUID();
|
const messageId = generateUUID();
|
||||||
writeAssistantTextChunk(writer, greetingMessage, "greeting-fast-path", messageId);
|
writeAssistantTextChunk(
|
||||||
|
writer,
|
||||||
|
canvasFastPathMessage,
|
||||||
|
isCapabilityQuestionRequest ? "capability-fast-path" : "greeting-fast-path",
|
||||||
|
messageId
|
||||||
|
);
|
||||||
|
|
||||||
await saveMessages({
|
await saveMessages({
|
||||||
messages: [
|
messages: [
|
||||||
@ -1287,7 +1316,7 @@ export async function POST(request: Request) {
|
|||||||
id: messageId,
|
id: messageId,
|
||||||
chatId: id,
|
chatId: id,
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
parts: [{ type: "text", text: greetingMessage }],
|
parts: [{ type: "text", text: canvasFastPathMessage }],
|
||||||
attachments: [],
|
attachments: [],
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
videoStorageStatus: null,
|
videoStorageStatus: null,
|
||||||
@ -8625,10 +8654,16 @@ export async function POST(request: Request) {
|
|||||||
selectedChatModel: finalModelId,
|
selectedChatModel: finalModelId,
|
||||||
requestHints,
|
requestHints,
|
||||||
});
|
});
|
||||||
const agentIdentityInstruction = shouldRunAgentConversation
|
const shouldUseProductIdentityInstruction =
|
||||||
|
shouldRunAgentConversation ||
|
||||||
|
isCanvasPageRequest ||
|
||||||
|
contextPayload?.source === "canvas" ||
|
||||||
|
Boolean(requestCanvasAgentContext);
|
||||||
|
const agentIdentityInstruction = shouldUseProductIdentityInstruction
|
||||||
? await buildAgentRoleIdentityInstruction({
|
? await buildAgentRoleIdentityInstruction({
|
||||||
allowVideoGeneration: allowAgentVideoGeneration,
|
allowVideoGeneration: allowAgentVideoGeneration,
|
||||||
hasCanvasContext: Boolean(
|
hasCanvasContext: Boolean(
|
||||||
|
isCanvasPageRequest ||
|
||||||
effectiveContextPayload?.source === "canvas" ||
|
effectiveContextPayload?.source === "canvas" ||
|
||||||
effectiveContextPayload?.selectedNodeIds?.length ||
|
effectiveContextPayload?.selectedNodeIds?.length ||
|
||||||
effectiveContextPayload?.canvasImageNodes?.length,
|
effectiveContextPayload?.canvasImageNodes?.length,
|
||||||
|
|||||||
@ -1595,21 +1595,15 @@ function OpenClawQuickActions({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) {
|
function CanvasAssistantTypingPlaceholder({
|
||||||
|
hint,
|
||||||
|
label = "分析中",
|
||||||
|
}: {
|
||||||
|
hint?: string | null;
|
||||||
|
label?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full justify-start">
|
<div className="flex w-full justify-start">
|
||||||
<style>{`
|
|
||||||
@keyframes clawdTypingDotBounce {
|
|
||||||
0%, 80%, 100% {
|
|
||||||
transform: translateY(0) scale(0.96);
|
|
||||||
opacity: 0.62;
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
transform: translateY(-2px) scale(1);
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
<div className="flex max-w-[min(78%,680px)] flex-col items-start gap-2">
|
<div className="flex max-w-[min(78%,680px)] flex-col items-start gap-2">
|
||||||
{hint ? (
|
{hint ? (
|
||||||
<div className="rounded-[18px] bg-[#f4f5f7] px-4 py-3 text-[15px] leading-6 text-[#2f343d] shadow-sm dark:bg-white/8 dark:text-zinc-200">
|
<div className="rounded-[18px] bg-[#f4f5f7] px-4 py-3 text-[15px] leading-6 text-[#2f343d] shadow-sm dark:bg-white/8 dark:text-zinc-200">
|
||||||
@ -1617,19 +1611,11 @@ function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div
|
<div
|
||||||
aria-label={`${CLAW_MODE_LABEL} 正在处理`}
|
aria-label={label}
|
||||||
className="inline-flex h-7 items-center gap-1 rounded-[14px] bg-[#f4f5f7] px-3 text-[#9aa3b2] dark:bg-white/8 dark:text-zinc-400"
|
className="inline-flex h-8 items-center gap-2 rounded-[16px] bg-[#f4f5f7] px-3.5 text-[15px] font-medium text-[#9aa3b2] dark:bg-white/8 dark:text-zinc-400"
|
||||||
>
|
>
|
||||||
{[0, 1, 2].map((index) => (
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
<span
|
<span>{label}</span>
|
||||||
key={index}
|
|
||||||
className="h-1.5 w-1.5 rounded-full bg-current"
|
|
||||||
style={{
|
|
||||||
animation: "clawdTypingDotBounce 0.9s ease-in-out infinite",
|
|
||||||
animationDelay: `${index * 140}ms`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -11910,9 +11896,11 @@ export function CanvasChatPanel({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isLoading = status === 'streaming' || status === 'submitted';
|
const isLoading = status === 'streaming' || status === 'submitted';
|
||||||
|
const activeChatSendGuard = getSendGuard(activeChatId);
|
||||||
|
const isActiveChatPending = isLoading || activeChatSendGuard.inFlight;
|
||||||
const isComposerUploading = Boolean(composerUploadProgress);
|
const isComposerUploading = Boolean(composerUploadProgress);
|
||||||
const isSubmitting =
|
const isSubmitting =
|
||||||
isLoading || isDetailGenerating || isClawdSubmitting || isComposerUploading;
|
isActiveChatPending || isDetailGenerating || isClawdSubmitting || isComposerUploading;
|
||||||
const hasInputOrAttachments = Boolean(
|
const hasInputOrAttachments = Boolean(
|
||||||
input.trim() || attachments.length > 0 || (!isDetailGenerationMode && selectedSkillTag)
|
input.trim() || attachments.length > 0 || (!isDetailGenerationMode && selectedSkillTag)
|
||||||
);
|
);
|
||||||
@ -12287,15 +12275,23 @@ export function CanvasChatPanel({
|
|||||||
const isDetailEmptyState =
|
const isDetailEmptyState =
|
||||||
isDetailGenerationMode &&
|
isDetailGenerationMode &&
|
||||||
renderableVisibleMessages.length === 0 &&
|
renderableVisibleMessages.length === 0 &&
|
||||||
!isLoading &&
|
!isActiveChatPending &&
|
||||||
!shouldShowImageLoading;
|
!shouldShowImageLoading;
|
||||||
const isPlainCanvasEmptyState =
|
const isPlainCanvasEmptyState =
|
||||||
!isWorkspacePresentation &&
|
!isWorkspacePresentation &&
|
||||||
!isDetailGenerationMode &&
|
!isDetailGenerationMode &&
|
||||||
!isVideoWorkspace &&
|
!isVideoWorkspace &&
|
||||||
renderableVisibleMessages.length === 0 &&
|
renderableVisibleMessages.length === 0 &&
|
||||||
!isLoading &&
|
!isActiveChatPending &&
|
||||||
!shouldShowImageLoading;
|
!shouldShowImageLoading;
|
||||||
|
const latestRenderableMessage = renderableVisibleMessages.at(-1);
|
||||||
|
const shouldRenderAssistantTypingPlaceholder =
|
||||||
|
!isClawdMode &&
|
||||||
|
!isDetailGenerationMode &&
|
||||||
|
!shouldRenderImageLoading &&
|
||||||
|
!hasActiveTypewriter &&
|
||||||
|
isActiveChatPending &&
|
||||||
|
latestRenderableMessage?.role === "user";
|
||||||
const workspaceEmptyTitle =
|
const workspaceEmptyTitle =
|
||||||
isClawdMode
|
isClawdMode
|
||||||
? `开始 ${CLAW_MODE_LABEL} 对话`
|
? `开始 ${CLAW_MODE_LABEL} 对话`
|
||||||
@ -13490,9 +13486,7 @@ export function CanvasChatPanel({
|
|||||||
shouldAnimateAnalysis ||
|
shouldAnimateAnalysis ||
|
||||||
shouldAnimatePlainText ||
|
shouldAnimatePlainText ||
|
||||||
isClawdSubmitting ||
|
isClawdSubmitting ||
|
||||||
getSendGuard(activeChatId).inFlight ||
|
isActiveChatPending
|
||||||
status === "streaming" ||
|
|
||||||
status === "submitted"
|
|
||||||
}
|
}
|
||||||
onAction={handleOpenClawQuickAction}
|
onAction={handleOpenClawQuickAction}
|
||||||
/>
|
/>
|
||||||
@ -13564,9 +13558,13 @@ export function CanvasChatPanel({
|
|||||||
estimatedWaitMs={loadingEstimatedWaitMs}
|
estimatedWaitMs={loadingEstimatedWaitMs}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{shouldRenderAssistantTypingPlaceholder ? (
|
||||||
|
<CanvasAssistantTypingPlaceholder label="分析中" />
|
||||||
|
) : null}
|
||||||
|
|
||||||
{isClawdMode && isClawdSubmitting ? (
|
{isClawdMode && isClawdSubmitting ? (
|
||||||
<CanvasAssistantTypingPlaceholder hint={clawdProcessingHint} />
|
<CanvasAssistantTypingPlaceholder hint={clawdProcessingHint} label="分析中" />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type {
|
|||||||
PendingGridConfirmation,
|
PendingGridConfirmation,
|
||||||
} from "@/lib/ai/conversation-action-resolver";
|
} from "@/lib/ai/conversation-action-resolver";
|
||||||
import { isExplicitPromptOptimizationRequest } from "@/lib/ai/smart-analyzer";
|
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";
|
import { detectStoryboardCommandIntent } from "@/lib/video-agent/storyboard-command-interpreter";
|
||||||
|
|
||||||
export type TaskBoundaryType =
|
export type TaskBoundaryType =
|
||||||
@ -88,9 +89,7 @@ function isLikelyChatOnlyRequest(text: string): boolean {
|
|||||||
const normalized = normalizeCompactText(text);
|
const normalized = normalizeCompactText(text);
|
||||||
if (!normalized) return false;
|
if (!normalized) return false;
|
||||||
|
|
||||||
return /你能做什么|你会什么|你可以做什么|你能帮我做什么|你能干什么|你可以帮我做什么|介绍一下你自己|介绍一下自己|说说你的能力|你有哪些功能|你有什么功能|怎么用|怎么玩|如何使用|帮助说明|帮助手册|功能介绍/.test(
|
return isCapabilityQuestionTurnRequest(normalized);
|
||||||
normalized,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLikelyAnalysisOnlyRequest(text: string, hasImageContext: boolean): boolean {
|
function isLikelyAnalysisOnlyRequest(text: string, hasImageContext: boolean): boolean {
|
||||||
|
|||||||
12
lib/ai/runtime/task-boundary-text.ts
Normal file
12
lib/ai/runtime/task-boundary-text.ts
Normal file
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -56,6 +56,7 @@ import {
|
|||||||
buildRecentDialogueMemory,
|
buildRecentDialogueMemory,
|
||||||
isRecentDialogueRecallRequest,
|
isRecentDialogueRecallRequest,
|
||||||
} from "@/lib/ai/runtime/recent-dialogue-memory";
|
} from "@/lib/ai/runtime/recent-dialogue-memory";
|
||||||
|
import { isCapabilityQuestionTurnRequest } from "@/lib/ai/runtime/task-boundary-text";
|
||||||
import {
|
import {
|
||||||
isAgentModeScopedChatId,
|
isAgentModeScopedChatId,
|
||||||
isScopedEmptyHistoryRequest,
|
isScopedEmptyHistoryRequest,
|
||||||
@ -326,6 +327,13 @@ test.describe("canvas chat pure helpers", () => {
|
|||||||
expect(upstream).toContain("| SKU | 销量 |");
|
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", () => {
|
test("builds bounded recent dialogue memory for user recall requests", () => {
|
||||||
const memory = buildRecentDialogueMemory({
|
const memory = buildRecentDialogueMemory({
|
||||||
messages: [
|
messages: [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user