fix(canvas): enforce configured local edit model
This commit is contained in:
parent
e7182e7faa
commit
667bba8f53
@ -41,6 +41,7 @@ import {
|
||||
type EnhanceProKreaModel,
|
||||
type EnhanceProPreset,
|
||||
} from "@/lib/enhance-pro/config";
|
||||
import { resolveCanvasLocalEditModelRef } from "@/lib/canvas/local-edit-config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
EnhanceProKeyRow,
|
||||
@ -114,8 +115,26 @@ export function LocalAiImagePipelineSections({
|
||||
const availableImageModels = (modelsData?.models || []).filter(
|
||||
(model) => model.supportsImageGen && model.isEnabled
|
||||
);
|
||||
const formatAiWashModelOptionLabel = (model: (typeof availableImageModels)[number]) =>
|
||||
`${model.displayName || model.modelId}${model.isHidden ? " · 前台隐藏" : ""}`;
|
||||
const duplicateRuntimeModelIds = new Set(
|
||||
availableImageModels
|
||||
.filter(
|
||||
(model, index, models) =>
|
||||
models.findIndex((candidate) => candidate.modelId === model.modelId) !==
|
||||
index
|
||||
)
|
||||
.map((model) => model.modelId)
|
||||
);
|
||||
const formatAiWashModelOptionLabel = (
|
||||
model: (typeof availableImageModels)[number]
|
||||
) =>
|
||||
`${model.displayName || model.modelId}${
|
||||
duplicateRuntimeModelIds.has(model.modelId)
|
||||
? ` · 供应商 ${model.providerId.slice(0, 8)}`
|
||||
: ""
|
||||
}${model.isHidden ? " · 前台隐藏" : ""}`;
|
||||
const resolveCanvasLocalEditSelectValue = (modelRef: string) =>
|
||||
resolveCanvasLocalEditModelRef(availableImageModels, modelRef)?.id ||
|
||||
"__none__";
|
||||
const getAiWashControlToneCopy = (controlId: AiWashControlId) => {
|
||||
if (controlId === "creativity") {
|
||||
return "每档可以单独指定模型;如果留空,前台才会开放模型下拉选择。";
|
||||
@ -1427,7 +1446,7 @@ export function LocalAiImagePipelineSections({
|
||||
<LocalAiSectionShell
|
||||
description={
|
||||
<>
|
||||
配置无限画布图片“局部编辑”和“比例调整”后端优先使用的生图模型。为空时仍沿用旧的 AI详情默认模型或用户当前选择,避免影响已有流程。
|
||||
配置无限画布图片“局部编辑”和“比例调整”使用的生图模型。指定后由后台强制使用该供应商的精确模型,不再被用户前端当前选择覆盖;为空时才沿用旧默认或用户当前选择。
|
||||
</>
|
||||
}
|
||||
expanded={isLocalAiSectionExpanded("canvas-local-edit")}
|
||||
@ -1465,7 +1484,9 @@ export function LocalAiImagePipelineSections({
|
||||
modelId: value === "__none__" ? "" : value,
|
||||
}))
|
||||
}
|
||||
value={canvasLocalEditConfig.modelId || "__none__"}
|
||||
value={resolveCanvasLocalEditSelectValue(
|
||||
canvasLocalEditConfig.modelId
|
||||
)}
|
||||
>
|
||||
<SelectTrigger id="canvasLocalEditModel">
|
||||
<SelectValue placeholder="不指定时使用当前模型或系统默认" />
|
||||
@ -1474,7 +1495,7 @@ export function LocalAiImagePipelineSections({
|
||||
<SelectItem value="__none__">不指定,沿用当前模型/系统默认</SelectItem>
|
||||
{availableImageModels.length > 0 ? (
|
||||
availableImageModels.map((model) => (
|
||||
<SelectItem key={model.id} value={model.modelId}>
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{formatAiWashModelOptionLabel(model)}
|
||||
</SelectItem>
|
||||
))
|
||||
@ -1499,7 +1520,9 @@ export function LocalAiImagePipelineSections({
|
||||
scaleAdjustModelId: value === "__none__" ? "" : value,
|
||||
}))
|
||||
}
|
||||
value={canvasLocalEditConfig.scaleAdjustModelId || "__none__"}
|
||||
value={resolveCanvasLocalEditSelectValue(
|
||||
canvasLocalEditConfig.scaleAdjustModelId
|
||||
)}
|
||||
>
|
||||
<SelectTrigger id="canvasScaleAdjustModel">
|
||||
<SelectValue placeholder="不指定时兼容旧 AI详情默认模型" />
|
||||
@ -1508,7 +1531,7 @@ export function LocalAiImagePipelineSections({
|
||||
<SelectItem value="__none__">不指定,兼容旧 AI详情默认模型</SelectItem>
|
||||
{availableImageModels.length > 0 ? (
|
||||
availableImageModels.map((model) => (
|
||||
<SelectItem key={model.id} value={model.modelId}>
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{formatAiWashModelOptionLabel(model)}
|
||||
</SelectItem>
|
||||
))
|
||||
|
||||
@ -56,6 +56,7 @@ import {
|
||||
import {
|
||||
CANVAS_LOCAL_EDIT_CONFIG_KEY,
|
||||
normalizeCanvasLocalEditConfig,
|
||||
resolveCanvasLocalEditModelRef,
|
||||
} from "@/lib/canvas/local-edit-config";
|
||||
import {
|
||||
DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY,
|
||||
@ -69,6 +70,7 @@ import {
|
||||
STYLE_PRO_CONFIG_KEY,
|
||||
normalizeStyleProConfig,
|
||||
} from "@/lib/style-pro/config";
|
||||
import { getStyleProImageModelEntries } from "@/lib/style-pro/model-options";
|
||||
import {
|
||||
EXTERNAL_IMAGE_API_CONFIG_KEY,
|
||||
normalizeExternalImageApiConfig,
|
||||
@ -380,7 +382,36 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
if (key === CANVAS_LOCAL_EDIT_CONFIG_KEY) {
|
||||
nextValue = normalizeCanvasLocalEditConfig(value);
|
||||
const normalizedValue = normalizeCanvasLocalEditConfig(value);
|
||||
const imageModels = (
|
||||
await getStyleProImageModelEntries({ includeHidden: true })
|
||||
).map(({ model }) => ({ id: model.id, modelId: model.modelId }));
|
||||
const localEditModel = resolveCanvasLocalEditModelRef(
|
||||
imageModels,
|
||||
normalizedValue.modelId
|
||||
);
|
||||
const scaleAdjustModel = resolveCanvasLocalEditModelRef(
|
||||
imageModels,
|
||||
normalizedValue.scaleAdjustModelId
|
||||
);
|
||||
|
||||
if (normalizedValue.modelId && !localEditModel) {
|
||||
return new ChatSDKError(
|
||||
"bad_request:api",
|
||||
"普通局部编辑模型不存在、已停用或不支持生图"
|
||||
).toResponse();
|
||||
}
|
||||
if (normalizedValue.scaleAdjustModelId && !scaleAdjustModel) {
|
||||
return new ChatSDKError(
|
||||
"bad_request:api",
|
||||
"比例调整模型不存在、已停用或不支持生图"
|
||||
).toResponse();
|
||||
}
|
||||
|
||||
nextValue = {
|
||||
modelId: localEditModel?.id || "",
|
||||
scaleAdjustModelId: scaleAdjustModel?.id || "",
|
||||
};
|
||||
}
|
||||
|
||||
if (key === DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY) {
|
||||
|
||||
@ -27,10 +27,12 @@ import {
|
||||
import {
|
||||
CANVAS_LOCAL_EDIT_CONFIG_KEY,
|
||||
normalizeCanvasLocalEditConfig,
|
||||
resolveCanvasLocalEditModelRef,
|
||||
} from "@/lib/canvas/local-edit-config";
|
||||
import { getImageToolCreditCost } from "@/lib/points/config";
|
||||
import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
import { getStyleProImageModelEntries } from "@/lib/style-pro/model-options";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const maxDuration = 120;
|
||||
@ -624,10 +626,16 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const [pointsConfig, siteSettingsForModel, localEditConfigSetting] = await Promise.all([
|
||||
const [
|
||||
pointsConfig,
|
||||
siteSettingsForModel,
|
||||
localEditConfigSetting,
|
||||
availableImageModelEntries,
|
||||
] = await Promise.all([
|
||||
getPointsSystemConfig(),
|
||||
getSiteSettings().catch(() => null),
|
||||
getSystemSetting({ key: CANVAS_LOCAL_EDIT_CONFIG_KEY }).catch(() => null),
|
||||
getStyleProImageModelEntries({ includeHidden: true }),
|
||||
]);
|
||||
const localEditConfig = normalizeCanvasLocalEditConfig(
|
||||
localEditConfigSetting?.value
|
||||
@ -636,9 +644,41 @@ export async function POST(request: NextRequest) {
|
||||
typeof siteSettingsForModel?.canvasDetailDefaultModelId === "string"
|
||||
? siteSettingsForModel.canvasDetailDefaultModelId.trim()
|
||||
: "";
|
||||
const configuredLocalEditModelId = localEditConfig.modelId;
|
||||
const configuredScaleAdjustModelId =
|
||||
localEditConfig.scaleAdjustModelId ||
|
||||
const modelIdentities = availableImageModelEntries.map(({ model }) => ({
|
||||
id: model.id,
|
||||
modelId: model.modelId,
|
||||
}));
|
||||
const configuredLocalEditModel = resolveCanvasLocalEditModelRef(
|
||||
modelIdentities,
|
||||
localEditConfig.modelId
|
||||
);
|
||||
const configuredScaleAdjustModel = resolveCanvasLocalEditModelRef(
|
||||
modelIdentities,
|
||||
localEditConfig.scaleAdjustModelId
|
||||
);
|
||||
if (localEditConfig.modelId && !configuredLocalEditModel) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
"后台配置的普通局部编辑模型不存在、已停用或不支持生图,请在后台重新选择",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
if (localEditConfig.scaleAdjustModelId && !configuredScaleAdjustModel) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
"后台配置的比例调整模型不存在、已停用或不支持生图,请在后台重新选择",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
const configuredLocalEditModelRef = configuredLocalEditModel?.id || "";
|
||||
const configuredScaleAdjustModelRef =
|
||||
configuredScaleAdjustModel?.id ||
|
||||
siteDetailDefaultModelId ||
|
||||
SCALE_ADJUST_REPAIR_MODEL_REF;
|
||||
const pointsCharge = await prepareFeaturePointsCharge({
|
||||
@ -809,7 +849,8 @@ export async function POST(request: NextRequest) {
|
||||
operation,
|
||||
requestedModel: requestedModel || null,
|
||||
resolvedAspectRatio: hasPositionMove ? "auto" : resolvedAspectRatio,
|
||||
resolvedModel: configuredScaleAdjustModelId,
|
||||
configuredModelRef: localEditConfig.scaleAdjustModelId || null,
|
||||
resolvedModel: configuredScaleAdjustModelRef,
|
||||
scaleAdjustProductLockMaskEditApplied: true,
|
||||
scaleAdjustPersonExclusionApplied:
|
||||
productLockAssets.personExclusionApplied,
|
||||
@ -836,7 +877,7 @@ export async function POST(request: NextRequest) {
|
||||
displayPrompt,
|
||||
imageCount: 1,
|
||||
imageSize: requestedImageSize,
|
||||
model: configuredScaleAdjustModelId,
|
||||
model: configuredScaleAdjustModelRef,
|
||||
negativePrompt:
|
||||
"框线、箭头、坐标、标注、文字、UI、缩放整张图、裁剪整张图、拉伸主体、复制主体、替换主体、旧主体残影",
|
||||
prompt,
|
||||
@ -869,7 +910,7 @@ export async function POST(request: NextRequest) {
|
||||
sourceCropImageUrl: semanticSourceCropUploads[index]?.url || "",
|
||||
target: region.targetRect,
|
||||
})),
|
||||
relightModel: configuredScaleAdjustModelId,
|
||||
relightModel: configuredScaleAdjustModelRef,
|
||||
sourceImageUrl,
|
||||
},
|
||||
}
|
||||
@ -898,7 +939,7 @@ export async function POST(request: NextRequest) {
|
||||
...(hasPositionMove
|
||||
? {
|
||||
scaleAdjustModelRelight: {
|
||||
model: configuredScaleAdjustModelId,
|
||||
model: configuredScaleAdjustModelRef,
|
||||
overlayImageUrl: overlayUpload.url,
|
||||
quality: "medium" as const,
|
||||
regions: scaleAdjustIntentRegions.map((region) => ({
|
||||
@ -958,6 +999,8 @@ export async function POST(request: NextRequest) {
|
||||
typeof body.model === "string" && body.model.trim()
|
||||
? body.model.trim()
|
||||
: undefined;
|
||||
const resolvedLocalEditModelRef =
|
||||
configuredLocalEditModelRef || requestedModel;
|
||||
const annotatedBuffer = await sharp(sourceBuffer, { failOn: "none" })
|
||||
.composite([
|
||||
{
|
||||
@ -997,7 +1040,8 @@ export async function POST(request: NextRequest) {
|
||||
displayPrompt,
|
||||
operation,
|
||||
requestedModel: requestedModel || null,
|
||||
resolvedModel: requestedModel || configuredLocalEditModelId || null,
|
||||
configuredModelRef: localEditConfig.modelId || null,
|
||||
resolvedModel: resolvedLocalEditModelRef || null,
|
||||
resolvedAspectRatio,
|
||||
manualRegionReferenceImageCount: manualRegionReferenceImageUrls.length,
|
||||
regionReferenceImageCount: referenceImages.length,
|
||||
@ -1009,7 +1053,7 @@ export async function POST(request: NextRequest) {
|
||||
displayPrompt,
|
||||
imageCount: 1,
|
||||
imageSize: requestedImageSize,
|
||||
model: requestedModel || configuredLocalEditModelId || undefined,
|
||||
model: resolvedLocalEditModelRef,
|
||||
negativePrompt:
|
||||
"彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸、主体截断、贴边裁切、矩形贴片、硬边、拼接线、补丁感、色差边缘、背景错位、地面纹理断裂、椅脚错位",
|
||||
prompt,
|
||||
|
||||
@ -5,6 +5,11 @@ export type CanvasLocalEditConfig = {
|
||||
scaleAdjustModelId: string;
|
||||
};
|
||||
|
||||
export type CanvasLocalEditModelIdentity = {
|
||||
id: string;
|
||||
modelId: string;
|
||||
};
|
||||
|
||||
export function defaultCanvasLocalEditConfig(): CanvasLocalEditConfig {
|
||||
return {
|
||||
modelId: "",
|
||||
@ -16,6 +21,22 @@ function normalizeModelRef(value: unknown) {
|
||||
return String(value || "").trim().slice(0, 128);
|
||||
}
|
||||
|
||||
export function resolveCanvasLocalEditModelRef<
|
||||
T extends CanvasLocalEditModelIdentity,
|
||||
>(models: T[], value: unknown): T | null {
|
||||
const modelRef = normalizeModelRef(value);
|
||||
if (!modelRef) return null;
|
||||
|
||||
// New settings persist the database UUID so identically named runtime
|
||||
// models from different providers cannot be confused. Keep modelId lookup
|
||||
// as a compatibility path for settings saved before UUID persistence.
|
||||
return (
|
||||
models.find((model) => model.id === modelRef) ||
|
||||
models.find((model) => model.modelId === modelRef) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeCanvasLocalEditConfig(
|
||||
value: unknown
|
||||
): CanvasLocalEditConfig {
|
||||
|
||||
@ -4,6 +4,7 @@ import { resolve } from "node:path";
|
||||
|
||||
import { hasScaleAdjustTargetChanged } from "../../components/canvas/nodes/image-node/image-node-local-edit";
|
||||
import { hasScaleAdjustPositionMoved } from "../../lib/canvas/scale-adjust-geometry";
|
||||
import { resolveCanvasLocalEditModelRef } from "../../lib/canvas/local-edit-config";
|
||||
import {
|
||||
buildSemanticScaleAdjustCleanupPrompt,
|
||||
buildSemanticScaleAdjustCompositeRegions,
|
||||
@ -16,6 +17,43 @@ function readSource(path: string) {
|
||||
}
|
||||
|
||||
test.describe("canvas scale adjust product lock", () => {
|
||||
test("resolves local-edit models by exact UUID before legacy runtime IDs", () => {
|
||||
const models = [
|
||||
{ id: "provider-a-model", modelId: "banana-pro" },
|
||||
{ id: "provider-b-model", modelId: "banana-pro" },
|
||||
];
|
||||
|
||||
expect(resolveCanvasLocalEditModelRef(models, "provider-b-model")?.id).toBe(
|
||||
"provider-b-model"
|
||||
);
|
||||
expect(resolveCanvasLocalEditModelRef(models, "banana-pro")?.id).toBe(
|
||||
"provider-a-model"
|
||||
);
|
||||
expect(resolveCanvasLocalEditModelRef(models, "missing-model")).toBeNull();
|
||||
});
|
||||
|
||||
test("forces the configured local-edit provider over the frontend selection", () => {
|
||||
const route = readSource("app/api/canvas/local-edit/route.ts");
|
||||
const settingsRoute = readSource("app/api/admin/settings/route.ts");
|
||||
const settingsPanel = readSource(
|
||||
"app/(admin)/settings/_components/local-ai-sections/image-pipeline-sections.tsx"
|
||||
);
|
||||
|
||||
expect(route).toContain(
|
||||
"configuredLocalEditModelRef || requestedModel"
|
||||
);
|
||||
expect(route).not.toContain(
|
||||
"requestedModel || configuredLocalEditModelId"
|
||||
);
|
||||
expect(route).toContain("configuredModelRef: localEditConfig.modelId");
|
||||
expect(settingsRoute).toContain("modelId: localEditModel?.id || \"\"");
|
||||
expect(settingsPanel).toContain("resolveCanvasLocalEditSelectValue(");
|
||||
expect(settingsPanel).toContain("canvasLocalEditConfig.modelId");
|
||||
expect(settingsPanel).toContain(
|
||||
'<SelectItem key={model.id} value={model.id}>'
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts target movement even when target size stays unchanged", () => {
|
||||
const source = { height: 0.4, width: 0.3, x: 0.2, y: 0.25 };
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user