Compare commits
21 Commits
134411f7f8
...
7c1d985c09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c1d985c09 | ||
|
|
67b526408a | ||
|
|
f46c0b192d | ||
|
|
15fc9ebea9 | ||
|
|
961ce30500 | ||
|
|
0a66dbc0bc | ||
|
|
6b70865235 | ||
|
|
c60a3bb226 | ||
|
|
3828ec792d | ||
|
|
40ce24feb8 | ||
|
|
a201ebb346 | ||
|
|
bf5d10f99f | ||
|
|
56c1c96395 | ||
|
|
e4ec78f56e | ||
|
|
5d4286f7c2 | ||
|
|
376881d9f9 | ||
|
|
abfc813e42 | ||
|
|
fde5661436 | ||
|
|
bf8a03cf73 | ||
|
|
6b29a3acf7 | ||
|
|
82b277bdbd |
@ -250,21 +250,18 @@ import {
|
||||
pruneTaskBoundaryContext,
|
||||
pruneTaskBoundaryHistoryMessages,
|
||||
} from "@/lib/ai/runtime/task-boundary-context-pruner";
|
||||
import { buildRecentDialogueMemory } from "@/lib/ai/runtime/recent-dialogue-memory";
|
||||
import {
|
||||
resolveAgentModelSlotDescriptor,
|
||||
upsertAgentModelSlot,
|
||||
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,
|
||||
recordProviderHealthSuccess,
|
||||
shouldAvoidProvider,
|
||||
} from "@/lib/ai/runtime/provider-health";
|
||||
import { resolveCanvasAgentRequestPolicy } from "@/lib/ai/runtime/canvas-agent-kernel";
|
||||
import { arbitrateAgentIntent } from "@/lib/ai/runtime/intent-arbiter";
|
||||
import { buildAgentRoleIdentityInstruction } from "@/lib/ai/runtime/agent-role-profile";
|
||||
import { getVideoStoryboardConfig } from "@/lib/video-agent/config";
|
||||
@ -544,65 +541,6 @@ function writeCanvasOperationShadowChunk(params: {
|
||||
);
|
||||
}
|
||||
|
||||
const AGENT_EXECUTION_PREFACE_INTERNAL_PATTERN =
|
||||
/(?:^|\n)\s*(?:#{1,6}\s*)?(?:需求理解|图片分析|执行计划|输出设置|优化后提示词|使用建议)(?:\s|[::]|$)|【\s*(?:需求理解|图片分析|执行计划|输出设置|优化后提示词|使用建议)\s*】|提示词|自动优化|模型|提交生成|回写|流程|步骤/;
|
||||
|
||||
function buildAgentExecutionPrefaceText(params: {
|
||||
friendlyMessage?: string | null;
|
||||
fallback: string;
|
||||
}) {
|
||||
const fallback = String(params.fallback || "").replace(/\s+/g, " ").trim();
|
||||
const rawMessage = String(params.friendlyMessage || "")
|
||||
.replace(/<!--[\s\S]*?-->/g, "")
|
||||
.trim();
|
||||
|
||||
if (!rawMessage || AGENT_EXECUTION_PREFACE_INTERNAL_PATTERN.test(rawMessage)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const candidate = rawMessage
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.replace(/^[-*]\s*/, "").trim())
|
||||
.find(Boolean);
|
||||
|
||||
if (!candidate || AGENT_EXECUTION_PREFACE_INTERNAL_PATTERN.test(candidate)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const firstSentence = candidate.match(/^(.{1,80}?[。!?!?~~])/u)?.[1] || candidate;
|
||||
const compact = firstSentence.replace(/\s+/g, " ").trim().slice(0, 80);
|
||||
return /[。!?!?~~]$/.test(compact) ? compact : `${compact}。`;
|
||||
}
|
||||
|
||||
async function writeAgentExecutionPrefaceMessage(params: {
|
||||
writer: StreamWriter;
|
||||
chatId: string;
|
||||
label: string;
|
||||
friendlyMessage?: string | null;
|
||||
fallback: string;
|
||||
}) {
|
||||
const text = buildAgentExecutionPrefaceText({
|
||||
friendlyMessage: params.friendlyMessage,
|
||||
fallback: params.fallback,
|
||||
});
|
||||
if (!text) return null;
|
||||
|
||||
const messageId = generateUUID();
|
||||
writeAssistantTextChunk(params.writer, text, params.label, messageId);
|
||||
|
||||
try {
|
||||
await saveAssistantTextOnlyMessage({
|
||||
chatId: params.chatId,
|
||||
messageId,
|
||||
text,
|
||||
});
|
||||
} catch (error) {
|
||||
chatApiLogger.warn("[Chat API] Failed to persist agent execution preface:", error);
|
||||
}
|
||||
|
||||
return messageId;
|
||||
}
|
||||
|
||||
async function replayAssistantMessagesForDuplicateRequest({
|
||||
chatId,
|
||||
userMessageId,
|
||||
@ -847,19 +785,6 @@ 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));
|
||||
|
||||
@ -1100,10 +1025,6 @@ export async function POST(request: Request) {
|
||||
let confirmedPromptForConversationState: string | undefined;
|
||||
let smartAutoHistoryMessagesFromDb: DBMessage[] = [];
|
||||
let smartAutoMediaHistoryMessagesFromDb: DBMessage[] = [];
|
||||
let recentDialogueMemory = buildRecentDialogueMemory({
|
||||
messages: [],
|
||||
currentText: requestTextContent,
|
||||
});
|
||||
let preferredAgentModelId: string | undefined;
|
||||
let configuredAgentConversationModelId: string | undefined;
|
||||
let agentModelSlots: AgentModelSlots = {};
|
||||
@ -1115,19 +1036,8 @@ export async function POST(request: Request) {
|
||||
const shouldRespectDirectComposerMode =
|
||||
composerMode === "image" || composerMode === "video" || composerMode === "detail";
|
||||
const shouldIsolateDirectComposerContext = shouldRespectDirectComposerMode;
|
||||
const requestPolicy = resolveCanvasAgentRequestPolicy({
|
||||
composerMode,
|
||||
agentEntryMode: agentPresetId ? "preset" : "auto",
|
||||
selectedChatModel: finalModelId,
|
||||
hasCanvasControlIntentFastPath,
|
||||
});
|
||||
finalModelId = requestPolicy.effectiveSelectedChatModel;
|
||||
let shouldRunAgentConversation =
|
||||
finalModelId === "auto" || requestPolicy.shouldForceToolOrchestration;
|
||||
let shouldRunAgentConversation = finalModelId === "auto";
|
||||
let shouldKeepSelectedModel = false;
|
||||
if (requestPolicy.reason === "agent_auto_composer") {
|
||||
chatApiLogger.info("[Smart Auto Mode] 🧭 Agent composer forces tool orchestration, ignoring stale selected model");
|
||||
}
|
||||
if (shouldRespectDirectComposerMode && shouldRunAgentConversation) {
|
||||
shouldRunAgentConversation = false;
|
||||
chatApiLogger.info(`[Smart Auto Mode] ⛳ Direct composer mode (${composerMode}) disables initial auto-agent routing`);
|
||||
@ -1248,20 +1158,9 @@ 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 ||
|
||||
isCanvasPageRequest ||
|
||||
contextPayload?.source === "canvas" ||
|
||||
Boolean(requestCanvasAgentContext)) &&
|
||||
canvasFastPathMessage &&
|
||||
shouldRunAgentConversation &&
|
||||
isPureGreetingTurnRequest(requestTextContent) &&
|
||||
!hasCurrentMessageMediaInput
|
||||
) {
|
||||
const session = await auth();
|
||||
@ -1270,6 +1169,7 @@ export async function POST(request: Request) {
|
||||
return new ChatSDKError("unauthorized:chat").toResponse();
|
||||
}
|
||||
|
||||
const greetingMessage = "你好,我在。你可以直接告诉我想做什么:生图、修图、生成视频、分析图片都可以。";
|
||||
const fastPathTitle = requestTextContent.trim().slice(0, 80) || "新对话";
|
||||
await saveChat({
|
||||
id,
|
||||
@ -1290,12 +1190,7 @@ export async function POST(request: Request) {
|
||||
const greetingStream = createUIMessageStream({
|
||||
execute: async ({ writer }) => {
|
||||
const messageId = generateUUID();
|
||||
writeAssistantTextChunk(
|
||||
writer,
|
||||
canvasFastPathMessage,
|
||||
isCapabilityQuestionRequest ? "capability-fast-path" : "greeting-fast-path",
|
||||
messageId
|
||||
);
|
||||
writeAssistantTextChunk(writer, greetingMessage, "greeting-fast-path", messageId);
|
||||
|
||||
await saveMessages({
|
||||
messages: [
|
||||
@ -1316,7 +1211,7 @@ export async function POST(request: Request) {
|
||||
id: messageId,
|
||||
chatId: id,
|
||||
role: "assistant",
|
||||
parts: [{ type: "text", text: canvasFastPathMessage }],
|
||||
parts: [{ type: "text", text: greetingMessage }],
|
||||
attachments: [],
|
||||
createdAt: new Date(),
|
||||
videoStorageStatus: null,
|
||||
@ -1523,17 +1418,6 @@ export async function POST(request: Request) {
|
||||
taskType: taskBoundaryDecision.taskType,
|
||||
taskMode: taskBoundaryDecision.taskMode,
|
||||
});
|
||||
recentDialogueMemory = buildRecentDialogueMemory({
|
||||
messages: loadedHistoryMessages,
|
||||
currentText: smartAutoInputText,
|
||||
maxEntries: taskBoundaryDecision.taskType === "chat" ? 6 : 4,
|
||||
maxTotalChars: taskBoundaryDecision.taskType === "chat" ? 1000 : 700,
|
||||
});
|
||||
chatApiLogger.info("[RecentDialogueMemory] Smart auto recall:", {
|
||||
entries: recentDialogueMemory.entries.length,
|
||||
isRecallRequest: recentDialogueMemory.isRecallRequest,
|
||||
taskType: taskBoundaryDecision.taskType,
|
||||
});
|
||||
|
||||
autoTextForFallback = smartAutoInputText;
|
||||
if (!requestWorkingState && !shouldIsolateDirectComposerContext) {
|
||||
@ -1608,12 +1492,23 @@ export async function POST(request: Request) {
|
||||
resolutionConfidence: requestTargetResolution.confidence,
|
||||
});
|
||||
|
||||
const historyMessages: { role: "user" | "assistant"; content: string }[] =
|
||||
recentDialogueMemory.entries.map((entry) => ({
|
||||
role: entry.role,
|
||||
content: entry.content,
|
||||
}));
|
||||
chatApiLogger.info("[Smart Auto Mode] History context:", historyMessages.length, "memory entries");
|
||||
// 🆕 从历史消息中提取对话文本(用于上下文理解)
|
||||
const historyMessages: { role: "user" | "assistant"; content: string }[] = [];
|
||||
for (const msg of smartAutoHistoryMessagesFromDb.slice(-10)) { // 最多取最近10条
|
||||
const parts = msg.parts as any[];
|
||||
if (parts && parts.length > 0) {
|
||||
// 提取文本内容
|
||||
const textParts = parts.filter((p: any) => p.type === 'text');
|
||||
const content = textParts.map((p: any) => p.text || '').join(' ').trim();
|
||||
if (content) {
|
||||
historyMessages.push({
|
||||
role: msg.role as "user" | "assistant",
|
||||
content: content.substring(0, 300), // 限制长度
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
chatApiLogger.info("[Smart Auto Mode] History context:", historyMessages.length, "messages");
|
||||
|
||||
// 2. 使用 LangGraph 驱动的自动模式工作流
|
||||
const { runAutoAgentGraph } = await import("@/lib/ai/auto-agent-graph");
|
||||
@ -1646,15 +1541,8 @@ export async function POST(request: Request) {
|
||||
workflowState: composedConversationState.threadState.workflowState,
|
||||
memoryHints: composedConversationState.threadState.memoryHints,
|
||||
contextSummary: composedConversationState.threadState.contextSummary,
|
||||
recentDialogueMemory: recentDialogueMemory.contextSummary,
|
||||
},
|
||||
});
|
||||
const agentGraphContext = [
|
||||
composedConversationState.composedConversationMemory.contextSummary,
|
||||
recentDialogueMemory.contextSummary,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
const autoGraphResult = await withOperationTimeout(
|
||||
runAutoAgentGraph({
|
||||
text: smartAutoInputText,
|
||||
@ -1682,7 +1570,10 @@ export async function POST(request: Request) {
|
||||
requestResolvedMediaInput.historyUserImageUrls.length,
|
||||
currentUploadImageCount: requestResolvedMediaInput.currentMessageImageUrls.length,
|
||||
historyMessages,
|
||||
context: agentGraphContext || effectiveAutoContextText || undefined,
|
||||
context:
|
||||
composedConversationState.composedConversationMemory.contextSummary ||
|
||||
effectiveAutoContextText ||
|
||||
undefined,
|
||||
preferredModelId: preferredAgentModelId,
|
||||
threadState: composedConversationState.threadState,
|
||||
}),
|
||||
@ -2785,21 +2676,6 @@ export async function POST(request: Request) {
|
||||
purpose: "media",
|
||||
hasCurrentImageInput: hasImageInputForConversationAction,
|
||||
});
|
||||
recentDialogueMemory = shouldIsolateDirectComposerContext
|
||||
? recentDialogueMemory
|
||||
: buildRecentDialogueMemory({
|
||||
messages: loadedMessagesForBoundary,
|
||||
currentText: requestTextContent,
|
||||
maxEntries: taskBoundaryDecision.taskType === "chat" ? 6 : 4,
|
||||
maxTotalChars: taskBoundaryDecision.taskType === "chat" ? 1000 : 700,
|
||||
});
|
||||
if (recentDialogueMemory.entries.length > 0) {
|
||||
chatApiLogger.info("[RecentDialogueMemory] Request recall:", {
|
||||
entries: recentDialogueMemory.entries.length,
|
||||
isRecallRequest: recentDialogueMemory.isRecallRequest,
|
||||
taskType: taskBoundaryDecision.taskType,
|
||||
});
|
||||
}
|
||||
|
||||
if (!requestWorkingState && !shouldIsolateDirectComposerContext) {
|
||||
requestWorkingState = await getConversationWorkingStateByChatId({
|
||||
@ -7839,16 +7715,6 @@ export async function POST(request: Request) {
|
||||
) {
|
||||
const directImageStream = createUIMessageStream({
|
||||
execute: async ({ writer }) => {
|
||||
await writeAgentExecutionPrefaceMessage({
|
||||
writer,
|
||||
chatId: id,
|
||||
label: "agent:generate_image:preface",
|
||||
friendlyMessage: smartAutoFriendlyMessage,
|
||||
fallback:
|
||||
directAgentToolTargetImageUrls.length > 0
|
||||
? "收到,我会参考你选的图片来生成新画面,稍等一下。"
|
||||
: "好的,我按你的描述来做一版画面,稍等我处理一下。",
|
||||
});
|
||||
writeAgentProgressChunk(
|
||||
writer,
|
||||
buildDirectAgentImageContextProgress({
|
||||
@ -7896,32 +7762,6 @@ export async function POST(request: Request) {
|
||||
operationLabel: "agent-generate-image",
|
||||
resultType: "image",
|
||||
});
|
||||
const directImageStatusPayload = {
|
||||
providerType: requestedModelProviderType,
|
||||
modelName: finalModelId,
|
||||
imageCount:
|
||||
effectiveImageParams?.imageCount ||
|
||||
defaultImageQualityParams.imageCount ||
|
||||
1,
|
||||
};
|
||||
const stopDirectImageKeepalive = startStreamKeepalive(
|
||||
writer,
|
||||
"agent:generate_image",
|
||||
DEFAULT_STREAM_KEEPALIVE_INTERVAL_MS,
|
||||
);
|
||||
const stopDirectImageStatus =
|
||||
contextPayload?.source === "canvas"
|
||||
? setInterval(() => {
|
||||
writeImageGenerationStatusChunk(
|
||||
writer,
|
||||
{
|
||||
phase: "processing",
|
||||
...directImageStatusPayload,
|
||||
},
|
||||
"agent:generate_image:status-processing-keepalive",
|
||||
);
|
||||
}, DEFAULT_STREAM_KEEPALIVE_INTERVAL_MS)
|
||||
: null;
|
||||
try {
|
||||
const directImageResult = await executeDirectAgentImageGeneration({
|
||||
requestTextContent,
|
||||
@ -8063,11 +7903,6 @@ export async function POST(request: Request) {
|
||||
"<!-- IMAGE_GENERATION_END -->",
|
||||
"agent:generate_image:end-error",
|
||||
);
|
||||
} finally {
|
||||
stopDirectImageKeepalive();
|
||||
if (stopDirectImageStatus) {
|
||||
clearInterval(stopDirectImageStatus);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -8082,16 +7917,6 @@ export async function POST(request: Request) {
|
||||
) {
|
||||
const directVideoStream = createUIMessageStream({
|
||||
execute: async ({ writer }) => {
|
||||
await writeAgentExecutionPrefaceMessage({
|
||||
writer,
|
||||
chatId: id,
|
||||
label: "agent:generate_video:preface",
|
||||
friendlyMessage: smartAutoFriendlyMessage,
|
||||
fallback:
|
||||
directAgentToolTargetImageUrls.length > 0
|
||||
? "收到,我会参考你选的画面来生成视频,稍等一下。"
|
||||
: "好的,我按你的需求来做这段视频,稍等我处理一下。",
|
||||
});
|
||||
const assistantMessageId = generateUUID();
|
||||
writeAgentProgressChunk(
|
||||
writer,
|
||||
@ -8298,13 +8123,6 @@ export async function POST(request: Request) {
|
||||
const directToolLabel = getDirectAgentToolLabel(directAgentToolCapability);
|
||||
const directToolStream = createUIMessageStream({
|
||||
execute: async ({ writer }) => {
|
||||
await writeAgentExecutionPrefaceMessage({
|
||||
writer,
|
||||
chatId: id,
|
||||
label: `agent:${directAgentToolCapability}:preface`,
|
||||
friendlyMessage: smartAutoFriendlyMessage,
|
||||
fallback: `好的,我来帮你${directToolLabel},稍等我处理一下。`,
|
||||
});
|
||||
writeAgentProgressChunk(
|
||||
writer,
|
||||
buildDirectAgentImageContextProgress({
|
||||
@ -8675,39 +8493,16 @@ export async function POST(request: Request) {
|
||||
resolvedRuntimeModelLookupId = actualModelId;
|
||||
}
|
||||
|
||||
const recentDialogueSystemContext = recentDialogueMemory.contextSummary
|
||||
? [
|
||||
recentDialogueMemory.contextSummary,
|
||||
...recentDialogueMemory.memoryHints,
|
||||
].join("\n")
|
||||
: "";
|
||||
const baseConversationSystemPrompt = systemPrompt({
|
||||
selectedChatModel: finalModelId,
|
||||
requestHints,
|
||||
});
|
||||
const shouldUseProductIdentityInstruction =
|
||||
shouldRunAgentConversation ||
|
||||
isCanvasPageRequest ||
|
||||
contextPayload?.source === "canvas" ||
|
||||
Boolean(requestCanvasAgentContext);
|
||||
const agentIdentityInstruction = shouldUseProductIdentityInstruction
|
||||
? await buildAgentRoleIdentityInstruction({
|
||||
const effectiveConversationSystemPrompt = shouldRunAgentConversation
|
||||
? `${await buildAgentRoleIdentityInstruction({
|
||||
allowVideoGeneration: allowAgentVideoGeneration,
|
||||
hasCanvasContext: Boolean(
|
||||
isCanvasPageRequest ||
|
||||
effectiveContextPayload?.source === "canvas" ||
|
||||
effectiveContextPayload?.selectedNodeIds?.length ||
|
||||
effectiveContextPayload?.canvasImageNodes?.length,
|
||||
),
|
||||
})
|
||||
: "";
|
||||
const effectiveConversationSystemPrompt = [
|
||||
agentIdentityInstruction,
|
||||
recentDialogueSystemContext,
|
||||
baseConversationSystemPrompt,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
})}\n\n${systemPrompt({ selectedChatModel: finalModelId, requestHints })}`
|
||||
: systemPrompt({ selectedChatModel: finalModelId, requestHints });
|
||||
|
||||
const activeRuntimeModelDescriptor = await resolveAgentModelSlotDescriptor(
|
||||
resolvedRuntimeModelLookupId,
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { getClawdChatsByUserId } from "@/lib/db/queries";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth().catch(() => null);
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const limit = Math.max(
|
||||
1,
|
||||
Math.min(100, Math.round(Number(url.searchParams.get("limit") || 50)))
|
||||
);
|
||||
|
||||
const chats = await getClawdChatsByUserId({
|
||||
userId: session.user.id,
|
||||
limit,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
chats: chats.map((chat) => ({
|
||||
id: chat.id,
|
||||
title: chat.title,
|
||||
createdAt: chat.createdAt?.toISOString?.() || chat.createdAt,
|
||||
updatedAt: chat.updatedAt?.toISOString?.() || chat.updatedAt,
|
||||
messageCount: Number(chat.messageCount || 0),
|
||||
})),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control": "no-store, max-age=0",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -126,12 +126,14 @@ type ScaleAdjustCropReference = {
|
||||
type ScaleAdjustPatchReference = {
|
||||
backgroundPlateFullUrl?: string | null;
|
||||
compositeClipMaskUrl?: string | null;
|
||||
productLockBackgroundRepairMaskUrl?: string | null;
|
||||
productLockMethod?: string | null;
|
||||
productLockEditMaskUrl?: string | null;
|
||||
productLockFalEditMaskUrl?: string | null;
|
||||
productLockOverlayUrl?: string | null;
|
||||
productLockProtectMaskUrl?: string | null;
|
||||
sourceCropUrl: string;
|
||||
sourceFullUrl?: string | null;
|
||||
structureLockedDraft?: boolean;
|
||||
targetDraftFullUrl?: string | null;
|
||||
targetDraftUrl: string;
|
||||
@ -469,6 +471,22 @@ function resolveScaleAdjustPreferredModelId(params: {
|
||||
const maskCapableGptImage2 = gptImage2Candidates
|
||||
.filter(isScaleAdjustMaskCapableModel)
|
||||
.filter((entry) => entry.model.healthStatus !== "auto_disabled");
|
||||
const requestedMaskCapableGptImage2 = requestedModel
|
||||
? maskCapableGptImage2.find(
|
||||
(entry) =>
|
||||
entry.model.id === requestedModel.model.id ||
|
||||
entry.model.modelId === requestedModel.model.modelId
|
||||
)
|
||||
: null;
|
||||
if (requestedMaskCapableGptImage2) {
|
||||
return requestedMaskCapableGptImage2.model.id;
|
||||
}
|
||||
const nonOpenAiMaskCapableGptImage2 = maskCapableGptImage2.find(
|
||||
(entry) => entry.provider.type !== "openai-image" && isScaleAdjustHealthyModel(entry)
|
||||
);
|
||||
if (nonOpenAiMaskCapableGptImage2) {
|
||||
return nonOpenAiMaskCapableGptImage2.model.id;
|
||||
}
|
||||
if (maskCapableGptImage2[0]) {
|
||||
return maskCapableGptImage2[0].model.id;
|
||||
}
|
||||
@ -2293,6 +2311,47 @@ async function buildScaleAdjustOpaqueEditMaskFromOpacityMask(params: {
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
async function buildScaleAdjustProductLockBackgroundRepairMaskBuffer(params: {
|
||||
productSourceMaskBuffer: Buffer;
|
||||
sourceHeight: number;
|
||||
sourceWidth: number;
|
||||
}) {
|
||||
const {
|
||||
productSourceMaskBuffer,
|
||||
sourceHeight,
|
||||
sourceWidth,
|
||||
} = params;
|
||||
const productAlpha = await sharp(productSourceMaskBuffer, { failOn: "none" })
|
||||
.rotate()
|
||||
.resize(sourceWidth, sourceHeight, { fit: "fill" })
|
||||
.ensureAlpha()
|
||||
.extractChannel(3)
|
||||
.greyscale()
|
||||
.raw()
|
||||
.toBuffer();
|
||||
const minSide = Math.max(1, Math.min(sourceWidth, sourceHeight));
|
||||
const repairAlpha = await sharp(productAlpha, {
|
||||
raw: { channels: 1, height: sourceHeight, width: sourceWidth },
|
||||
})
|
||||
.blur(Math.max(1.4, Math.min(10, Math.round(minSide * 0.0045))))
|
||||
.raw()
|
||||
.toBuffer();
|
||||
const rgba = Buffer.alloc(sourceWidth * sourceHeight * 4);
|
||||
for (let index = 0; index < sourceWidth * sourceHeight; index += 1) {
|
||||
const offset = index * 4;
|
||||
const value = Math.max(productAlpha[index] || 0, repairAlpha[index] || 0);
|
||||
rgba[offset] = 255;
|
||||
rgba[offset + 1] = 255;
|
||||
rgba[offset + 2] = 255;
|
||||
rgba[offset + 3] = value;
|
||||
}
|
||||
return sharp(rgba, {
|
||||
raw: { channels: 4, height: sourceHeight, width: sourceWidth },
|
||||
})
|
||||
.png({ compressionLevel: 9 })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
function getScaleAdjustAlphaBoundingBox(params: {
|
||||
height: number;
|
||||
rawRgba: Buffer;
|
||||
@ -2523,6 +2582,7 @@ async function resizeScaleAdjustAlphaMask(params: {
|
||||
|
||||
async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
placement: ReturnType<typeof getScaleAdjustVisualGuidePlacement>;
|
||||
preferPixelStructure?: boolean;
|
||||
protectedPersonSide?: ScaleAdjustProtectedPersonSide | null;
|
||||
raw: Buffer;
|
||||
sourceBuffer: Buffer;
|
||||
@ -2532,6 +2592,7 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
}): Promise<ScaleAdjustStructureLockedComposite | null> {
|
||||
const {
|
||||
placement,
|
||||
preferPixelStructure = false,
|
||||
protectedPersonSide = null,
|
||||
raw,
|
||||
sourceBuffer,
|
||||
@ -2550,7 +2611,7 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
userId,
|
||||
imageBase64: sourceCropBuffer.toString("base64"),
|
||||
imageBuffer: sourceCropBuffer,
|
||||
prompt: protectedPersonSide
|
||||
prompt: preferPixelStructure || protectedPersonSide
|
||||
? "比例调整家具/商品抠图:只保留框选区域内需要缩放的目标家具/商品主体和它本身的配件结构,明确排除非目标人物、手、脚、腿、衣服、书本、身体遮挡物和背景,输出透明背景 PNG"
|
||||
: "比例调整主体抠图:只保留框选的目标人物/家具/商品主体,输出透明背景 PNG",
|
||||
skipGallerySave: true,
|
||||
@ -2605,16 +2666,9 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
}),
|
||||
})
|
||||
: null;
|
||||
const protectedPersonRemovalMask =
|
||||
protectedPersonMask ||
|
||||
(protectedPersonSide && sameSizeCutout
|
||||
? buildScaleAdjustProtectedPersonGeometryMask({
|
||||
box: { width: cutoutWidth, height: cutoutHeight },
|
||||
side: protectedPersonSide,
|
||||
})
|
||||
: null);
|
||||
const protectedPersonRemovalMask = protectedPersonMask;
|
||||
const sideProtectedCleanedAlpha =
|
||||
protectedPersonSide && sameSizeCutout && sourceCropRaw
|
||||
protectedPersonSide && sameSizeCutout && sourceCropRaw && !preferPixelStructure
|
||||
? suppressScaleAdjustProtectedFurnitureDarkAlpha({
|
||||
alphaMask: Buffer.from(cleanedAlpha),
|
||||
bbox: { left: 0, top: 0, width: cutoutWidth, height: cutoutHeight },
|
||||
@ -2837,14 +2891,7 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
.toBuffer();
|
||||
const targetClipMaskPatch =
|
||||
await buildScaleAdjustOverlayClipMaskPatch(structureOverlay);
|
||||
const productLockProtectMask =
|
||||
protectedPersonRemovalMask ||
|
||||
(protectedPersonSide
|
||||
? buildScaleAdjustProtectedPersonGeometryMask({
|
||||
box: placement.sourceBox,
|
||||
side: protectedPersonSide,
|
||||
})
|
||||
: null);
|
||||
const productLockProtectMask = protectedPersonMask;
|
||||
const productLockProtectMaskPatch = productLockProtectMask
|
||||
? await buildScaleAdjustCompositeClipMaskPatch({
|
||||
box: placement.sourceBox,
|
||||
@ -2874,7 +2921,7 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
placedWidth,
|
||||
protectedPersonSide,
|
||||
protectedPersonGeometryMaskApplied:
|
||||
Boolean(protectedPersonRemovalMask) && !protectedPersonMask,
|
||||
false,
|
||||
protectedPersonMaskApplied: Boolean(protectedPersonMask),
|
||||
sameSizeCutout,
|
||||
alphaBandingStats,
|
||||
@ -3128,7 +3175,7 @@ async function buildScaleAdjustStructureLockedComposite(params: {
|
||||
} = params;
|
||||
const allowRmbgStructureLock =
|
||||
process.env.CANVAS_SCALE_ADJUST_ENABLE_RMBG_PRODUCT_LOCK !== "0" &&
|
||||
((preferPixelStructure && Boolean(protectedPersonSide)) ||
|
||||
(preferPixelStructure ||
|
||||
process.env.CANVAS_SCALE_ADJUST_ENABLE_RMBG_STRUCTURE === "1" ||
|
||||
(includePersonPixels &&
|
||||
process.env.CANVAS_SCALE_ADJUST_DISABLE_PERSON_RMBG !== "1"));
|
||||
@ -3136,6 +3183,7 @@ async function buildScaleAdjustStructureLockedComposite(params: {
|
||||
? null
|
||||
: await buildScaleAdjustRmbgStructureLockedComposite({
|
||||
placement,
|
||||
preferPixelStructure,
|
||||
protectedPersonSide,
|
||||
raw,
|
||||
sourceBuffer,
|
||||
@ -3552,9 +3600,7 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
||||
const preferPixelStructure = isScaleAdjustFurnitureProductLikePrompt(
|
||||
region.prompt
|
||||
);
|
||||
const protectedPersonSide = preferPixelStructure
|
||||
? getScaleAdjustProtectedPersonSide(region.prompt)
|
||||
: null;
|
||||
const protectedPersonSide = getScaleAdjustProtectedPersonSide(region.prompt);
|
||||
const hasPersonPixels =
|
||||
!preferPixelStructure &&
|
||||
!!sourceRaw &&
|
||||
@ -3761,19 +3807,35 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
||||
width: sourceWidth,
|
||||
})
|
||||
: null;
|
||||
const sourceOnlyRepairMaskBuffer = compositeClipComposites.length
|
||||
? await buildScaleAdjustCompositeClipMaskBuffer({
|
||||
height: sourceHeight,
|
||||
sourceMaskComposites: compositeClipComposites,
|
||||
targetMaskComposites: [],
|
||||
width: sourceWidth,
|
||||
})
|
||||
: null;
|
||||
const productLockBackgroundRepairMaskBuffer =
|
||||
productLockComposites.length && sourceOnlyRepairMaskBuffer
|
||||
? await buildScaleAdjustProductLockBackgroundRepairMaskBuffer({
|
||||
productSourceMaskBuffer: sourceOnlyRepairMaskBuffer,
|
||||
sourceHeight,
|
||||
sourceWidth,
|
||||
})
|
||||
: null;
|
||||
const productLockEditMaskBuffer =
|
||||
productLockComposites.length && compositeClipMaskBuffer
|
||||
productLockComposites.length && productLockBackgroundRepairMaskBuffer
|
||||
? await buildScaleAdjustTransparentEditMaskFromOpacityMask({
|
||||
height: sourceHeight,
|
||||
maskBuffer: compositeClipMaskBuffer,
|
||||
maskBuffer: productLockBackgroundRepairMaskBuffer,
|
||||
width: sourceWidth,
|
||||
})
|
||||
: null;
|
||||
const productLockFalEditMaskBuffer =
|
||||
productLockComposites.length && compositeClipMaskBuffer
|
||||
productLockComposites.length && productLockBackgroundRepairMaskBuffer
|
||||
? await buildScaleAdjustOpaqueEditMaskFromOpacityMask({
|
||||
height: sourceHeight,
|
||||
maskBuffer: compositeClipMaskBuffer,
|
||||
maskBuffer: productLockBackgroundRepairMaskBuffer,
|
||||
width: sourceWidth,
|
||||
})
|
||||
: null;
|
||||
@ -3782,6 +3844,7 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
||||
backgroundPlateBuffer,
|
||||
buffer,
|
||||
compositeClipMaskBuffer,
|
||||
productLockBackgroundRepairMaskBuffer,
|
||||
productLockEditMaskBuffer,
|
||||
productLockFalEditMaskBuffer,
|
||||
productLockOverlayBuffer,
|
||||
@ -3791,6 +3854,7 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
||||
|
||||
async function createScaleAdjustPatchReference(params: {
|
||||
fullFrameReferenceOnly?: boolean;
|
||||
minimalProductLockMaskEditReferences?: boolean;
|
||||
regions: NormalizedLocalEditRegion[];
|
||||
scalePercent?: ScaleAdjustPercent | null;
|
||||
sourceBuffer: Buffer;
|
||||
@ -3829,15 +3893,80 @@ async function createScaleAdjustPatchReference(params: {
|
||||
const targetDraftBuffer = targetDraftReference.buffer;
|
||||
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
if (params.fullFrameReferenceOnly && structureLockAppliedRef.value) {
|
||||
if (
|
||||
params.minimalProductLockMaskEditReferences &&
|
||||
sourceImageUrl &&
|
||||
targetDraftReference.productLockOverlayBuffer &&
|
||||
targetDraftReference.productLockEditMaskBuffer
|
||||
) {
|
||||
const productLockOverlayUpload = await uploadFile(
|
||||
`canvas-scale-adjust-product-lock-overlay-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.productLockOverlayBuffer)
|
||||
);
|
||||
const productLockProtectMaskUpload = targetDraftReference.productLockProtectMaskBuffer
|
||||
? await uploadFile(
|
||||
`canvas-scale-adjust-product-lock-protect-mask-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.productLockProtectMaskBuffer)
|
||||
)
|
||||
: null;
|
||||
const productLockBackgroundRepairMaskUpload =
|
||||
targetDraftReference.productLockBackgroundRepairMaskBuffer
|
||||
? await uploadFile(
|
||||
`canvas-scale-adjust-product-lock-background-repair-mask-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.productLockBackgroundRepairMaskBuffer)
|
||||
)
|
||||
: null;
|
||||
const productLockEditMaskUpload = await uploadFile(
|
||||
`canvas-scale-adjust-product-lock-edit-mask-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.productLockEditMaskBuffer)
|
||||
);
|
||||
const productLockFalEditMaskUpload = targetDraftReference.productLockFalEditMaskBuffer
|
||||
? await uploadFile(
|
||||
`canvas-scale-adjust-product-lock-fal-edit-mask-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.productLockFalEditMaskBuffer)
|
||||
)
|
||||
: null;
|
||||
|
||||
return {
|
||||
backgroundPlateFullUrl: null,
|
||||
compositeClipMaskUrl: null,
|
||||
productLockBackgroundRepairMaskUrl:
|
||||
productLockBackgroundRepairMaskUpload?.url || null,
|
||||
productLockMethod: "rmbg-cutout",
|
||||
productLockEditMaskUrl: productLockEditMaskUpload.url,
|
||||
productLockFalEditMaskUrl: productLockFalEditMaskUpload?.url || null,
|
||||
productLockOverlayUrl: productLockOverlayUpload.url,
|
||||
productLockProtectMaskUrl: productLockProtectMaskUpload?.url || null,
|
||||
sourceCropUrl: sourceImageUrl,
|
||||
sourceFullUrl: sourceImageUrl,
|
||||
structureLockedDraft: structureLockAppliedRef.value,
|
||||
targetDraftFullUrl: sourceImageUrl,
|
||||
targetDraftUrl: sourceImageUrl,
|
||||
workspacePixelRect,
|
||||
workspaceRect,
|
||||
};
|
||||
}
|
||||
|
||||
const [
|
||||
sourceFullUpload,
|
||||
targetDraftFullUpload,
|
||||
backgroundPlateFullUpload,
|
||||
productLockOverlayUpload,
|
||||
productLockProtectMaskUpload,
|
||||
compositeClipMaskUpload,
|
||||
productLockBackgroundRepairMaskUpload,
|
||||
productLockEditMaskUpload,
|
||||
productLockFalEditMaskUpload,
|
||||
] = await Promise.all([
|
||||
sharp(sourceBuffer, { failOn: "none" })
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer()
|
||||
.then((sourceFullBuffer) =>
|
||||
uploadFile(
|
||||
`canvas-scale-adjust-source-full-${suffix}.png`,
|
||||
toArrayBuffer(sourceFullBuffer)
|
||||
)
|
||||
),
|
||||
uploadFile(
|
||||
`canvas-scale-adjust-full-target-draft-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftBuffer)
|
||||
@ -3866,6 +3995,12 @@ async function createScaleAdjustPatchReference(params: {
|
||||
toArrayBuffer(targetDraftReference.compositeClipMaskBuffer)
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
targetDraftReference.productLockBackgroundRepairMaskBuffer
|
||||
? uploadFile(
|
||||
`canvas-scale-adjust-product-lock-background-repair-mask-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.productLockBackgroundRepairMaskBuffer)
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
targetDraftReference.productLockEditMaskBuffer
|
||||
? uploadFile(
|
||||
`canvas-scale-adjust-product-lock-edit-mask-${suffix}.png`,
|
||||
@ -3883,12 +4018,15 @@ async function createScaleAdjustPatchReference(params: {
|
||||
return {
|
||||
backgroundPlateFullUrl: backgroundPlateFullUpload?.url || null,
|
||||
compositeClipMaskUrl: compositeClipMaskUpload?.url || null,
|
||||
productLockBackgroundRepairMaskUrl:
|
||||
productLockBackgroundRepairMaskUpload?.url || null,
|
||||
productLockMethod: productLockOverlayUpload ? "rmbg-cutout" : null,
|
||||
productLockEditMaskUrl: productLockEditMaskUpload?.url || null,
|
||||
productLockFalEditMaskUrl: productLockFalEditMaskUpload?.url || null,
|
||||
productLockOverlayUrl: productLockOverlayUpload?.url || null,
|
||||
productLockProtectMaskUrl: productLockProtectMaskUpload?.url || null,
|
||||
sourceCropUrl: sourceImageUrl || targetDraftFullUpload.url,
|
||||
sourceFullUrl: sourceFullUpload.url,
|
||||
structureLockedDraft: structureLockAppliedRef.value,
|
||||
targetDraftFullUrl: targetDraftFullUpload.url,
|
||||
targetDraftUrl: targetDraftFullUpload.url,
|
||||
@ -3915,6 +4053,7 @@ async function createScaleAdjustPatchReference(params: {
|
||||
productLockOverlayUpload,
|
||||
productLockProtectMaskUpload,
|
||||
compositeClipMaskUpload,
|
||||
productLockBackgroundRepairMaskUpload,
|
||||
productLockEditMaskUpload,
|
||||
productLockFalEditMaskUpload,
|
||||
] = await Promise.all([
|
||||
@ -3954,6 +4093,12 @@ async function createScaleAdjustPatchReference(params: {
|
||||
toArrayBuffer(targetDraftReference.compositeClipMaskBuffer)
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
targetDraftReference.productLockBackgroundRepairMaskBuffer
|
||||
? uploadFile(
|
||||
`canvas-scale-adjust-product-lock-background-repair-mask-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.productLockBackgroundRepairMaskBuffer)
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
targetDraftReference.productLockEditMaskBuffer
|
||||
? uploadFile(
|
||||
`canvas-scale-adjust-product-lock-edit-mask-${suffix}.png`,
|
||||
@ -3971,6 +4116,8 @@ async function createScaleAdjustPatchReference(params: {
|
||||
return {
|
||||
backgroundPlateFullUrl: backgroundPlateFullUpload?.url || null,
|
||||
compositeClipMaskUrl: compositeClipMaskUpload?.url || null,
|
||||
productLockBackgroundRepairMaskUrl:
|
||||
productLockBackgroundRepairMaskUpload?.url || null,
|
||||
productLockMethod: productLockOverlayUpload ? "rmbg-cutout" : null,
|
||||
productLockEditMaskUrl: productLockEditMaskUpload?.url || null,
|
||||
productLockFalEditMaskUrl: productLockFalEditMaskUpload?.url || null,
|
||||
@ -4965,7 +5112,7 @@ function buildScaleAdjustProductLockBackgroundPrompt(
|
||||
regions: NormalizedLocalEditRegion[],
|
||||
scalePercent: ScaleAdjustPercent | null | undefined,
|
||||
sourceSize: { width: number; height: number },
|
||||
options: { draftInput?: boolean; maskEdit?: boolean } = {}
|
||||
options: { draftInput?: boolean; guidedFinalInput?: boolean; maskEdit?: boolean } = {}
|
||||
) {
|
||||
const regionLines = regions
|
||||
.map((region) => {
|
||||
@ -4973,29 +5120,44 @@ function buildScaleAdjustProductLockBackgroundPrompt(
|
||||
const targetPixelRect =
|
||||
region.targetPixelRect ||
|
||||
rectToPixels(targetRect, sourceSize.width, sourceSize.height);
|
||||
return `Region ${region.index}: remove the selected furniture/product from the original area x=${region.pixelRect.left}, y=${region.pixelRect.top}, w=${region.pixelRect.width}, h=${region.pixelRect.height}; keep the final target footprint x=${targetPixelRect.left}, y=${targetPixelRect.top}, w=${targetPixelRect.width}, h=${targetPixelRect.height} visually clean and empty because the locked product pixels will be composited there after generation. User request: ${region.prompt}.`;
|
||||
return `Region ${region.index}: the selected furniture/product originally occupies x=${region.pixelRect.left}, y=${region.pixelRect.top}, w=${region.pixelRect.width}, h=${region.pixelRect.height}. The final locked product will later occupy x=${targetPixelRect.left}, y=${targetPixelRect.top}, w=${targetPixelRect.width}, h=${targetPixelRect.height}. In this edit call, remove the old product and any old rectangular haze/ghost/plate inside the white mask; synthesize only the room background. User request: ${region.prompt}.`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return [
|
||||
"Full-frame background repair for product-locked scale adjustment.",
|
||||
"Return exactly one clean realistic full image with the same framing, camera, aspect ratio, and canvas extent as input image 1. Do not crop, zoom, pan, or resize the whole photo.",
|
||||
options.maskEdit
|
||||
options.guidedFinalInput
|
||||
? "Full-frame guided cleanup for product-locked scale adjustment."
|
||||
: "Full-frame masked background repair for product-locked scale adjustment.",
|
||||
"Return exactly one clean realistic full image with the same framing, camera, aspect ratio, and canvas extent as input image 1. Do not crop, zoom, pan, resize, or redesign the whole photo.",
|
||||
options.guidedFinalInput
|
||||
? "Input image 1 is the current full photo. Input image 2 is a rough target-layout draft used only to identify where the locked product will be composited later. If an additional background-plate reference is provided, use it only as a cleanup hint for the old footprint."
|
||||
: options.maskEdit
|
||||
? options.draftInput
|
||||
? "Input image 1 is a full-photo repair draft from the original photo. The old selected furniture/product footprint has been roughly wiped and the locked product may already appear near its final target footprint. The supplied edit mask marks the rough/old footprint area that must be repaired into natural background and seamless integration; do not edit outside the masked area."
|
||||
: "Input image 1 is the original clean full photo. The supplied edit mask marks the old selected furniture/product footprint that must be repaired as natural background; do not edit outside the masked area."
|
||||
? "Input image 1 is a full-photo rough background plate from the original photo. The old selected furniture/product footprint has been roughly wiped, and the target product is intentionally absent. The supplied edit mask marks only that rough old footprint area; repair it into natural background and do not edit outside the masked area."
|
||||
: "Input image 1 is the current full photo. The supplied edit mask is the only allowed edit area: white or transparent pixels must be repaired; black/opaque pixels outside the mask must be copied exactly from input image 1."
|
||||
: "Input image 1 is the original clean full photo. Input image 2 is a background plate reference that removes the selected furniture/product footprint and keeps the room/person/background layout stable.",
|
||||
"Important: do not draw, recreate, move, scale, duplicate, or redesign the selected furniture/product in the final image. The system will composite the locked original product pixels into the final target footprint after your background image is returned.",
|
||||
options.guidedFinalInput
|
||||
? "Important: return a background plate only. Use input image 2 only to understand the target placement, but do not render the selected furniture/product at either the old footprint or target footprint. The system will composite the locked original product pixels after your image is returned, so prioritize natural background, contact shadow, and clean fusion."
|
||||
: "Important: do not draw, recreate, move, scale, duplicate, or redesign the selected furniture/product in the returned image. The returned image must be a background plate only; the system will composite the locked original product pixels into the final target footprint after your background image is returned.",
|
||||
options.draftInput
|
||||
? "Your job is only to turn the rough wiped footprint and any visible draft haze, gray wash, ghost product silhouette, or edge patch into natural wall, curtain, floor, rug, shadow, sofa edge, and background. If the locked product is visible in the draft, keep the surrounding background and contact shadows natural, but do not invent a second product or preserve old-size remnants."
|
||||
: "Your job is only to erase the old selected furniture/product footprint, complete newly exposed wall, curtain, floor, rug, shadow, sofa edge, and background naturally, and keep the target footprint clean enough for the locked product layer to sit there.",
|
||||
? "Your job is only to turn the rough wiped footprint and any visible draft haze, gray wash, ghost product silhouette, or edge patch into natural wall, curtain, floor, rug, shadow, sofa edge, and background. Keep the target footprint empty and clean for the locked product layer; do not invent a product, second product, or old-size remnant."
|
||||
: options.guidedFinalInput
|
||||
? "Your job is to clean the rough target-layout draft into a natural product-free background plate: erase the old selected furniture/product from its original location, remove translucent old-size silhouettes and rectangular haze/panels, complete the exposed wall, curtain, floor, rug, shadows, sofa edge, and background, and keep the target footprint empty for the locked product layer."
|
||||
: "Your job is only to erase the old selected furniture/product, erase any translucent old-size silhouette, erase any rectangular haze/panel/plate left by previous edits, and complete newly exposed wall, curtain, floor, rug, shadow, sofa edge, and background naturally. Leave no placeholder where the product used to be.",
|
||||
options.guidedFinalInput
|
||||
? "Do not copy rough draft haze, wash blocks, hard mask shapes, pasted edges, or the rough target product from the target-layout draft. It is a placement hint, not final content."
|
||||
: options.maskEdit
|
||||
? "The mask may be wider than the object silhouette because it also covers old edit artifacts. Do not interpret the mask shape as a box or object to render. Fill the entire editable white/transparent area with continuous real room background."
|
||||
: "Use the background plate reference only as a repair guide, not as visible final content.",
|
||||
"Preserve all non-target people exactly as foreground/background context: do not move, cover, deform, redraw, change face, change body, change hands, change legs, change clothes, or change pose.",
|
||||
"Keep all non-target content stable: sofa, window, curtain, cabinet, lamp, wall art, floor, rug, lighting, textural grain, and global color must remain consistent.",
|
||||
"Keep all non-target content stable outside the editable mask: sofa, window, curtain, cabinet, lamp, wall art, floor, rug, lighting, textural grain, and global color must remain consistent.",
|
||||
options.maskEdit
|
||||
? "The edit mask is only an invisible repair boundary. Do not output pale wash blocks, rough mask shapes, hard rectangular edges, black holes, ghost fragments, pasted silhouettes, or old-size residues."
|
||||
? "The edit mask is only an invisible repair permission map. Do not output pale wash blocks, rough mask shapes, hard rectangular edges, black holes, ghost fragments, pasted silhouettes, old-size residues, or a gray copy of the removed product."
|
||||
: "Remove draft artifacts from input image 2: pale wash blocks, rough masks, missing patches, hard rectangular edges, black holes, ghost fragments, pasted silhouettes, and old-size residues. They are only edit guides, not final content.",
|
||||
"Do not output boxes, masks, labels, coordinate marks, UI marks, arrows, guide overlays, crop borders, or any annotation artifacts.",
|
||||
"中文硬约束:这是产品结构锁定流程。你只生成背景底图,不要生成、移动、缩放或重绘目标家具/商品;目标家具/商品会在你输出后由系统用原图锁定像素叠回目标位置。必须擦除旧位置的目标家具/商品并自然补全墙面、窗帘、地面、阴影和背景。非目标人物必须保持原样,不能变脸、变形、改变衣服、手、腿、脚或姿势。最终背景图不能有矩形雾块、贴片边、黑洞、残影、标注框,也不能出现第二个目标家具/商品。",
|
||||
options.guidedFinalInput
|
||||
? "中文硬约束:这是产品结构锁定流程。输入2只是目标位置草稿,不能把草稿的灰雾、贴片边、硬边、旧残影或目标产品带到最终图。你返回的是无产品背景底图:旧位置和目标位置都不能出现目标家具/商品,只能自然补全墙面、窗帘、地面、阴影和背景。目标家具/商品最终会由系统用原图锁定像素覆盖,所以你不要生成产品本体;重点做好背景补全、接触阴影和融合。非目标人物必须保持原样,不能变脸、变形、改变衣服、手、腿、脚或姿势。最终图不能有矩形雾块、贴片边、黑洞、残影、标注框,也不能出现第二个目标家具/商品。"
|
||||
: "中文硬约束:这是产品结构锁定流程。你只生成背景底图,不要生成、移动、缩放或重绘目标家具/商品;目标家具/商品会在你输出后由系统用原图锁定像素叠回目标位置。白色/透明 mask 内必须清空旧产品、旧尺寸残影、灰色雾块、矩形贴片和旧标注痕迹,只补成连续自然的墙面、窗帘、地面、阴影和背景。黑色/不透明 mask 外必须保持原图不变。非目标人物必须保持原样,不能变脸、变形、改变衣服、手、腿、脚或姿势。最终背景图不能有矩形雾块、贴片边、黑洞、残影、标注框,也不能出现第二个目标家具/商品。",
|
||||
"",
|
||||
"Region instructions:",
|
||||
regionLines,
|
||||
@ -5136,7 +5298,9 @@ export async function POST(request: NextRequest) {
|
||||
const preferScaleAdjustFalNanoBananaEdit =
|
||||
hasScaleAdjustShrink && !hasScaleAdjustEnlarge;
|
||||
const preferScaleAdjustProviderTypes =
|
||||
hasScaleAdjustShrink && !hasScaleAdjustEnlarge ? ["grsai-gpt-image"] : [];
|
||||
hasScaleAdjustShrink && !hasScaleAdjustEnlarge
|
||||
? ["grsai-gpt-image", "fal-image"]
|
||||
: [];
|
||||
const displayPrompt = buildLocalEditDisplayPrompt({ operation, scalePercent });
|
||||
const resolvedAspectRatio = resolveLocalEditAspectRatio(
|
||||
body.aspectRatio,
|
||||
@ -5314,16 +5478,33 @@ export async function POST(request: NextRequest) {
|
||||
regions.some((region) =>
|
||||
isScaleAdjustFurnitureProductLikePrompt(region.prompt)
|
||||
),
|
||||
minimalProductLockMaskEditReferences:
|
||||
useScaleAdjustStructureLockedPatchWorkflow &&
|
||||
isScaleAdjustMaskCapableModel(resolvedScaleAdjustModelEntry) &&
|
||||
regions.some((region) =>
|
||||
isScaleAdjustFurnitureProductLikePrompt(region.prompt)
|
||||
),
|
||||
userId: session.user.id,
|
||||
});
|
||||
} catch (error) {
|
||||
scaleAdjustPatchReferenceFailed = true;
|
||||
console.warn(
|
||||
"[Canvas Local Edit] Scale adjust patch reference failed; falling back to source-only coordinate prompt:",
|
||||
"[Canvas Local Edit] Scale adjust patch reference failed:",
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
structureLockedPatchWorkflow: useScaleAdjustStructureLockedPatchWorkflow,
|
||||
}
|
||||
);
|
||||
if (useScaleAdjustStructureLockedPatchWorkflow) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
"比例调整参考图上传失败,请稍后重试;已停止本次生成以避免返回无效结果。",
|
||||
},
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const shouldCreateScaleAdjustCropReferences =
|
||||
@ -5349,14 +5530,24 @@ export async function POST(request: NextRequest) {
|
||||
regions.some((region) =>
|
||||
isScaleAdjustFurnitureProductLikePrompt(region.prompt)
|
||||
);
|
||||
const useScaleAdjustProductLockComposite =
|
||||
const useScaleAdjustProductLockBase =
|
||||
useScaleAdjustFullFrameStructurePatch &&
|
||||
Boolean(scaleAdjustPatchReference?.backgroundPlateFullUrl) &&
|
||||
Boolean(scaleAdjustPatchReference?.productLockOverlayUrl);
|
||||
const useScaleAdjustProductLockMaskEdit =
|
||||
useScaleAdjustProductLockComposite &&
|
||||
useScaleAdjustProductLockBase &&
|
||||
isScaleAdjustMaskCapableModel(resolvedScaleAdjustModelEntry) &&
|
||||
Boolean(scaleAdjustPatchReference?.productLockEditMaskUrl);
|
||||
const useScaleAdjustProductLockComposite =
|
||||
useScaleAdjustProductLockBase &&
|
||||
(Boolean(scaleAdjustPatchReference?.backgroundPlateFullUrl) ||
|
||||
useScaleAdjustProductLockMaskEdit);
|
||||
const useScaleAdjustProductLockDirectMaskPlate =
|
||||
useScaleAdjustProductLockComposite && useScaleAdjustProductLockMaskEdit;
|
||||
const useScaleAdjustProductLockGuidedFinal =
|
||||
useScaleAdjustProductLockComposite &&
|
||||
!useScaleAdjustProductLockMaskEdit &&
|
||||
process.env.CANVAS_SCALE_ADJUST_ENABLE_PRODUCT_LOCK_GUIDED_FINAL === "1" &&
|
||||
Boolean(scaleAdjustPatchReference?.targetDraftFullUrl);
|
||||
const useScaleAdjustPatchTargetDraftPrimary = Boolean(scaleAdjustPatchReference);
|
||||
const includeScaleAdjustPatchSourceReference =
|
||||
Boolean(scaleAdjustPatchReference);
|
||||
@ -5373,7 +5564,7 @@ export async function POST(request: NextRequest) {
|
||||
scalePercent,
|
||||
{ width: sourceWidth, height: sourceHeight },
|
||||
{
|
||||
draftInput: useScaleAdjustProductLockMaskEdit,
|
||||
guidedFinalInput: useScaleAdjustProductLockGuidedFinal,
|
||||
maskEdit: useScaleAdjustProductLockMaskEdit,
|
||||
}
|
||||
)
|
||||
@ -5436,6 +5627,8 @@ export async function POST(request: NextRequest) {
|
||||
"boxes, labels, UI marks, arrows, text, overlays, seams, rectangular patch edge, haze block, ghost copy, old-size subject, unchanged subject, duplicate subject, stretched subject, distorted product, changed product structure, changed person, changed face, deformed person, extra limbs, changed hands, changed feet, whole-image zoom, crop, pan";
|
||||
const scaleAdjustProductLockBackgroundNegativePrompt =
|
||||
"selected furniture, selected product, moved furniture, scaled furniture, recreated product, duplicate furniture, duplicate product, old-size furniture, target object visible, product ghost, product remnant, boxes, labels, UI marks, arrows, text, overlays, seams, rectangular patch edge, haze block, black hole, changed person, changed face, deformed person, extra limbs, changed hands, changed feet, whole-image zoom, crop, pan";
|
||||
const scaleAdjustProductLockGuidedNegativePrompt =
|
||||
"selected furniture, selected product, target furniture visible, target product visible, old-size furniture, old-position furniture, duplicate furniture, duplicate product, second copy, product ghost, product remnant, pasted edge, rectangular patch edge, rectangular haze block, gray wash block, black hole, boxes, labels, UI marks, arrows, text, overlays, changed person, changed face, deformed person, extra limbs, changed hands, changed feet, whole-image zoom, crop, pan";
|
||||
const manualRegionReferenceImageUrls = regions.flatMap((region) =>
|
||||
region.referenceImages.map((reference) => reference.url)
|
||||
);
|
||||
@ -5448,17 +5641,26 @@ export async function POST(request: NextRequest) {
|
||||
? useScaleAdjustProductLockComposite
|
||||
? useScaleAdjustProductLockMaskEdit
|
||||
? [
|
||||
scaleAdjustPatchReference.targetDraftFullUrl ||
|
||||
sourceImageUrl,
|
||||
scaleAdjustPatchReference.sourceFullUrl || sourceImageUrl,
|
||||
...scaleAdjustCropReferenceImageUrls,
|
||||
...manualRegionReferenceImageUrls,
|
||||
]
|
||||
: useScaleAdjustProductLockGuidedFinal
|
||||
? [
|
||||
scaleAdjustPatchReference.sourceFullUrl || sourceImageUrl,
|
||||
scaleAdjustPatchReference.targetDraftFullUrl as string,
|
||||
...(scaleAdjustPatchReference.backgroundPlateFullUrl
|
||||
? [scaleAdjustPatchReference.backgroundPlateFullUrl]
|
||||
: []),
|
||||
...scaleAdjustCropReferenceImageUrls,
|
||||
...manualRegionReferenceImageUrls,
|
||||
]
|
||||
: [
|
||||
sourceImageUrl,
|
||||
scaleAdjustPatchReference.backgroundPlateFullUrl as string,
|
||||
...scaleAdjustCropReferenceImageUrls,
|
||||
...manualRegionReferenceImageUrls,
|
||||
]
|
||||
sourceImageUrl,
|
||||
scaleAdjustPatchReference.backgroundPlateFullUrl as string,
|
||||
...scaleAdjustCropReferenceImageUrls,
|
||||
...manualRegionReferenceImageUrls,
|
||||
]
|
||||
: [
|
||||
sourceImageUrl,
|
||||
scaleAdjustPatchReference.targetDraftFullUrl as string,
|
||||
@ -5503,6 +5705,12 @@ export async function POST(request: NextRequest) {
|
||||
isScaleAdjustOperation && scaleAdjustPatchReference
|
||||
? scaleAdjustProtectedChangeRegions
|
||||
: scaleAdjustCompositeRegions;
|
||||
const scaleAdjustProductLockRepairClipMaskUrl =
|
||||
useScaleAdjustProductLockComposite && useScaleAdjustProductLockMaskEdit
|
||||
? scaleAdjustPatchReference?.productLockBackgroundRepairMaskUrl ||
|
||||
scaleAdjustPatchReference?.compositeClipMaskUrl ||
|
||||
undefined
|
||||
: scaleAdjustPatchReference?.compositeClipMaskUrl || undefined;
|
||||
const scaleAdjustVerificationConfig = isScaleAdjustOperation
|
||||
? {
|
||||
allowOutsideChangedWhenChangedInside: false,
|
||||
@ -5607,7 +5815,9 @@ export async function POST(request: NextRequest) {
|
||||
scaleAdjustPatchReferenceOrder:
|
||||
scaleAdjustPatchReference && useScaleAdjustFullFrameStructurePatch
|
||||
? useScaleAdjustProductLockMaskEdit
|
||||
? "full-source-product-lock-edit-mask"
|
||||
? "product-lock-source-edit-mask"
|
||||
: useScaleAdjustProductLockGuidedFinal
|
||||
? "product-lock-source-target-draft-guide"
|
||||
: "full-source-full-target-draft"
|
||||
: scaleAdjustPatchReference && useScaleAdjustPatchTargetDraftPrimary
|
||||
? includeScaleAdjustPatchSourceReference
|
||||
@ -5618,6 +5828,8 @@ export async function POST(request: NextRequest) {
|
||||
: null,
|
||||
scaleAdjustPatchSourceCropUrl:
|
||||
scaleAdjustPatchReference?.sourceCropUrl || null,
|
||||
scaleAdjustPatchSourceFullUrl:
|
||||
scaleAdjustPatchReference?.sourceFullUrl || null,
|
||||
scaleAdjustPatchTargetDraftUrl:
|
||||
scaleAdjustPatchReference?.targetDraftUrl || null,
|
||||
scaleAdjustPatchTargetDraftFullUrl:
|
||||
@ -5630,12 +5842,16 @@ export async function POST(request: NextRequest) {
|
||||
scaleAdjustPatchReference?.productLockMethod || null,
|
||||
scaleAdjustProductLockMaskEditApplied:
|
||||
useScaleAdjustProductLockMaskEdit,
|
||||
scaleAdjustProductLockGuidedFinalApplied:
|
||||
useScaleAdjustProductLockGuidedFinal,
|
||||
scaleAdjustRequiresProviderMaskInput:
|
||||
useScaleAdjustProductLockMaskEdit,
|
||||
scaleAdjustProductLockEditMaskUrl:
|
||||
scaleAdjustPatchReference?.productLockEditMaskUrl || null,
|
||||
scaleAdjustProductLockFalEditMaskUrl:
|
||||
scaleAdjustPatchReference?.productLockFalEditMaskUrl || null,
|
||||
scaleAdjustProductLockBackgroundRepairMaskUrl:
|
||||
scaleAdjustPatchReference?.productLockBackgroundRepairMaskUrl || null,
|
||||
scaleAdjustProductLockOverlayUrl:
|
||||
scaleAdjustPatchReference?.productLockOverlayUrl || null,
|
||||
scaleAdjustProductLockProtectMaskUrl:
|
||||
@ -5699,7 +5915,9 @@ export async function POST(request: NextRequest) {
|
||||
? scaleAdjustPatchReference
|
||||
? useScaleAdjustProductLockComposite
|
||||
? useScaleAdjustProductLockMaskEdit
|
||||
? "product-lock-mask-edit-rmbg-source-patch-one-shot"
|
||||
? "product-lock-source-mask-edit-rmbg-one-shot"
|
||||
: useScaleAdjustProductLockGuidedFinal
|
||||
? "product-lock-target-draft-guided-background-one-shot"
|
||||
: "product-lock-background-plate-rmbg-v2-source-patch-one-shot"
|
||||
: useScaleAdjustFullFrameStructurePatch
|
||||
? "full-frame-source-locked-target-guide-v8-one-shot"
|
||||
@ -5746,7 +5964,9 @@ export async function POST(request: NextRequest) {
|
||||
? scaleAdjustPatchReference
|
||||
? useScaleAdjustFullFrameStructurePatch
|
||||
? useScaleAdjustProductLockComposite
|
||||
? scaleAdjustProductLockBackgroundNegativePrompt
|
||||
? useScaleAdjustProductLockGuidedFinal
|
||||
? scaleAdjustProductLockGuidedNegativePrompt
|
||||
: scaleAdjustProductLockBackgroundNegativePrompt
|
||||
: scaleAdjustFullFramePatchNegativePrompt
|
||||
: useFalScaleAdjustPatchPrompt
|
||||
? scaleAdjustFalPatchNegativePrompt
|
||||
@ -5771,22 +5991,22 @@ export async function POST(request: NextRequest) {
|
||||
? { maskImageUrl: scaleAdjustEditMaskUpload.url }
|
||||
: undefined,
|
||||
referenceImages,
|
||||
scaleAdjustPatchComposite: useScaleAdjustProductLockComposite
|
||||
scaleAdjustPatchComposite:
|
||||
useScaleAdjustProductLockComposite
|
||||
? {
|
||||
diffAlphaCutoff: 18,
|
||||
diffAlphaMultiplier: 2.6,
|
||||
diffCompareBlurPx: 1.1,
|
||||
diffHighThreshold: 64,
|
||||
diffLowThreshold: 20,
|
||||
featherMaxPx: 34,
|
||||
featherMinPx: 8,
|
||||
featherRatio: 0.015,
|
||||
featherMaxPx: useScaleAdjustProductLockDirectMaskPlate ? 72 : 34,
|
||||
featherMinPx: useScaleAdjustProductLockDirectMaskPlate ? 18 : 8,
|
||||
featherRatio: useScaleAdjustProductLockDirectMaskPlate ? 0.026 : 0.015,
|
||||
generatedFrame: "full",
|
||||
maskMode: "diff",
|
||||
maskMode: useScaleAdjustProductLockDirectMaskPlate ? "workspace" : "diff",
|
||||
regions: scaleAdjustPatchCompositeRegions,
|
||||
sourceImageUrl,
|
||||
clipMaskImageUrl:
|
||||
scaleAdjustPatchReference?.compositeClipMaskUrl || undefined,
|
||||
clipMaskImageUrl: scaleAdjustProductLockRepairClipMaskUrl,
|
||||
workspaceRegion:
|
||||
(scaleAdjustPatchReference as ScaleAdjustPatchReference)
|
||||
.workspaceRect,
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
import {
|
||||
ChevronDown,
|
||||
Clock3,
|
||||
Download,
|
||||
History as HistoryIcon,
|
||||
ImageIcon,
|
||||
Loader2,
|
||||
@ -46,7 +45,6 @@ import {
|
||||
type AiWashControlLevel,
|
||||
type AiWashPreset,
|
||||
} from "@/lib/ai-wash/config";
|
||||
import { downloadCanvasImageAsPng } from "@/lib/client/canvas-image-download";
|
||||
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { imageGenerationService } from "@/services/canvas";
|
||||
@ -370,11 +368,6 @@ function getAiWashStatusLabel(status?: AiWashResultStatus) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildAiWashDownloadName(result: AiWashResultCard) {
|
||||
const createdAt = result.createdAt || Date.now();
|
||||
return `ai-wash-${result.title || result.presetId || "result"}-${createdAt}`;
|
||||
}
|
||||
|
||||
function composePresetPrompt(params: {
|
||||
preset: AiWashPreset;
|
||||
selectedLevelIndexByControl: Record<string, number>;
|
||||
@ -463,9 +456,6 @@ export function AiWashWorkspace({
|
||||
useState<Record<string, number>>({});
|
||||
const [historySourceOverride, setHistorySourceOverride] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [downloadingResultId, setDownloadingResultId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [isHistoryExpanded, setIsHistoryExpanded] = useState(false);
|
||||
const [lastErrorMessage, setLastErrorMessage] = useState("");
|
||||
const [comparePositionById, setComparePositionById] = useState<
|
||||
@ -987,30 +977,6 @@ export function AiWashWorkspace({
|
||||
});
|
||||
}, [compareSourceUrl, onApplyResult, selectedResult]);
|
||||
|
||||
const handleDownloadSelectedResult = useCallback(async () => {
|
||||
if (!selectedResult?.imageUrl) {
|
||||
toast.error("请先选择一张洗图结果");
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadId = selectedResult.presetId || selectedResult.imageUrl;
|
||||
setDownloadingResultId(downloadId);
|
||||
try {
|
||||
await downloadCanvasImageAsPng({
|
||||
url: selectedResult.imageUrl,
|
||||
name: buildAiWashDownloadName(selectedResult),
|
||||
candidates: [selectedResult.imageUrl],
|
||||
});
|
||||
toast.success("图片已开始下载");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "下载失败");
|
||||
} finally {
|
||||
setDownloadingResultId((current) =>
|
||||
current === downloadId ? null : current
|
||||
);
|
||||
}
|
||||
}, [selectedResult]);
|
||||
|
||||
const handleLoadHistoryBatch = useCallback((entry: AiWashHistoryBatch) => {
|
||||
const loadedPresetId =
|
||||
(entry.items.find((item) => item.isDefault) || entry.items[0])?.presetId || "";
|
||||
@ -1464,25 +1430,6 @@ export function AiWashWorkspace({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedResult?.imageUrl ? (
|
||||
<button
|
||||
className="absolute bottom-2 right-2 z-30 inline-flex h-10 items-center gap-2 rounded-full border border-[#e5e5e5] bg-white/92 px-4 text-xs font-medium text-[#333333] shadow-[0_10px_24px_rgba(0,0,0,0.07)] backdrop-blur transition hover:border-[#111111] hover:text-[#111111] disabled:cursor-not-allowed disabled:opacity-55"
|
||||
disabled={Boolean(downloadingResultId)}
|
||||
onClick={() => {
|
||||
void handleDownloadSelectedResult();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{downloadingResultId ===
|
||||
(selectedResult.presetId || selectedResult.imageUrl) ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-4 w-4" />
|
||||
)}
|
||||
下载
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
className="absolute bottom-2 left-2 z-30 inline-flex h-10 items-center gap-2 rounded-full border border-[#e5e5e5] bg-white/90 px-4 text-xs text-[#666666] shadow-[0_10px_24px_rgba(0,0,0,0.06)] backdrop-blur transition hover:border-[#111111] hover:text-[#111111]"
|
||||
onClick={() => setIsHistoryExpanded((current) => !current)}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -206,26 +206,14 @@ export type AgentProgressStreamPayload = {
|
||||
capability?: string;
|
||||
};
|
||||
|
||||
export type AgentProgressStepState =
|
||||
| "upcoming"
|
||||
| "current"
|
||||
| "completed"
|
||||
| "failed";
|
||||
|
||||
export type AgentProgressNodeStep = {
|
||||
key: NonNullable<AgentProgressStreamPayload["currentStep"]>;
|
||||
label: string;
|
||||
state: AgentProgressStepState;
|
||||
};
|
||||
|
||||
export const AGENT_PROGRESS_STEPS: Array<{
|
||||
key: NonNullable<AgentProgressStreamPayload["currentStep"]>;
|
||||
label: string;
|
||||
}> = [
|
||||
{ key: "lock_target", label: "理解用户需求" },
|
||||
{ key: "analyze", label: "分析使用工具" },
|
||||
{ key: "select_tool", label: "确认执行工具" },
|
||||
{ key: "execute", label: "执行中" },
|
||||
{ key: "lock_target", label: "整理素材" },
|
||||
{ key: "analyze", label: "理解需求" },
|
||||
{ key: "select_tool", label: "优化提示" },
|
||||
{ key: "execute", label: "提交生成" },
|
||||
{ key: "finalize", label: "整理结果" },
|
||||
];
|
||||
|
||||
@ -272,94 +260,3 @@ export function getAgentProgressFallbackTitle(payload?: AgentProgressStreamPaylo
|
||||
return "正在理解需求";
|
||||
}
|
||||
}
|
||||
|
||||
export function getAgentProgressActionLabel(
|
||||
payload?: AgentProgressStreamPayload | null
|
||||
) {
|
||||
switch (payload?.capability) {
|
||||
case "analyze":
|
||||
return "图片分析";
|
||||
case "generic_edit":
|
||||
return "修图";
|
||||
case "remove_background":
|
||||
return "抠图";
|
||||
case "remove_watermark":
|
||||
return "去水印";
|
||||
case "enhance":
|
||||
return "图像增强";
|
||||
case "resize":
|
||||
return "尺寸调整";
|
||||
case "extend":
|
||||
return "扩图";
|
||||
case "angle_variant":
|
||||
return "多视角";
|
||||
case "local_edit":
|
||||
return "局部编辑";
|
||||
case "box_cutout":
|
||||
return "框选抠图";
|
||||
case "generate_image":
|
||||
return "生图";
|
||||
case "generate_video":
|
||||
return "视频生成";
|
||||
default:
|
||||
return "工具";
|
||||
}
|
||||
}
|
||||
|
||||
export function getAgentProgressStepLabel(
|
||||
stepKey: NonNullable<AgentProgressStreamPayload["currentStep"]>,
|
||||
payload?: AgentProgressStreamPayload | null
|
||||
) {
|
||||
const actionLabel = getAgentProgressActionLabel(payload);
|
||||
switch (stepKey) {
|
||||
case "lock_target":
|
||||
return "理解用户需求";
|
||||
case "analyze":
|
||||
return payload?.capability === "analyze" ? "分析图片" : "分析使用工具";
|
||||
case "select_tool":
|
||||
return actionLabel === "工具" ? "确认执行工具" : `确认${actionLabel}工具`;
|
||||
case "execute":
|
||||
return actionLabel === "工具" ? "执行中" : `${actionLabel}中`;
|
||||
case "finalize":
|
||||
return "整理结果";
|
||||
default:
|
||||
return "处理中";
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAgentProgressNodeSteps(
|
||||
payload?: AgentProgressStreamPayload | null
|
||||
): AgentProgressNodeStep[] {
|
||||
const currentStep = payload?.currentStep || "lock_target";
|
||||
const currentStepIndex = Math.max(
|
||||
0,
|
||||
AGENT_PROGRESS_STEPS.findIndex((step) => step.key === currentStep)
|
||||
);
|
||||
|
||||
return AGENT_PROGRESS_STEPS.map((step, index) => {
|
||||
let state: AgentProgressStepState = "upcoming";
|
||||
if (payload?.stage === "failed") {
|
||||
state =
|
||||
index < currentStepIndex
|
||||
? "completed"
|
||||
: index === currentStepIndex
|
||||
? "failed"
|
||||
: "upcoming";
|
||||
} else if (payload?.stage === "completed") {
|
||||
state = "completed";
|
||||
} else {
|
||||
state =
|
||||
index < currentStepIndex
|
||||
? "completed"
|
||||
: index === currentStepIndex
|
||||
? "current"
|
||||
: "upcoming";
|
||||
}
|
||||
|
||||
return {
|
||||
key: step.key,
|
||||
label: getAgentProgressStepLabel(step.key, payload),
|
||||
state,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@ -381,15 +381,11 @@ export function sortCanvasMessagesByCreatedAt(messages: ChatMessage[]): ChatMess
|
||||
.sort((a, b) => {
|
||||
const messageA = a.message;
|
||||
const messageB = b.message;
|
||||
const timeA = getMessageTimestamp(messageA);
|
||||
const timeB = getMessageTimestamp(messageB);
|
||||
const hasTimeA = timeA !== null;
|
||||
const hasTimeB = timeB !== null;
|
||||
const timeA = getMessageTimestamp(messageA) ?? 0;
|
||||
const timeB = getMessageTimestamp(messageB) ?? 0;
|
||||
if (timeA !== timeB) return timeA - timeB;
|
||||
|
||||
if (hasTimeA && hasTimeB && timeA !== timeB) return timeA - timeB;
|
||||
if (hasTimeA !== hasTimeB) return hasTimeA ? -1 : 1;
|
||||
|
||||
if (hasTimeA && hasTimeB && messageA.role !== messageB.role) {
|
||||
if (messageA.role !== messageB.role) {
|
||||
if (messageA.role === "user") return -1;
|
||||
if (messageB.role === "user") return 1;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeft, Clock3, Download, Loader2, LogOut, Sparkles, Upload } from "lucide-react";
|
||||
import { ArrowLeft, Clock3, Loader2, LogOut, Sparkles, Upload } from "lucide-react";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import {
|
||||
type CSSProperties,
|
||||
@ -29,7 +29,6 @@ import {
|
||||
SelectTrigger,
|
||||
} from "@/components/ui/select";
|
||||
import { imageGenerationService } from "@/services/canvas";
|
||||
import { downloadCanvasImageAsPng } from "@/lib/client/canvas-image-download";
|
||||
import { cn, fetcher } from "@/lib/utils";
|
||||
import type {
|
||||
PublicStyleProCategory,
|
||||
@ -458,15 +457,6 @@ function formatStyleProHistoryDateTime(value?: string | null) {
|
||||
return `${dateText} ${timeText}`;
|
||||
}
|
||||
|
||||
function buildStyleProDownloadName(item: StyleProHistoryItem) {
|
||||
const mode =
|
||||
item.generationMode === "realistic" ? "style-pro-realistic" : "style-pro";
|
||||
const createdAt = item.createdAt
|
||||
? item.createdAt.replace(/[T:.]/g, "-").slice(0, 19)
|
||||
: String(Date.now());
|
||||
return `${mode}-${createdAt}`;
|
||||
}
|
||||
|
||||
function formatStyleProEstimatedWait(estimatedWaitMs?: number | null) {
|
||||
const numeric = Number(estimatedWaitMs || 0);
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||||
@ -827,16 +817,10 @@ function GeneratingImagePlaceholder({
|
||||
function BeforeAfterSlider({
|
||||
afterUrl,
|
||||
beforeUrl,
|
||||
downloadDisabled,
|
||||
downloading,
|
||||
onDownload,
|
||||
taskState,
|
||||
}: {
|
||||
afterUrl?: string | null;
|
||||
beforeUrl?: string | null;
|
||||
downloadDisabled?: boolean;
|
||||
downloading?: boolean;
|
||||
onDownload?: () => void;
|
||||
taskState?: StyleProTaskState | null;
|
||||
}) {
|
||||
const showTaskState =
|
||||
@ -962,24 +946,6 @@ function BeforeAfterSlider({
|
||||
<div className="absolute right-4 top-4 rounded-full bg-emerald-500/90 px-3 py-1 text-xs font-semibold text-black">
|
||||
结果
|
||||
</div>
|
||||
{afterUrl ? (
|
||||
<button
|
||||
className="absolute bottom-4 right-4 z-20 inline-flex h-11 items-center gap-2 rounded-full border border-white/25 bg-black/72 px-4 text-[13px] font-semibold text-white shadow-[0_14px_32px_rgba(0,0,0,0.24)] backdrop-blur transition hover:bg-black disabled:cursor-not-allowed disabled:opacity-55"
|
||||
disabled={downloadDisabled || downloading}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onDownload?.();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{downloading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-4 w-4" />
|
||||
)}
|
||||
下载
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1055,9 +1021,6 @@ export function StyleProWorkspace({
|
||||
useState<StyleProGenerationMode>("style");
|
||||
const [selectedModelId, setSelectedModelId] = useState("");
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [downloadingImageId, setDownloadingImageId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [generationTaskState, setGenerationTaskState] =
|
||||
useState<StyleProTaskState | null>(null);
|
||||
const generating =
|
||||
@ -1495,34 +1458,6 @@ export function StyleProWorkspace({
|
||||
[config]
|
||||
);
|
||||
|
||||
const handleDownloadResult = useCallback(
|
||||
async (item?: StyleProHistoryItem | null) => {
|
||||
const target = item || currentResult;
|
||||
if (!target?.resultImageUrl) {
|
||||
toast.error("暂无可下载的结果图");
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadId = target.id || target.resultImageUrl;
|
||||
setDownloadingImageId(downloadId);
|
||||
try {
|
||||
await downloadCanvasImageAsPng({
|
||||
url: target.resultImageUrl,
|
||||
name: buildStyleProDownloadName(target),
|
||||
candidates: [target.resultImageUrl],
|
||||
});
|
||||
toast.success("图片已开始下载");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "下载失败");
|
||||
} finally {
|
||||
setDownloadingImageId((current) =>
|
||||
current === downloadId ? null : current
|
||||
);
|
||||
}
|
||||
},
|
||||
[currentResult]
|
||||
);
|
||||
|
||||
const disabledReason = !sourceImageUrl
|
||||
? "请先上传图片"
|
||||
: generationMode === "realistic" && config?.realisticModeAvailable === false
|
||||
@ -1940,15 +1875,6 @@ export function StyleProWorkspace({
|
||||
<BeforeAfterSlider
|
||||
afterUrl={currentResult?.resultImageUrl}
|
||||
beforeUrl={currentResult?.sourceImageUrl || sourceImageUrl}
|
||||
downloadDisabled={!currentResult?.resultImageUrl}
|
||||
downloading={Boolean(
|
||||
currentResult &&
|
||||
downloadingImageId ===
|
||||
(currentResult.id || currentResult.resultImageUrl)
|
||||
)}
|
||||
onDownload={() => {
|
||||
void handleDownloadResult(currentResult);
|
||||
}}
|
||||
taskState={generationTaskState}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -308,7 +308,6 @@ export function resolveChatMediaInput(
|
||||
resolvedImageRefs,
|
||||
currentMessageImageUrls: currentMessageImageRefs.map((ref) => ref.url),
|
||||
selectedImageUrls: selectedImageRefs.map((ref) => ref.url),
|
||||
canvasImageUrls: canvasImageRefs.map((ref) => ref.url),
|
||||
historyUserImageUrls: history.latestUser.map((ref) => ref.url),
|
||||
historyAssistantImageUrls: history.latestAssistant.map((ref) => ref.url),
|
||||
resolvedImageUrls: resolvedImageRefs.map((ref) => ref.url),
|
||||
|
||||
@ -5,17 +5,16 @@ import type {
|
||||
ResolvedImageRef,
|
||||
ResolvedTargetAssets,
|
||||
} from "@/lib/ai/context-memory/types";
|
||||
import {
|
||||
hasCanvasAgentActionableImageEditIntent,
|
||||
hasCanvasAgentDeicticTarget,
|
||||
resolveCanvasAgentTargetPreference,
|
||||
} from "@/lib/ai/runtime/canvas-agent-kernel";
|
||||
|
||||
const ORIGINAL_IMAGE_REGEX =
|
||||
/(原图|上传图|参考图|最初那张|第一张图|素材图|源图|原始图片|原始那张|我发的那张|上传的那张|原来的图)/i;
|
||||
const LATEST_RESULT_REGEX =
|
||||
/(上一张|上一轮|刚生成|最新生成|这个结果|该结果|按这个结果|继续在这张|基于这张结果|延续上一次结果|上一版|刚才那张结果|生成的图|生成图|结果图|出图结果|刚出的图|上一张生成图|上一轮结果图|现在这张生成图)/i;
|
||||
const DEICTIC_TARGET_REGEX =
|
||||
/(这张图|这只|这个|这款|这套|当前这张|当前这个|当前这只|这一张|这一只|这个产品|这个物体|这只猫|这只狗)/i;
|
||||
const SELECTION_REGEX = /(选中的|当前这张|我选的|这几张|这些图|选中图)/i;
|
||||
const ACTIONABLE_EDIT_REGEX =
|
||||
/(改|调整|优化|换|替换|去掉|抠|重绘|增强|修|处理|按照|按|保留|统一|色差|背景|清晰|风格|亮一点|暗一点|白底|透明底)/i;
|
||||
const ACTIONABLE_GENERATION_REGEX =
|
||||
/(生图|生成|画一张|出图|来一张|再来一张|做一张|做图|做视频|视频生成|生成视频|动起来)/i;
|
||||
const IMPLICIT_HISTORY_TARGET_REGEX =
|
||||
@ -256,7 +255,6 @@ export function resolveTargetAssets(
|
||||
const focusPolicy = params.contextPayload?.focusPolicy || "auto";
|
||||
const workingStateGeneratedUrls = normalizeStringArray(workingState?.lastGeneratedImageUrls);
|
||||
const workingStateUploadedUrls = normalizeStringArray(workingState?.lastUploadedImageUrls);
|
||||
const workingStateResolvedUrls = normalizeStringArray(workingState?.lastResolvedTargetImageUrls);
|
||||
const generatedRefs =
|
||||
workingStateGeneratedUrls.length > 0 ? toWorkingMemoryRefs(workingStateGeneratedUrls) : assistantRefs;
|
||||
const originalRefs =
|
||||
@ -386,39 +384,7 @@ export function resolveTargetAssets(
|
||||
});
|
||||
}
|
||||
|
||||
const canvasAgentTargetPreference = resolveCanvasAgentTargetPreference({
|
||||
text,
|
||||
selectedImageCount: selectedRefs.length,
|
||||
latestResultImageCount: generatedRefs.length,
|
||||
workingMemoryTargetImageCount: workingStateResolvedUrls.length,
|
||||
});
|
||||
if (canvasAgentTargetPreference.shouldPreferLatestResult) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: generatedRefs,
|
||||
candidateRefs,
|
||||
resolutionMode:
|
||||
workingStateGeneratedUrls.length > 0 ? "working_memory" : "latest_result",
|
||||
confidence: canvasAgentTargetPreference.confidence,
|
||||
needsClarification: false,
|
||||
reason: "用户使用指代表达并提出图片编辑意图,默认续接最近生成结果",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (canvasAgentTargetPreference.shouldPreferWorkingMemoryTarget) {
|
||||
const refs = toWorkingMemoryRefs(workingStateResolvedUrls);
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: refs,
|
||||
candidateRefs,
|
||||
resolutionMode: "working_memory",
|
||||
confidence: canvasAgentTargetPreference.confidence,
|
||||
needsClarification: false,
|
||||
reason: "用户使用指代表达并提出图片编辑意图,使用工作记忆中的上一轮目标图",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasCanvasAgentDeicticTarget(text)) {
|
||||
if (DEICTIC_TARGET_REGEX.test(text)) {
|
||||
if (selectedRefs.length > 0) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: selectedRefs,
|
||||
@ -455,7 +421,7 @@ export function resolveTargetAssets(
|
||||
}
|
||||
|
||||
if (
|
||||
hasCanvasAgentActionableImageEditIntent(text) &&
|
||||
ACTIONABLE_EDIT_REGEX.test(text) &&
|
||||
currentRefs.length === 0 &&
|
||||
generatedRefs.length > 0 &&
|
||||
selectedLooksLikeOriginal &&
|
||||
@ -497,28 +463,14 @@ export function resolveTargetAssets(
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
canvasRefs.length === 1 &&
|
||||
(hasCanvasAgentActionableImageEditIntent(text) || focusPolicy === "auto")
|
||||
) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: [canvasRefs[0]],
|
||||
candidateRefs,
|
||||
resolutionMode: "text_reference",
|
||||
confidence: 84,
|
||||
needsClarification: false,
|
||||
reason: "用户使用画布/图片指代表达,且当前画布仅有一张可处理图片",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
const workingStateResolvedUrls = normalizeStringArray(workingState?.lastResolvedTargetImageUrls);
|
||||
const allowsImplicitHistoryTarget =
|
||||
IMPLICIT_HISTORY_TARGET_REGEX.test(text) &&
|
||||
(hasCanvasAgentActionableImageEditIntent(text) ||
|
||||
(ACTIONABLE_EDIT_REGEX.test(text) ||
|
||||
ACTIONABLE_GENERATION_REGEX.test(text) ||
|
||||
LATEST_RESULT_REGEX.test(text) ||
|
||||
ORIGINAL_IMAGE_REGEX.test(text) ||
|
||||
hasCanvasAgentDeicticTarget(text) ||
|
||||
DEICTIC_TARGET_REGEX.test(text) ||
|
||||
SELECTION_REGEX.test(text));
|
||||
|
||||
if (allowsImplicitHistoryTarget) {
|
||||
@ -573,19 +525,7 @@ export function resolveTargetAssets(
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCanvasAgentActionableImageEditIntent(text)) {
|
||||
if (canvasRefs.length === 1) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: [canvasRefs[0]],
|
||||
candidateRefs,
|
||||
resolutionMode: "text_reference",
|
||||
confidence: 82,
|
||||
needsClarification: false,
|
||||
reason: "用户表达了图片编辑意图,且当前画布仅有一张可处理图片",
|
||||
fromHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (ACTIONABLE_EDIT_REGEX.test(text)) {
|
||||
return buildResolvedTargetAssets({
|
||||
targetRefs: [],
|
||||
candidateRefs,
|
||||
|
||||
@ -57,7 +57,6 @@ export interface ResolvedChatMediaInput {
|
||||
resolvedImageRefs: ResolvedImageRef[];
|
||||
currentMessageImageUrls: string[];
|
||||
selectedImageUrls: string[];
|
||||
canvasImageUrls: string[];
|
||||
historyUserImageUrls: string[];
|
||||
historyAssistantImageUrls: string[];
|
||||
resolvedImageUrls: string[];
|
||||
|
||||
@ -695,6 +695,7 @@ function buildFalRequestPayload(params: {
|
||||
normalizeOptionalHttpUrl(params.imageParams?.falMaskImageUrl) ||
|
||||
normalizeOptionalHttpUrl(params.imageParams?.maskImageUrl);
|
||||
if (maskImageUrl) {
|
||||
input.mask_image_url = maskImageUrl;
|
||||
input.mask_url = maskImageUrl;
|
||||
}
|
||||
|
||||
|
||||
@ -1,156 +0,0 @@
|
||||
import type { DirectAgentToolCapability } from "@/lib/ai/runtime/direct-agent-execution";
|
||||
|
||||
export type CanvasAgentComposerMode = "agent" | "image" | "video" | "detail" | "clawd";
|
||||
export type CanvasAgentEntryMode = "auto" | "preset";
|
||||
|
||||
export interface CanvasAgentRequestPolicy {
|
||||
effectiveSelectedChatModel: string;
|
||||
shouldForceToolOrchestration: boolean;
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
export interface CanvasAgentTargetPreference {
|
||||
shouldPreferLatestResult: boolean;
|
||||
shouldPreferWorkingMemoryTarget: boolean;
|
||||
confidence: number;
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
const DEICTIC_TARGET_REGEX =
|
||||
/(这张图|这只|这个|这款|这套|当前这张|当前这个|当前这只|这一张|这一只|这个产品|这个物体|这只猫|这只狗)/i;
|
||||
|
||||
const ACTIONABLE_IMAGE_EDIT_REGEX =
|
||||
/(抠图|去背景|透明背景|去背|扣图|抠出|扣出|抠出来|扣出来|移除背景|去水印|水印|去logo|去\s*logo|移除logo|去除logo|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|多视角|换视角|改视角|视角变体|新角度|换角度|改角度|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|调色|调整颜色|亮度|对比度|饱和度|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调|处理)/i;
|
||||
|
||||
export function normalizeCanvasAgentRequestText(text: string) {
|
||||
return String(text || "").toLowerCase().replace(/\s+/g, "").trim();
|
||||
}
|
||||
|
||||
export function hasCanvasAgentDeicticTarget(text: string) {
|
||||
return DEICTIC_TARGET_REGEX.test(String(text || ""));
|
||||
}
|
||||
|
||||
export function hasCanvasAgentActionableImageEditIntent(text: string) {
|
||||
return ACTIONABLE_IMAGE_EDIT_REGEX.test(String(text || ""));
|
||||
}
|
||||
|
||||
export function inferCanvasAgentImageEditCapability(
|
||||
text: string,
|
||||
): DirectAgentToolCapability | null {
|
||||
const normalizedText = normalizeCanvasAgentRequestText(text);
|
||||
if (!normalizedText) return null;
|
||||
if (/去水印|水印|去logo|移除logo|去除logo/.test(normalizedText)) {
|
||||
return "remove_watermark";
|
||||
}
|
||||
if (/抠图|去背景|透明背景|去背|扣图|抠出|扣出|抠出来|扣出来|移除背景/.test(normalizedText)) {
|
||||
return "remove_background";
|
||||
}
|
||||
if (/多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位/.test(normalizedText)) {
|
||||
return "angle_variant";
|
||||
}
|
||||
if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|比例|画幅|竖版|横版|竖图|横图|9:16|16:9|1:1/.test(normalizedText)) {
|
||||
return "extend";
|
||||
}
|
||||
if (/调整尺寸|裁剪|resize|crop|缩放|放大|缩小/.test(normalizedText)) {
|
||||
return "resize";
|
||||
}
|
||||
if (/局部|框选|选区|区域|涂抹|蒙版/.test(normalizedText)) {
|
||||
return "local_edit";
|
||||
}
|
||||
if (/高清|清晰|锐化|降噪|提升质量|修复|清晰化/.test(normalizedText)) {
|
||||
return "enhance";
|
||||
}
|
||||
if (hasCanvasAgentActionableImageEditIntent(text)) {
|
||||
return "generic_edit";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveCanvasAgentRequestPolicy(params: {
|
||||
composerMode?: string | null;
|
||||
agentEntryMode?: string | null;
|
||||
selectedChatModel?: string | null;
|
||||
hasCanvasControlIntentFastPath?: boolean;
|
||||
}): CanvasAgentRequestPolicy {
|
||||
const composerMode = String(params.composerMode || "").trim();
|
||||
const agentEntryMode = String(params.agentEntryMode || "").trim();
|
||||
const selectedChatModel = String(params.selectedChatModel || "").trim() || "auto";
|
||||
const isAgentAutoComposer = composerMode === "agent" && agentEntryMode !== "preset";
|
||||
const shouldForceToolOrchestration =
|
||||
isAgentAutoComposer || Boolean(params.hasCanvasControlIntentFastPath);
|
||||
|
||||
if (isAgentAutoComposer) {
|
||||
return {
|
||||
effectiveSelectedChatModel: "auto",
|
||||
shouldForceToolOrchestration: true,
|
||||
reason: "agent_auto_composer",
|
||||
};
|
||||
}
|
||||
|
||||
if (params.hasCanvasControlIntentFastPath) {
|
||||
return {
|
||||
effectiveSelectedChatModel: selectedChatModel,
|
||||
shouldForceToolOrchestration: true,
|
||||
reason: "canvas_control_fast_path",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
effectiveSelectedChatModel: selectedChatModel,
|
||||
shouldForceToolOrchestration,
|
||||
reason: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveCanvasAgentTargetPreference(params: {
|
||||
text: string;
|
||||
selectedImageCount: number;
|
||||
latestResultImageCount: number;
|
||||
workingMemoryTargetImageCount: number;
|
||||
}): CanvasAgentTargetPreference {
|
||||
const hasDeicticTarget = hasCanvasAgentDeicticTarget(params.text);
|
||||
const hasEditIntent = hasCanvasAgentActionableImageEditIntent(params.text);
|
||||
const selectedImageCount = Math.max(0, Math.round(params.selectedImageCount || 0));
|
||||
const latestResultImageCount = Math.max(
|
||||
0,
|
||||
Math.round(params.latestResultImageCount || 0),
|
||||
);
|
||||
const workingMemoryTargetImageCount = Math.max(
|
||||
0,
|
||||
Math.round(params.workingMemoryTargetImageCount || 0),
|
||||
);
|
||||
|
||||
if (!hasDeicticTarget || !hasEditIntent || selectedImageCount > 0) {
|
||||
return {
|
||||
shouldPreferLatestResult: false,
|
||||
shouldPreferWorkingMemoryTarget: false,
|
||||
confidence: 0,
|
||||
reason: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (latestResultImageCount > 0) {
|
||||
return {
|
||||
shouldPreferLatestResult: true,
|
||||
shouldPreferWorkingMemoryTarget: false,
|
||||
confidence: 90,
|
||||
reason: "deictic_edit_prefers_latest_result",
|
||||
};
|
||||
}
|
||||
|
||||
if (workingMemoryTargetImageCount > 0) {
|
||||
return {
|
||||
shouldPreferLatestResult: false,
|
||||
shouldPreferWorkingMemoryTarget: true,
|
||||
confidence: 86,
|
||||
reason: "deictic_edit_uses_working_memory_target",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
shouldPreferLatestResult: false,
|
||||
shouldPreferWorkingMemoryTarget: false,
|
||||
confidence: 0,
|
||||
reason: null,
|
||||
};
|
||||
}
|
||||
@ -653,7 +653,6 @@ export function pickAgentSourceImageUrls(params: {
|
||||
params.requestTargetResolution?.targetImageUrls || [],
|
||||
params.requestResolvedMediaInput?.selectedImageUrls || [],
|
||||
params.requestResolvedMediaInput?.currentMessageImageUrls || [],
|
||||
params.requestResolvedMediaInput?.canvasImageUrls || [],
|
||||
params.requestWorkingState?.lastResolvedTargetImageUrls || [],
|
||||
params.requestWorkingState?.lastGeneratedImageUrls || [],
|
||||
params.requestResolvedMediaInput?.historyAssistantImageUrls || [],
|
||||
|
||||
@ -10,56 +10,6 @@ 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;
|
||||
@ -82,11 +32,12 @@ export function buildDirectAgentImageGenerationAssistantText(params: {
|
||||
modelDisplayName?: string | null;
|
||||
}) {
|
||||
return prependImageModelBadge(
|
||||
buildDirectAgentResultCard({
|
||||
result: getDirectAgentToolSuccessMessage("generate_image"),
|
||||
mediaMarkdown: params.imageMarkdown,
|
||||
followUp: getDirectAgentFollowUpMessage("generate_image"),
|
||||
}),
|
||||
[
|
||||
getDirectAgentToolSuccessMessage("generate_image"),
|
||||
params.imageMarkdown,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
params.modelDisplayName,
|
||||
);
|
||||
}
|
||||
@ -96,11 +47,12 @@ export function buildDirectAgentVideoGenerationAssistantText(params: {
|
||||
modelDisplayName?: string | null;
|
||||
}) {
|
||||
return prependImageModelBadge(
|
||||
buildDirectAgentResultCard({
|
||||
result: getDirectAgentToolSuccessMessage("generate_video"),
|
||||
mediaMarkdown: buildAgentToolVideoMarkdown(params.videoUrl),
|
||||
followUp: getDirectAgentFollowUpMessage("generate_video"),
|
||||
}),
|
||||
[
|
||||
getDirectAgentToolSuccessMessage("generate_video"),
|
||||
buildAgentToolVideoMarkdown(params.videoUrl),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
params.modelDisplayName,
|
||||
);
|
||||
}
|
||||
@ -112,15 +64,16 @@ export function buildDirectAgentImageEditAssistantText(params: {
|
||||
imageSize?: string;
|
||||
}) {
|
||||
return prependImageModelBadge(
|
||||
buildDirectAgentResultCard({
|
||||
result: getDirectAgentToolSuccessMessage(params.capability),
|
||||
mediaMarkdown: buildAgentToolImageMarkdown({
|
||||
[
|
||||
getDirectAgentToolSuccessMessage(params.capability),
|
||||
buildAgentToolImageMarkdown({
|
||||
originalUrl: params.originalUrl,
|
||||
thumbnailUrl: params.thumbnailUrl,
|
||||
imageSize: params.imageSize,
|
||||
}),
|
||||
followUp: getDirectAgentFollowUpMessage(params.capability),
|
||||
}),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
getDirectAgentToolLabel(params.capability),
|
||||
);
|
||||
}
|
||||
@ -134,11 +87,12 @@ export function buildDirectAgentBatchImageEditAssistantText(params: {
|
||||
}>;
|
||||
}) {
|
||||
return prependImageModelBadge(
|
||||
buildDirectAgentResultCard({
|
||||
result: getDirectAgentToolSuccessMessage(params.capability),
|
||||
mediaMarkdown: buildAgentToolImagesMarkdown(params.results),
|
||||
followUp: getDirectAgentFollowUpMessage(params.capability),
|
||||
}),
|
||||
[
|
||||
getDirectAgentToolSuccessMessage(params.capability),
|
||||
buildAgentToolImagesMarkdown(params.results),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
getDirectAgentToolLabel(params.capability),
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,10 +5,6 @@ import type { ResolvedTargetAssets } from "@/lib/ai/context-memory/types";
|
||||
import type { AnalysisResult } from "@/lib/ai/smart-analyzer";
|
||||
import type { TaskBoundaryDecision } from "@/lib/ai/runtime/task-boundary-classifier";
|
||||
import { hasDirectAgentLocalEditIntent } from "@/lib/ai/runtime/local-edit-options";
|
||||
import {
|
||||
hasCanvasAgentActionableImageEditIntent,
|
||||
inferCanvasAgentImageEditCapability,
|
||||
} from "@/lib/ai/runtime/canvas-agent-kernel";
|
||||
|
||||
export type IntentArbiterMode =
|
||||
| "respond"
|
||||
@ -94,10 +90,10 @@ function isExplicitImageGenerationRequest(text: string): boolean {
|
||||
const visualTarget =
|
||||
"(?:图|图片|海报|头像|插画|场景|主图|产品图|商品图|电商图|壁纸|封面|logo|图标)";
|
||||
const createVisualTarget = new RegExp(
|
||||
`(?:生成|创作|创建|制作|设计|做|出|画(?!布)|绘制)${countToken}?张?.{0,32}${visualTarget}`,
|
||||
`(?:生成|创作|创建|制作|设计|做|出|画|绘制)${countToken}?张?.{0,32}${visualTarget}`,
|
||||
);
|
||||
const drawSubject = new RegExp(
|
||||
`(?:画(?!布)|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`,
|
||||
`(?:画|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`,
|
||||
);
|
||||
const continueVariants = new RegExp(
|
||||
`(?:再来|再出|再做|再给我|补|多来|继续生成|继续出)${countToken}?张`,
|
||||
@ -120,21 +116,23 @@ function isExplicitImageGenerationRequest(text: string): boolean {
|
||||
}
|
||||
|
||||
function isLikelyImageEditRequest(text: string): boolean {
|
||||
return hasDirectAgentLocalEditIntent(text) || hasCanvasAgentActionableImageEditIntent(text);
|
||||
const normalizedText = String(text || "").toLowerCase();
|
||||
return hasDirectAgentLocalEditIntent(text) || /抠图|去背景|透明背景|去背|扣图|移除背景|去水印|水印|去logo|去\s*logo|移除logo|去除logo|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
|
||||
normalizedText,
|
||||
);
|
||||
}
|
||||
|
||||
function inferDefaultImageEditSubIntent(text: string): string {
|
||||
const inferredCapability = inferCanvasAgentImageEditCapability(text);
|
||||
if (
|
||||
inferredCapability &&
|
||||
inferredCapability !== "generic_edit" &&
|
||||
inferredCapability !== "generate_image" &&
|
||||
inferredCapability !== "generate_video" &&
|
||||
inferredCapability !== "analyze"
|
||||
) {
|
||||
return inferredCapability;
|
||||
}
|
||||
const lowerText = String(text || "").toLowerCase();
|
||||
if (/去水印|水印|去logo|去\s*logo|移除logo|去除logo/.test(lowerText)) return "remove_watermark";
|
||||
if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(lowerText)) return "remove_background";
|
||||
if (/换背景|改背景|背景换成|背景改成|白底|透明底/.test(lowerText)) return "replace_background";
|
||||
if (/多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位/.test(lowerText)) return "angle_variant";
|
||||
if (hasDirectAgentLocalEditIntent(text)) return "local_edit";
|
||||
if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|比例|画幅|竖版|横版|竖图|横图|9:16|16:9|1:1/.test(lowerText)) return "extend";
|
||||
if (/风格|卡通|动漫|油画|像素风|写实|插画/.test(lowerText)) return "style_transfer";
|
||||
if (/颜色|调色|亮度|对比度|饱和度|改成.*色|换成.*色/.test(lowerText)) return "color_adjust";
|
||||
if (/去除|消除|擦掉|删除|移除|p掉|重绘/.test(lowerText)) return "inpaint";
|
||||
return "enhance";
|
||||
}
|
||||
|
||||
@ -197,9 +195,6 @@ function buildExecutionFromAnalysis(params: {
|
||||
targetSource: ResolvedTargetAssets["resolutionMode"];
|
||||
}): IntentArbiterExecution {
|
||||
const mode = mapIntentToMode(params.analysis.intent, params.analysis.needsClarification);
|
||||
const shouldKeepPlannedToolCalls =
|
||||
params.execution?.mode === mode &&
|
||||
params.execution?.routeIntent === params.analysis.intent;
|
||||
|
||||
return {
|
||||
mode,
|
||||
@ -221,7 +216,7 @@ function buildExecutionFromAnalysis(params: {
|
||||
shouldStayConversational:
|
||||
mode === "respond" || mode === "analyze_image" || mode === "clarify",
|
||||
shouldPublishToCanvas: params.execution?.shouldPublishToCanvas || false,
|
||||
toolCalls: shouldKeepPlannedToolCalls ? params.execution?.toolCalls || [] : [],
|
||||
toolCalls: params.execution?.toolCalls || [],
|
||||
availableTools: params.execution?.availableTools || [],
|
||||
};
|
||||
}
|
||||
@ -265,14 +260,6 @@ function resolvePolicyIntent(params: {
|
||||
};
|
||||
}
|
||||
|
||||
if (hasUsableImage && likelyEdit) {
|
||||
return {
|
||||
intent: "image_edit",
|
||||
subIntent: params.analysis.subIntent || inferDefaultImageEditSubIntent(params.text),
|
||||
reason: "image_edit_policy_override",
|
||||
};
|
||||
}
|
||||
|
||||
switch (params.actionResolution.kind) {
|
||||
case "image_edit_followup":
|
||||
return {
|
||||
@ -303,6 +290,13 @@ function resolvePolicyIntent(params: {
|
||||
reason: "task_boundary_execution_video",
|
||||
};
|
||||
case "execution_image":
|
||||
if (hasUsableImage && likelyEdit && !explicitImageGen) {
|
||||
return {
|
||||
intent: "image_edit",
|
||||
subIntent: params.analysis.subIntent || inferDefaultImageEditSubIntent(params.text),
|
||||
reason: "task_boundary_execution_image_edit",
|
||||
};
|
||||
}
|
||||
return {
|
||||
intent: "image_gen",
|
||||
reason: "task_boundary_execution_image_generate",
|
||||
@ -333,6 +327,14 @@ function resolvePolicyIntent(params: {
|
||||
};
|
||||
}
|
||||
|
||||
if (hasUsableImage && likelyEdit && params.analysis.intent === "chat") {
|
||||
return {
|
||||
intent: "image_edit",
|
||||
subIntent: params.analysis.subIntent || inferDefaultImageEditSubIntent(params.text),
|
||||
reason: "image_edit_policy_override",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
intent: params.analysis.intent,
|
||||
subIntent:
|
||||
|
||||
@ -1,146 +0,0 @@
|
||||
import type { DBMessage } from "@/lib/db/schema";
|
||||
|
||||
export interface RecentDialogueMemoryEntry {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
messageId?: string;
|
||||
}
|
||||
|
||||
export interface RecentDialogueMemory {
|
||||
entries: RecentDialogueMemoryEntry[];
|
||||
contextSummary: string;
|
||||
memoryHints: string[];
|
||||
isRecallRequest: boolean;
|
||||
}
|
||||
|
||||
const USER_RECALL_PATTERN =
|
||||
/(?:我|用户)?(?:刚才|刚刚|前面|之前|上面|上一(?:句|条|轮)|最近).{0,12}(?:说|发|问|讲|提|输入|发送).{0,12}(?:什么|啥|哪句|哪些|内容)|(?:我说了什么|我刚才说啥|我前面说啥|我之前说啥|我发了什么|我问了什么|记得我.{0,12}说|没有记忆)/i;
|
||||
|
||||
const ASSISTANT_RECALL_PATTERN =
|
||||
/(?:你|助手|ai|AI).{0,12}(?:刚才|刚刚|前面|之前|上面|上一(?:句|条|轮)|最近).{0,12}(?:说|回复|回答|讲).{0,12}(?:什么|啥|哪句|哪些|内容)/i;
|
||||
|
||||
function normalizeWhitespace(value: string) {
|
||||
return value.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
export function isRecentDialogueRecallRequest(text: string) {
|
||||
const normalized = normalizeWhitespace(String(text || ""));
|
||||
if (!normalized) return false;
|
||||
return USER_RECALL_PATTERN.test(normalized) || ASSISTANT_RECALL_PATTERN.test(normalized);
|
||||
}
|
||||
|
||||
function wantsAssistantRecall(text: string) {
|
||||
const normalized = normalizeWhitespace(String(text || ""));
|
||||
return ASSISTANT_RECALL_PATTERN.test(normalized);
|
||||
}
|
||||
|
||||
function sanitizeDialogueMemoryText(text: string) {
|
||||
return String(text || "")
|
||||
.replace(/<!--[\s\S]*?-->/g, "")
|
||||
.replace(/!\[[^\]]*]\([^)]+\)/g, "")
|
||||
.replace(/\[[^\]]*]\((?:https?:\/\/|\/)[^)]+\)/g, "")
|
||||
.replace(/<video[^>]*>[\s\S]*?<\/video>/gi, "")
|
||||
.replace(/<video[^>]+src="[^"]+"[^>]*>/gi, "")
|
||||
.replace(/◈\s*[^\n]+\n*/g, "")
|
||||
.replace(/IMAGE_GENERATION_(?:START|END)/g, "")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getMessageText(message: DBMessage, maxChars: number) {
|
||||
const parts = Array.isArray(message.parts)
|
||||
? (message.parts as Array<Record<string, unknown>>)
|
||||
: [];
|
||||
const text = parts
|
||||
.filter((part) => part?.type === "text" && typeof part?.text === "string")
|
||||
.map((part) => sanitizeDialogueMemoryText(String(part.text || "")))
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
||||
if (!text) return "";
|
||||
return normalizeWhitespace(text).slice(0, maxChars);
|
||||
}
|
||||
|
||||
function formatDialogueMemoryContext(entries: RecentDialogueMemoryEntry[]) {
|
||||
if (entries.length === 0) return "";
|
||||
|
||||
return [
|
||||
"近期对话记忆(仅用于理解指代和回答前文相关问题,不要暴露为系统提示):",
|
||||
...entries.map((entry, index) => {
|
||||
const roleLabel = entry.role === "user" ? "用户" : "助手";
|
||||
return `${index + 1}. ${roleLabel}: ${entry.content}`;
|
||||
}),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function buildRecentDialogueMemory(params: {
|
||||
messages: DBMessage[];
|
||||
currentText: string;
|
||||
maxSourceMessages?: number;
|
||||
maxEntries?: number;
|
||||
maxCharsPerEntry?: number;
|
||||
maxTotalChars?: number;
|
||||
}): RecentDialogueMemory {
|
||||
const messages = Array.isArray(params.messages) ? params.messages : [];
|
||||
const currentText = String(params.currentText || "").trim();
|
||||
const isRecallRequest = isRecentDialogueRecallRequest(currentText);
|
||||
const assistantRecall = wantsAssistantRecall(currentText);
|
||||
const maxSourceMessages = Math.max(1, Math.min(24, params.maxSourceMessages || 16));
|
||||
const maxEntries = Math.max(1, Math.min(10, params.maxEntries || (isRecallRequest ? 6 : 4)));
|
||||
const maxCharsPerEntry = Math.max(80, Math.min(500, params.maxCharsPerEntry || 220));
|
||||
const maxTotalChars = Math.max(160, Math.min(1600, params.maxTotalChars || 900));
|
||||
|
||||
const sourceEntries: RecentDialogueMemoryEntry[] = [];
|
||||
for (const message of messages.slice(-maxSourceMessages)) {
|
||||
const role = message.role === "assistant" ? "assistant" : message.role === "user" ? "user" : null;
|
||||
if (!role) continue;
|
||||
|
||||
const content = getMessageText(message, maxCharsPerEntry);
|
||||
if (!content) continue;
|
||||
|
||||
sourceEntries.push({
|
||||
role,
|
||||
content,
|
||||
messageId: message.id,
|
||||
});
|
||||
}
|
||||
|
||||
const preferredEntries =
|
||||
isRecallRequest && !assistantRecall
|
||||
? sourceEntries.filter((entry) => entry.role === "user")
|
||||
: isRecallRequest && assistantRecall
|
||||
? sourceEntries.filter((entry) => entry.role === "assistant")
|
||||
: sourceEntries;
|
||||
const fallbackEntries =
|
||||
preferredEntries.length > 0 ? preferredEntries : sourceEntries;
|
||||
|
||||
const selectedReversed: RecentDialogueMemoryEntry[] = [];
|
||||
let totalChars = 0;
|
||||
for (let index = fallbackEntries.length - 1; index >= 0; index -= 1) {
|
||||
const entry = fallbackEntries[index];
|
||||
if (!entry) continue;
|
||||
if (selectedReversed.length >= maxEntries) break;
|
||||
if (totalChars + entry.content.length > maxTotalChars && selectedReversed.length > 0) {
|
||||
continue;
|
||||
}
|
||||
selectedReversed.push(entry);
|
||||
totalChars += entry.content.length;
|
||||
}
|
||||
|
||||
const entries = selectedReversed.reverse();
|
||||
const memoryHints = entries.length
|
||||
? [
|
||||
isRecallRequest
|
||||
? "用户正在询问前文或记忆,应优先根据近期对话记忆回答,不要声称没有上下文。"
|
||||
: "近期对话记忆只用于理解指代和延续语境,不要逐字复述,除非用户明确询问前文。",
|
||||
]
|
||||
: [];
|
||||
|
||||
return {
|
||||
entries,
|
||||
contextSummary: formatDialogueMemoryContext(entries),
|
||||
memoryHints,
|
||||
isRecallRequest,
|
||||
};
|
||||
}
|
||||
@ -5,7 +5,6 @@ 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 =
|
||||
@ -89,7 +88,9 @@ function isLikelyChatOnlyRequest(text: string): boolean {
|
||||
const normalized = normalizeCompactText(text);
|
||||
if (!normalized) return false;
|
||||
|
||||
return isCapabilityQuestionTurnRequest(normalized);
|
||||
return /你能做什么|你会什么|你可以做什么|你能帮我做什么|你能干什么|你可以帮我做什么|介绍一下你自己|介绍一下自己|说说你的能力|你有哪些功能|你有什么功能|怎么用|怎么玩|如何使用|帮助说明|帮助手册|功能介绍/.test(
|
||||
normalized,
|
||||
);
|
||||
}
|
||||
|
||||
function isLikelyAnalysisOnlyRequest(text: string, hasImageContext: boolean): boolean {
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
@ -33,7 +33,7 @@ export interface AnalysisResult {
|
||||
function inferDefaultImageEditSubIntent(text: string): AnalysisResult["subIntent"] {
|
||||
const lowerText = text.toLowerCase();
|
||||
if (/去水印|水印|去logo|去\s*logo|移除logo|去除logo/.test(lowerText)) return "remove_watermark";
|
||||
if (/抠图|去背景|透明背景|去背|扣图|抠出|扣出|抠出来|扣出来|移除背景/.test(lowerText)) return "remove_background";
|
||||
if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(lowerText)) return "remove_background";
|
||||
if (/换背景|改背景|背景换成|背景改成|白底|透明底/.test(lowerText)) return "replace_background";
|
||||
if (/多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位/.test(lowerText)) return "angle_variant";
|
||||
if (hasDirectAgentLocalEditIntent(text)) return "local_edit";
|
||||
@ -112,10 +112,10 @@ export function isExplicitImageGenerationRequest(text: string): boolean {
|
||||
const visualTarget =
|
||||
"(?:图|图片|海报|头像|插画|场景|主图|产品图|商品图|电商图|壁纸|封面|logo|图标)";
|
||||
const createVisualTarget = new RegExp(
|
||||
`(?:生成|创作|创建|制作|设计|做|出|画(?!布)|绘制)${countToken}?张?.{0,32}${visualTarget}`,
|
||||
`(?:生成|创作|创建|制作|设计|做|出|画|绘制)${countToken}?张?.{0,32}${visualTarget}`,
|
||||
);
|
||||
const drawSubject = new RegExp(
|
||||
`(?:画(?!布)|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`,
|
||||
`(?:画|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`,
|
||||
);
|
||||
const continueVariants = new RegExp(
|
||||
`(?:再来|再出|再做|再给我|补|多来|继续生成|继续出)${countToken}?张`,
|
||||
@ -141,7 +141,7 @@ export function isExplicitImageGenerationRequest(text: string): boolean {
|
||||
|
||||
function isLikelyImageEditRequest(text: string): boolean {
|
||||
const normalizedText = text.toLowerCase();
|
||||
return hasDirectAgentLocalEditIntent(text) || /抠图|去背景|透明背景|去背|扣图|抠出|扣出|抠出来|扣出来|移除背景|去水印|水印|去logo|去\s*logo|移除logo|去除logo|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
|
||||
return hasDirectAgentLocalEditIntent(text) || /抠图|去背景|透明背景|去背|扣图|移除背景|去水印|水印|去logo|去\s*logo|移除logo|去除logo|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
|
||||
normalizedText,
|
||||
);
|
||||
}
|
||||
@ -2331,7 +2331,7 @@ export function fallbackKeywordAnalysis(
|
||||
}
|
||||
|
||||
// 抠图/去背景
|
||||
if (/抠图|去背景|透明背景|去背|扣图|抠出|扣出|抠出来|扣出来|移除背景/.test(lowerText)) {
|
||||
if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(lowerText)) {
|
||||
return {
|
||||
intent: "image_edit",
|
||||
subIntent: "remove_background",
|
||||
|
||||
@ -250,7 +250,7 @@ function normalizeEditCapability(
|
||||
subIntent: "remove_watermark",
|
||||
};
|
||||
}
|
||||
if (subIntent === "remove_background" || /抠图|去背|去背景|透明底|抠出|扣出|抠出来|扣出来/.test(normalizedText)) {
|
||||
if (subIntent === "remove_background" || /抠图|去背|去背景|透明底/.test(normalizedText)) {
|
||||
return {
|
||||
capability: "remove_background",
|
||||
toolName: "removeBackground",
|
||||
|
||||
@ -3059,6 +3059,13 @@ async function applyScaleAdjustPatchCompositeToImages(params: {
|
||||
touchesTop,
|
||||
width: workspaceBox.width,
|
||||
});
|
||||
const staticPatchMaskRaw = staticPatchMask
|
||||
? await normalizeAlphaMaskToRaw({
|
||||
height: workspaceBox.height,
|
||||
maskBuffer: staticPatchMask,
|
||||
width: workspaceBox.width,
|
||||
})
|
||||
: null;
|
||||
const regionClipPatchMask = maskRegionBoxes?.length
|
||||
? buildScaleAdjustPatchCompositeMaskSvg({
|
||||
featherPx,
|
||||
@ -3177,11 +3184,21 @@ async function applyScaleAdjustPatchCompositeToImages(params: {
|
||||
imageBuffer: resizedGeneratedBuffer,
|
||||
width: workspaceBox.width,
|
||||
})
|
||||
: await sharp(resizedGeneratedBuffer, { failOn: "none" })
|
||||
.ensureAlpha()
|
||||
.composite([{ input: patchMask, blend: "dest-in" }])
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer();
|
||||
: activeClipPatchMaskRaw || staticPatchMaskRaw
|
||||
? await applyRawAlphaToImage({
|
||||
alphaMaskRaw:
|
||||
activeClipPatchMaskRaw && staticPatchMaskRaw
|
||||
? multiplyAlphaMasks(staticPatchMaskRaw, activeClipPatchMaskRaw)
|
||||
: activeClipPatchMaskRaw || staticPatchMaskRaw!,
|
||||
height: workspaceBox.height,
|
||||
imageBuffer: resizedGeneratedBuffer,
|
||||
width: workspaceBox.width,
|
||||
})
|
||||
: await sharp(resizedGeneratedBuffer, { failOn: "none" })
|
||||
.ensureAlpha()
|
||||
.composite([{ input: patchMask, blend: "dest-in" }])
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer();
|
||||
const mergedBuffer = await sharp(sourceBuffer, { failOn: "none" })
|
||||
.ensureAlpha()
|
||||
.composite([
|
||||
|
||||
@ -29,13 +29,6 @@ export function isCanvasImageReadyForAi(node: CanvasNode | null | undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = (node.data || {}) as Record<string, any>;
|
||||
const src = String(
|
||||
data.src ||
|
||||
data.previewImage ||
|
||||
data.imageUrl ||
|
||||
data.renderArtifacts?.screenshotUrl ||
|
||||
""
|
||||
).trim();
|
||||
const src = String(((node.data as Record<string, any>)?.src || "")).trim();
|
||||
return /^https?:\/\//i.test(src);
|
||||
}
|
||||
|
||||
@ -186,44 +186,6 @@ export async function saveChat({
|
||||
}
|
||||
}
|
||||
|
||||
export async function getClawdChatsByUserId({
|
||||
userId,
|
||||
limit = 50,
|
||||
}: {
|
||||
userId: string;
|
||||
limit?: number;
|
||||
}) {
|
||||
const safeLimit = Math.max(1, Math.min(100, Math.round(limit) || 50));
|
||||
const latestMessageAt = sql<Date>`max(${message.createdAt})`;
|
||||
|
||||
try {
|
||||
return await db
|
||||
.select({
|
||||
id: chat.id,
|
||||
title: chat.title,
|
||||
createdAt: chat.createdAt,
|
||||
updatedAt: latestMessageAt,
|
||||
messageCount: sql<number>`count(${message.id})::int`,
|
||||
})
|
||||
.from(chat)
|
||||
.innerJoin(message, eq(message.chatId, chat.id))
|
||||
.where(
|
||||
and(
|
||||
eq(chat.userId, userId),
|
||||
sql`coalesce(${message.videoMetadata}->>'composerMode', '') = 'clawd'`
|
||||
)
|
||||
)
|
||||
.groupBy(chat.id, chat.title, chat.createdAt)
|
||||
.orderBy(desc(latestMessageAt))
|
||||
.limit(safeLimit);
|
||||
} catch (_error) {
|
||||
throw new ChatSDKError(
|
||||
"bad_request:database",
|
||||
"Failed to get Claw chats by user id"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteChatById({ id }: { id: string }) {
|
||||
try {
|
||||
await db.delete(vote).where(eq(vote.chatId, id));
|
||||
|
||||
@ -45,7 +45,7 @@ export type StorageUploadFunction = (
|
||||
|
||||
let lastKnownGoodDbS3Config: S3UploadConfig | null = null;
|
||||
const STORAGE_FAILOVER_CONFIG_KEY = "storage_failover_config";
|
||||
const DEFAULT_S3_REQUEST_TIMEOUT_MS = 30_000;
|
||||
const DEFAULT_S3_REQUEST_TIMEOUT_MS = 180_000;
|
||||
|
||||
function getStorageTimeoutMs(name: string, fallback: number, min: number, max: number) {
|
||||
const parsed = Number(process.env[name]);
|
||||
|
||||
@ -4,15 +4,12 @@ import {
|
||||
extractAssistantImageUrls,
|
||||
extractVideoUrls,
|
||||
normalizeMediaUrl,
|
||||
sortCanvasMessagesByCreatedAt,
|
||||
stripMediaMarkup,
|
||||
} from "@/components/canvas/chat-panel/canvas-chat-media";
|
||||
import {
|
||||
filterCanvasMediaPayloadForUnhandledItems,
|
||||
buildAgentProgressNodeSteps,
|
||||
getCanvasMediaPayloadHandledItemIds,
|
||||
getCanvasOperationHandledMediaItemIds,
|
||||
getAgentProgressStepLabel,
|
||||
isCanvasAgentViewOperationClientEnabled,
|
||||
normalizeCanvasAgentClientBoolean,
|
||||
shouldRetryCanvasViewOperationAfterRefresh,
|
||||
@ -47,18 +44,7 @@ import {
|
||||
} from "@/lib/canvas-toolbar-config";
|
||||
import { shouldBypassCanvasAgentMissingTargetClarification } from "@/lib/ai/runtime/canvas-agent-clarification";
|
||||
import { hasCanvasAgentRoutingContext } from "@/lib/ai/canvas-agent-routing";
|
||||
import {
|
||||
inferCanvasAgentImageEditCapability,
|
||||
resolveCanvasAgentRequestPolicy,
|
||||
resolveCanvasAgentTargetPreference,
|
||||
} from "@/lib/ai/runtime/canvas-agent-kernel";
|
||||
import { isExplicitImageGenerationRequest } from "@/lib/ai/smart-analyzer";
|
||||
import { inferAgentImageCountFromText } from "@/lib/ai/runtime/image-count-intent";
|
||||
import {
|
||||
buildRecentDialogueMemory,
|
||||
isRecentDialogueRecallRequest,
|
||||
} from "@/lib/ai/runtime/recent-dialogue-memory";
|
||||
import { isCapabilityQuestionTurnRequest } from "@/lib/ai/runtime/task-boundary-text";
|
||||
import {
|
||||
isAgentModeScopedChatId,
|
||||
isScopedEmptyHistoryRequest,
|
||||
@ -141,14 +127,9 @@ import {
|
||||
getDirectAgentToolSuccessMessage,
|
||||
hasExplicitDirectAgentBatchImageEditIntent,
|
||||
isDirectAgentBatchImageEditCapability,
|
||||
resolveDirectAgentToolContext,
|
||||
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,
|
||||
@ -228,69 +209,8 @@ import {
|
||||
import type { PickedObjectAttachment } from "@/components/canvas/chat-panel/canvas-chat-session";
|
||||
import type { CanvasAgentContext } from "@/types/canvas/agent-protocol";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import type { DBMessage } from "@/lib/db/schema";
|
||||
|
||||
function dbTextMessage(
|
||||
id: string,
|
||||
role: "user" | "assistant",
|
||||
text: string,
|
||||
): DBMessage {
|
||||
return {
|
||||
id,
|
||||
role,
|
||||
parts: [{ type: "text", text }],
|
||||
} as unknown as DBMessage;
|
||||
}
|
||||
|
||||
test.describe("canvas chat pure helpers", () => {
|
||||
test("keeps optimistic messages without timestamps after persisted history", () => {
|
||||
const messages = [
|
||||
{
|
||||
id: "optimistic-latest-user",
|
||||
role: "user",
|
||||
parts: [{ type: "text", text: "你能为我做什么?列出表给我" }],
|
||||
},
|
||||
{
|
||||
id: "persisted-first-user",
|
||||
role: "user",
|
||||
createdAt: "2026-07-07T01:00:00.000Z",
|
||||
parts: [{ type: "text", text: "你好" }],
|
||||
},
|
||||
{
|
||||
id: "persisted-first-assistant",
|
||||
role: "assistant",
|
||||
createdAt: "2026-07-07T01:00:01.000Z",
|
||||
parts: [{ type: "text", text: "你好,我在。" }],
|
||||
},
|
||||
] as ChatMessage[];
|
||||
|
||||
expect(sortCanvasMessagesByCreatedAt(messages).map((message) => message.id)).toEqual([
|
||||
"persisted-first-user",
|
||||
"persisted-first-assistant",
|
||||
"optimistic-latest-user",
|
||||
]);
|
||||
});
|
||||
|
||||
test("preserves relative order among messages without timestamps", () => {
|
||||
const messages = [
|
||||
{
|
||||
id: "pending-user",
|
||||
role: "user",
|
||||
parts: [{ type: "text", text: "生成一张图" }],
|
||||
},
|
||||
{
|
||||
id: "pending-assistant",
|
||||
role: "assistant",
|
||||
parts: [{ type: "text", text: "分析中" }],
|
||||
},
|
||||
] as ChatMessage[];
|
||||
|
||||
expect(sortCanvasMessagesByCreatedAt(messages).map((message) => message.id)).toEqual([
|
||||
"pending-user",
|
||||
"pending-assistant",
|
||||
]);
|
||||
});
|
||||
|
||||
test("formats uploaded spreadsheet rows as safe markdown tables for OpenClaw", () => {
|
||||
const markdown = rowsToOpenClawMarkdownTable([
|
||||
["名称", "备注"],
|
||||
@ -329,52 +249,6 @@ 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: [
|
||||
dbTextMessage("m1", "user", "你好"),
|
||||
dbTextMessage("m2", "assistant", "你好,我在。"),
|
||||
dbTextMessage("m3", "user", "你是一个小可爱"),
|
||||
dbTextMessage("m4", "assistant", "嘿嘿,谢谢你的夸奖。"),
|
||||
],
|
||||
currentText: "我前面说了什么?",
|
||||
});
|
||||
|
||||
expect(isRecentDialogueRecallRequest("我前面说了什么?")).toBe(true);
|
||||
expect(memory.isRecallRequest).toBe(true);
|
||||
expect(memory.entries.map((entry) => entry.role)).toEqual(["user", "user"]);
|
||||
expect(memory.entries.at(-1)?.content).toBe("你是一个小可爱");
|
||||
expect(memory.contextSummary).toContain("用户: 你是一个小可爱");
|
||||
expect(memory.memoryHints.join(" ")).toContain("不要声称没有上下文");
|
||||
});
|
||||
|
||||
test("sanitizes media markup from recent dialogue memory", () => {
|
||||
const memory = buildRecentDialogueMemory({
|
||||
messages: [
|
||||
dbTextMessage("m1", "user", "生成一只小猫"),
|
||||
dbTextMessage(
|
||||
"m2",
|
||||
"assistant",
|
||||
"<!-- IMAGE_GENERATION_START -->\n\n已完成本次生成"
|
||||
),
|
||||
],
|
||||
currentText: "你刚才回复了什么?",
|
||||
});
|
||||
|
||||
expect(memory.entries).toHaveLength(1);
|
||||
expect(memory.entries[0].role).toBe("assistant");
|
||||
expect(memory.entries[0].content).toBe("已完成本次生成");
|
||||
expect(memory.contextSummary).not.toContain("https://example.com");
|
||||
expect(memory.contextSummary).not.toContain("IMAGE_GENERATION_START");
|
||||
});
|
||||
|
||||
test("infers explicit agent image counts from user text", () => {
|
||||
expect(inferAgentImageCountFromText("再出三版")).toBe(3);
|
||||
expect(inferAgentImageCountFromText("基于这张图再来 3 张")).toBe(3);
|
||||
@ -1256,273 +1130,6 @@ test.describe("canvas chat pure helpers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("formats direct agent image results as structured markdown cards", () => {
|
||||
const generatedText = buildDirectAgentImageGenerationAssistantText({
|
||||
imageMarkdown: "",
|
||||
modelDisplayName: "Test Image Model",
|
||||
});
|
||||
|
||||
expect(generatedText).toContain("### 执行结果");
|
||||
expect(generatedText).toContain("- 已完成图片生成,结果如下。");
|
||||
expect(generatedText).toContain("");
|
||||
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("builds capability-aware agent progress node labels", () => {
|
||||
const selectingSteps = buildAgentProgressNodeSteps({
|
||||
stage: "planning",
|
||||
currentStep: "select_tool",
|
||||
capability: "remove_background",
|
||||
});
|
||||
|
||||
expect(selectingSteps.map((step) => step.label)).toEqual([
|
||||
"理解用户需求",
|
||||
"分析使用工具",
|
||||
"确认抠图工具",
|
||||
"抠图中",
|
||||
"整理结果",
|
||||
]);
|
||||
expect(selectingSteps.map((step) => step.state)).toEqual([
|
||||
"completed",
|
||||
"completed",
|
||||
"current",
|
||||
"upcoming",
|
||||
"upcoming",
|
||||
]);
|
||||
|
||||
const executingSteps = buildAgentProgressNodeSteps({
|
||||
stage: "executing",
|
||||
currentStep: "execute",
|
||||
capability: "remove_background",
|
||||
});
|
||||
|
||||
expect(executingSteps[2]?.state).toBe("completed");
|
||||
expect(executingSteps[3]).toMatchObject({
|
||||
label: "抠图中",
|
||||
state: "current",
|
||||
});
|
||||
expect(getAgentProgressStepLabel("execute", { capability: "generate_video" })).toBe(
|
||||
"视频生成中"
|
||||
);
|
||||
});
|
||||
|
||||
test("resolves deictic canvas edit requests to latest results and direct tools", () => {
|
||||
const latestResultUrl = "https://cdn.example.com/latest-cat-result.png";
|
||||
const requestPolicy = resolveCanvasAgentRequestPolicy({
|
||||
composerMode: "agent",
|
||||
selectedChatModel: "conversation-model-id",
|
||||
});
|
||||
|
||||
expect(requestPolicy.effectiveSelectedChatModel).toBe("auto");
|
||||
expect(requestPolicy.shouldForceToolOrchestration).toBe(true);
|
||||
|
||||
const targetPreference = resolveCanvasAgentTargetPreference({
|
||||
text: "将这只猫从图中扣图扣出来",
|
||||
selectedImageCount: 0,
|
||||
latestResultImageCount: 1,
|
||||
workingMemoryTargetImageCount: 1,
|
||||
});
|
||||
|
||||
expect(targetPreference.shouldPreferLatestResult).toBe(true);
|
||||
|
||||
const mediaInput = {
|
||||
imageRefs: [],
|
||||
currentMessageImageRefs: [],
|
||||
selectedImageRefs: [],
|
||||
canvasImageRefs: [],
|
||||
historyUserImageRefs: [],
|
||||
historyAssistantImageRefs: [
|
||||
{
|
||||
url: latestResultUrl,
|
||||
normalizedUrl: latestResultUrl,
|
||||
source: "history_assistant_result",
|
||||
role: "history_result",
|
||||
},
|
||||
],
|
||||
resolvedImageRefs: [],
|
||||
currentMessageImageUrls: [],
|
||||
selectedImageUrls: [],
|
||||
historyUserImageUrls: [],
|
||||
historyAssistantImageUrls: [latestResultUrl],
|
||||
resolvedImageUrls: [latestResultUrl],
|
||||
selectedAssetIds: [],
|
||||
candidateAssetIds: [],
|
||||
uploadedAssetIds: [],
|
||||
hasImage: true,
|
||||
source: "history",
|
||||
fromHistory: true,
|
||||
} as any;
|
||||
const workingState = {
|
||||
lastGeneratedImageUrls: [latestResultUrl],
|
||||
lastResolvedTargetImageUrls: [latestResultUrl],
|
||||
};
|
||||
const targetResolution = {
|
||||
targetRefs: [],
|
||||
candidateRefs: [],
|
||||
targetImageUrls: [latestResultUrl],
|
||||
targetAssetIds: [],
|
||||
candidateImageUrls: [],
|
||||
candidateAssetIds: [],
|
||||
resolutionMode: "working_memory",
|
||||
confidence: targetPreference.confidence,
|
||||
needsClarification: false,
|
||||
reason: targetPreference.reason,
|
||||
fromHistory: true,
|
||||
} as any;
|
||||
|
||||
const directContext = resolveDirectAgentToolContext({
|
||||
execution: {
|
||||
mode: "edit_image",
|
||||
routeIntent: "image_edit",
|
||||
routeSubIntent: "remove_background",
|
||||
targetSource: targetResolution.resolutionMode,
|
||||
preferredCapability: "remove_background",
|
||||
shouldUseStructuredReply: true,
|
||||
shouldOptimizePrompt: true,
|
||||
shouldApplyImagePipeline: true,
|
||||
shouldApplyVideoPipeline: false,
|
||||
shouldStayConversational: false,
|
||||
shouldPublishToCanvas: false,
|
||||
toolCalls: [],
|
||||
availableTools: ["removeBackground"],
|
||||
} as any,
|
||||
requestResolvedMediaInput: mediaInput,
|
||||
requestTargetResolution: targetResolution,
|
||||
requestWorkingState: workingState,
|
||||
});
|
||||
|
||||
expect(directContext?.execution.toolName).toBe("removeBackground");
|
||||
expect(directContext?.targetImageUrl).toBe(latestResultUrl);
|
||||
});
|
||||
|
||||
test("falls back to canvas images for direct agent edit tools", () => {
|
||||
const canvasImageUrl = "https://cdn.example.com/canvas-cat.png";
|
||||
const mediaInput = {
|
||||
imageRefs: [],
|
||||
currentMessageImageRefs: [],
|
||||
selectedImageRefs: [],
|
||||
canvasImageRefs: [
|
||||
{
|
||||
url: canvasImageUrl,
|
||||
normalizedUrl: canvasImageUrl,
|
||||
nodeId: "cat-node",
|
||||
source: "canvas_context",
|
||||
role: "reference",
|
||||
x: 120,
|
||||
y: 80,
|
||||
width: 640,
|
||||
height: 640,
|
||||
isSelected: false,
|
||||
},
|
||||
],
|
||||
historyUserImageRefs: [],
|
||||
historyAssistantImageRefs: [],
|
||||
resolvedImageRefs: [],
|
||||
currentMessageImageUrls: [],
|
||||
selectedImageUrls: [],
|
||||
canvasImageUrls: [canvasImageUrl],
|
||||
historyUserImageUrls: [],
|
||||
historyAssistantImageUrls: [],
|
||||
resolvedImageUrls: [],
|
||||
selectedAssetIds: [],
|
||||
candidateAssetIds: [],
|
||||
uploadedAssetIds: [],
|
||||
hasImage: false,
|
||||
source: "none",
|
||||
fromHistory: false,
|
||||
} as any;
|
||||
|
||||
const directContext = resolveDirectAgentToolContext({
|
||||
execution: {
|
||||
mode: "edit_image",
|
||||
routeIntent: "image_edit",
|
||||
routeSubIntent: "remove_background",
|
||||
targetSource: "text_reference",
|
||||
preferredCapability: "remove_background",
|
||||
shouldUseStructuredReply: true,
|
||||
shouldOptimizePrompt: true,
|
||||
shouldApplyImagePipeline: true,
|
||||
shouldApplyVideoPipeline: false,
|
||||
shouldStayConversational: false,
|
||||
shouldPublishToCanvas: false,
|
||||
toolCalls: [],
|
||||
availableTools: ["removeBackground"],
|
||||
} as any,
|
||||
requestResolvedMediaInput: mediaInput,
|
||||
});
|
||||
|
||||
expect(directContext?.execution.toolName).toBe("removeBackground");
|
||||
expect(directContext?.targetImageUrl).toBe(canvasImageUrl);
|
||||
});
|
||||
|
||||
test("routes canvas subject cutout wording to remove background instead of image generation", () => {
|
||||
const canvasImageUrl = "https://cdn.example.com/canvas-dog.png";
|
||||
const userText = "帮我将画布中的狗的图扣出来";
|
||||
|
||||
expect(isExplicitImageGenerationRequest(userText)).toBe(false);
|
||||
expect(inferCanvasAgentImageEditCapability(userText)).toBe("remove_background");
|
||||
|
||||
const directContext = resolveDirectAgentToolContext({
|
||||
execution: {
|
||||
mode: "edit_image",
|
||||
routeIntent: "image_edit",
|
||||
routeSubIntent: "remove_background",
|
||||
targetSource: "text_reference",
|
||||
preferredCapability: "remove_background",
|
||||
shouldUseStructuredReply: true,
|
||||
shouldOptimizePrompt: true,
|
||||
shouldApplyImagePipeline: true,
|
||||
shouldApplyVideoPipeline: false,
|
||||
shouldStayConversational: false,
|
||||
shouldPublishToCanvas: false,
|
||||
toolCalls: [],
|
||||
availableTools: ["resolveTargetImage", "removeBackground"],
|
||||
} as any,
|
||||
requestResolvedMediaInput: {
|
||||
imageRefs: [],
|
||||
currentMessageImageRefs: [],
|
||||
selectedImageRefs: [],
|
||||
canvasImageRefs: [
|
||||
{
|
||||
url: canvasImageUrl,
|
||||
normalizedUrl: canvasImageUrl,
|
||||
nodeId: "dog-node",
|
||||
source: "canvas_context",
|
||||
role: "reference",
|
||||
},
|
||||
],
|
||||
historyUserImageRefs: [],
|
||||
historyAssistantImageRefs: [],
|
||||
resolvedImageRefs: [],
|
||||
currentMessageImageUrls: [],
|
||||
selectedImageUrls: [],
|
||||
canvasImageUrls: [canvasImageUrl],
|
||||
historyUserImageUrls: [],
|
||||
historyAssistantImageUrls: [],
|
||||
resolvedImageUrls: [],
|
||||
selectedAssetIds: [],
|
||||
candidateAssetIds: [],
|
||||
uploadedAssetIds: [],
|
||||
hasImage: true,
|
||||
source: "canvas_context",
|
||||
fromHistory: false,
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(directContext?.execution.toolName).toBe("removeBackground");
|
||||
expect(directContext?.targetImageUrl).toBe(canvasImageUrl);
|
||||
});
|
||||
|
||||
test("only runs direct image edit batches for explicit safe batch requests", () => {
|
||||
expect(
|
||||
shouldRunDirectAgentBatchImageEdit({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user