fix: harden canvas image generation flows
This commit is contained in:
parent
f7fc3561f5
commit
78b0da908c
@ -75,6 +75,8 @@ export function LocalAiImagePipelineSections({
|
||||
aiWashConfig,
|
||||
setAiWashConfig,
|
||||
aiWashModeIconUploading,
|
||||
canvasLocalEditConfig,
|
||||
setCanvasLocalEditConfig,
|
||||
extendConfig,
|
||||
setExtendConfig,
|
||||
watermarkRemoveConfig,
|
||||
@ -103,6 +105,7 @@ export function LocalAiImagePipelineSections({
|
||||
handleMoveEnhanceProPreset,
|
||||
handleAiWashModeIconUpload,
|
||||
handleSaveAiWashConfig,
|
||||
handleSaveCanvasLocalEditConfig,
|
||||
handleSaveExtendConfig,
|
||||
handleSaveWatermarkRemoveConfig,
|
||||
handleSaveAngleEditConfig,
|
||||
@ -1421,6 +1424,112 @@ export function LocalAiImagePipelineSections({
|
||||
</Button>
|
||||
</LocalAiSectionShell>
|
||||
|
||||
<LocalAiSectionShell
|
||||
description={
|
||||
<>
|
||||
配置无限画布图片“局部编辑”和“比例调整”后端优先使用的生图模型。为空时仍沿用旧的 AI详情默认模型或用户当前选择,避免影响已有流程。
|
||||
</>
|
||||
}
|
||||
expanded={isLocalAiSectionExpanded("canvas-local-edit")}
|
||||
id="local-ai-section-canvas-local-edit"
|
||||
onToggle={() => toggleLocalAiSection("canvas-local-edit")}
|
||||
summary={
|
||||
<LocalAiSectionSummary
|
||||
items={[
|
||||
{
|
||||
label: canvasLocalEditConfig.modelId
|
||||
? "普通局部编辑已指定"
|
||||
: "普通局部编辑继承默认",
|
||||
tone: canvasLocalEditConfig.modelId ? "enabled" : "neutral",
|
||||
},
|
||||
{
|
||||
label: canvasLocalEditConfig.scaleAdjustModelId
|
||||
? "比例调整已指定"
|
||||
: "比例调整兼容旧默认",
|
||||
tone: canvasLocalEditConfig.scaleAdjustModelId
|
||||
? "enabled"
|
||||
: "neutral",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
title="局部编辑模型配置"
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="canvasLocalEditModel">普通局部编辑默认模型</Label>
|
||||
<Select
|
||||
onValueChange={(value) =>
|
||||
setCanvasLocalEditConfig((prev) => ({
|
||||
...prev,
|
||||
modelId: value === "__none__" ? "" : value,
|
||||
}))
|
||||
}
|
||||
value={canvasLocalEditConfig.modelId || "__none__"}
|
||||
>
|
||||
<SelectTrigger id="canvasLocalEditModel">
|
||||
<SelectValue placeholder="不指定时使用当前模型或系统默认" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">不指定,沿用当前模型/系统默认</SelectItem>
|
||||
{availableImageModels.length > 0 ? (
|
||||
availableImageModels.map((model) => (
|
||||
<SelectItem key={model.id} value={model.modelId}>
|
||||
{formatAiWashModelOptionLabel(model)}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem disabled value="__empty__">
|
||||
暂无可用生图模型,请先到 AI 模型里启用
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
适用于手动画框并填写提示词的局部编辑。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="canvasScaleAdjustModel">比例调整/目标框模型</Label>
|
||||
<Select
|
||||
onValueChange={(value) =>
|
||||
setCanvasLocalEditConfig((prev) => ({
|
||||
...prev,
|
||||
scaleAdjustModelId: value === "__none__" ? "" : value,
|
||||
}))
|
||||
}
|
||||
value={canvasLocalEditConfig.scaleAdjustModelId || "__none__"}
|
||||
>
|
||||
<SelectTrigger id="canvasScaleAdjustModel">
|
||||
<SelectValue placeholder="不指定时兼容旧 AI详情默认模型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">不指定,兼容旧 AI详情默认模型</SelectItem>
|
||||
{availableImageModels.length > 0 ? (
|
||||
availableImageModels.map((model) => (
|
||||
<SelectItem key={model.id} value={model.modelId}>
|
||||
{formatAiWashModelOptionLabel(model)}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem disabled value="__empty__">
|
||||
暂无可用生图模型,请先到 AI 模型里启用
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
适用于截图中“局部编辑/比例”按钮,把商品缩放到青色目标框的流程。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleSaveCanvasLocalEditConfig}>
|
||||
<Save className="mr-2 h-4 w-4" /> 保存局部编辑模型配置
|
||||
</Button>
|
||||
</LocalAiSectionShell>
|
||||
|
||||
<LocalAiSectionShell
|
||||
description={
|
||||
<>
|
||||
|
||||
@ -12,6 +12,7 @@ export const DEFAULT_LOCAL_AI_EXPANDED_SECTIONS: Record<string, boolean> = {
|
||||
rmbg: false,
|
||||
enhance: false,
|
||||
"enhance-pro": false,
|
||||
"canvas-local-edit": false,
|
||||
"ai-wash": false,
|
||||
extend: false,
|
||||
"watermark-remove": false,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { Dispatch, DragEvent, ReactNode, SetStateAction } from "react";
|
||||
import type { CanvasImageTaskQueueConfig } from "@/lib/canvas/image-task-config";
|
||||
import type { CanvasLocalEditConfig } from "@/lib/canvas/local-edit-config";
|
||||
import type { AiWashConfig } from "@/lib/ai-wash/config";
|
||||
import type {
|
||||
EnhanceProKreaConfig,
|
||||
@ -330,6 +331,8 @@ export type LocalAiImagePipelineSectionsProps = LocalAiSectionControlProps & {
|
||||
aiWashConfig: AiWashConfig;
|
||||
setAiWashConfig: Dispatch<SetStateAction<AiWashConfig>>;
|
||||
aiWashModeIconUploading: boolean;
|
||||
canvasLocalEditConfig: CanvasLocalEditConfig;
|
||||
setCanvasLocalEditConfig: Dispatch<SetStateAction<CanvasLocalEditConfig>>;
|
||||
extendConfig: ExtendConfig;
|
||||
setExtendConfig: Dispatch<SetStateAction<ExtendConfig>>;
|
||||
watermarkRemoveConfig: WatermarkRemoveConfig;
|
||||
@ -372,6 +375,7 @@ export type LocalAiImagePipelineSectionsProps = LocalAiSectionControlProps & {
|
||||
handleMoveEnhanceProPreset: (fromIndex: number, toIndex: number) => void;
|
||||
handleAiWashModeIconUpload: (file: File) => Promise<void>;
|
||||
handleSaveAiWashConfig: AsyncAction;
|
||||
handleSaveCanvasLocalEditConfig: AsyncAction;
|
||||
handleSaveExtendConfig: AsyncAction;
|
||||
handleSaveWatermarkRemoveConfig: AsyncAction;
|
||||
handleSaveAngleEditConfig: AsyncAction;
|
||||
|
||||
@ -192,6 +192,11 @@ import {
|
||||
MIN_CANVAS_IMAGE_TASK_CONCURRENCY,
|
||||
normalizeCanvasImageTaskQueueConfig,
|
||||
} from "@/lib/canvas/image-task-config";
|
||||
import {
|
||||
CANVAS_LOCAL_EDIT_CONFIG_KEY,
|
||||
defaultCanvasLocalEditConfig,
|
||||
normalizeCanvasLocalEditConfig,
|
||||
} from "@/lib/canvas/local-edit-config";
|
||||
import {
|
||||
type CanvasSkillTag,
|
||||
DEFAULT_CANVAS_SKILL_TAGS,
|
||||
@ -1857,6 +1862,13 @@ export function AdminSettingsClient() {
|
||||
const { data: aiWashConfigData, mutate: mutateAiWashConfig } = useSWR<{
|
||||
setting?: { value?: unknown };
|
||||
}>(`/api/admin/settings?key=${AI_WASH_CONFIG_KEY}`, fetcher);
|
||||
const {
|
||||
data: canvasLocalEditConfigData,
|
||||
mutate: mutateCanvasLocalEditConfig,
|
||||
} = useSWR<{ setting?: { value?: unknown } }>(
|
||||
`/api/admin/settings?key=${CANVAS_LOCAL_EDIT_CONFIG_KEY}`,
|
||||
fetcher
|
||||
);
|
||||
const {
|
||||
data: canvasImageTaskQueueConfigData,
|
||||
mutate: mutateCanvasImageTaskQueueConfig,
|
||||
@ -2563,6 +2575,9 @@ export function AdminSettingsClient() {
|
||||
const [draggingEnhanceProPresetId, setDraggingEnhanceProPresetId] =
|
||||
useState("");
|
||||
const [aiWashConfig, setAiWashConfig] = useState(defaultAiWashConfig());
|
||||
const [canvasLocalEditConfig, setCanvasLocalEditConfig] = useState(
|
||||
defaultCanvasLocalEditConfig()
|
||||
);
|
||||
|
||||
// 图像拓展API配置状态
|
||||
const [extendConfig, setExtendConfig] = useState({
|
||||
@ -2804,6 +2819,13 @@ export function AdminSettingsClient() {
|
||||
setAiWashConfig(normalizeAiWashConfig(aiWashConfigData.setting?.value));
|
||||
}, [aiWashConfigData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasLocalEditConfigData) return;
|
||||
setCanvasLocalEditConfig(
|
||||
normalizeCanvasLocalEditConfig(canvasLocalEditConfigData.setting?.value)
|
||||
);
|
||||
}, [canvasLocalEditConfigData]);
|
||||
|
||||
const enhanceProApiKeysDraftCount = useMemo(
|
||||
() =>
|
||||
normalizeEnhanceProKreaConfig({
|
||||
@ -5189,6 +5211,34 @@ export function AdminSettingsClient() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveCanvasLocalEditConfig = async () => {
|
||||
try {
|
||||
const normalizedConfig =
|
||||
normalizeCanvasLocalEditConfig(canvasLocalEditConfig);
|
||||
const response = await fetch("/api/admin/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
key: CANVAS_LOCAL_EDIT_CONFIG_KEY,
|
||||
value: normalizedConfig,
|
||||
}),
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
String(payload?.message || payload?.error || "保存失败")
|
||||
);
|
||||
}
|
||||
|
||||
mutateCanvasLocalEditConfig();
|
||||
toast.success("局部编辑模型配置保存成功");
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "保存局部编辑模型配置失败"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteEnhanceProPoolKey = (index: number) => {
|
||||
setEnhanceProConfig((prev) => ({
|
||||
...prev,
|
||||
@ -18429,6 +18479,7 @@ export function AdminSettingsClient() {
|
||||
angleEditConfig={angleEditConfig}
|
||||
aiWashConfig={aiWashConfig}
|
||||
aiWashModeIconUploading={aiWashModeIconUploading}
|
||||
canvasLocalEditConfig={canvasLocalEditConfig}
|
||||
draftEnhanceProKeyRows={draftEnhanceProKeyRows}
|
||||
draggingEnhanceProPresetId={draggingEnhanceProPresetId}
|
||||
enhanceConfig={enhanceConfig}
|
||||
@ -18449,6 +18500,7 @@ export function AdminSettingsClient() {
|
||||
handleResetEnhanceProPresets={handleResetEnhanceProPresets}
|
||||
handleSaveAngleEditConfig={handleSaveAngleEditConfig}
|
||||
handleSaveAiWashConfig={handleSaveAiWashConfig}
|
||||
handleSaveCanvasLocalEditConfig={handleSaveCanvasLocalEditConfig}
|
||||
handleSaveEnhanceConfig={handleSaveEnhanceConfig}
|
||||
handleSaveEnhanceProConfig={handleSaveEnhanceProConfig}
|
||||
handleSaveExtendConfig={handleSaveExtendConfig}
|
||||
@ -18470,6 +18522,7 @@ export function AdminSettingsClient() {
|
||||
selectedEnhanceProPresetIndex={selectedEnhanceProPresetIndex}
|
||||
setAngleEditConfig={setAngleEditConfig}
|
||||
setAiWashConfig={setAiWashConfig}
|
||||
setCanvasLocalEditConfig={setCanvasLocalEditConfig}
|
||||
setEnhanceConfig={setEnhanceConfig}
|
||||
setEnhanceProApiKeysDraft={setEnhanceProApiKeysDraft}
|
||||
setEnhanceProConfig={setEnhanceProConfig}
|
||||
|
||||
@ -51,6 +51,10 @@ import {
|
||||
CANVAS_IMAGE_TASK_QUEUE_CONFIG_KEY,
|
||||
normalizeCanvasImageTaskQueueConfig,
|
||||
} from "@/lib/canvas/image-task-config";
|
||||
import {
|
||||
CANVAS_LOCAL_EDIT_CONFIG_KEY,
|
||||
normalizeCanvasLocalEditConfig,
|
||||
} from "@/lib/canvas/local-edit-config";
|
||||
import {
|
||||
DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY,
|
||||
normalizeDingTalkRobotNotificationConfig,
|
||||
@ -124,6 +128,12 @@ export async function GET(request: Request) {
|
||||
setting: setting ? { ...setting, value } : { key, value },
|
||||
});
|
||||
}
|
||||
if (key === CANVAS_LOCAL_EDIT_CONFIG_KEY) {
|
||||
const value = normalizeCanvasLocalEditConfig(setting?.value);
|
||||
return Response.json({
|
||||
setting: setting ? { ...setting, value } : { key, value },
|
||||
});
|
||||
}
|
||||
if (key === DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY) {
|
||||
const value = normalizeDingTalkRobotNotificationConfig(setting?.value);
|
||||
return Response.json({
|
||||
@ -359,6 +369,10 @@ export async function POST(request: Request) {
|
||||
nextValue = normalizeCanvasImageTaskQueueConfig(value);
|
||||
}
|
||||
|
||||
if (key === CANVAS_LOCAL_EDIT_CONFIG_KEY) {
|
||||
nextValue = normalizeCanvasLocalEditConfig(value);
|
||||
}
|
||||
|
||||
if (key === DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY) {
|
||||
nextValue = normalizeDingTalkRobotNotificationConfig(value);
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
findStyleProImageModelByRef,
|
||||
getStyleProImageModelEntries,
|
||||
} from "@/lib/style-pro/model-options";
|
||||
import { getAiModelFrontendAvailability } from "@/lib/ai/model-frontend-availability";
|
||||
|
||||
const requestSchema = z.object({
|
||||
prompt: z.string().min(1).max(5000),
|
||||
@ -52,6 +53,16 @@ export async function POST(request: Request) {
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const availability = getAiModelFrontendAvailability(selectedModel.model);
|
||||
if (availability.selectable === false) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: availability.disabledReason || "所选 AI洗图 生图模型当前不可用",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const imageSize = body.imageSize?.trim() || "2K";
|
||||
const unitCost = getModelGenerationCreditCost(selectedModel.model as any, "image", {
|
||||
|
||||
@ -16,7 +16,12 @@ import {
|
||||
getEnabledAiModelsWithProvider,
|
||||
getPointsSystemConfig,
|
||||
getSiteSettings,
|
||||
getSystemSetting,
|
||||
} from "@/lib/db/queries";
|
||||
import {
|
||||
CANVAS_LOCAL_EDIT_CONFIG_KEY,
|
||||
normalizeCanvasLocalEditConfig,
|
||||
} from "@/lib/canvas/local-edit-config";
|
||||
import {
|
||||
modelLacksReferenceImageInputSupport,
|
||||
} from "@/lib/ai/reference-image-capability";
|
||||
@ -4466,10 +4471,17 @@ export async function POST(request: NextRequest) {
|
||||
const shouldUseScaleAdjustDetailModel =
|
||||
operation === SCALE_ADJUST_OPERATION &&
|
||||
(hasScaleAdjustTargetRects || scalePercent !== null);
|
||||
const [pointsConfig, siteSettingsForModel, scaleAdjustModelCandidates] =
|
||||
await Promise.all([
|
||||
const [
|
||||
pointsConfig,
|
||||
siteSettingsForModel,
|
||||
localEditConfigSetting,
|
||||
scaleAdjustModelCandidates,
|
||||
] = await Promise.all([
|
||||
getPointsSystemConfig(),
|
||||
getSiteSettings().catch(() => null),
|
||||
getSystemSetting({ key: CANVAS_LOCAL_EDIT_CONFIG_KEY }).catch(
|
||||
() => null
|
||||
),
|
||||
shouldUseScaleAdjustDetailModel
|
||||
? getEnabledAiModelsWithProvider(false, true).catch((error) => {
|
||||
console.warn(
|
||||
@ -4483,10 +4495,18 @@ export async function POST(request: NextRequest) {
|
||||
const pointsCharge = await prepareFeaturePointsCharge({
|
||||
userId: session.user.id,
|
||||
unitCost: getImageToolCreditCost(pointsConfig, "repaint"),
|
||||
reason: operation === SCALE_ADJUST_OPERATION ? "image_scale_adjust" : "image_repaint",
|
||||
reason:
|
||||
operation === SCALE_ADJUST_OPERATION
|
||||
? "image_scale_adjust"
|
||||
: "image_repaint",
|
||||
modelId:
|
||||
operation === SCALE_ADJUST_OPERATION ? "canvas-scale-adjust" : "canvas-local-edit",
|
||||
note: operation === SCALE_ADJUST_OPERATION ? "比例调整消耗积分" : "局部编辑消耗积分",
|
||||
operation === SCALE_ADJUST_OPERATION
|
||||
? "canvas-scale-adjust"
|
||||
: "canvas-local-edit",
|
||||
note:
|
||||
operation === SCALE_ADJUST_OPERATION
|
||||
? "比例调整消耗积分"
|
||||
: "局部编辑消耗积分",
|
||||
pointsConfig,
|
||||
});
|
||||
if (!pointsCharge.ok) {
|
||||
@ -4518,13 +4538,19 @@ export async function POST(request: NextRequest) {
|
||||
typeof body.model === "string" && body.model.trim()
|
||||
? body.model.trim()
|
||||
: undefined;
|
||||
const localEditConfig = normalizeCanvasLocalEditConfig(
|
||||
localEditConfigSetting?.value
|
||||
);
|
||||
const configuredDetailModel =
|
||||
typeof siteSettingsForModel?.canvasDetailDefaultModelId === "string"
|
||||
? siteSettingsForModel.canvasDetailDefaultModelId.trim()
|
||||
: "";
|
||||
const configuredLocalEditModel = localEditConfig.modelId;
|
||||
const configuredScaleAdjustModel =
|
||||
localEditConfig.scaleAdjustModelId || configuredDetailModel;
|
||||
const detailFixedModel = isScaleAdjustOperation
|
||||
? resolveScaleAdjustPreferredModelId({
|
||||
configuredModelId: configuredDetailModel,
|
||||
configuredModelId: configuredScaleAdjustModel,
|
||||
honorRequestedModelBeforeGpt: hasScaleAdjustEnlarge,
|
||||
preferFalNanoBananaEdit: preferScaleAdjustFalNanoBananaEdit,
|
||||
preferMaskCapableGpt: preferScaleAdjustMaskCapableModel,
|
||||
@ -4536,15 +4562,17 @@ export async function POST(request: NextRequest) {
|
||||
if (
|
||||
isScaleAdjustOperation &&
|
||||
detailFixedModel &&
|
||||
detailFixedModel !== configuredDetailModel
|
||||
detailFixedModel !== configuredScaleAdjustModel
|
||||
) {
|
||||
console.info("[Canvas Local Edit] Override scale adjust model to detail line:", {
|
||||
configuredDetailModel: configuredDetailModel || null,
|
||||
configuredLocalEditScaleModel: localEditConfig.scaleAdjustModelId || null,
|
||||
requestedModel: requestedModel || null,
|
||||
resolvedModel: detailFixedModel,
|
||||
});
|
||||
}
|
||||
const resolvedModel = detailFixedModel || requestedModel;
|
||||
const resolvedModel =
|
||||
detailFixedModel || requestedModel || configuredLocalEditModel;
|
||||
const resolvedScaleAdjustModelEntry = isScaleAdjustOperation
|
||||
? findModelSelectionEntry(scaleAdjustModelCandidates, resolvedModel || "")
|
||||
: null;
|
||||
|
||||
@ -610,7 +610,9 @@ export function AiWashWorkspace({
|
||||
[presetExecutionStates, selectedPreviewResult?.presetId, selectedResultId]
|
||||
);
|
||||
const selectedPresetHasExecutableModel = Boolean(
|
||||
selectedExecutionState?.effectiveModelRef
|
||||
selectedExecutionState?.effectiveModelRef &&
|
||||
selectedExecutionState?.resolvedModel &&
|
||||
selectedExecutionState.resolvedModel.selectable !== false
|
||||
);
|
||||
const shouldShowUserModelSelector = Boolean(
|
||||
!config.hideModelSelector && selectedExecutionState?.usesUserSelectableModel
|
||||
@ -757,7 +759,10 @@ export function AiWashWorkspace({
|
||||
return;
|
||||
}
|
||||
if (!selectedPresetHasExecutableModel) {
|
||||
toast.error("当前预设没有可用的 AI洗图 生图模型,请先在系统设置中配置模型");
|
||||
toast.error(
|
||||
selectedExecutionState?.resolvedModel?.disabledReason ||
|
||||
"当前预设没有可用的 AI洗图 生图模型,请先在系统设置中配置模型"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -818,6 +823,15 @@ export function AiWashWorkspace({
|
||||
if (!executionState.effectiveModelRef) {
|
||||
throw new Error(`${preset.title} 未配置可用模型`);
|
||||
}
|
||||
if (
|
||||
!executionState.resolvedModel ||
|
||||
executionState.resolvedModel.selectable === false
|
||||
) {
|
||||
throw new Error(
|
||||
executionState.resolvedModel?.disabledReason ||
|
||||
`${preset.title} 配置的模型不可用`
|
||||
);
|
||||
}
|
||||
|
||||
const submitResponse = await fetch("/api/ai-wash/generate", {
|
||||
method: "POST",
|
||||
@ -825,7 +839,9 @@ export function AiWashWorkspace({
|
||||
body: JSON.stringify({
|
||||
prompt: executionState.resolvedPrompt,
|
||||
referenceImage: requestReferenceImage,
|
||||
selectedModelId: executionState.effectiveModelRef,
|
||||
selectedModelId:
|
||||
executionState.resolvedModel.id ||
|
||||
executionState.effectiveModelRef,
|
||||
imageSize: config.imageSize,
|
||||
promptExtend: config.promptExtend,
|
||||
title: preset.title,
|
||||
|
||||
@ -278,6 +278,10 @@ function configureMaterialTextures(
|
||||
geometry.setAttribute("uv2", geometry.attributes.uv);
|
||||
}
|
||||
|
||||
if (geometry?.attributes.color && "vertexColors" in materialRecord) {
|
||||
materialRecord.vertexColors = true;
|
||||
}
|
||||
|
||||
for (const key of MATERIAL_TEXTURE_KEYS) {
|
||||
const texture = materialRecord[key];
|
||||
if (!texture?.isTexture) continue;
|
||||
@ -287,6 +291,7 @@ function configureMaterialTextures(
|
||||
? THREE.SRGBColorSpace
|
||||
: THREE.NoColorSpace;
|
||||
}
|
||||
texture.anisotropy = Math.max(Number(texture.anisotropy || 1), 8);
|
||||
texture.needsUpdate = true;
|
||||
}
|
||||
|
||||
@ -652,7 +657,7 @@ export function Model3DViewer({
|
||||
const resolvedModelUrl = useMemo(() => {
|
||||
return buildThreeAssetUrl(modelUrl);
|
||||
}, [modelUrl]);
|
||||
const gridY = Number.isFinite(groundY) ? groundY - 0.01 : -0.01;
|
||||
const gridY = Number.isFinite(groundY) ? groundY - 0.08 : -0.08;
|
||||
const handleGroundYChange = React.useCallback((nextGroundY: number) => {
|
||||
setGroundY((previousGroundY) =>
|
||||
Math.abs(previousGroundY - nextGroundY) < 0.001
|
||||
|
||||
@ -1801,7 +1801,8 @@ export function ImageNode({
|
||||
: "",
|
||||
[aiWashQuickLevels, selectedAiWashQuickPreset]
|
||||
);
|
||||
const shouldShowAiWashQuickModelSelector = !aiWashQuickAdminModelRef;
|
||||
const shouldShowAiWashQuickModelSelector =
|
||||
!aiWashQuickConfig.hideModelSelector && !aiWashQuickAdminModelRef;
|
||||
const effectiveAiWashQuickModelRef =
|
||||
aiWashQuickAdminModelRef || String(aiWashQuickModelId || "").trim();
|
||||
const selectedAiWashQuickModel = useMemo(
|
||||
@ -1811,6 +1812,11 @@ export function ImageNode({
|
||||
null,
|
||||
[aiWashQuickModelId, aiWashQuickModelOptionByRef, effectiveAiWashQuickModelRef]
|
||||
);
|
||||
const aiWashQuickHasExecutableModel = Boolean(
|
||||
effectiveAiWashQuickModelRef &&
|
||||
selectedAiWashQuickModel &&
|
||||
selectedAiWashQuickModel.selectable !== false
|
||||
);
|
||||
const aiWashQuickPrompt = useMemo(
|
||||
() =>
|
||||
selectedAiWashQuickPreset
|
||||
@ -2271,7 +2277,7 @@ export function ImageNode({
|
||||
pointsEnabled && pointsSystemConfig.enhanceProPricing.enabled === true,
|
||||
});
|
||||
const aiWashQuickCostShortLabel =
|
||||
pointsEnabled && selectedAiWashQuickModel
|
||||
pointsEnabled && !aiWashQuickConfig.hideModelSelector && selectedAiWashQuickModel
|
||||
? formatImageNodeFeatureCostShortLabel({
|
||||
cost: selectedAiWashQuickModel.unitCost,
|
||||
isHighVip,
|
||||
@ -6471,8 +6477,11 @@ export function ImageNode({
|
||||
toast.error("当前没有可执行的 AI洗图预设,请联系管理员完成配置");
|
||||
return;
|
||||
}
|
||||
if (!effectiveAiWashQuickModelRef) {
|
||||
toast.error("请先选择一个可用的生图模型");
|
||||
if (!aiWashQuickHasExecutableModel || !selectedAiWashQuickModel) {
|
||||
toast.error(
|
||||
selectedAiWashQuickModel?.disabledReason ||
|
||||
"当前 AI洗图模型不可用,请在后台重新配置模型"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isProcessing || isSubmittingAiWashQuick) {
|
||||
@ -6528,7 +6537,7 @@ export function ImageNode({
|
||||
body: JSON.stringify({
|
||||
prompt: aiWashQuickPrompt,
|
||||
referenceImage: data.src,
|
||||
selectedModelId: effectiveAiWashQuickModelRef,
|
||||
selectedModelId: selectedAiWashQuickModel.id || effectiveAiWashQuickModelRef,
|
||||
imageSize: aiWashQuickConfig.imageSize,
|
||||
promptExtend: aiWashQuickConfig.promptExtend,
|
||||
title: selectedAiWashQuickPreset.title,
|
||||
@ -6625,6 +6634,7 @@ export function ImageNode({
|
||||
}, [
|
||||
addNode,
|
||||
aiWashQuickConfig.enabled,
|
||||
aiWashQuickConfig.hideModelSelector,
|
||||
aiWashQuickConfig.imageSize,
|
||||
aiWashQuickConfig.promptExtend,
|
||||
aiWashQuickLevels,
|
||||
@ -6632,6 +6642,7 @@ export function ImageNode({
|
||||
aiWashQuickPrompt,
|
||||
data.src,
|
||||
displayTitle,
|
||||
aiWashQuickHasExecutableModel,
|
||||
effectiveAiWashQuickModelRef,
|
||||
isProcessing,
|
||||
isSubmittingAiWashQuick,
|
||||
@ -6639,7 +6650,9 @@ export function ImageNode({
|
||||
resetBoxCutoutState,
|
||||
resetLocalEditState,
|
||||
selectNode,
|
||||
selectedAiWashQuickModel?.disabledReason,
|
||||
selectedAiWashQuickModel?.displayName,
|
||||
selectedAiWashQuickModel?.id,
|
||||
selectedAiWashQuickModel?.modelId,
|
||||
selectedAiWashQuickPreset,
|
||||
updateNode,
|
||||
@ -10096,13 +10109,6 @@ export function ImageNode({
|
||||
: "没有可选模型"}
|
||||
</div>
|
||||
</div>
|
||||
) : selectedAiWashQuickModel ? (
|
||||
<div className="mb-3 rounded-[10px] border border-slate-200 bg-slate-50 px-3 py-2 text-[12px] text-slate-500 dark:border-white/10 dark:bg-white/6 dark:text-white/52">
|
||||
{selectedAiWashQuickModel.displayName}
|
||||
{selectedAiWashQuickModel.unitCost > 0
|
||||
? ` · ${selectedAiWashQuickModel.unitCost} 积分/张`
|
||||
: ""}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="-mx-4 divide-y divide-slate-100 dark:divide-white/10">
|
||||
@ -10154,7 +10160,7 @@ export function ImageNode({
|
||||
disabled={
|
||||
isSubmittingAiWashQuick ||
|
||||
!selectedAiWashQuickPreset ||
|
||||
!effectiveAiWashQuickModelRef
|
||||
!aiWashQuickHasExecutableModel
|
||||
}
|
||||
onClick={() => void handleSubmitAiWashQuick()}
|
||||
type="button"
|
||||
@ -10167,9 +10173,10 @@ export function ImageNode({
|
||||
{isSubmittingAiWashQuick
|
||||
? "生成中"
|
||||
: `${aiWashQuickModeLabel}${
|
||||
!aiWashQuickConfig.hideModelSelector &&
|
||||
selectedAiWashQuickModel?.unitCost
|
||||
? ` · ${selectedAiWashQuickModel.unitCost} 积分`
|
||||
: ""
|
||||
: ""
|
||||
}`}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -117,7 +117,7 @@ const DEFAULT_MESH_SETTINGS: Model3DViewerSettings = {
|
||||
const WORLDLABS_ASSET_ROTATION: [number, number, number] = [Math.PI, 0, 0];
|
||||
const WORLDLABS_REFERENCE_VIEW_AXIS = new THREE.Vector3(0, 0, 1);
|
||||
const WORLDLABS_DISPLAY_GROUND_BIAS_RATIO = 0.08;
|
||||
const WORLDLABS_DISPLAY_GROUND_HELPER_BIAS = 0.22;
|
||||
const WORLDLABS_DISPLAY_GROUND_HELPER_BIAS = 0.45;
|
||||
|
||||
const EMPTY_MOVEMENT_STATE: MovementState = {
|
||||
forward: false,
|
||||
@ -1494,6 +1494,7 @@ function WorldSplatScene({
|
||||
semanticsMetadata?.ground_plane_offset
|
||||
);
|
||||
const groundHelperOffset = getWorldLabsDisplayGroundHelperOffset(groundPlaneOffset);
|
||||
const shouldShowGroundHelper = groundHelperOffset !== null && !firstPerson;
|
||||
const handleOrbitControlsRef = React.useCallback((controls: any | null) => {
|
||||
orbitControlsRef.current = controls;
|
||||
setControlsMountVersion((version) => version + 1);
|
||||
@ -1779,7 +1780,7 @@ function WorldSplatScene({
|
||||
<>
|
||||
<ambientLight intensity={1.6} />
|
||||
<directionalLight intensity={1.25} position={[6, 10, 4]} />
|
||||
{groundHelperOffset !== null ? (
|
||||
{shouldShowGroundHelper ? (
|
||||
<gridHelper
|
||||
args={[18, 18, "#334155", "#0f172a"]}
|
||||
position={[0, groundHelperOffset, 0]}
|
||||
|
||||
@ -129,7 +129,7 @@ export function classifyAiModelHealthFailure(error: unknown): {
|
||||
return {
|
||||
eventType: "unavailable",
|
||||
severity: "warning",
|
||||
counted: false,
|
||||
counted: true,
|
||||
reason: "transport_failure_no_provider_response",
|
||||
};
|
||||
}
|
||||
@ -184,7 +184,7 @@ export function classifyAiModelHealthFailure(error: unknown): {
|
||||
return {
|
||||
eventType: "timeout",
|
||||
severity: "warning",
|
||||
counted: false,
|
||||
counted: true,
|
||||
reason: "timeout_without_provider_response",
|
||||
};
|
||||
}
|
||||
|
||||
@ -55,12 +55,14 @@ interface QueueState {
|
||||
nextBatchEligibleAt: Map<string, number>;
|
||||
nextBatchTimers: Map<string, ReturnType<typeof setTimeout>>;
|
||||
awaitingNextBatch: Set<string>;
|
||||
lastStaleRunningCleanupAt: number;
|
||||
draining: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_RUNNING_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
const SCALE_ADJUST_RUNNING_TIMEOUT_MS = 15 * 60 * 1000;
|
||||
const LOCAL_ACTIVE_RUNNING_HARD_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
const STALE_RUNNING_CLEANUP_INTERVAL_MS = 60 * 1000;
|
||||
const PENDING_LOOKAHEAD_LIMIT = 100;
|
||||
const QUEUE_STATUS_PENDING_LOOKAHEAD_LIMIT = 500;
|
||||
const QUEUE_STATUS_RECENT_SAMPLE_LIMIT = 120;
|
||||
@ -81,6 +83,7 @@ function getQueueState(): QueueState {
|
||||
nextBatchEligibleAt: new Map<string, number>(),
|
||||
nextBatchTimers: new Map<string, ReturnType<typeof setTimeout>>(),
|
||||
awaitingNextBatch: new Set<string>(),
|
||||
lastStaleRunningCleanupAt: 0,
|
||||
draining: false,
|
||||
};
|
||||
}
|
||||
@ -393,6 +396,48 @@ async function claimNextPendingTask(
|
||||
return null;
|
||||
}
|
||||
|
||||
async function markStaleDatabaseRunningTasksFailed() {
|
||||
const state = getQueueState();
|
||||
const now = Date.now();
|
||||
if (now - state.lastStaleRunningCleanupAt < STALE_RUNNING_CLEANUP_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
state.lastStaleRunningCleanupAt = now;
|
||||
|
||||
const localRunningIds = Array.from(state.running.keys());
|
||||
const staleBefore = new Date(now - LOCAL_ACTIVE_RUNNING_HARD_TIMEOUT_MS);
|
||||
const staleWhere =
|
||||
localRunningIds.length > 0
|
||||
? and(
|
||||
eq(canvasImageTask.status, "running"),
|
||||
lt(canvasImageTask.startedAt, staleBefore),
|
||||
notInArray(canvasImageTask.id, localRunningIds)
|
||||
)
|
||||
: and(
|
||||
eq(canvasImageTask.status, "running"),
|
||||
lt(canvasImageTask.startedAt, staleBefore)
|
||||
);
|
||||
|
||||
const completedAt = new Date(now);
|
||||
const staleTasks = await db
|
||||
.update(canvasImageTask)
|
||||
.set({
|
||||
status: "failed",
|
||||
error: "图片生成任务已超时,请重新发起生成",
|
||||
completedAt,
|
||||
updatedAt: completedAt,
|
||||
})
|
||||
.where(staleWhere)
|
||||
.returning({ id: canvasImageTask.id });
|
||||
|
||||
if (staleTasks.length > 0) {
|
||||
console.warn("[Canvas Image Task Queue] Marked stale running tasks failed:", {
|
||||
count: staleTasks.length,
|
||||
taskIds: staleTasks.slice(0, 10).map((task) => task.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function runSingleTask(entry: PendingTaskEntry) {
|
||||
try {
|
||||
const [task] = await db
|
||||
@ -474,6 +519,7 @@ export async function drainCanvasImageTaskQueue() {
|
||||
|
||||
state.draining = true;
|
||||
try {
|
||||
await markStaleDatabaseRunningTasksFailed();
|
||||
const maxConcurrentPerModel = await getCanvasImageSingleModelConcurrency();
|
||||
while (true) {
|
||||
const nextTask = await claimNextPendingTask(maxConcurrentPerModel);
|
||||
|
||||
32
lib/canvas/local-edit-config.ts
Normal file
32
lib/canvas/local-edit-config.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export const CANVAS_LOCAL_EDIT_CONFIG_KEY = "canvas_local_edit_config";
|
||||
|
||||
export type CanvasLocalEditConfig = {
|
||||
modelId: string;
|
||||
scaleAdjustModelId: string;
|
||||
};
|
||||
|
||||
export function defaultCanvasLocalEditConfig(): CanvasLocalEditConfig {
|
||||
return {
|
||||
modelId: "",
|
||||
scaleAdjustModelId: "",
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeModelRef(value: unknown) {
|
||||
return String(value || "").trim().slice(0, 128);
|
||||
}
|
||||
|
||||
export function normalizeCanvasLocalEditConfig(
|
||||
value: unknown
|
||||
): CanvasLocalEditConfig {
|
||||
const defaults = defaultCanvasLocalEditConfig();
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
const raw = value as Record<string, unknown>;
|
||||
return {
|
||||
modelId: normalizeModelRef(raw.modelId),
|
||||
scaleAdjustModelId: normalizeModelRef(raw.scaleAdjustModelId),
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user