fix: harden canvas image generation flows
This commit is contained in:
parent
f7fc3561f5
commit
78b0da908c
@ -75,6 +75,8 @@ export function LocalAiImagePipelineSections({
|
|||||||
aiWashConfig,
|
aiWashConfig,
|
||||||
setAiWashConfig,
|
setAiWashConfig,
|
||||||
aiWashModeIconUploading,
|
aiWashModeIconUploading,
|
||||||
|
canvasLocalEditConfig,
|
||||||
|
setCanvasLocalEditConfig,
|
||||||
extendConfig,
|
extendConfig,
|
||||||
setExtendConfig,
|
setExtendConfig,
|
||||||
watermarkRemoveConfig,
|
watermarkRemoveConfig,
|
||||||
@ -103,6 +105,7 @@ export function LocalAiImagePipelineSections({
|
|||||||
handleMoveEnhanceProPreset,
|
handleMoveEnhanceProPreset,
|
||||||
handleAiWashModeIconUpload,
|
handleAiWashModeIconUpload,
|
||||||
handleSaveAiWashConfig,
|
handleSaveAiWashConfig,
|
||||||
|
handleSaveCanvasLocalEditConfig,
|
||||||
handleSaveExtendConfig,
|
handleSaveExtendConfig,
|
||||||
handleSaveWatermarkRemoveConfig,
|
handleSaveWatermarkRemoveConfig,
|
||||||
handleSaveAngleEditConfig,
|
handleSaveAngleEditConfig,
|
||||||
@ -1421,6 +1424,112 @@ export function LocalAiImagePipelineSections({
|
|||||||
</Button>
|
</Button>
|
||||||
</LocalAiSectionShell>
|
</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
|
<LocalAiSectionShell
|
||||||
description={
|
description={
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export const DEFAULT_LOCAL_AI_EXPANDED_SECTIONS: Record<string, boolean> = {
|
|||||||
rmbg: false,
|
rmbg: false,
|
||||||
enhance: false,
|
enhance: false,
|
||||||
"enhance-pro": false,
|
"enhance-pro": false,
|
||||||
|
"canvas-local-edit": false,
|
||||||
"ai-wash": false,
|
"ai-wash": false,
|
||||||
extend: false,
|
extend: false,
|
||||||
"watermark-remove": false,
|
"watermark-remove": false,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import type { Dispatch, DragEvent, ReactNode, SetStateAction } from "react";
|
import type { Dispatch, DragEvent, ReactNode, SetStateAction } from "react";
|
||||||
import type { CanvasImageTaskQueueConfig } from "@/lib/canvas/image-task-config";
|
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 { AiWashConfig } from "@/lib/ai-wash/config";
|
||||||
import type {
|
import type {
|
||||||
EnhanceProKreaConfig,
|
EnhanceProKreaConfig,
|
||||||
@ -330,6 +331,8 @@ export type LocalAiImagePipelineSectionsProps = LocalAiSectionControlProps & {
|
|||||||
aiWashConfig: AiWashConfig;
|
aiWashConfig: AiWashConfig;
|
||||||
setAiWashConfig: Dispatch<SetStateAction<AiWashConfig>>;
|
setAiWashConfig: Dispatch<SetStateAction<AiWashConfig>>;
|
||||||
aiWashModeIconUploading: boolean;
|
aiWashModeIconUploading: boolean;
|
||||||
|
canvasLocalEditConfig: CanvasLocalEditConfig;
|
||||||
|
setCanvasLocalEditConfig: Dispatch<SetStateAction<CanvasLocalEditConfig>>;
|
||||||
extendConfig: ExtendConfig;
|
extendConfig: ExtendConfig;
|
||||||
setExtendConfig: Dispatch<SetStateAction<ExtendConfig>>;
|
setExtendConfig: Dispatch<SetStateAction<ExtendConfig>>;
|
||||||
watermarkRemoveConfig: WatermarkRemoveConfig;
|
watermarkRemoveConfig: WatermarkRemoveConfig;
|
||||||
@ -372,6 +375,7 @@ export type LocalAiImagePipelineSectionsProps = LocalAiSectionControlProps & {
|
|||||||
handleMoveEnhanceProPreset: (fromIndex: number, toIndex: number) => void;
|
handleMoveEnhanceProPreset: (fromIndex: number, toIndex: number) => void;
|
||||||
handleAiWashModeIconUpload: (file: File) => Promise<void>;
|
handleAiWashModeIconUpload: (file: File) => Promise<void>;
|
||||||
handleSaveAiWashConfig: AsyncAction;
|
handleSaveAiWashConfig: AsyncAction;
|
||||||
|
handleSaveCanvasLocalEditConfig: AsyncAction;
|
||||||
handleSaveExtendConfig: AsyncAction;
|
handleSaveExtendConfig: AsyncAction;
|
||||||
handleSaveWatermarkRemoveConfig: AsyncAction;
|
handleSaveWatermarkRemoveConfig: AsyncAction;
|
||||||
handleSaveAngleEditConfig: AsyncAction;
|
handleSaveAngleEditConfig: AsyncAction;
|
||||||
|
|||||||
@ -192,6 +192,11 @@ import {
|
|||||||
MIN_CANVAS_IMAGE_TASK_CONCURRENCY,
|
MIN_CANVAS_IMAGE_TASK_CONCURRENCY,
|
||||||
normalizeCanvasImageTaskQueueConfig,
|
normalizeCanvasImageTaskQueueConfig,
|
||||||
} from "@/lib/canvas/image-task-config";
|
} from "@/lib/canvas/image-task-config";
|
||||||
|
import {
|
||||||
|
CANVAS_LOCAL_EDIT_CONFIG_KEY,
|
||||||
|
defaultCanvasLocalEditConfig,
|
||||||
|
normalizeCanvasLocalEditConfig,
|
||||||
|
} from "@/lib/canvas/local-edit-config";
|
||||||
import {
|
import {
|
||||||
type CanvasSkillTag,
|
type CanvasSkillTag,
|
||||||
DEFAULT_CANVAS_SKILL_TAGS,
|
DEFAULT_CANVAS_SKILL_TAGS,
|
||||||
@ -1857,6 +1862,13 @@ export function AdminSettingsClient() {
|
|||||||
const { data: aiWashConfigData, mutate: mutateAiWashConfig } = useSWR<{
|
const { data: aiWashConfigData, mutate: mutateAiWashConfig } = useSWR<{
|
||||||
setting?: { value?: unknown };
|
setting?: { value?: unknown };
|
||||||
}>(`/api/admin/settings?key=${AI_WASH_CONFIG_KEY}`, fetcher);
|
}>(`/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 {
|
const {
|
||||||
data: canvasImageTaskQueueConfigData,
|
data: canvasImageTaskQueueConfigData,
|
||||||
mutate: mutateCanvasImageTaskQueueConfig,
|
mutate: mutateCanvasImageTaskQueueConfig,
|
||||||
@ -2563,6 +2575,9 @@ export function AdminSettingsClient() {
|
|||||||
const [draggingEnhanceProPresetId, setDraggingEnhanceProPresetId] =
|
const [draggingEnhanceProPresetId, setDraggingEnhanceProPresetId] =
|
||||||
useState("");
|
useState("");
|
||||||
const [aiWashConfig, setAiWashConfig] = useState(defaultAiWashConfig());
|
const [aiWashConfig, setAiWashConfig] = useState(defaultAiWashConfig());
|
||||||
|
const [canvasLocalEditConfig, setCanvasLocalEditConfig] = useState(
|
||||||
|
defaultCanvasLocalEditConfig()
|
||||||
|
);
|
||||||
|
|
||||||
// 图像拓展API配置状态
|
// 图像拓展API配置状态
|
||||||
const [extendConfig, setExtendConfig] = useState({
|
const [extendConfig, setExtendConfig] = useState({
|
||||||
@ -2804,6 +2819,13 @@ export function AdminSettingsClient() {
|
|||||||
setAiWashConfig(normalizeAiWashConfig(aiWashConfigData.setting?.value));
|
setAiWashConfig(normalizeAiWashConfig(aiWashConfigData.setting?.value));
|
||||||
}, [aiWashConfigData]);
|
}, [aiWashConfigData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canvasLocalEditConfigData) return;
|
||||||
|
setCanvasLocalEditConfig(
|
||||||
|
normalizeCanvasLocalEditConfig(canvasLocalEditConfigData.setting?.value)
|
||||||
|
);
|
||||||
|
}, [canvasLocalEditConfigData]);
|
||||||
|
|
||||||
const enhanceProApiKeysDraftCount = useMemo(
|
const enhanceProApiKeysDraftCount = useMemo(
|
||||||
() =>
|
() =>
|
||||||
normalizeEnhanceProKreaConfig({
|
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) => {
|
const handleDeleteEnhanceProPoolKey = (index: number) => {
|
||||||
setEnhanceProConfig((prev) => ({
|
setEnhanceProConfig((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@ -18429,6 +18479,7 @@ export function AdminSettingsClient() {
|
|||||||
angleEditConfig={angleEditConfig}
|
angleEditConfig={angleEditConfig}
|
||||||
aiWashConfig={aiWashConfig}
|
aiWashConfig={aiWashConfig}
|
||||||
aiWashModeIconUploading={aiWashModeIconUploading}
|
aiWashModeIconUploading={aiWashModeIconUploading}
|
||||||
|
canvasLocalEditConfig={canvasLocalEditConfig}
|
||||||
draftEnhanceProKeyRows={draftEnhanceProKeyRows}
|
draftEnhanceProKeyRows={draftEnhanceProKeyRows}
|
||||||
draggingEnhanceProPresetId={draggingEnhanceProPresetId}
|
draggingEnhanceProPresetId={draggingEnhanceProPresetId}
|
||||||
enhanceConfig={enhanceConfig}
|
enhanceConfig={enhanceConfig}
|
||||||
@ -18449,6 +18500,7 @@ export function AdminSettingsClient() {
|
|||||||
handleResetEnhanceProPresets={handleResetEnhanceProPresets}
|
handleResetEnhanceProPresets={handleResetEnhanceProPresets}
|
||||||
handleSaveAngleEditConfig={handleSaveAngleEditConfig}
|
handleSaveAngleEditConfig={handleSaveAngleEditConfig}
|
||||||
handleSaveAiWashConfig={handleSaveAiWashConfig}
|
handleSaveAiWashConfig={handleSaveAiWashConfig}
|
||||||
|
handleSaveCanvasLocalEditConfig={handleSaveCanvasLocalEditConfig}
|
||||||
handleSaveEnhanceConfig={handleSaveEnhanceConfig}
|
handleSaveEnhanceConfig={handleSaveEnhanceConfig}
|
||||||
handleSaveEnhanceProConfig={handleSaveEnhanceProConfig}
|
handleSaveEnhanceProConfig={handleSaveEnhanceProConfig}
|
||||||
handleSaveExtendConfig={handleSaveExtendConfig}
|
handleSaveExtendConfig={handleSaveExtendConfig}
|
||||||
@ -18470,6 +18522,7 @@ export function AdminSettingsClient() {
|
|||||||
selectedEnhanceProPresetIndex={selectedEnhanceProPresetIndex}
|
selectedEnhanceProPresetIndex={selectedEnhanceProPresetIndex}
|
||||||
setAngleEditConfig={setAngleEditConfig}
|
setAngleEditConfig={setAngleEditConfig}
|
||||||
setAiWashConfig={setAiWashConfig}
|
setAiWashConfig={setAiWashConfig}
|
||||||
|
setCanvasLocalEditConfig={setCanvasLocalEditConfig}
|
||||||
setEnhanceConfig={setEnhanceConfig}
|
setEnhanceConfig={setEnhanceConfig}
|
||||||
setEnhanceProApiKeysDraft={setEnhanceProApiKeysDraft}
|
setEnhanceProApiKeysDraft={setEnhanceProApiKeysDraft}
|
||||||
setEnhanceProConfig={setEnhanceProConfig}
|
setEnhanceProConfig={setEnhanceProConfig}
|
||||||
|
|||||||
@ -51,6 +51,10 @@ import {
|
|||||||
CANVAS_IMAGE_TASK_QUEUE_CONFIG_KEY,
|
CANVAS_IMAGE_TASK_QUEUE_CONFIG_KEY,
|
||||||
normalizeCanvasImageTaskQueueConfig,
|
normalizeCanvasImageTaskQueueConfig,
|
||||||
} from "@/lib/canvas/image-task-config";
|
} from "@/lib/canvas/image-task-config";
|
||||||
|
import {
|
||||||
|
CANVAS_LOCAL_EDIT_CONFIG_KEY,
|
||||||
|
normalizeCanvasLocalEditConfig,
|
||||||
|
} from "@/lib/canvas/local-edit-config";
|
||||||
import {
|
import {
|
||||||
DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY,
|
DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY,
|
||||||
normalizeDingTalkRobotNotificationConfig,
|
normalizeDingTalkRobotNotificationConfig,
|
||||||
@ -124,6 +128,12 @@ export async function GET(request: Request) {
|
|||||||
setting: setting ? { ...setting, value } : { key, value },
|
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) {
|
if (key === DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY) {
|
||||||
const value = normalizeDingTalkRobotNotificationConfig(setting?.value);
|
const value = normalizeDingTalkRobotNotificationConfig(setting?.value);
|
||||||
return Response.json({
|
return Response.json({
|
||||||
@ -359,6 +369,10 @@ export async function POST(request: Request) {
|
|||||||
nextValue = normalizeCanvasImageTaskQueueConfig(value);
|
nextValue = normalizeCanvasImageTaskQueueConfig(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key === CANVAS_LOCAL_EDIT_CONFIG_KEY) {
|
||||||
|
nextValue = normalizeCanvasLocalEditConfig(value);
|
||||||
|
}
|
||||||
|
|
||||||
if (key === DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY) {
|
if (key === DINGTALK_ROBOT_NOTIFICATION_CONFIG_KEY) {
|
||||||
nextValue = normalizeDingTalkRobotNotificationConfig(value);
|
nextValue = normalizeDingTalkRobotNotificationConfig(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
findStyleProImageModelByRef,
|
findStyleProImageModelByRef,
|
||||||
getStyleProImageModelEntries,
|
getStyleProImageModelEntries,
|
||||||
} from "@/lib/style-pro/model-options";
|
} from "@/lib/style-pro/model-options";
|
||||||
|
import { getAiModelFrontendAvailability } from "@/lib/ai/model-frontend-availability";
|
||||||
|
|
||||||
const requestSchema = z.object({
|
const requestSchema = z.object({
|
||||||
prompt: z.string().min(1).max(5000),
|
prompt: z.string().min(1).max(5000),
|
||||||
@ -52,6 +53,16 @@ export async function POST(request: Request) {
|
|||||||
{ status: 400 }
|
{ 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 imageSize = body.imageSize?.trim() || "2K";
|
||||||
const unitCost = getModelGenerationCreditCost(selectedModel.model as any, "image", {
|
const unitCost = getModelGenerationCreditCost(selectedModel.model as any, "image", {
|
||||||
|
|||||||
@ -16,7 +16,12 @@ import {
|
|||||||
getEnabledAiModelsWithProvider,
|
getEnabledAiModelsWithProvider,
|
||||||
getPointsSystemConfig,
|
getPointsSystemConfig,
|
||||||
getSiteSettings,
|
getSiteSettings,
|
||||||
|
getSystemSetting,
|
||||||
} from "@/lib/db/queries";
|
} from "@/lib/db/queries";
|
||||||
|
import {
|
||||||
|
CANVAS_LOCAL_EDIT_CONFIG_KEY,
|
||||||
|
normalizeCanvasLocalEditConfig,
|
||||||
|
} from "@/lib/canvas/local-edit-config";
|
||||||
import {
|
import {
|
||||||
modelLacksReferenceImageInputSupport,
|
modelLacksReferenceImageInputSupport,
|
||||||
} from "@/lib/ai/reference-image-capability";
|
} from "@/lib/ai/reference-image-capability";
|
||||||
@ -4466,10 +4471,17 @@ export async function POST(request: NextRequest) {
|
|||||||
const shouldUseScaleAdjustDetailModel =
|
const shouldUseScaleAdjustDetailModel =
|
||||||
operation === SCALE_ADJUST_OPERATION &&
|
operation === SCALE_ADJUST_OPERATION &&
|
||||||
(hasScaleAdjustTargetRects || scalePercent !== null);
|
(hasScaleAdjustTargetRects || scalePercent !== null);
|
||||||
const [pointsConfig, siteSettingsForModel, scaleAdjustModelCandidates] =
|
const [
|
||||||
await Promise.all([
|
pointsConfig,
|
||||||
|
siteSettingsForModel,
|
||||||
|
localEditConfigSetting,
|
||||||
|
scaleAdjustModelCandidates,
|
||||||
|
] = await Promise.all([
|
||||||
getPointsSystemConfig(),
|
getPointsSystemConfig(),
|
||||||
getSiteSettings().catch(() => null),
|
getSiteSettings().catch(() => null),
|
||||||
|
getSystemSetting({ key: CANVAS_LOCAL_EDIT_CONFIG_KEY }).catch(
|
||||||
|
() => null
|
||||||
|
),
|
||||||
shouldUseScaleAdjustDetailModel
|
shouldUseScaleAdjustDetailModel
|
||||||
? getEnabledAiModelsWithProvider(false, true).catch((error) => {
|
? getEnabledAiModelsWithProvider(false, true).catch((error) => {
|
||||||
console.warn(
|
console.warn(
|
||||||
@ -4483,10 +4495,18 @@ export async function POST(request: NextRequest) {
|
|||||||
const pointsCharge = await prepareFeaturePointsCharge({
|
const pointsCharge = await prepareFeaturePointsCharge({
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
unitCost: getImageToolCreditCost(pointsConfig, "repaint"),
|
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:
|
modelId:
|
||||||
operation === SCALE_ADJUST_OPERATION ? "canvas-scale-adjust" : "canvas-local-edit",
|
operation === SCALE_ADJUST_OPERATION
|
||||||
note: operation === SCALE_ADJUST_OPERATION ? "比例调整消耗积分" : "局部编辑消耗积分",
|
? "canvas-scale-adjust"
|
||||||
|
: "canvas-local-edit",
|
||||||
|
note:
|
||||||
|
operation === SCALE_ADJUST_OPERATION
|
||||||
|
? "比例调整消耗积分"
|
||||||
|
: "局部编辑消耗积分",
|
||||||
pointsConfig,
|
pointsConfig,
|
||||||
});
|
});
|
||||||
if (!pointsCharge.ok) {
|
if (!pointsCharge.ok) {
|
||||||
@ -4518,13 +4538,19 @@ export async function POST(request: NextRequest) {
|
|||||||
typeof body.model === "string" && body.model.trim()
|
typeof body.model === "string" && body.model.trim()
|
||||||
? body.model.trim()
|
? body.model.trim()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const localEditConfig = normalizeCanvasLocalEditConfig(
|
||||||
|
localEditConfigSetting?.value
|
||||||
|
);
|
||||||
const configuredDetailModel =
|
const configuredDetailModel =
|
||||||
typeof siteSettingsForModel?.canvasDetailDefaultModelId === "string"
|
typeof siteSettingsForModel?.canvasDetailDefaultModelId === "string"
|
||||||
? siteSettingsForModel.canvasDetailDefaultModelId.trim()
|
? siteSettingsForModel.canvasDetailDefaultModelId.trim()
|
||||||
: "";
|
: "";
|
||||||
|
const configuredLocalEditModel = localEditConfig.modelId;
|
||||||
|
const configuredScaleAdjustModel =
|
||||||
|
localEditConfig.scaleAdjustModelId || configuredDetailModel;
|
||||||
const detailFixedModel = isScaleAdjustOperation
|
const detailFixedModel = isScaleAdjustOperation
|
||||||
? resolveScaleAdjustPreferredModelId({
|
? resolveScaleAdjustPreferredModelId({
|
||||||
configuredModelId: configuredDetailModel,
|
configuredModelId: configuredScaleAdjustModel,
|
||||||
honorRequestedModelBeforeGpt: hasScaleAdjustEnlarge,
|
honorRequestedModelBeforeGpt: hasScaleAdjustEnlarge,
|
||||||
preferFalNanoBananaEdit: preferScaleAdjustFalNanoBananaEdit,
|
preferFalNanoBananaEdit: preferScaleAdjustFalNanoBananaEdit,
|
||||||
preferMaskCapableGpt: preferScaleAdjustMaskCapableModel,
|
preferMaskCapableGpt: preferScaleAdjustMaskCapableModel,
|
||||||
@ -4536,15 +4562,17 @@ export async function POST(request: NextRequest) {
|
|||||||
if (
|
if (
|
||||||
isScaleAdjustOperation &&
|
isScaleAdjustOperation &&
|
||||||
detailFixedModel &&
|
detailFixedModel &&
|
||||||
detailFixedModel !== configuredDetailModel
|
detailFixedModel !== configuredScaleAdjustModel
|
||||||
) {
|
) {
|
||||||
console.info("[Canvas Local Edit] Override scale adjust model to detail line:", {
|
console.info("[Canvas Local Edit] Override scale adjust model to detail line:", {
|
||||||
configuredDetailModel: configuredDetailModel || null,
|
configuredDetailModel: configuredDetailModel || null,
|
||||||
|
configuredLocalEditScaleModel: localEditConfig.scaleAdjustModelId || null,
|
||||||
requestedModel: requestedModel || null,
|
requestedModel: requestedModel || null,
|
||||||
resolvedModel: detailFixedModel,
|
resolvedModel: detailFixedModel,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const resolvedModel = detailFixedModel || requestedModel;
|
const resolvedModel =
|
||||||
|
detailFixedModel || requestedModel || configuredLocalEditModel;
|
||||||
const resolvedScaleAdjustModelEntry = isScaleAdjustOperation
|
const resolvedScaleAdjustModelEntry = isScaleAdjustOperation
|
||||||
? findModelSelectionEntry(scaleAdjustModelCandidates, resolvedModel || "")
|
? findModelSelectionEntry(scaleAdjustModelCandidates, resolvedModel || "")
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@ -610,7 +610,9 @@ export function AiWashWorkspace({
|
|||||||
[presetExecutionStates, selectedPreviewResult?.presetId, selectedResultId]
|
[presetExecutionStates, selectedPreviewResult?.presetId, selectedResultId]
|
||||||
);
|
);
|
||||||
const selectedPresetHasExecutableModel = Boolean(
|
const selectedPresetHasExecutableModel = Boolean(
|
||||||
selectedExecutionState?.effectiveModelRef
|
selectedExecutionState?.effectiveModelRef &&
|
||||||
|
selectedExecutionState?.resolvedModel &&
|
||||||
|
selectedExecutionState.resolvedModel.selectable !== false
|
||||||
);
|
);
|
||||||
const shouldShowUserModelSelector = Boolean(
|
const shouldShowUserModelSelector = Boolean(
|
||||||
!config.hideModelSelector && selectedExecutionState?.usesUserSelectableModel
|
!config.hideModelSelector && selectedExecutionState?.usesUserSelectableModel
|
||||||
@ -757,7 +759,10 @@ export function AiWashWorkspace({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!selectedPresetHasExecutableModel) {
|
if (!selectedPresetHasExecutableModel) {
|
||||||
toast.error("当前预设没有可用的 AI洗图 生图模型,请先在系统设置中配置模型");
|
toast.error(
|
||||||
|
selectedExecutionState?.resolvedModel?.disabledReason ||
|
||||||
|
"当前预设没有可用的 AI洗图 生图模型,请先在系统设置中配置模型"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -818,6 +823,15 @@ export function AiWashWorkspace({
|
|||||||
if (!executionState.effectiveModelRef) {
|
if (!executionState.effectiveModelRef) {
|
||||||
throw new Error(`${preset.title} 未配置可用模型`);
|
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", {
|
const submitResponse = await fetch("/api/ai-wash/generate", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -825,7 +839,9 @@ export function AiWashWorkspace({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
prompt: executionState.resolvedPrompt,
|
prompt: executionState.resolvedPrompt,
|
||||||
referenceImage: requestReferenceImage,
|
referenceImage: requestReferenceImage,
|
||||||
selectedModelId: executionState.effectiveModelRef,
|
selectedModelId:
|
||||||
|
executionState.resolvedModel.id ||
|
||||||
|
executionState.effectiveModelRef,
|
||||||
imageSize: config.imageSize,
|
imageSize: config.imageSize,
|
||||||
promptExtend: config.promptExtend,
|
promptExtend: config.promptExtend,
|
||||||
title: preset.title,
|
title: preset.title,
|
||||||
|
|||||||
@ -278,6 +278,10 @@ function configureMaterialTextures(
|
|||||||
geometry.setAttribute("uv2", geometry.attributes.uv);
|
geometry.setAttribute("uv2", geometry.attributes.uv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (geometry?.attributes.color && "vertexColors" in materialRecord) {
|
||||||
|
materialRecord.vertexColors = true;
|
||||||
|
}
|
||||||
|
|
||||||
for (const key of MATERIAL_TEXTURE_KEYS) {
|
for (const key of MATERIAL_TEXTURE_KEYS) {
|
||||||
const texture = materialRecord[key];
|
const texture = materialRecord[key];
|
||||||
if (!texture?.isTexture) continue;
|
if (!texture?.isTexture) continue;
|
||||||
@ -287,6 +291,7 @@ function configureMaterialTextures(
|
|||||||
? THREE.SRGBColorSpace
|
? THREE.SRGBColorSpace
|
||||||
: THREE.NoColorSpace;
|
: THREE.NoColorSpace;
|
||||||
}
|
}
|
||||||
|
texture.anisotropy = Math.max(Number(texture.anisotropy || 1), 8);
|
||||||
texture.needsUpdate = true;
|
texture.needsUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -652,7 +657,7 @@ export function Model3DViewer({
|
|||||||
const resolvedModelUrl = useMemo(() => {
|
const resolvedModelUrl = useMemo(() => {
|
||||||
return buildThreeAssetUrl(modelUrl);
|
return buildThreeAssetUrl(modelUrl);
|
||||||
}, [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) => {
|
const handleGroundYChange = React.useCallback((nextGroundY: number) => {
|
||||||
setGroundY((previousGroundY) =>
|
setGroundY((previousGroundY) =>
|
||||||
Math.abs(previousGroundY - nextGroundY) < 0.001
|
Math.abs(previousGroundY - nextGroundY) < 0.001
|
||||||
|
|||||||
@ -1801,7 +1801,8 @@ export function ImageNode({
|
|||||||
: "",
|
: "",
|
||||||
[aiWashQuickLevels, selectedAiWashQuickPreset]
|
[aiWashQuickLevels, selectedAiWashQuickPreset]
|
||||||
);
|
);
|
||||||
const shouldShowAiWashQuickModelSelector = !aiWashQuickAdminModelRef;
|
const shouldShowAiWashQuickModelSelector =
|
||||||
|
!aiWashQuickConfig.hideModelSelector && !aiWashQuickAdminModelRef;
|
||||||
const effectiveAiWashQuickModelRef =
|
const effectiveAiWashQuickModelRef =
|
||||||
aiWashQuickAdminModelRef || String(aiWashQuickModelId || "").trim();
|
aiWashQuickAdminModelRef || String(aiWashQuickModelId || "").trim();
|
||||||
const selectedAiWashQuickModel = useMemo(
|
const selectedAiWashQuickModel = useMemo(
|
||||||
@ -1811,6 +1812,11 @@ export function ImageNode({
|
|||||||
null,
|
null,
|
||||||
[aiWashQuickModelId, aiWashQuickModelOptionByRef, effectiveAiWashQuickModelRef]
|
[aiWashQuickModelId, aiWashQuickModelOptionByRef, effectiveAiWashQuickModelRef]
|
||||||
);
|
);
|
||||||
|
const aiWashQuickHasExecutableModel = Boolean(
|
||||||
|
effectiveAiWashQuickModelRef &&
|
||||||
|
selectedAiWashQuickModel &&
|
||||||
|
selectedAiWashQuickModel.selectable !== false
|
||||||
|
);
|
||||||
const aiWashQuickPrompt = useMemo(
|
const aiWashQuickPrompt = useMemo(
|
||||||
() =>
|
() =>
|
||||||
selectedAiWashQuickPreset
|
selectedAiWashQuickPreset
|
||||||
@ -2271,7 +2277,7 @@ export function ImageNode({
|
|||||||
pointsEnabled && pointsSystemConfig.enhanceProPricing.enabled === true,
|
pointsEnabled && pointsSystemConfig.enhanceProPricing.enabled === true,
|
||||||
});
|
});
|
||||||
const aiWashQuickCostShortLabel =
|
const aiWashQuickCostShortLabel =
|
||||||
pointsEnabled && selectedAiWashQuickModel
|
pointsEnabled && !aiWashQuickConfig.hideModelSelector && selectedAiWashQuickModel
|
||||||
? formatImageNodeFeatureCostShortLabel({
|
? formatImageNodeFeatureCostShortLabel({
|
||||||
cost: selectedAiWashQuickModel.unitCost,
|
cost: selectedAiWashQuickModel.unitCost,
|
||||||
isHighVip,
|
isHighVip,
|
||||||
@ -6471,8 +6477,11 @@ export function ImageNode({
|
|||||||
toast.error("当前没有可执行的 AI洗图预设,请联系管理员完成配置");
|
toast.error("当前没有可执行的 AI洗图预设,请联系管理员完成配置");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!effectiveAiWashQuickModelRef) {
|
if (!aiWashQuickHasExecutableModel || !selectedAiWashQuickModel) {
|
||||||
toast.error("请先选择一个可用的生图模型");
|
toast.error(
|
||||||
|
selectedAiWashQuickModel?.disabledReason ||
|
||||||
|
"当前 AI洗图模型不可用,请在后台重新配置模型"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isProcessing || isSubmittingAiWashQuick) {
|
if (isProcessing || isSubmittingAiWashQuick) {
|
||||||
@ -6528,7 +6537,7 @@ export function ImageNode({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
prompt: aiWashQuickPrompt,
|
prompt: aiWashQuickPrompt,
|
||||||
referenceImage: data.src,
|
referenceImage: data.src,
|
||||||
selectedModelId: effectiveAiWashQuickModelRef,
|
selectedModelId: selectedAiWashQuickModel.id || effectiveAiWashQuickModelRef,
|
||||||
imageSize: aiWashQuickConfig.imageSize,
|
imageSize: aiWashQuickConfig.imageSize,
|
||||||
promptExtend: aiWashQuickConfig.promptExtend,
|
promptExtend: aiWashQuickConfig.promptExtend,
|
||||||
title: selectedAiWashQuickPreset.title,
|
title: selectedAiWashQuickPreset.title,
|
||||||
@ -6625,6 +6634,7 @@ export function ImageNode({
|
|||||||
}, [
|
}, [
|
||||||
addNode,
|
addNode,
|
||||||
aiWashQuickConfig.enabled,
|
aiWashQuickConfig.enabled,
|
||||||
|
aiWashQuickConfig.hideModelSelector,
|
||||||
aiWashQuickConfig.imageSize,
|
aiWashQuickConfig.imageSize,
|
||||||
aiWashQuickConfig.promptExtend,
|
aiWashQuickConfig.promptExtend,
|
||||||
aiWashQuickLevels,
|
aiWashQuickLevels,
|
||||||
@ -6632,6 +6642,7 @@ export function ImageNode({
|
|||||||
aiWashQuickPrompt,
|
aiWashQuickPrompt,
|
||||||
data.src,
|
data.src,
|
||||||
displayTitle,
|
displayTitle,
|
||||||
|
aiWashQuickHasExecutableModel,
|
||||||
effectiveAiWashQuickModelRef,
|
effectiveAiWashQuickModelRef,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
isSubmittingAiWashQuick,
|
isSubmittingAiWashQuick,
|
||||||
@ -6639,7 +6650,9 @@ export function ImageNode({
|
|||||||
resetBoxCutoutState,
|
resetBoxCutoutState,
|
||||||
resetLocalEditState,
|
resetLocalEditState,
|
||||||
selectNode,
|
selectNode,
|
||||||
|
selectedAiWashQuickModel?.disabledReason,
|
||||||
selectedAiWashQuickModel?.displayName,
|
selectedAiWashQuickModel?.displayName,
|
||||||
|
selectedAiWashQuickModel?.id,
|
||||||
selectedAiWashQuickModel?.modelId,
|
selectedAiWashQuickModel?.modelId,
|
||||||
selectedAiWashQuickPreset,
|
selectedAiWashQuickPreset,
|
||||||
updateNode,
|
updateNode,
|
||||||
@ -10096,13 +10109,6 @@ export function ImageNode({
|
|||||||
: "没有可选模型"}
|
: "没有可选模型"}
|
||||||
</div>
|
</div>
|
||||||
</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}
|
) : null}
|
||||||
|
|
||||||
<div className="-mx-4 divide-y divide-slate-100 dark:divide-white/10">
|
<div className="-mx-4 divide-y divide-slate-100 dark:divide-white/10">
|
||||||
@ -10154,7 +10160,7 @@ export function ImageNode({
|
|||||||
disabled={
|
disabled={
|
||||||
isSubmittingAiWashQuick ||
|
isSubmittingAiWashQuick ||
|
||||||
!selectedAiWashQuickPreset ||
|
!selectedAiWashQuickPreset ||
|
||||||
!effectiveAiWashQuickModelRef
|
!aiWashQuickHasExecutableModel
|
||||||
}
|
}
|
||||||
onClick={() => void handleSubmitAiWashQuick()}
|
onClick={() => void handleSubmitAiWashQuick()}
|
||||||
type="button"
|
type="button"
|
||||||
@ -10167,9 +10173,10 @@ export function ImageNode({
|
|||||||
{isSubmittingAiWashQuick
|
{isSubmittingAiWashQuick
|
||||||
? "生成中"
|
? "生成中"
|
||||||
: `${aiWashQuickModeLabel}${
|
: `${aiWashQuickModeLabel}${
|
||||||
|
!aiWashQuickConfig.hideModelSelector &&
|
||||||
selectedAiWashQuickModel?.unitCost
|
selectedAiWashQuickModel?.unitCost
|
||||||
? ` · ${selectedAiWashQuickModel.unitCost} 积分`
|
? ` · ${selectedAiWashQuickModel.unitCost} 积分`
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -117,7 +117,7 @@ const DEFAULT_MESH_SETTINGS: Model3DViewerSettings = {
|
|||||||
const WORLDLABS_ASSET_ROTATION: [number, number, number] = [Math.PI, 0, 0];
|
const WORLDLABS_ASSET_ROTATION: [number, number, number] = [Math.PI, 0, 0];
|
||||||
const WORLDLABS_REFERENCE_VIEW_AXIS = new THREE.Vector3(0, 0, 1);
|
const WORLDLABS_REFERENCE_VIEW_AXIS = new THREE.Vector3(0, 0, 1);
|
||||||
const WORLDLABS_DISPLAY_GROUND_BIAS_RATIO = 0.08;
|
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 = {
|
const EMPTY_MOVEMENT_STATE: MovementState = {
|
||||||
forward: false,
|
forward: false,
|
||||||
@ -1494,6 +1494,7 @@ function WorldSplatScene({
|
|||||||
semanticsMetadata?.ground_plane_offset
|
semanticsMetadata?.ground_plane_offset
|
||||||
);
|
);
|
||||||
const groundHelperOffset = getWorldLabsDisplayGroundHelperOffset(groundPlaneOffset);
|
const groundHelperOffset = getWorldLabsDisplayGroundHelperOffset(groundPlaneOffset);
|
||||||
|
const shouldShowGroundHelper = groundHelperOffset !== null && !firstPerson;
|
||||||
const handleOrbitControlsRef = React.useCallback((controls: any | null) => {
|
const handleOrbitControlsRef = React.useCallback((controls: any | null) => {
|
||||||
orbitControlsRef.current = controls;
|
orbitControlsRef.current = controls;
|
||||||
setControlsMountVersion((version) => version + 1);
|
setControlsMountVersion((version) => version + 1);
|
||||||
@ -1779,7 +1780,7 @@ function WorldSplatScene({
|
|||||||
<>
|
<>
|
||||||
<ambientLight intensity={1.6} />
|
<ambientLight intensity={1.6} />
|
||||||
<directionalLight intensity={1.25} position={[6, 10, 4]} />
|
<directionalLight intensity={1.25} position={[6, 10, 4]} />
|
||||||
{groundHelperOffset !== null ? (
|
{shouldShowGroundHelper ? (
|
||||||
<gridHelper
|
<gridHelper
|
||||||
args={[18, 18, "#334155", "#0f172a"]}
|
args={[18, 18, "#334155", "#0f172a"]}
|
||||||
position={[0, groundHelperOffset, 0]}
|
position={[0, groundHelperOffset, 0]}
|
||||||
|
|||||||
@ -129,7 +129,7 @@ export function classifyAiModelHealthFailure(error: unknown): {
|
|||||||
return {
|
return {
|
||||||
eventType: "unavailable",
|
eventType: "unavailable",
|
||||||
severity: "warning",
|
severity: "warning",
|
||||||
counted: false,
|
counted: true,
|
||||||
reason: "transport_failure_no_provider_response",
|
reason: "transport_failure_no_provider_response",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -184,7 +184,7 @@ export function classifyAiModelHealthFailure(error: unknown): {
|
|||||||
return {
|
return {
|
||||||
eventType: "timeout",
|
eventType: "timeout",
|
||||||
severity: "warning",
|
severity: "warning",
|
||||||
counted: false,
|
counted: true,
|
||||||
reason: "timeout_without_provider_response",
|
reason: "timeout_without_provider_response",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,12 +55,14 @@ interface QueueState {
|
|||||||
nextBatchEligibleAt: Map<string, number>;
|
nextBatchEligibleAt: Map<string, number>;
|
||||||
nextBatchTimers: Map<string, ReturnType<typeof setTimeout>>;
|
nextBatchTimers: Map<string, ReturnType<typeof setTimeout>>;
|
||||||
awaitingNextBatch: Set<string>;
|
awaitingNextBatch: Set<string>;
|
||||||
|
lastStaleRunningCleanupAt: number;
|
||||||
draining: boolean;
|
draining: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_RUNNING_TIMEOUT_MS = 10 * 60 * 1000;
|
const DEFAULT_RUNNING_TIMEOUT_MS = 10 * 60 * 1000;
|
||||||
const SCALE_ADJUST_RUNNING_TIMEOUT_MS = 15 * 60 * 1000;
|
const SCALE_ADJUST_RUNNING_TIMEOUT_MS = 15 * 60 * 1000;
|
||||||
const LOCAL_ACTIVE_RUNNING_HARD_TIMEOUT_MS = 30 * 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 PENDING_LOOKAHEAD_LIMIT = 100;
|
||||||
const QUEUE_STATUS_PENDING_LOOKAHEAD_LIMIT = 500;
|
const QUEUE_STATUS_PENDING_LOOKAHEAD_LIMIT = 500;
|
||||||
const QUEUE_STATUS_RECENT_SAMPLE_LIMIT = 120;
|
const QUEUE_STATUS_RECENT_SAMPLE_LIMIT = 120;
|
||||||
@ -81,6 +83,7 @@ function getQueueState(): QueueState {
|
|||||||
nextBatchEligibleAt: new Map<string, number>(),
|
nextBatchEligibleAt: new Map<string, number>(),
|
||||||
nextBatchTimers: new Map<string, ReturnType<typeof setTimeout>>(),
|
nextBatchTimers: new Map<string, ReturnType<typeof setTimeout>>(),
|
||||||
awaitingNextBatch: new Set<string>(),
|
awaitingNextBatch: new Set<string>(),
|
||||||
|
lastStaleRunningCleanupAt: 0,
|
||||||
draining: false,
|
draining: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -393,6 +396,48 @@ async function claimNextPendingTask(
|
|||||||
return null;
|
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) {
|
async function runSingleTask(entry: PendingTaskEntry) {
|
||||||
try {
|
try {
|
||||||
const [task] = await db
|
const [task] = await db
|
||||||
@ -474,6 +519,7 @@ export async function drainCanvasImageTaskQueue() {
|
|||||||
|
|
||||||
state.draining = true;
|
state.draining = true;
|
||||||
try {
|
try {
|
||||||
|
await markStaleDatabaseRunningTasksFailed();
|
||||||
const maxConcurrentPerModel = await getCanvasImageSingleModelConcurrency();
|
const maxConcurrentPerModel = await getCanvasImageSingleModelConcurrency();
|
||||||
while (true) {
|
while (true) {
|
||||||
const nextTask = await claimNextPendingTask(maxConcurrentPerModel);
|
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