Merge pull request '对齐详情替换模型并支持整套图片' (#68) from codex/detail-replacement-full-set into main
This commit is contained in:
commit
eb9ee0ce1e
@ -4,7 +4,8 @@ import { fetchImageBufferFromUrl } from "@/lib/ai/rmbg/service";
|
||||
import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
|
||||
import {
|
||||
DETAIL_REPLACEMENT_IMAGE_MODEL_CONFIG_KEY,
|
||||
normalizeDetailReplacementImageModelConfig,
|
||||
findConfiguredDetailReplacementImageModel,
|
||||
isDetailReplacementImageModel,
|
||||
} from "@/lib/ai/detail-replacement-image-model-config";
|
||||
import {
|
||||
compositeReplacementWithReferenceLocks,
|
||||
@ -29,7 +30,6 @@ import {
|
||||
} from "@/lib/db/queries";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
import { bufferToArrayBuffer } from "./persistence";
|
||||
import { isVideoProviderType } from "./video-models";
|
||||
import type { AgentPresetMessageImagePart } from "./agent-scene-workflow";
|
||||
import type { PostRequestBody } from "../schema";
|
||||
|
||||
@ -2161,22 +2161,10 @@ export async function resolveDirectReplacementModelRef(params: {
|
||||
models?: Awaited<ReturnType<typeof getEnabledAiModelsWithProvider>>;
|
||||
}) {
|
||||
const models = params.models || (await getEnabledAiModelsWithProvider(false));
|
||||
const imageModels = models.filter(
|
||||
({ model, provider }) =>
|
||||
model.supportsImageGen &&
|
||||
!model.isSubModel &&
|
||||
!isVideoProviderType(provider?.type, model),
|
||||
);
|
||||
const configuredModelSetting = await getSystemSetting({
|
||||
key: DETAIL_REPLACEMENT_IMAGE_MODEL_CONFIG_KEY,
|
||||
}).catch(() => null);
|
||||
const configuredModelId = normalizeDetailReplacementImageModelConfig(
|
||||
configuredModelSetting?.value,
|
||||
).modelId;
|
||||
const configuredModel = imageModels.find(
|
||||
({ model }) =>
|
||||
model.id === configuredModelId || model.modelId === configuredModelId,
|
||||
);
|
||||
const configuredModel = findConfiguredDetailReplacementImageModel(models, configuredModelSetting?.value);
|
||||
if (configuredModel) {
|
||||
return configuredModel.model.id;
|
||||
}
|
||||
@ -2190,6 +2178,7 @@ export async function resolveDirectReplacementModelRef(params: {
|
||||
return routedModel;
|
||||
}
|
||||
|
||||
const imageModels = models.filter(isDetailReplacementImageModel);
|
||||
const preferred =
|
||||
imageModels.find(({ model }) => model.isDefault) ||
|
||||
imageModels.find(({ provider }) => String(provider?.type || "").includes("image")) ||
|
||||
|
||||
@ -10,6 +10,10 @@ import {
|
||||
} from "@/lib/db/queries";
|
||||
import { getAiModelFrontendAvailability } from "@/lib/ai/model-frontend-availability";
|
||||
import { buildVideoToolCapability } from "@/lib/ai/video-tool-capability";
|
||||
import {
|
||||
DETAIL_REPLACEMENT_IMAGE_MODEL_CONFIG_KEY,
|
||||
findConfiguredDetailReplacementImageModel,
|
||||
} from "@/lib/ai/detail-replacement-image-model-config";
|
||||
import {
|
||||
isImageGenerationModelDescriptor,
|
||||
isVideoModelDescriptor,
|
||||
@ -157,10 +161,17 @@ export async function GET(request: Request) {
|
||||
: await getEnabledAiModelsWithProvider(false, true);
|
||||
const agentModelWithProvider = await getAgentAiModelWithProvider();
|
||||
const defaultModel = await getDefaultAiModel();
|
||||
const [siteSettings, agentWorkflowUiConfigSetting] = await Promise.all([
|
||||
const [siteSettings, agentWorkflowUiConfigSetting, detailReplacementModelSetting] = await Promise.all([
|
||||
getSiteSettings(),
|
||||
getSystemSetting({ key: AGENT_WORKFLOW_UI_CONFIG_KEY }).catch(() => null),
|
||||
getSystemSetting({ key: DETAIL_REPLACEMENT_IMAGE_MODEL_CONFIG_KEY }).catch(() => null),
|
||||
]);
|
||||
const detailReplacementModel = findConfiguredDetailReplacementImageModel(
|
||||
includeUnavailable ? modelsWithProvider.filter(({ model, provider }) =>
|
||||
provider.isEnabled && model.isEnabled && getAiModelFrontendAvailability(model).selectable !== false
|
||||
) : modelsWithProvider,
|
||||
detailReplacementModelSetting?.value,
|
||||
);
|
||||
type ModelWithProvider = (typeof modelsWithProvider)[number];
|
||||
const serializeModel = ({ model, provider }: ModelWithProvider) => ({
|
||||
id: model.id,
|
||||
@ -309,6 +320,7 @@ export async function GET(request: Request) {
|
||||
: null,
|
||||
agentWorkflowDefaultModelId: agentWorkflowDefaultModel?.model.id || null,
|
||||
agentWorkflowConfiguredModels,
|
||||
detailReplacementModel: detailReplacementModel ? serializeModel(detailReplacementModel) : null,
|
||||
agentModel: agentModelWithProvider
|
||||
? {
|
||||
id: agentModelWithProvider.model.id,
|
||||
|
||||
@ -4,6 +4,7 @@ import { auth } from "@/app/(auth)/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { canvasImageTask, canvasProject, message } from "@/lib/db/schema";
|
||||
import { extractAgentProjectStateFromCanvasData, isAgentProjectRow } from "@/lib/agents/agent-project-compat";
|
||||
import { MAX_DETAIL_REPLACEMENT_REFERENCES } from "@/lib/agents/mode-workspace-state";
|
||||
import {
|
||||
createCanvasImageTask,
|
||||
enqueueCanvasImageTask,
|
||||
@ -146,6 +147,14 @@ export async function POST(request: Request) {
|
||||
.map((item) => [String(item.id), String(item.imageUrl)]),
|
||||
);
|
||||
const requested = Array.isArray(body.references) ? body.references : [];
|
||||
if (requested.length === 0) {
|
||||
return NextResponse.json({ error: "请先选择详情参考图" }, { status: 400 });
|
||||
}
|
||||
if (requested.length > MAX_DETAIL_REPLACEMENT_REFERENCES) {
|
||||
return NextResponse.json({
|
||||
error: `本次选中了 ${requested.length} 张详情参考图,一次最多替换 ${MAX_DETAIL_REPLACEMENT_REFERENCES} 张,请减少本次选择后重试`,
|
||||
}, { status: 400 });
|
||||
}
|
||||
const references: DetailReplacementBatchReference[] = requested.map((raw: unknown) => {
|
||||
const item = raw && typeof raw === "object" ? raw as Record<string, unknown> : {};
|
||||
return {
|
||||
@ -160,7 +169,6 @@ export async function POST(request: Request) {
|
||||
};
|
||||
});
|
||||
if (
|
||||
references.length === 0 || references.length > 40 ||
|
||||
new Set(references.map((item) => item.id)).size !== references.length ||
|
||||
references.some((item) => !item.id || knownReferences.get(item.id) !== item.imageUrl)
|
||||
) {
|
||||
@ -187,6 +195,12 @@ export async function POST(request: Request) {
|
||||
if (!model || model === "auto" || !models.some((entry) => entry.model.id === model)) {
|
||||
return NextResponse.json({ error: "详情替换模型不可用,请检查模型配置" }, { status: 400 });
|
||||
}
|
||||
if (typeof body.expectedModelId === "string" && body.expectedModelId !== model) {
|
||||
return NextResponse.json({
|
||||
code: "DETAIL_REPLACEMENT_MODEL_CHANGED",
|
||||
error: "详情替换模型已更新,请确认页面显示的模型后重新提交",
|
||||
}, { status: 409 });
|
||||
}
|
||||
const quota = await checkUserImageQuota(session.user.id);
|
||||
if (!quota.allowed) {
|
||||
return NextResponse.json({ error: "本月图片生成配额已用完" }, { status: 429 });
|
||||
|
||||
@ -159,6 +159,7 @@ import type {
|
||||
} from "@/types/agent";
|
||||
import {
|
||||
createEmptyAgentModeWorkspaceState,
|
||||
MAX_DETAIL_REPLACEMENT_REFERENCES,
|
||||
} from "@/lib/agents/mode-workspace-state";
|
||||
import {
|
||||
createEmptyAgentGenerationProjectWorkspaceState,
|
||||
@ -213,6 +214,7 @@ export interface AgentGenerationWorkspaceDarkV2Props {
|
||||
interface AgentWorkflowModelOption {
|
||||
id: string;
|
||||
displayName: string;
|
||||
providerName?: string;
|
||||
providerType: string;
|
||||
supportsImageGen?: boolean;
|
||||
isMultimodal?: boolean;
|
||||
@ -2643,7 +2645,9 @@ function getStoredAgentPendingGenerations(): LocalGenerationPendingState[] {
|
||||
? record.requestedCardIds
|
||||
.map((entry) => String(entry || "").trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, MAX_AGENT_MODE_REFERENCE_COUNT)
|
||||
.slice(0, record.modeType === "detail_replacement"
|
||||
? MAX_DETAIL_REPLACEMENT_REFERENCES
|
||||
: MAX_AGENT_MODE_REFERENCE_COUNT)
|
||||
: null,
|
||||
} satisfies LocalGenerationPendingState;
|
||||
});
|
||||
@ -3384,12 +3388,13 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
}
|
||||
);
|
||||
|
||||
const { data: modelsData } = useSWR<{
|
||||
const { data: modelsData, error: modelsError, mutate: refreshModels } = useSWR<{
|
||||
models: AgentWorkflowModelOption[];
|
||||
defaultModelId?: string | null;
|
||||
agentWorkflowConfiguredDefaultModelId?: string | null;
|
||||
agentWorkflowDefaultModelId?: string | null;
|
||||
agentWorkflowConfiguredModels?: AgentWorkflowModelOption[];
|
||||
detailReplacementModel?: AgentWorkflowModelOption | null;
|
||||
agentModel?: any;
|
||||
}>("/api/models", fetcher);
|
||||
|
||||
@ -3435,17 +3440,6 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const activeBusinessModeDefaultModelId = !requiresPresetSelection
|
||||
? String(activeBusinessMode?.defaultModelId || "").trim() || null
|
||||
: null;
|
||||
const lockedWorkflowModelId =
|
||||
activeBusinessModeDefaultModelId ||
|
||||
selectedAgentPreset?.defaultModelId ||
|
||||
modelsData?.agentWorkflowConfiguredDefaultModelId ||
|
||||
modelsData?.agentWorkflowDefaultModelId ||
|
||||
null;
|
||||
const isWorkflowModelLocked = Boolean(lockedWorkflowModelId);
|
||||
const effectiveDefaultModelId =
|
||||
lockedWorkflowModelId ||
|
||||
modelsData?.defaultModelId ||
|
||||
null;
|
||||
const presetDrivenResultModeType: AgentGenerationResultModeType =
|
||||
selectedAgentPreset?.type === "image" && presetModeConfig.modeType === "detail_replacement"
|
||||
? detailReplacementMethod === "ai_detail_generate"
|
||||
@ -3461,6 +3455,19 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const currentResultModeType: AgentGenerationResultModeType = requiresPresetSelection
|
||||
? presetDrivenResultModeType
|
||||
: businessModeResultModeType;
|
||||
const detailReplacementModel = currentResultModeType === "detail_replacement"
|
||||
? modelsData?.detailReplacementModel
|
||||
: null;
|
||||
const isDetailReplacementModelLoading = currentResultModeType === "detail_replacement" && !modelsData;
|
||||
const lockedWorkflowModelId =
|
||||
detailReplacementModel?.id ||
|
||||
activeBusinessModeDefaultModelId ||
|
||||
selectedAgentPreset?.defaultModelId ||
|
||||
modelsData?.agentWorkflowConfiguredDefaultModelId ||
|
||||
modelsData?.agentWorkflowDefaultModelId ||
|
||||
null;
|
||||
const isWorkflowModelLocked = Boolean(lockedWorkflowModelId);
|
||||
const effectiveDefaultModelId = lockedWorkflowModelId || modelsData?.defaultModelId || null;
|
||||
const isSceneGenerationPreset = currentResultModeType === "scene_generation";
|
||||
const isDetailReplacementPreset =
|
||||
currentResultModeType === "detail_replacement" ||
|
||||
@ -4112,12 +4119,15 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
);
|
||||
const executableModelOptions = useMemo(() => {
|
||||
const seen = new Set<string>();
|
||||
return [...selectableModels, ...configuredWorkflowModels].filter((model) => {
|
||||
const workflowModels = detailReplacementModel
|
||||
? [detailReplacementModel, ...configuredWorkflowModels]
|
||||
: configuredWorkflowModels;
|
||||
return [...selectableModels, ...workflowModels].filter((model) => {
|
||||
if (seen.has(model.id)) return false;
|
||||
seen.add(model.id);
|
||||
return true;
|
||||
});
|
||||
}, [configuredWorkflowModels, selectableModels]);
|
||||
}, [configuredWorkflowModels, detailReplacementModel, selectableModels]);
|
||||
const executableModelIdSet = useMemo(
|
||||
() => new Set(executableModelOptions.map((model) => model.id)),
|
||||
[executableModelOptions]
|
||||
@ -4136,8 +4146,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
selectableModels[0],
|
||||
[executableModelOptions, lockedWorkflowModel, selectableModels, selectedModelId]
|
||||
);
|
||||
const lockedWorkflowModelLabel =
|
||||
lockedWorkflowModel?.displayName ||
|
||||
const lockedWorkflowModelLabel = detailReplacementModel?.providerName
|
||||
? `${detailReplacementModel.displayName} 「${detailReplacementModel.providerName}」`
|
||||
: lockedWorkflowModel?.displayName ||
|
||||
selectedAgentPreset?.defaultModel?.displayName ||
|
||||
(lockedWorkflowModelId ? `配置模型不可用(${lockedWorkflowModelId})` : "未配置");
|
||||
const [activeToolStep, setActiveToolStep] = useState<ToolStepKey>("settings");
|
||||
@ -6316,6 +6327,15 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
toast.error(`请先查询并选择${detailReferenceSourceLabel}参考图`);
|
||||
return;
|
||||
}
|
||||
if (isDetailReplacementModelLoading) {
|
||||
if (modelsError) {
|
||||
toast.error("详情替换模型加载失败,正在重新读取");
|
||||
void refreshModels().catch(() => {});
|
||||
} else {
|
||||
toast.info("正在读取详情替换模型,请稍候");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isSkuReplacementPreset && skuReplacementReferenceCards.length === 0) {
|
||||
toast.error(`请先查询并选择${skuReferenceSourceLabel}参考图`);
|
||||
return;
|
||||
@ -6383,6 +6403,10 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
}
|
||||
if (replacementModeType === "detail_replacement") {
|
||||
if (detailBatchSubmittingRef.current) return;
|
||||
if (replacementExecutionCards.length > MAX_DETAIL_REPLACEMENT_REFERENCES) {
|
||||
toast.error(`本次选中了 ${replacementExecutionCards.length} 张详情参考图,一次最多替换 ${MAX_DETAIL_REPLACEMENT_REFERENCES} 张,请减少本次选择后重试`);
|
||||
return;
|
||||
}
|
||||
if (!isValidChatUuid(projectId)) {
|
||||
toast.error("当前风格化项目尚未保存,请先保存项目后再执行详情替换");
|
||||
return;
|
||||
@ -6403,13 +6427,14 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const saved = await onPersistProject?.();
|
||||
if (saved === false) throw new Error("项目尚未保存,请重试");
|
||||
const requestId = generateUUID();
|
||||
const response = await fetchWithErrorHandlers("/api/agents/detail-replacement/batches", {
|
||||
const response = await fetch("/api/agents/detail-replacement/batches", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
projectId,
|
||||
requestId,
|
||||
model: resolvedModelId,
|
||||
expectedModelId: resolvedModelId === "auto" ? undefined : resolvedModelId,
|
||||
imageSize: imageParams.imageSize,
|
||||
replacementStrategy: detailReplacementStrategy,
|
||||
prompt: detailReplacementStrategy === "product_only"
|
||||
@ -6433,6 +6458,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !payload?.task?.id) {
|
||||
if (payload?.code === "DETAIL_REPLACEMENT_MODEL_CHANGED") void refreshModels().catch(() => {});
|
||||
throw new Error(payload?.error || "详情替换任务提交失败");
|
||||
}
|
||||
setPendingGeneration({
|
||||
@ -6620,6 +6646,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
input,
|
||||
isDetailAiGenerationMethod,
|
||||
isDetailReplacementPreset,
|
||||
isDetailReplacementModelLoading,
|
||||
modelsError,
|
||||
isSelectedBusinessModeUnavailable,
|
||||
isSkuReplacementPreset,
|
||||
effectiveDefaultModelId,
|
||||
@ -6647,6 +6675,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
onPersistProject,
|
||||
projectId,
|
||||
refreshDetailBatch,
|
||||
refreshModels,
|
||||
]);
|
||||
const handleStopGeneration = useCallback(() => {
|
||||
detailGenerationCancelRef.current = true;
|
||||
@ -9419,9 +9448,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</Select>
|
||||
) : (
|
||||
<div className="rounded-[24px] border border-white/10 bg-black/30 px-4 py-3 text-sm text-white/70">
|
||||
{isWorkflowModelLocked ? "后台已锁定生图模型:" : "当前生图模式使用默认模型:"}
|
||||
{isDetailReplacementModelLoading
|
||||
? modelsError ? "详情替换模型加载失败,点击批量替换可重试" : "正在读取详情替换模型"
|
||||
: isWorkflowModelLocked ? "后台已锁定生图模型:" : "当前生图模式使用默认模型:"}
|
||||
<span className="ml-2 font-medium text-white">
|
||||
{isWorkflowModelLocked
|
||||
{isDetailReplacementModelLoading ? null : isWorkflowModelLocked
|
||||
? lockedWorkflowModelLabel
|
||||
: currentModel?.displayName || "自动选择"}
|
||||
</span>
|
||||
|
||||
@ -14,6 +14,8 @@ import {
|
||||
sanitizeAgentDetailBrief,
|
||||
} from "@/lib/agents/detail-generation-config";
|
||||
|
||||
export const MAX_DETAIL_REPLACEMENT_REFERENCES = 64;
|
||||
|
||||
export function createEmptyAgentModeWorkspaceState(): AgentModeWorkspaceState {
|
||||
return {
|
||||
detailReplacement: {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { isVideoModelDescriptor } from "@/lib/ai/model-classification";
|
||||
|
||||
export const DETAIL_REPLACEMENT_IMAGE_MODEL_CONFIG_KEY =
|
||||
"detail_replacement_image_model_config";
|
||||
|
||||
@ -20,3 +22,28 @@ export function normalizeDetailReplacementImageModelConfig(
|
||||
modelId: modelId || DEFAULT_DETAIL_REPLACEMENT_IMAGE_MODEL_ID,
|
||||
};
|
||||
}
|
||||
|
||||
export function isDetailReplacementImageModel({
|
||||
model,
|
||||
provider,
|
||||
}: {
|
||||
model: { modelId: string; supportsImageGen: boolean; isSubModel: boolean; capabilities?: unknown };
|
||||
provider?: { type?: string | null } | null;
|
||||
}) {
|
||||
return model.supportsImageGen && !model.isSubModel && !isVideoModelDescriptor({
|
||||
providerType: provider?.type,
|
||||
modelId: model.modelId,
|
||||
capabilities: model.capabilities,
|
||||
supportsImageGen: model.supportsImageGen,
|
||||
});
|
||||
}
|
||||
|
||||
export function findConfiguredDetailReplacementImageModel<
|
||||
T extends Parameters<typeof isDetailReplacementImageModel>[0] & { model: { id: string } },
|
||||
>(models: T[], setting: unknown): T | undefined {
|
||||
const { modelId } = normalizeDetailReplacementImageModelConfig(setting);
|
||||
return models.find((entry) =>
|
||||
(entry.model.id === modelId || entry.model.modelId === modelId) &&
|
||||
isDetailReplacementImageModel(entry)
|
||||
);
|
||||
}
|
||||
|
||||
@ -72,6 +72,85 @@ function projectRoute() {
|
||||
return { route: load("app/api/agent-projects/route.ts"), projects, setUser: (value: string | null) => { userId = value; } };
|
||||
}
|
||||
|
||||
test("a saved full detail set is queued, while overflow and changed references have distinct errors", async () => {
|
||||
const references = Array.from({ length: 65 }, (_, index) => ({
|
||||
id: `detail-${index + 1}`,
|
||||
imageUrl: `https://example.test/detail-${index + 1}.webp`,
|
||||
}));
|
||||
const project = {
|
||||
id, userId: owner, title: "全套详情", workspaceType: "agent",
|
||||
canvasData: { agentProjectState: { workflowState: {
|
||||
agentModeState: { detailReplacement: { references } },
|
||||
} } },
|
||||
};
|
||||
const canvasProject = { id: "id", userId: "userId" };
|
||||
const created: Array<{ id: string; request: { references: typeof references }; status: string }> = [];
|
||||
const queued: string[] = [];
|
||||
const db = {
|
||||
select: () => ({ from: (table: unknown) => ({
|
||||
where: () => ({
|
||||
limit: async () => table === canvasProject ? [project] : [],
|
||||
orderBy: () => ({ limit: async () => [] }),
|
||||
}),
|
||||
}) }),
|
||||
insert: () => ({ values: () => ({ onConflictDoNothing: async () => {} }) }),
|
||||
};
|
||||
const route = loadRuntime({
|
||||
"@/app/(auth)/auth": { auth: async () => ({ user: { id: owner } }) },
|
||||
"@/lib/db": { db },
|
||||
"@/lib/db/schema": { canvasProject, canvasImageTask: { request: "request" }, message: { id: "id" } },
|
||||
"drizzle-orm": { eq: () => null, and: () => null, inArray: () => null, desc: () => null, sql: () => null },
|
||||
"@/lib/canvas/image-task-queue": {
|
||||
createCanvasImageTask: async ({ taskId, request }: { taskId: string; request: { references: typeof references } }) => {
|
||||
const task = { id: taskId, request, status: "pending" };
|
||||
created.push(task);
|
||||
return task;
|
||||
},
|
||||
getCanvasImageTaskForUser: async ({ taskId }: { taskId: string }) => created.find((task) => task.id === taskId),
|
||||
enqueueCanvasImageTask: async (taskId: string) => { queued.push(taskId); },
|
||||
},
|
||||
"@/lib/db/queries": {
|
||||
getEnabledAiModelsWithProvider: async () => [{ model: { id: "fixture-model" } }],
|
||||
checkUserImageQuota: async () => ({ allowed: true, limit: null }),
|
||||
getPointsSystemConfig: async () => ({ enabled: false }),
|
||||
getChatById: async () => ({ userId: owner }),
|
||||
},
|
||||
"@/app/(chat)/api/chat/_lib/detail-replacement": { resolveDirectReplacementModelRef: async () => "fixture-model" },
|
||||
"@/app/(chat)/api/chat/_lib/agent-scene-workflow": {
|
||||
buildAgentSlotPrompt: ({ workflowPrompt }: { workflowPrompt: string }) => workflowPrompt,
|
||||
normalizeAgentSupplementPrompt: () => "",
|
||||
},
|
||||
})("app/api/agents/detail-replacement/batches/route.ts");
|
||||
const post = (items: typeof references, replacementStrategy = "product_only", expectedModelId = "fixture-model") => route.POST(
|
||||
new Request("http://localhost/api/agents/detail-replacement/batches", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
projectId: id, requestId: randomUUID(), references: items, replacementStrategy, expectedModelId,
|
||||
prompt: "沿用品牌场景", productImageUrls: ["https://example.test/product.webp"],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
for (const [count, strategy] of [[48, "product_only"], [64, "scene_replace"]] as const) {
|
||||
const expectedIds = references.slice(0, count).map((ref) => ref.id);
|
||||
const response = await post(references.slice(0, count), strategy);
|
||||
expect(response.status).toBe(200);
|
||||
expect((await response.json()).task.requestedCardIds).toEqual(expectedIds);
|
||||
expect(created.at(-1)?.request.references.map((ref) => ref.id)).toEqual(expectedIds);
|
||||
}
|
||||
const overflow = await post(references);
|
||||
expect(overflow.status).toBe(400);
|
||||
expect((await overflow.json()).error).toContain("一次最多替换 64 张");
|
||||
const changed = await post([{ ...references[0], imageUrl: "https://example.test/changed.webp" }]);
|
||||
expect(changed.status).toBe(400);
|
||||
expect((await changed.json()).error).toContain("尚未保存或已发生变化");
|
||||
const changedModel = await post(references.slice(0, 48), "scene_replace", "outdated-model");
|
||||
expect(changedModel.status).toBe(409);
|
||||
expect((await changedModel.json()).code).toBe("DETAIL_REPLACEMENT_MODEL_CHANGED");
|
||||
expect(queued).toEqual(created.map((task) => task.id));
|
||||
expect(queued).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("duplicate POSTs return one project without overwriting saved state", async () => {
|
||||
const { route, projects } = projectRoute();
|
||||
const post = (title: string) => route.POST(new Request("http://localhost/api/agent-projects", { method: "POST", body: JSON.stringify({ id, title }) }));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user