diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx
index b5a1638a3..b55835116 100644
--- a/app/(admin)/settings/admin-settings-client.tsx
+++ b/app/(admin)/settings/admin-settings-client.tsx
@@ -167,6 +167,11 @@ import {
isImageGenerationModelDescriptor,
isVideoModelDescriptor,
} from "@/lib/ai/model-classification";
+import {
+ buildVideoDurationCostMap,
+ getConfiguredVideoDurationOptions,
+ normalizeVideoDurationOptions,
+} from "@/lib/ai/video-model-config";
import type { AgentWorkflowUiConfig } from "@/types/agent";
const fetcher = (url: string) => fetch(url).then((res) => res.json());
@@ -360,10 +365,59 @@ type ImageGenConfig = {
supportsSeed?: boolean; // 是否支持随机种子
creditCostPerImage?: number; // 每张图片积分消耗
creditCostPerVideo?: number; // 每条视频积分消耗
+ videoDurationOptions?: number[]; // 视频秒数选项
+ creditCostPerVideoByDuration?: Record; // 各秒数积分
enableLoadingPlaceholder?: boolean; // 是否启用加载占位图
loadingColor?: string; // 占位图主题色,如 "purple", "blue", "green"
};
+function createDefaultImageGenConfig(): ImageGenConfig {
+ return {
+ maxImages: 2,
+ supportsSize: false,
+ supports1K: true,
+ supports2K: true,
+ supports4K: true,
+ supportsPromptExtend: false,
+ supportsNegativePrompt: false,
+ supportsWatermark: false,
+ supportsSeed: false,
+ creditCostPerImage: 0,
+ creditCostPerVideo: 0,
+ videoDurationOptions: [],
+ creditCostPerVideoByDuration: {},
+ enableLoadingPlaceholder: true,
+ loadingColor: "purple",
+ };
+}
+
+function formatVideoDurationOptionsInput(options?: number[]) {
+ return getConfiguredVideoDurationOptions({ videoDurationOptions: options }).join(", ");
+}
+
+function parseVideoDurationOptionsInput(value: string) {
+ return normalizeVideoDurationOptions(
+ String(value || "")
+ .split(/[,\n,、\s]+/)
+ .map((item) => item.trim())
+ .filter(Boolean)
+ );
+}
+
+function syncVideoDurationPricing(config: ImageGenConfig | undefined, durationOptions: number[]) {
+ const nextConfig = config || createDefaultImageGenConfig();
+ return {
+ ...nextConfig,
+ videoDurationOptions: durationOptions,
+ creditCostPerVideoByDuration:
+ buildVideoDurationCostMap({
+ durationOptions,
+ costMap: nextConfig.creditCostPerVideoByDuration,
+ fallbackCost: nextConfig.creditCostPerVideo,
+ }) || {},
+ };
+}
+
type AiModel = {
id: string;
providerId: string;
@@ -2316,21 +2370,7 @@ export function AdminSettingsClient() {
fallbackModelId2: "", // 备用模型 ID 2
excludeFromFailover: false,
// 🆕 图片生成模型特性配置
- imageGenConfig: {
- maxImages: 2,
- supportsSize: false,
- supports1K: true,
- supports2K: true,
- supports4K: true,
- supportsPromptExtend: false,
- supportsNegativePrompt: false,
- supportsWatermark: false,
- supportsSeed: false,
- creditCostPerImage: 0,
- creditCostPerVideo: 0,
- enableLoadingPlaceholder: true,
- loadingColor: "purple",
- } as ImageGenConfig,
+ imageGenConfig: createDefaultImageGenConfig(),
});
const [showModelDialog, setShowModelDialog] = useState(false);
const [editingModel, setEditingModel] = useState(null);
@@ -2394,6 +2434,9 @@ export function AdminSettingsClient() {
modelId: modelForm.modelId,
supportsImageGen: modelForm.supportsImageGen,
});
+ const configuredVideoDurationOptions = getConfiguredVideoDurationOptions(
+ modelForm.imageGenConfig
+ );
const modelKindHint =
selectedModelProviderType === "runway" && !modelForm.modelId.trim()
? "Runway 同时支持图片与视频模型,填写模型 ID 后系统会自动识别标签。"
@@ -4830,18 +4873,7 @@ export function AdminSettingsClient() {
fallbackModelId1: "",
fallbackModelId2: "",
excludeFromFailover: false,
- imageGenConfig: {
- maxImages: 2,
- supportsSize: false,
- supportsPromptExtend: false,
- supportsNegativePrompt: false,
- supportsWatermark: false,
- supportsSeed: false,
- creditCostPerImage: 0,
- creditCostPerVideo: 0,
- enableLoadingPlaceholder: true,
- loadingColor: "purple",
- },
+ imageGenConfig: createDefaultImageGenConfig(),
});
setEditingModel(null);
};
@@ -11596,6 +11628,104 @@ export function AdminSettingsClient() {
+ {isVideoProviderForModelConfig && (
+
+
+
+
{
+ const durationOptions = parseVideoDurationOptionsInput(
+ e.target.value
+ );
+ setModelForm({
+ ...modelForm,
+ imageGenConfig: syncVideoDurationPricing(
+ modelForm.imageGenConfig,
+ durationOptions
+ ),
+ });
+ }}
+ placeholder="例如:4, 6, 8 或 5, 10, 15"
+ />
+
+ 前端会按这里显示可选视频秒数。留空时继续使用服务商默认能力。
+
+
+
+ {configuredVideoDurationOptions.length > 0 ? (
+
+
+ 各秒数积分消耗
+
+
+ {configuredVideoDurationOptions.map((duration) => {
+ const durationKey = String(duration);
+ return (
+
+
+ {
+ const nextValue = Math.max(
+ 0,
+ Number.parseInt(
+ e.target.value,
+ 10
+ ) || 0
+ );
+ setModelForm({
+ ...modelForm,
+ imageGenConfig: {
+ ...syncVideoDurationPricing(
+ modelForm.imageGenConfig,
+ configuredVideoDurationOptions
+ ),
+ creditCostPerVideoByDuration: {
+ ...(syncVideoDurationPricing(
+ modelForm.imageGenConfig,
+ configuredVideoDurationOptions
+ )
+ .creditCostPerVideoByDuration ||
+ {}),
+ [durationKey]: nextValue,
+ },
+ },
+ });
+ }}
+ />
+
+ );
+ })}
+
+
+ 配置后会优先按用户选择的秒数扣积分;未命中的秒数才回退到“每条视频积分消耗”。
+
+
+ ) : (
+
+ 当前还没有配置秒数档位,将按“每条视频积分消耗”统一扣费。
+
+ )}
+
+ )}
+
-
-
-
- 深度 {layer.depth}
-
+
+
+
+
+ 深度 {layer.depth}
+
+
+
diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts
index 46925c7e9..6e5b3f790 100644
--- a/app/(chat)/api/chat/route.ts
+++ b/app/(chat)/api/chat/route.ts
@@ -109,6 +109,7 @@ import {
isImageGenerationModelDescriptor,
isVideoModelDescriptor,
} from "@/lib/ai/model-classification";
+import { getVideoCreditCostFromConfig } from "@/lib/ai/video-model-config";
import {
bootstrapConversationStateProtocol,
composeConversationStateProtocol,
@@ -144,6 +145,22 @@ import {
resolveClarificationMessage,
} from "@/lib/ai/runtime/clarification-interruption";
import { resolveCanvasAgentClarificationGate } from "@/lib/ai/runtime/middlewares/canvas-agent-clarification-gate";
+import { buildDirectAgentImageAnalysisAnswer } from "@/lib/ai/runtime/direct-agent-analysis-answer";
+import {
+ analyzeOptimizedPromptMerge,
+ buildDirectAgentExecutionPrompt,
+ buildOptimizedPromptPreview,
+} from "@/lib/ai/runtime/optimized-prompt";
+import {
+ findAgentToolCallArgs,
+ getDirectAgentToolErrorMessage,
+ getDirectAgentToolLabel,
+ getDirectAgentToolSuccessMessage,
+ getDirectAgentToolTargetMissingMessage,
+ pickAgentSourceImageUrls,
+ resolveDirectAgentToolExecution,
+ type DirectAgentToolCapability,
+} from "@/lib/ai/runtime/direct-agent-execution";
import {
pruneTaskBoundaryContext,
pruneTaskBoundaryHistoryMessages,
@@ -186,6 +203,10 @@ import type { ChatMessage } from "@/lib/types";
import type { AppUsage } from "@/lib/usage";
import { convertToUIMessages, generateUUID } from "@/lib/utils";
import { isAiModelAgentAuto } from "@/lib/db/ai-model-flags";
+import type {
+ AgentExecutionPlan,
+ AgentPreparedExecution,
+} from "@/lib/ai/auto-agent-graph";
import { generateTitleFromUserMessage } from "../../actions";
import { type PostRequestBody, postRequestBodySchema } from "./schema";
import { isAiModelMultimodal } from "@/lib/db/ai-model-flags";
@@ -310,57 +331,6 @@ function applyOptimizedPromptToLatestUserMessage(
return cloned;
}
-function analyzeOptimizedPromptMerge(
- originalText: string,
- optimizedPrompt?: string,
- hiddenPromptPrefix = "",
-) {
- const prompt = optimizedPrompt?.trim() || "";
- const trimmedOriginal = String(originalText || "").trim();
- const normalizedOriginal =
- hiddenPromptPrefix && trimmedOriginal.startsWith(hiddenPromptPrefix)
- ? trimmedOriginal.slice(hiddenPromptPrefix.length).trim()
- : trimmedOriginal;
- const hasOriginalDemand = normalizedOriginal.length > 0;
- const hasOriginalInOptimized =
- hasOriginalDemand &&
- (prompt.includes(normalizedOriginal) || normalizedOriginal.includes(prompt));
-
- return {
- prompt,
- normalizedOriginal,
- hasOriginalDemand,
- hasOriginalInOptimized,
- };
-}
-
-function buildOptimizedPromptPreview(params: {
- originalText: string;
- optimizedPrompt?: string;
- hiddenPromptPrefix?: string;
-}): string | undefined {
- const { prompt, normalizedOriginal, hasOriginalDemand, hasOriginalInOptimized } =
- analyzeOptimizedPromptMerge(
- params.originalText,
- params.optimizedPrompt,
- params.hiddenPromptPrefix || "",
- );
-
- if (!prompt) return undefined;
-
- const normalizedPrompt = prompt.replace(/\s+/g, " ").trim();
- if (!normalizedPrompt) return undefined;
-
- if (hasOriginalDemand && !hasOriginalInOptimized) {
- return [
- `- 核心目标:${normalizedOriginal.replace(/\s+/g, " ").trim()}`,
- `- 优化提示词:${normalizedPrompt}`,
- ].join("\n");
- }
-
- return `- ${normalizedPrompt}`;
-}
-
function injectOptimizedPromptSection(params: {
message?: string;
originalText: string;
@@ -1098,483 +1068,11 @@ type AgentVideoPlan = {
recommendedVideoParams?: AgentRecommendedVideoParams;
};
-type AgentPlanNextAction =
- | "respond"
- | "clarify"
- | "analyze_image"
- | "generate_image"
- | "edit_image"
- | "generate_video";
-
-type AgentPlannedToolCall = {
- name: string;
- reason?: string;
- args?: Record;
-};
-
-type AgentExecutionPlan = {
- goal: string;
- nextAction: AgentPlanNextAction;
- intent: "image_edit" | "vision" | "image_gen" | "video_gen" | "chat";
- subIntent?: string;
- targetSource:
- | "explicit_selection"
- | "current_upload"
- | "text_reference"
- | "working_memory"
- | "latest_result"
- | "clarification"
- | "none";
- preferredCapability:
- | "respond"
- | "analyze"
- | "generic_edit"
- | "remove_background"
- | "enhance"
- | "extend"
- | "resize"
- | "box_cutout"
- | "generate_image"
- | "generate_video"
- | "publish_canvas";
- responseMode: "direct_answer" | "tool_then_answer" | "tool_then_execute" | "clarify";
- shouldUseHistoryImage: boolean;
- shouldUseStructuredReply: boolean;
- confidence: number;
- toolCalls: AgentPlannedToolCall[];
-};
-
-type AgentPreparedExecution = {
- mode: AgentPlanNextAction;
- routeIntent: "image_edit" | "vision" | "image_gen" | "video_gen" | "chat";
- routeSubIntent?: string;
- targetSource: AgentExecutionPlan["targetSource"];
- preferredCapability: AgentExecutionPlan["preferredCapability"];
- shouldUseStructuredReply: boolean;
- shouldOptimizePrompt: boolean;
- shouldApplyImagePipeline: boolean;
- shouldApplyVideoPipeline: boolean;
- shouldStayConversational: boolean;
- shouldPublishToCanvas: boolean;
- toolCalls: AgentPlannedToolCall[];
- availableTools: string[];
-};
-
-type DirectAgentToolCapability =
- | "analyze"
- | "generic_edit"
- | "remove_background"
- | "enhance"
- | "resize"
- | "extend"
- | "box_cutout"
- | "generate_image"
- | "generate_video";
-
-type DirectAgentToolName =
- | "analyzeImage"
- | "editImage"
- | "removeBackground"
- | "enhanceImage"
- | "resizeImage"
- | "extendImage"
- | "boxCutout"
- | "generateImage"
- | "generateVideo";
-
-type DirectAgentSourceToolName =
- | "useSelectedCanvasImage"
- | "useLatestResultImage"
- | "useOriginalUploadImage";
-
-type ResolvedDirectAgentToolExecution = {
- toolName: DirectAgentToolName;
- capability: DirectAgentToolCapability;
- sourceTools: DirectAgentSourceToolName[];
- publishToCanvas: boolean;
-};
-
-function collectAgentToolCalls(
- execution?: AgentPreparedExecution,
- plan?: AgentExecutionPlan,
-): AgentPlannedToolCall[] {
- const merged = [...(execution?.toolCalls || []), ...(plan?.toolCalls || [])];
- const seen = new Set();
- const result: AgentPlannedToolCall[] = [];
-
- for (const tool of merged) {
- const name = String(tool?.name || "").trim();
- if (!name || seen.has(name)) continue;
- seen.add(name);
- result.push(tool);
- }
-
- return result;
-}
-
-function mapDirectToolNameToCapability(
- toolName: DirectAgentToolName,
-): DirectAgentToolCapability {
- switch (toolName) {
- case "analyzeImage":
- return "analyze";
- case "editImage":
- return "generic_edit";
- case "removeBackground":
- return "remove_background";
- case "enhanceImage":
- return "enhance";
- case "resizeImage":
- return "resize";
- case "extendImage":
- return "extend";
- case "boxCutout":
- return "box_cutout";
- case "generateImage":
- return "generate_image";
- case "generateVideo":
- return "generate_video";
- default:
- return "generate_image";
- }
-}
-
-function resolveDirectAgentToolExecution(
- execution?: AgentPreparedExecution,
- plan?: AgentExecutionPlan,
-): ResolvedDirectAgentToolExecution | null {
- const mergedToolCalls = collectAgentToolCalls(execution, plan);
- const toolNames = mergedToolCalls.map((tool) => tool.name);
- const hasToolCall = (toolName: DirectAgentToolName) => toolNames.includes(toolName);
- const sourceTools = toolNames.filter((toolName): toolName is DirectAgentSourceToolName =>
- toolName === "useSelectedCanvasImage" ||
- toolName === "useLatestResultImage" ||
- toolName === "useOriginalUploadImage",
- );
-
- const directToolName =
- (hasToolCall("analyzeImage")
- ? "analyzeImage"
- : hasToolCall("removeBackground")
- ? "removeBackground"
- : hasToolCall("enhanceImage")
- ? "enhanceImage"
- : hasToolCall("resizeImage")
- ? "resizeImage"
- : hasToolCall("extendImage")
- ? "extendImage"
- : hasToolCall("boxCutout")
- ? "boxCutout"
- : hasToolCall("editImage")
- ? "editImage"
- : hasToolCall("generateImage")
- ? "generateImage"
- : hasToolCall("generateVideo")
- ? "generateVideo"
- : null) ||
- (() => {
- if (
- execution?.preferredCapability === "analyze" ||
- execution?.routeIntent === "vision" ||
- execution?.mode === "analyze_image"
- ) {
- return "analyzeImage";
- }
- if (
- execution?.preferredCapability === "remove_background" ||
- execution?.routeSubIntent === "remove_background"
- ) {
- return "removeBackground";
- }
- if (
- execution?.preferredCapability === "enhance" ||
- execution?.routeSubIntent === "enhance"
- ) {
- return "enhanceImage";
- }
- if (
- execution?.preferredCapability === "resize" ||
- execution?.routeSubIntent === "resize"
- ) {
- return "resizeImage";
- }
- if (
- execution?.preferredCapability === "extend" ||
- execution?.routeSubIntent === "extend"
- ) {
- return "extendImage";
- }
- if (
- execution?.preferredCapability === "box_cutout" ||
- execution?.routeSubIntent === "box_cutout"
- ) {
- return "boxCutout";
- }
- if (
- execution?.preferredCapability === "generic_edit" ||
- execution?.routeIntent === "image_edit" ||
- execution?.mode === "edit_image"
- ) {
- return "editImage";
- }
- if (execution?.mode === "generate_image") {
- return "generateImage";
- }
- if (execution?.mode === "generate_video") {
- return "generateVideo";
- }
- return null;
- })();
-
- if (!directToolName) {
- return null;
- }
-
- return {
- toolName: directToolName,
- capability: mapDirectToolNameToCapability(directToolName),
- sourceTools,
- publishToCanvas:
- execution?.shouldPublishToCanvas === true || toolNames.includes("publishToCanvas"),
- };
-}
-
-function getDirectAgentToolLabel(capability: DirectAgentToolCapability): string {
- switch (capability) {
- case "analyze":
- return "Agent工具:图片分析";
- case "generic_edit":
- return "Agent工具:图片编辑";
- case "remove_background":
- return "Agent工具:抠图";
- case "enhance":
- return "Agent工具:图像增强";
- case "resize":
- return "Agent工具:尺寸调整";
- case "extend":
- return "Agent工具:图像拓展";
- case "box_cutout":
- return "Agent工具:框选抠图";
- case "generate_image":
- return "Agent工具:图片生成";
- case "generate_video":
- return "Agent工具:视频生成";
- default:
- return "Agent工具";
- }
-}
-
-function findAgentToolCallArgs(
- toolName: AgentPlannedToolCall["name"],
- execution?: AgentPreparedExecution,
- plan?: AgentExecutionPlan,
-): Record | undefined {
- const matchedTool =
- execution?.toolCalls?.find((tool) => tool.name === toolName) ||
- plan?.toolCalls?.find((tool) => tool.name === toolName);
-
- if (!matchedTool?.args || typeof matchedTool.args !== "object") {
- return undefined;
- }
-
- return matchedTool.args;
-}
-
function toFiniteNumber(value: unknown): number | undefined {
const normalized = Number(value);
return Number.isFinite(normalized) ? normalized : undefined;
}
-function getDirectAgentToolSuccessMessage(capability: DirectAgentToolCapability): string {
- switch (capability) {
- case "analyze":
- return "已完成当前目标图分析,结果如下:";
- case "generic_edit":
- return "已完成图片编辑,结果如下:";
- case "remove_background":
- return "已完成抠图处理,结果如下:";
- case "enhance":
- return "已完成图像增强,结果如下:";
- case "resize":
- return "已完成尺寸调整,结果如下:";
- case "extend":
- return "已完成图像拓展,结果如下:";
- case "box_cutout":
- return "已完成框选抠图,结果如下:";
- case "generate_image":
- return "已完成图片生成,结果如下:";
- case "generate_video":
- return "已完成视频生成,结果如下:";
- default:
- return "已完成处理,结果如下:";
- }
-}
-
-function getDirectAgentToolTargetMissingMessage(
- capability: DirectAgentToolCapability,
-): string {
- switch (capability) {
- case "analyze":
- return "请先上传或选中一张图片,我才能继续帮你分析。";
- case "generic_edit":
- return "请先上传或选中一张图片,我才能继续帮你修改。";
- case "remove_background":
- return "请先上传或选中一张图片,我才能继续帮你执行抠图。";
- case "enhance":
- return "请先上传或选中一张图片,我才能继续帮你做图像增强。";
- case "resize":
- return "请先上传或选中一张图片,我才能继续帮你调整尺寸。";
- case "extend":
- return "请先上传或选中一张图片,我才能继续帮你做图像拓展。";
- case "box_cutout":
- return "请先选中一张图片并提供框选区域,我才能继续帮你做框选抠图。";
- case "generate_image":
- return "请先告诉我你想生成什么,或者提供参考图。";
- case "generate_video":
- return "请先告诉我你想生成什么视频,必要时再提供参考图。";
- default:
- return "请先提供要处理的图片。";
- }
-}
-
-function getDirectAgentToolErrorMessage(
- capability: DirectAgentToolCapability,
- error: unknown,
-): string {
- const errorMessage =
- error instanceof Error ? String(error.message || "").trim() : String(error || "").trim();
- const errorDetail =
- error && typeof error === "object" && "detail" in error
- ? String((error as { detail?: unknown }).detail || "").trim()
- : "";
- const detail = errorDetail || errorMessage || "请稍后重试。";
-
- switch (capability) {
- case "analyze":
- return `图片分析暂时失败:${detail}`;
- case "generic_edit":
- return `图片编辑暂时失败:${detail}`;
- case "remove_background":
- return `抠图暂时失败:${detail}`;
- case "enhance":
- return `图像增强暂时失败:${detail}`;
- case "resize":
- return `尺寸调整暂时失败:${detail}`;
- case "extend":
- return `图像拓展暂时失败:${detail}`;
- case "box_cutout":
- return `框选抠图暂时失败:${detail}`;
- case "generate_image":
- return `图片生成暂时失败:${detail}`;
- case "generate_video":
- return `视频生成暂时失败:${detail}`;
- default:
- return `处理暂时失败:${detail}`;
- }
-}
-
-async function buildDirectAgentImageAnalysisAnswer(params: {
- question: string;
- summary: string;
- modelId?: string;
-}) {
- const normalizedQuestion = String(params.question || "").trim();
- const normalizedSummary = String(params.summary || "").trim();
-
- if (!normalizedSummary) {
- return "";
- }
-
- const buildFallbackAnswer = () => {
- const intro = normalizedQuestion
- ? "我先根据当前锁定图片给你一个直接结论:"
- : "我先根据当前锁定图片做一个直接分析:";
- return [
- intro,
- normalizedSummary,
- normalizedQuestion
- ? "如果你要,我可以继续往主体构图、风格、问题点或优化建议里再拆细。"
- : "",
- ]
- .filter(Boolean)
- .join("\n\n");
- };
-
- const candidateModelIds = Array.from(
- new Set(
- [params.modelId]
- .map((value) => String(value || "").trim())
- .filter(Boolean),
- ),
- );
-
- for (const candidateModelId of candidateModelIds) {
- try {
- const model = await getLanguageModelById(candidateModelId);
- if (!model) continue;
-
- const { text } = await generateText({
- model,
- system: [
- "你是无限画布里的图片分析助手。",
- "只能根据给定的图像观察摘要回答,不要编造图片中不可见的事实。",
- "如果用户问题超出已知信息,要明确说信息不足,不要硬猜。",
- "优先给出直接结论,再补充2到4条简洁依据或建议。",
- ].join("\n"),
- prompt: [
- normalizedQuestion ? `用户问题:${normalizedQuestion}` : "用户要求:请分析当前图片。",
- `图像观察摘要:\n${normalizedSummary}`,
- "请用中文直接回答,不要复述“根据摘要/根据图片”,也不要输出多余寒暄。",
- ].join("\n\n"),
- temperature: 0.2,
- maxTokens: 320,
- abortSignal: AbortSignal.timeout(20_000),
- } as any);
-
- const normalizedText = String(text || "").trim();
- if (normalizedText) {
- return normalizedText;
- }
- } catch (error) {
- console.warn("[Chat API] Direct agent image analysis answer failed:", {
- modelId: candidateModelId,
- error,
- });
- }
- }
-
- return buildFallbackAnswer();
-}
-
-function buildDirectAgentExecutionPrompt(params: {
- originalText: string;
- optimizedPrompt?: string;
- hiddenPromptPrefix?: string;
- fallback?: string;
-}) {
- const originalText = String(params.originalText || "").trim();
- const optimizedPrompt = String(params.optimizedPrompt || "").trim();
- const hiddenPromptPrefix = String(params.hiddenPromptPrefix || "");
-
- if (!optimizedPrompt) {
- return `${hiddenPromptPrefix}${originalText || params.fallback || ""}`.trim();
- }
-
- const { normalizedOriginal, hasOriginalDemand, hasOriginalInOptimized } =
- analyzeOptimizedPromptMerge(originalText, optimizedPrompt, hiddenPromptPrefix);
-
- return hasOriginalDemand && !hasOriginalInOptimized
- ? [
- hiddenPromptPrefix.trim(),
- `【核心目标(必须优先满足)】${normalizedOriginal}`,
- `【执行优化细节】${optimizedPrompt}`,
- ]
- .filter(Boolean)
- .join("\n\n")
- .trim()
- : `${hiddenPromptPrefix}${optimizedPrompt}`.trim();
-}
-
async function executeDirectAgentEnhanceJob(params: {
requestUrl: string;
requestHeaders: Headers;
@@ -1761,52 +1259,6 @@ function buildAgentToolVideoMarkdown(videoUrl: string) {
].join("\n\n");
}
-function pickAgentSourceImageUrls(params: {
- sourceTools: DirectAgentSourceToolName[];
- requestResolvedMediaInput?: ResolvedChatMediaInput | null;
- requestTargetResolution?: ResolvedTargetAssets | null;
- requestWorkingState?: {
- lastResolvedTargetImageUrls?: string[] | null;
- lastGeneratedImageUrls?: string[] | null;
- lastUploadedImageUrls?: string[] | null;
- } | null;
-}): string[] {
- const preferredBuckets: string[][] = [];
- const sourceTools = params.sourceTools || [];
-
- if (sourceTools.includes("useSelectedCanvasImage")) {
- preferredBuckets.push(params.requestResolvedMediaInput?.selectedImageUrls || []);
- }
- if (sourceTools.includes("useLatestResultImage")) {
- preferredBuckets.push(params.requestWorkingState?.lastGeneratedImageUrls || []);
- preferredBuckets.push(params.requestResolvedMediaInput?.historyAssistantImageUrls || []);
- }
- if (sourceTools.includes("useOriginalUploadImage")) {
- preferredBuckets.push(params.requestResolvedMediaInput?.currentMessageImageUrls || []);
- preferredBuckets.push(params.requestWorkingState?.lastUploadedImageUrls || []);
- preferredBuckets.push(params.requestResolvedMediaInput?.historyUserImageUrls || []);
- }
-
- preferredBuckets.push(
- params.requestTargetResolution?.targetImageUrls || [],
- params.requestResolvedMediaInput?.selectedImageUrls || [],
- params.requestResolvedMediaInput?.currentMessageImageUrls || [],
- params.requestWorkingState?.lastResolvedTargetImageUrls || [],
- params.requestWorkingState?.lastGeneratedImageUrls || [],
- params.requestResolvedMediaInput?.historyAssistantImageUrls || [],
- params.requestResolvedMediaInput?.historyUserImageUrls || [],
- );
-
- return Array.from(
- new Set(
- preferredBuckets
- .flat()
- .map((url) => String(url || "").trim())
- .filter((url) => /^https?:\/\//i.test(url)),
- ),
- );
-}
-
const IMAGE_FAILOVER_PROVIDER_ALLOWLIST = new Set([
"alibaba-image",
"bfl-image",
@@ -4644,11 +4096,19 @@ function buildAgentReplacementPrompt(params: {
const MODEL_UUID_REGEX =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
-function getModelCreditCost(modelConfig: any, target: "image" | "video"): number {
+function getModelCreditCost(
+ modelConfig: any,
+ target: "image" | "video",
+ options?: { duration?: number | null }
+): number {
const config = (modelConfig as any)?.imageGenConfig || {};
const sourceValue =
target === "video"
- ? config.creditCostPerVideo ?? config.creditCostPerImage ?? 0
+ ? (() => {
+ const resolvedVideoCost = getVideoCreditCostFromConfig(config, options?.duration);
+ if (resolvedVideoCost > 0) return resolvedVideoCost;
+ return config.creditCostPerVideo ?? config.creditCostPerImage ?? 0;
+ })()
: config.creditCostPerImage ?? 0;
const normalized = Number(sourceValue);
return Number.isFinite(normalized) ? Math.max(0, Math.round(normalized)) : 0;
@@ -4673,6 +4133,7 @@ async function consumeGeneratedVideoPoints(params: {
chatId: string;
messageId: string;
modelRefs: Array;
+ duration?: number | null;
}) {
try {
const pointsConfig = await getPointsSystemConfig();
@@ -4690,12 +4151,14 @@ async function consumeGeneratedVideoPoints(params: {
const resolved = await resolveBillingModelByReference(ref);
if (!resolved) continue;
billingModel = resolved;
- if (getModelCreditCost(resolved, "video") > 0) break;
+ if (getModelCreditCost(resolved, "video", { duration: params.duration }) > 0) break;
}
if (!billingModel) return;
- const unitCost = getModelCreditCost(billingModel, "video");
+ const unitCost = getModelCreditCost(billingModel, "video", {
+ duration: params.duration,
+ });
if (unitCost <= 0) return;
const consumeResult = await consumeUserPoints({
@@ -4705,7 +4168,7 @@ async function consumeGeneratedVideoPoints(params: {
modelId: billingModel.id,
chatId: params.chatId,
messageId: params.messageId,
- note: `模型(${billingModel.displayName || billingModel.modelId || billingModel.id}) 生视频 1 条`,
+ note: `模型(${billingModel.displayName || billingModel.modelId || billingModel.id}) 生视频 1 条${params.duration ? `(${params.duration} 秒)` : ""}`,
});
if (!consumeResult.ok) {
@@ -7343,6 +6806,7 @@ export async function POST(request: Request) {
chatId: id,
messageId: finalMessageId,
modelRefs: executionResult.modelRefs,
+ duration: Number((finalVideoMetadata as any)?.duration) || undefined,
});
scheduleVideoArchive({
@@ -8646,6 +8110,7 @@ export async function POST(request: Request) {
chatId: id,
messageId: assistantMessageId,
modelRefs: [result.modelRef || presetModelId],
+ duration: Number((result.metadata as any)?.duration) || undefined,
});
scheduleVideoArchive({
@@ -8792,7 +8257,7 @@ export async function POST(request: Request) {
: null;
const directAgentToolTargetImageUrls = directAgentExecution
? pickAgentSourceImageUrls({
- sourceTools: directAgentExecution.sourceTools,
+ sourcePreferences: directAgentExecution.sourcePreferences,
requestResolvedMediaInput,
requestTargetResolution,
requestWorkingState,
@@ -9393,6 +8858,10 @@ export async function POST(request: Request) {
chatId: id,
messageId: assistantMessageId,
modelRefs: [result.modelRef || finalModelId],
+ duration:
+ Number((result.metadata as any)?.duration) ||
+ Number(effectiveVideoParams?.duration) ||
+ undefined,
});
if (videoUrl) {
@@ -10142,7 +9611,9 @@ export async function POST(request: Request) {
const modelCreditCostPerImage = Number(
(currentModelConfig as any)?.imageGenConfig?.creditCostPerImage ?? 0
);
- const modelCreditCostPerVideo = getModelCreditCost(currentModelConfig, "video");
+ const modelCreditCostPerVideo = getModelCreditCost(currentModelConfig, "video", {
+ duration: Number(effectiveVideoParams?.duration) || undefined,
+ });
if (isImageGenerationModel) {
const quota = await checkUserImageQuota(session.user.id);
@@ -10219,6 +9690,10 @@ export async function POST(request: Request) {
normalizedVideoUnitCost > 0 &&
pointsConfig.blockOnInsufficient !== false
) {
+ const requestedVideoDuration = Math.max(
+ 1,
+ Math.round(Number(effectiveVideoParams?.duration || 0)) || 0
+ );
const expectedCost = normalizedVideoUnitCost;
const userData = await getUserById({ id: session.user.id });
if (!userData?.isHighVip) {
@@ -10228,10 +9703,13 @@ export async function POST(request: Request) {
return NextResponse.json(
{
error: "insufficient_points",
- message: `积分不足,当前余额 ${currentBalance},本次预计消耗 ${expectedCost}(${normalizedVideoUnitCost} x 1 条视频)。`,
+ message: requestedVideoDuration
+ ? `积分不足,当前余额 ${currentBalance},本次预计消耗 ${expectedCost}(${requestedVideoDuration} 秒视频)。`
+ : `积分不足,当前余额 ${currentBalance},本次预计消耗 ${expectedCost}(1 条视频)。`,
balance: currentBalance,
expectedCost,
unitCost: normalizedVideoUnitCost,
+ duration: requestedVideoDuration || undefined,
},
{ status: 402 }
);
@@ -11775,6 +11253,7 @@ export async function POST(request: Request) {
chatId: id,
messageId,
modelRefs: billedModelRefs,
+ duration: Number(effectiveVideoParams?.duration) || undefined,
});
}
@@ -12764,6 +12243,7 @@ export async function POST(request: Request) {
chatId: id,
messageId: videoMessageId,
modelRefs: [finalVideoModelData.id, providerModelData.id, finalModelId],
+ duration: Number(videoOptions.duration) || undefined,
});
dataStream.write({
diff --git a/app/(chat)/api/models/route.ts b/app/(chat)/api/models/route.ts
index 5fd6a2177..e2333823d 100644
--- a/app/(chat)/api/models/route.ts
+++ b/app/(chat)/api/models/route.ts
@@ -93,6 +93,9 @@ export async function GET() {
providerType: provider.type,
modelId: model.modelId,
rawCapabilities: model.capabilities,
+ imageGenConfig: (model.imageGenConfig || null) as
+ | { videoDurationOptions?: number[] | null }
+ | null,
}),
imageGenConfig: model.imageGenConfig,
sortOrder: model.sortOrder ?? 0,
diff --git a/app/(chat)/api/video-model-capabilities/route.ts b/app/(chat)/api/video-model-capabilities/route.ts
index 2c4400b7d..ac5a7c262 100644
--- a/app/(chat)/api/video-model-capabilities/route.ts
+++ b/app/(chat)/api/video-model-capabilities/route.ts
@@ -30,6 +30,9 @@ export async function GET(request: Request) {
providerType: provider.type,
modelId: model.modelId,
rawCapabilities: model.capabilities,
+ imageGenConfig: (model.imageGenConfig || null) as
+ | { videoDurationOptions?: number[] | null }
+ | null,
});
return Response.json({
diff --git a/app/api/admin/ai-models/route.ts b/app/api/admin/ai-models/route.ts
index 381160bf8..ac195d07e 100644
--- a/app/api/admin/ai-models/route.ts
+++ b/app/api/admin/ai-models/route.ts
@@ -15,6 +15,10 @@ import {
isAiModelAgentAuto,
isAiModelMultimodal,
} from "@/lib/db/ai-model-flags";
+import {
+ buildVideoDurationCostMap,
+ getConfiguredVideoDurationOptions,
+} from "@/lib/ai/video-model-config";
// 🆕 图片生成模型特性配置 Schema
const imageGenConfigSchema = z.object({
@@ -29,10 +33,29 @@ const imageGenConfigSchema = z.object({
supportsSeed: z.boolean().optional(),
creditCostPerImage: z.number().min(0).max(100000).optional(),
creditCostPerVideo: z.number().min(0).max(100000).optional(),
+ videoDurationOptions: z.array(z.number().min(1).max(600)).max(20).optional(),
+ creditCostPerVideoByDuration: z.record(z.string(), z.number().min(0).max(100000)).optional(),
enableLoadingPlaceholder: z.boolean().optional(),
loadingColor: z.string().optional(),
}).optional();
+function normalizeImageGenConfig(config: z.infer) {
+ if (!config) return config;
+
+ const videoDurationOptions = getConfiguredVideoDurationOptions(config);
+ const creditCostPerVideoByDuration = buildVideoDurationCostMap({
+ durationOptions: videoDurationOptions,
+ costMap: config.creditCostPerVideoByDuration,
+ fallbackCost: config.creditCostPerVideo,
+ });
+
+ return {
+ ...config,
+ videoDurationOptions: videoDurationOptions.length ? videoDurationOptions : undefined,
+ creditCostPerVideoByDuration,
+ };
+}
+
const createModelSchema = z.object({
providerId: z.string().uuid(),
modelId: z.string().min(1).max(128),
@@ -142,7 +165,10 @@ export async function POST(request: Request) {
return new ChatSDKError("bad_request:api", "Provider not found").toResponse();
}
- const model = await createAiModel(data);
+ const model = await createAiModel({
+ ...data,
+ imageGenConfig: normalizeImageGenConfig(data.imageGenConfig),
+ });
return Response.json({ model: normalizeModel(model[0]) });
} catch (error) {
@@ -165,7 +191,10 @@ export async function PUT(request: Request) {
const data = updateModelSchema.parse(body);
console.log("[Admin API] Validation passed, updating model..."); // Debug log
- const model = await updateAiModel(data);
+ const model = await updateAiModel({
+ ...data,
+ imageGenConfig: normalizeImageGenConfig(data.imageGenConfig),
+ });
if (model.length === 0) {
console.error("[Admin API] Model not found:", data.id); // Debug log
diff --git a/app/api/admin/psd-templates/[id]/route.ts b/app/api/admin/psd-templates/[id]/route.ts
index 5432354bc..dc265acb8 100644
--- a/app/api/admin/psd-templates/[id]/route.ts
+++ b/app/api/admin/psd-templates/[id]/route.ts
@@ -112,9 +112,6 @@ export async function PUT(
const nextMode = normalizeAgentPsdTemplateMode(body.mode);
const nextCategoryTags = normalizeAgentPsdTemplateCategoryTags(body.categoryTags);
let nextLayers = parseAgentPsdTemplateLayers(body.layers);
- if (nextLayers.length === 0) {
- nextLayers = currentTemplate.layers;
- }
if (currentTemplate.parseStatus !== "completed") {
if (nextStatus === "active") {
@@ -224,7 +221,7 @@ export async function PUT(
renderBaseUrl: uploadedRenderBase.url,
renderBasePath: uploadedRenderBase.pathname,
layers: nextLayers,
- layerCount: Math.max(currentTemplate.layerCount, nextLayers.length),
+ layerCount: nextLayers.length,
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
updatedAt: new Date().toISOString(),
};
diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx
index 2836b4a9c..b1800064c 100644
--- a/app/canvas/[id]/page.tsx
+++ b/app/canvas/[id]/page.tsx
@@ -1159,7 +1159,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
}
}, [isVideoMode]);
const [appearanceRecognitionEnabled, setAppearanceRecognitionEnabled] =
- useState(true);
+ useState(false);
const [selectedBrandModel, setSelectedBrandModel] =
useState(null);
const workflowInputsRef = useRef(workflowInputs);
@@ -2153,7 +2153,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
const persistedWorkflow = result.data.canvasData.workflowState;
if (!requestedAgentMode && persistedWorkflow) {
const nextAppearanceRecognitionEnabled =
- persistedWorkflow.appearanceRecognitionEnabled !== false;
+ persistedWorkflow.appearanceRecognitionEnabled === true;
const nextWorkflowMaterials = nextAppearanceRecognitionEnabled
? normalizeAgentWorkflowMaterialSelections(
persistedWorkflow.materials
@@ -2567,7 +2567,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
persistedState.workflowState ||
createEmptyAgentWorkflowPersistedState();
const nextAppearanceRecognitionEnabled =
- persistedWorkflow.appearanceRecognitionEnabled !== false;
+ persistedWorkflow.appearanceRecognitionEnabled === true;
const nextWorkflowMaterials = nextAppearanceRecognitionEnabled
? normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials)
: clearAppearanceSummaryFromMaterials(
@@ -2650,7 +2650,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
const parsed = JSON.parse(raw) as AgentWorkflowPersistedState;
const nextAppearanceRecognitionEnabled =
- parsed.appearanceRecognitionEnabled !== false;
+ parsed.appearanceRecognitionEnabled === true;
const nextWorkflowMaterials = nextAppearanceRecognitionEnabled
? normalizeAgentWorkflowMaterialSelections(parsed.materials)
: clearAppearanceSummaryFromMaterials(
diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx
index 6debec61e..2acedd3ef 100644
--- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx
+++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx
@@ -80,7 +80,6 @@ import {
sanitizeDetailImageModelPrompt,
} from "@/lib/agents/detail-generation-config";
import {
- getAgentRequiredUploadCount,
normalizeAgentPresetModeConfig,
} from "@/lib/agents/preset-mode-config";
import { buildAgentSceneImageRolePrompt } from "@/lib/agents/scene-image-role-prompt";
@@ -1044,6 +1043,7 @@ export function AgentGenerationWorkspaceDarkV2({
useState("scene_generation");
const [fullscreenPreview, setFullscreenPreview] = useState(null);
const [sceneResizeEditorTargetUrl, setSceneResizeEditorTargetUrl] = useState(null);
+ const [isPsdTemplateDialogOpen, setIsPsdTemplateDialogOpen] = useState(false);
const [psdTemplateDialogSourceUrl, setPsdTemplateDialogSourceUrl] = useState(null);
const [psdTemplateDialogMode, setPsdTemplateDialogMode] = useState("main");
const [batchDownloadState, setBatchDownloadState] = useState(null);
@@ -1074,6 +1074,28 @@ export function AgentGenerationWorkspaceDarkV2({
const [detailGenerationRunResults, setDetailGenerationRunResults] = useState<
DirectDetailGenerationImage[]
>([]);
+ const closePsdTemplateDialog = useCallback(() => {
+ setIsPsdTemplateDialogOpen(false);
+ setPsdTemplateDialogSourceUrl(null);
+ setPsdTemplateDialogMode("main");
+ }, []);
+ const openPsdTemplateDialog = useCallback(
+ (params?: {
+ sourceImageUrl?: string | null;
+ mode?: AgentPsdTemplateMode | null;
+ }) => {
+ setPsdTemplateDialogSourceUrl(
+ isValidChatHttpUrl(params?.sourceImageUrl)
+ ? String(params?.sourceImageUrl).trim()
+ : null
+ );
+ setPsdTemplateDialogMode(
+ params?.mode === "sku" ? "sku" : params?.mode === "main" ? "main" : "main"
+ );
+ setIsPsdTemplateDialogOpen(true);
+ },
+ []
+ );
const normalizedDeletedResultCollections = useMemo(
() => ({
scene_generation: normalizeResultCollectionUrls(
@@ -1215,13 +1237,7 @@ export function AgentGenerationWorkspaceDarkV2({
},
[hasAgentUploadSlots, modelSlotEnabled, modelSlotIndex, productSlotCount]
);
- const requiredUploadCount = hasAgentUploadSlots
- ? getAgentRequiredUploadCount({
- presetType: selectedAgentPreset?.type || "image",
- defaultParams: selectedAgentPreset?.defaultParams || null,
- fallback: modelSlotEnabled ? 3 : productSlotCount,
- })
- : 0;
+ const requiredProductUploadCount = hasAgentUploadSlots ? productSlotCount : 0;
const detailReferenceTarget = presetModeConfig.detail.referenceImageSource;
const skuReferenceTarget = presetModeConfig.sku.referenceImageSource;
const detailReferenceSourceLabel = getTaskDetailReferenceSourceLabel(detailReferenceTarget);
@@ -1365,16 +1381,20 @@ export function AgentGenerationWorkspaceDarkV2({
selectedModelAttachment,
selectedProductAttachments,
]);
- const filledAttachmentCount = attachments.filter(Boolean).length;
- const missingAttachmentCount = Math.max(requiredUploadCount - filledAttachmentCount, 0);
+ const filledProductAttachmentCount = selectedProductAttachments.length;
+ const missingAttachmentCount = Math.max(
+ requiredProductUploadCount - filledProductAttachmentCount,
+ 0
+ );
const productAttachmentSummary = `${selectedProductAttachments.length}/${productSlotCount} 张产品图`;
const modelAttachmentSummary = modelSlotEnabled
? selectedModelAttachment
? "模特图已带入"
- : "模特图待补充"
+ : "模特图可不选"
: null;
const attachmentSummaryReady =
- hasAgentUploadSlots && (requiredUploadCount === 0 || filledAttachmentCount >= requiredUploadCount);
+ hasAgentUploadSlots &&
+ (requiredProductUploadCount === 0 || filledProductAttachmentCount >= requiredProductUploadCount);
const sceneImageRolePromptPreview = useMemo(() => {
const sceneHasModelReference = Boolean(selectedModelAttachment);
if (
@@ -1463,18 +1483,13 @@ export function AgentGenerationWorkspaceDarkV2({
`产品图不足(${detailGenerationProductCount}/${detailGenerationRequiredProductCount})`
);
}
- if (modelSlotEnabled && !detailGenerationModelReady) {
- issues.push("缺少模特图(1/1)");
- }
return issues;
}, [
detailGenerationBrief.coreNeed,
detailGenerationBrief.targetAudience,
detailGenerationBrief.usageScenario,
- detailGenerationModelReady,
detailGenerationProductCount,
detailGenerationRequiredProductCount,
- modelSlotEnabled,
]);
const skuReplacementState = resolvedAgentModeState.skuReplacement;
const skuReplacementQueryInput = skuReplacementState.query;
@@ -2228,17 +2243,17 @@ export function AgentGenerationWorkspaceDarkV2({
((activeResultModeType === "scene_generation" && isSceneResultModeGenerating) ||
(activeResultModeType === "detail_generation" &&
(isDetailGenerationResultModeGenerating || hasDetailGenerationPlaceholderState)));
- const attachmentCount = attachments.filter(Boolean).length;
const hasBrandModelSelection = Boolean(
selectedBrandModel?.imageUrl || referenceImages.some((image) => image.source === "brandModel")
);
const hasMaterialSelections = referenceImages.some(
(image) => image.source === "primary" || image.source === "secondary"
);
- const hasSatisfiedSlotRequirement = !hasAgentUploadSlots || attachmentCount >= requiredUploadCount;
+ const hasSatisfiedSlotRequirement =
+ !hasAgentUploadSlots || filledProductAttachmentCount >= requiredProductUploadCount;
const slotRequirementMessage =
hasAgentUploadSlots && !hasSatisfiedSlotRequirement
- ? `当前还缺少 ${missingAttachmentCount} 张输入图,请先在“选择 SPU / SKU”步骤补齐后再继续。`
+ ? `当前还缺少 ${missingAttachmentCount} 张产品图,请先在“选择 SPU / SKU”步骤补齐后再继续。`
: null;
const mergedSettingsStepLabel = hasMaterialStep ? "选图与生成" : "生成参数";
const mergedSettingsStepDescription = hasMaterialStep
@@ -2693,9 +2708,9 @@ export function AgentGenerationWorkspaceDarkV2({
toast.error(`请先查询并选择${skuReferenceSourceLabel}参考图`);
return;
}
- const uploadedAttachmentCount = attachments.filter(Boolean).length;
- if (hasAgentUploadSlots && uploadedAttachmentCount < requiredUploadCount) {
- toast.error(`当前模式需要上传 ${requiredUploadCount} 张图片`);
+ const uploadedProductAttachmentCount = selectedProductAttachments.length;
+ if (hasAgentUploadSlots && uploadedProductAttachmentCount < requiredProductUploadCount) {
+ toast.error(`当前模式需要上传 ${requiredProductUploadCount} 张产品图`);
return;
}
const resolvedModelId =
@@ -2715,8 +2730,12 @@ export function AgentGenerationWorkspaceDarkV2({
}
const parts: Array<{ type: "text"; text: string } | { type: "file"; mediaType: string; name: string; url: string; assetId?: string }> = [];
const sanitizedAttachments = sceneMessageAttachments;
- if (hasAgentUploadSlots && sanitizedAttachments.length < requiredUploadCount) {
- toast.error(`当前模式需要上传 ${requiredUploadCount} 张有效图片`);
+ if (
+ hasAgentUploadSlots &&
+ selectedProductAttachments.filter((attachment) => Boolean(attachment?.url)).length <
+ requiredProductUploadCount
+ ) {
+ toast.error(`当前模式需要上传 ${requiredProductUploadCount} 张有效产品图`);
return;
}
const primaryAttachmentCountForMessage = Math.min(
@@ -2818,7 +2837,7 @@ export function AgentGenerationWorkspaceDarkV2({
isDetailAiGenerationMethod,
isDetailReplacementPreset,
isSkuReplacementPreset,
- requiredUploadCount,
+ requiredProductUploadCount,
selectedAgentPreset,
selectedAgentPresetId,
selectableModels,
@@ -4008,7 +4027,7 @@ export function AgentGenerationWorkspaceDarkV2({
产品图 {detailGenerationProductCount}/{detailGenerationRequiredProductCount}
{modelSlotEnabled
- ? ` · 模特图 ${detailGenerationModelReady ? "1/1" : "0/1"}`
+ ? ` · 模特图 ${detailGenerationModelReady ? "已带入" : "可不选"}`
: ""}
{` · 搭配图 ${detailGenerationBundleCount}/3`}
@@ -4112,7 +4131,7 @@ export function AgentGenerationWorkspaceDarkV2({
"rounded-full border px-3 py-1 text-xs",
selectedModelAttachment
? "border-emerald-400/25 bg-emerald-500/[0.10] text-emerald-100"
- : "border-amber-400/25 bg-amber-500/[0.10] text-amber-100"
+ : "border-white/10 bg-black/20 text-white/60"
)}
>
{modelAttachmentSummary}
@@ -4125,8 +4144,8 @@ export function AgentGenerationWorkspaceDarkV2({
{missingAttachmentCount > 0
- ? `当前还需补充 ${missingAttachmentCount} 张输入素材,展开后可继续检查或替换。`
- : "素材已自动带入并准备完成,默认折叠显示;需要时再展开检查或替换即可。"}
+ ? `当前还需补充 ${missingAttachmentCount} 张产品图,展开后可继续检查或替换。`
+ : "产品图已自动带入并准备完成;模特图槽位可按需补充,不选也可以继续生成。"}
>
) : (
@@ -5006,6 +5025,18 @@ export function AgentGenerationWorkspaceDarkV2({
{detailGenerationExpectedCount}
) : null}
+
@@ -5845,10 +5876,10 @@ export function AgentGenerationWorkspaceDarkV2({
onClick={(event) => {
event.stopPropagation();
setSelectedResultUrl(card.url!);
- setPsdTemplateDialogSourceUrl(card.url!);
- setPsdTemplateDialogMode(
- getPsdTemplateModeByResultMode(activeResultModeType)
- );
+ openPsdTemplateDialog({
+ sourceImageUrl: card.url!,
+ mode: getPsdTemplateModeByResultMode(activeResultModeType),
+ });
}}
className="absolute left-14 top-3 flex items-center gap-1 rounded-full border border-white/10 bg-black/60 px-2.5 py-1 text-[11px] text-white transition-colors hover:border-white/20"
aria-label="套用 PSD 模板"
@@ -5867,10 +5898,10 @@ export function AgentGenerationWorkspaceDarkV2({
onClick={(event) => {
event.stopPropagation();
setSelectedResultUrl(card.url!);
- setPsdTemplateDialogSourceUrl(card.url!);
- setPsdTemplateDialogMode(
- getPsdTemplateModeByResultMode(activeResultModeType)
- );
+ openPsdTemplateDialog({
+ sourceImageUrl: card.url!,
+ mode: getPsdTemplateModeByResultMode(activeResultModeType),
+ });
}}
className="absolute left-3 top-3 flex items-center gap-1 rounded-full border border-white/10 bg-black/60 px-2.5 py-1 text-[11px] text-white transition-colors hover:border-white/20"
aria-label="套用 PSD 模板"
@@ -5965,6 +5996,22 @@ export function AgentGenerationWorkspaceDarkV2({
? "切换到对应生图模式并开始生成后,结果会保留在当前项目的对应分类中。"
: "请先从顶部流程线选择一个生图模式"}
+
+
+ 可直接进入模板页,并通过手动上传图片开始套版。
+
)}
@@ -5993,6 +6040,7 @@ export function AgentGenerationWorkspaceDarkV2({
{
if (!open) {
- setPsdTemplateDialogSourceUrl(null);
- setPsdTemplateDialogMode("main");
+ closePsdTemplateDialog();
}
}}
>
@@ -6027,11 +6074,10 @@ export function AgentGenerationWorkspaceDarkV2({
}
>
{
if (!open) {
- setPsdTemplateDialogSourceUrl(null);
- setPsdTemplateDialogMode("main");
+ closePsdTemplateDialog();
}
}}
sourceImageUrl={psdTemplateDialogSourceUrl}
diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx
index 7cd2aae38..59e2ffe25 100644
--- a/components/agents/AgentPsdTemplateEditorDialog.tsx
+++ b/components/agents/AgentPsdTemplateEditorDialog.tsx
@@ -1,9 +1,11 @@
"use client";
import {
+ Expand,
Image as ImageIcon,
Loader2,
Move,
+ Pointer,
RefreshCcw,
Sparkles,
Type,
@@ -46,6 +48,12 @@ import {
normalizeAgentPsdImagePlacement,
resolveAgentPsdImageBaseFrame,
} from "@/lib/agents/psd-image-placement";
+import {
+ AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG,
+ defaultAgentPsdTemplateCategoryTagConfig,
+ normalizeAgentPsdTemplateCategoryTagConfig,
+ normalizeAgentPsdTemplateCategoryTags,
+} from "@/lib/agents/psd-template-category-tag-config";
import {
type AgentPsdTemplate,
type AgentPsdTemplateImageLayerSlot,
@@ -62,12 +70,6 @@ import {
defaultAgentPsdTemplateSecondaryCategoryConfig,
normalizeAgentPsdTemplateSecondaryCategoryConfig,
} from "@/lib/agents/psd-template-secondary-category-config";
-import {
- AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG,
- defaultAgentPsdTemplateCategoryTagConfig,
- normalizeAgentPsdTemplateCategoryTagConfig,
- normalizeAgentPsdTemplateCategoryTags,
-} from "@/lib/agents/psd-template-category-tag-config";
import {
defaultAgentPsdTextColorConfig,
normalizeAgentPsdTextColorConfig,
@@ -223,6 +225,62 @@ function getTemplateMetaLabel(
return metaParts.join(" · ");
}
+function getTemplateCategoryTagLabels(template: AgentPsdTemplate) {
+ return normalizeAgentPsdTemplateCategoryTags(template.categoryTags)
+ .map((tag) =>
+ tag === AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG ? "适合所有" : tag
+ )
+ .filter(Boolean);
+}
+
+function renderTemplateCategoryTagRow(
+ template: AgentPsdTemplate,
+ params?: {
+ compact?: boolean;
+ }
+) {
+ const compact = params?.compact === true;
+ const labels = getTemplateCategoryTagLabels(template);
+ if (labels.length === 0) {
+ return null;
+ }
+
+ const visibleLabels = compact ? labels.slice(0, 2) : labels.slice(0, 3);
+ const hiddenCount = Math.max(0, labels.length - visibleLabels.length);
+
+ return (
+
+ {visibleLabels.map((label) => (
+
+ {label}
+
+ ))}
+ {hiddenCount > 0 ? (
+
+ +{hiddenCount}
+
+ ) : null}
+
+ );
+}
+
function renderTemplateSelectContent(
template: AgentPsdTemplate,
params?: {
@@ -234,7 +292,11 @@ function renderTemplateSelectContent(
const showSecondaryCategory = params?.showSecondaryCategory === true;
const showBrand = params?.showBrand === true;
const compact = params?.compact === true;
- const metaLabel = getTemplateMetaLabel(template, showSecondaryCategory, showBrand);
+ const metaLabel = getTemplateMetaLabel(
+ template,
+ showSecondaryCategory,
+ showBrand
+ );
return (
) : null}
+ {renderTemplateCategoryTagRow(template, { compact })}
);
@@ -294,11 +357,15 @@ function renderTemplatePickerCardContent(
const showSecondaryCategory = params?.showSecondaryCategory === true;
const showBrand = params?.showBrand === true;
const emphasize = params?.emphasize === true;
- const metaLabel = getTemplateMetaLabel(template, showSecondaryCategory, showBrand);
+ const metaLabel = getTemplateMetaLabel(
+ template,
+ showSecondaryCategory,
+ showBrand
+ );
return (
-
-
+
+
![{template.name}]()
{template.name}
@@ -320,6 +387,7 @@ function renderTemplatePickerCardContent(
{metaLabel}
) : null}
+ {renderTemplateCategoryTagRow(template)}
);
@@ -642,11 +710,11 @@ function getInlineTextEditorStyle(
}
function getTextLayerNameBadgeClassName() {
- return "pointer-events-none absolute left-0 top-0 -translate-y-[calc(100%+6px)] rounded-full bg-black/78 px-2 py-1 text-[11px] whitespace-nowrap text-white shadow-lg";
+ return "pointer-events-none absolute left-0 top-0 -translate-y-[calc(100%+6px)] rounded-full bg-black/70 px-2 py-1 text-[11px] whitespace-nowrap text-white shadow-lg backdrop-blur-md";
}
function getTextLayerHintBadgeClassName() {
- return "pointer-events-none absolute right-0 top-0 -translate-y-[calc(100%+6px)] rounded-full bg-sky-500/90 px-2 py-1 text-[11px] whitespace-nowrap text-white shadow-lg";
+ return "pointer-events-none absolute right-0 top-0 -translate-y-[calc(100%+6px)] rounded-full bg-sky-500/90 px-2 py-1 text-[11px] whitespace-nowrap text-white shadow-lg backdrop-blur-md";
}
export function AgentPsdTemplateEditorDialog({
@@ -747,6 +815,7 @@ export function AgentPsdTemplateEditorDialog({
Record
>({});
const [uploadingSlotId, setUploadingSlotId] = useState(null);
+ const [extendingSlotId, setExtendingSlotId] = useState(null);
const [isRendering, setIsRendering] = useState(false);
const [renderedResultUrl, setRenderedResultUrl] = useState("");
const [rightPanelView, setRightPanelView] = useState<"editor" | "export">(
@@ -771,7 +840,8 @@ export function AgentPsdTemplateEditorDialog({
[selectedTemplateId, templates]
);
const effectiveTemplateBrandName = useMemo(
- () => normalizedBrandName || String(selectedTemplate?.brandName || "").trim(),
+ () =>
+ normalizedBrandName || String(selectedTemplate?.brandName || "").trim(),
[normalizedBrandName, selectedTemplate?.brandName]
);
const textColorConfig = useMemo(
@@ -785,7 +855,9 @@ export function AgentPsdTemplateEditorDialog({
const shouldShowTemplateBrand = useMemo(
() =>
!normalizedBrandName &&
- templates.some((template) => Boolean(normalizeManagedBrandName(template.brandName))),
+ templates.some((template) =>
+ Boolean(normalizeManagedBrandName(template.brandName))
+ ),
[normalizedBrandName, templates]
);
const selectedCategorySummaryText = useMemo(
@@ -991,12 +1063,21 @@ export function AgentPsdTemplateEditorDialog({
? effectiveTextColorAssignments[selectedTextLayer.id] ||
selectedTextLayer.textColor
: "#111111";
+ const isSelectedImageExtending = Boolean(
+ selectedImageLayer && extendingSlotId === selectedImageLayer.id
+ );
const selectedImageAdjustmentPanel = selectedImageLayer ? (
-
+
{sourceImageUrl ? (
) : null}
+
{selectedImageAssignmentUrl ? (
<>
-
+
+