fix: restore canvas pending feedback and scoped capabilities
This commit is contained in:
parent
f7676716bb
commit
654829c4fd
@ -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,
|
||||
|
||||
@ -1595,21 +1595,15 @@ function OpenClawQuickActions({
|
||||
);
|
||||
}
|
||||
|
||||
function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) {
|
||||
function CanvasAssistantTypingPlaceholder({
|
||||
hint,
|
||||
label = "分析中",
|
||||
}: {
|
||||
hint?: string | null;
|
||||
label?: string;
|
||||
}) {
|
||||
return (
|
||||
<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">
|
||||
{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">
|
||||
@ -1617,19 +1611,11 @@ function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) {
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
aria-label={`${CLAW_MODE_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"
|
||||
aria-label={label}
|
||||
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) => (
|
||||
<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`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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 ? (
|
||||
<CanvasAssistantTypingPlaceholder label="分析中" />
|
||||
) : null}
|
||||
|
||||
{isClawdMode && isClawdSubmitting ? (
|
||||
<CanvasAssistantTypingPlaceholder hint={clawdProcessingHint} />
|
||||
<CanvasAssistantTypingPlaceholder hint={clawdProcessingHint} label="分析中" />
|
||||
) : null}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
|
||||
@ -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 {
|
||||
|
||||
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,
|
||||
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: [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user