fix: sync canvas angle availability
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-08 14:00:30 +08:00
parent 1f8e19598c
commit 2f52589469
5 changed files with 82 additions and 2 deletions

View File

@ -159,6 +159,7 @@ type RealtimeCanvasModeIconConfig = {
const DEFAULT_REALTIME_CANVAS_MODE_LABEL = "实时画布模式";
const DEFAULT_ENHANCE_CANVAS_MODE_LABEL = "图像增强模式";
const ANGLE_EDIT_CONFIG_KEY = "angle_edit_config";
const defaultRealtimeCanvasModeIconConfig: RealtimeCanvasModeIconConfig = {
realtimeIconUrl: "",
@ -182,6 +183,9 @@ async function readCanvasFeatureAvailability(valueByKey: Map<string, unknown>) {
]);
return {
angleEditEnabled: normalizeAngleEditEnabled(
valueByKey.get(ANGLE_EDIT_CONFIG_KEY),
),
enhanceProEnabled: normalizeEnhanceProKreaConfig(
valueByKey.get(ENHANCE_PRO_KREA_SETTING_KEY),
).enabled,
@ -441,6 +445,13 @@ function normalizeLayerSplitEnabled(value: unknown): boolean {
return (value as { enabled?: unknown }).enabled === true;
}
function normalizeAngleEditEnabled(value: unknown): boolean {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
}
return (value as { enabled?: unknown }).enabled === true;
}
function normalizeAgentCategoryTreeConfig(value: unknown): AgentCategoryTreeConfig {
const raw =
value && typeof value === "object" && !Array.isArray(value)
@ -596,6 +607,7 @@ async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) {
REALTIME_CANVAS_MODE_ICON_CONFIG_KEY,
CANVAS_TOOLBAR_CONFIG_SETTING_KEY,
LAYER_SPLIT_CONFIG_KEY,
ANGLE_EDIT_CONFIG_KEY,
CANVAS_AGENT_MODE_ENABLED_KEY,
VIDEO_CANVAS_ENABLED_KEY,
VIDEO_CANVAS_SIDEBAR_ENABLED_KEY,
@ -644,6 +656,7 @@ async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) {
layerSplitEnabled: normalizeLayerSplitEnabled(
valueByKey.get(LAYER_SPLIT_CONFIG_KEY)
),
angleEditEnabled: canvasFeatureAvailability.angleEditEnabled,
enhanceProEnabled: canvasFeatureAvailability.enhanceProEnabled,
tripo3dEnabled: canvasFeatureAvailability.tripo3dEnabled,
worldlabs3dEnabled: canvasFeatureAvailability.worldlabs3dEnabled,
@ -691,6 +704,7 @@ async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) {
REALTIME_CANVAS_MODE_ICON_CONFIG_KEY,
CANVAS_TOOLBAR_CONFIG_SETTING_KEY,
LAYER_SPLIT_CONFIG_KEY,
ANGLE_EDIT_CONFIG_KEY,
CANVAS_AGENT_MODE_ENABLED_KEY,
VIDEO_CANVAS_ENABLED_KEY,
VIDEO_CANVAS_SIDEBAR_ENABLED_KEY,
@ -727,6 +741,7 @@ async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) {
layerSplitEnabled: normalizeLayerSplitEnabled(
valueByKey.get(LAYER_SPLIT_CONFIG_KEY)
),
angleEditEnabled: canvasFeatureAvailability.angleEditEnabled,
enhanceProEnabled: canvasFeatureAvailability.enhanceProEnabled,
tripo3dEnabled: canvasFeatureAvailability.tripo3dEnabled,
worldlabs3dEnabled: canvasFeatureAvailability.worldlabs3dEnabled,

View File

@ -1784,6 +1784,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
canvasAgentModeEnabled?: boolean;
videoCanvasSidebarEnabled?: boolean;
layerSplitEnabled?: boolean;
angleEditEnabled?: boolean;
realtimeCanvasEnabled?: boolean;
realtimeCanvasServiceEnabled?: boolean;
enhanceProEnabled?: boolean;
@ -1831,6 +1832,8 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
}, [canvasSiteSettingsKey]);
const isLayerSplitEnabled =
hasResolvedCanvasSiteSettings && siteSettingsData?.layerSplitEnabled === true;
const isAngleEditEnabled =
hasResolvedCanvasSiteSettings && siteSettingsData?.angleEditEnabled === true;
const isTripo3dEnabled =
hasResolvedCanvasSiteSettings && siteSettingsData?.tripo3dEnabled === true;
const isWorldlabs3dEnabled =
@ -8976,6 +8979,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
workspaceLabel={workspaceLabel}
workspaceVariant={isVideoMode ? "video" : "canvas"}
layerSplitEnabled={isLayerSplitEnabled}
angleEditEnabled={isAngleEditEnabled}
tripo3dEnabled={isTripo3dEnabled}
worldlabs3dEnabled={isWorldlabs3dEnabled}
/>

View File

@ -191,6 +191,7 @@ interface CanvasWorkspaceProps {
workspaceVariant?: "canvas" | "video";
canvasToolbarConfig?: CanvasToolbarConfig;
layerSplitEnabled?: boolean;
angleEditEnabled?: boolean;
tripo3dEnabled?: boolean;
worldlabs3dEnabled?: boolean;
batchProcessingEnabled?: boolean;
@ -209,6 +210,7 @@ export function CanvasWorkspace({
workspaceVariant = "canvas",
canvasToolbarConfig: rawCanvasToolbarConfig,
layerSplitEnabled = false,
angleEditEnabled = false,
tripo3dEnabled = false,
worldlabs3dEnabled = false,
batchProcessingEnabled = true,
@ -1517,6 +1519,7 @@ export function CanvasWorkspace({
<ImageNode
batchProcessingEnabled={batchProcessingEnabled}
canvasToolbarConfig={canvasToolbarConfig}
angleEditEnabled={angleEditEnabled}
isLayerSplitEnabled={layerSplitEnabled}
isSelected={isSelected}
node={node}

View File

@ -1304,6 +1304,7 @@ interface ImageNodeProps {
isLayerSplitEnabled?: boolean;
batchProcessingEnabled?: boolean;
canvasToolbarConfig?: CanvasToolbarConfig;
angleEditEnabled?: boolean;
tripo3dEnabled?: boolean;
worldlabs3dEnabled?: boolean;
}
@ -1314,6 +1315,7 @@ export function ImageNode({
isLayerSplitEnabled = false,
batchProcessingEnabled = true,
canvasToolbarConfig,
angleEditEnabled = false,
tripo3dEnabled = false,
worldlabs3dEnabled = false,
}: ImageNodeProps) {
@ -1395,7 +1397,7 @@ export function ImageNode({
const imageMultiAngleEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageMultiAngle"
);
) && angleEditEnabled;
const imagePromptRegenerateEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imagePromptRegenerate"
@ -4489,6 +4491,12 @@ export function ImageNode({
]
);
useEffect(() => {
if (!angleEditEnabled && anglePanelOpen) {
setAnglePanelOpen(false);
}
}, [angleEditEnabled, anglePanelOpen]);
async function handleConfirmThreedGeneration() {
if (threedGenerationDialogMode === "model") {
const targetModelVersion = selectedTripoModelEntry?.id || "";
@ -4544,6 +4552,10 @@ export function ImageNode({
return;
}
if (action === "angle") {
if (!angleEditEnabled) {
toast.info("多视角功能未开启");
return;
}
setCropPanelOpen(false);
setCropInteraction(null);
setImageAdjustPanelOpen(false);

View File

@ -26,6 +26,8 @@ interface UploadedImage {
const DEFAULT_IMAGE_SIZE = "2K";
const DEFAULT_TIMEOUT_MS = 300000;
const OPENAI_IMAGE_MIN_TOTAL_PIXELS = 3_686_400;
const BLTCY_HOST_REGEX = /(^|\.)bltcy\.ai$/i;
const KONTEXT_MODEL_REGEX = /kontext/i;
const SUPPORTED_ASPECT_RATIOS = [
"1:1",
"1:4",
@ -55,6 +57,19 @@ function joinEndpoint(baseURL: string, path: string) {
return `${normalizeBaseURL(baseURL)}${normalizedPath}`;
}
function isBltcyBaseURL(baseURL: string) {
try {
const parsed = new URL(normalizeBaseURL(baseURL));
return BLTCY_HOST_REGEX.test(parsed.hostname);
} catch {
return false;
}
}
function shouldUseBltcyFluxCompatParams(baseURL: string, model: string) {
return isBltcyBaseURL(baseURL) && KONTEXT_MODEL_REGEX.test(String(model || "").trim());
}
function normalizeImageSizeLabel(imageSize?: string): "1K" | "2K" | "4K" {
const normalized = String(imageSize || "").trim().toUpperCase();
if (normalized === "1K" || normalized === "2K" || normalized === "4K") return normalized;
@ -375,12 +390,31 @@ async function requestOpenAIImageGeneration(params: {
model: string;
prompt: string;
size?: string;
aspectRatio?: string;
imageSizeLabel?: "1K" | "2K" | "4K";
imageUrls: string[];
quality?: string;
background?: string;
outputFormat?: string;
}) {
const { baseURL, apiKey, model, prompt, size, imageUrls, quality, background, outputFormat } = params;
const {
baseURL,
apiKey,
model,
prompt,
size,
aspectRatio,
imageSizeLabel,
imageUrls,
quality,
background,
outputFormat,
} = params;
const useBltcyFluxCompatParams = shouldUseBltcyFluxCompatParams(baseURL, model);
const explicitAspectRatio =
typeof aspectRatio === "string" && aspectRatio.trim() && aspectRatio !== "auto"
? aspectRatio.trim()
: undefined;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
@ -392,6 +426,10 @@ async function requestOpenAIImageGeneration(params: {
formData.append("model", model);
formData.append("prompt", prompt);
if (size) formData.append("size", size);
if (useBltcyFluxCompatParams) {
if (explicitAspectRatio) formData.append("aspect_ratio", explicitAspectRatio);
if (imageSizeLabel) formData.append("image_size", imageSizeLabel);
}
formData.append("response_format", "url");
if (quality) formData.append("quality", quality);
@ -434,6 +472,10 @@ async function requestOpenAIImageGeneration(params: {
model,
prompt,
...(size ? { size } : {}),
...(useBltcyFluxCompatParams && explicitAspectRatio
? { aspect_ratio: explicitAspectRatio }
: {}),
...(useBltcyFluxCompatParams && imageSizeLabel ? { image_size: imageSizeLabel } : {}),
response_format: "url",
...(quality ? { quality } : {}),
...(background ? { background } : {}),
@ -504,6 +546,8 @@ export function createOpenAIImage({ apiKey, baseURL }: OpenAIImageConfig) {
model: activeModelId,
size,
aspectRatio: aspectRatio || "auto",
imageSize,
useBltcyFluxCompatParams: shouldUseBltcyFluxCompatParams(baseURL, activeModelId),
imageCount: imageUrls.length,
baseURL: normalizeBaseURL(baseURL),
});
@ -514,6 +558,8 @@ export function createOpenAIImage({ apiKey, baseURL }: OpenAIImageConfig) {
model: activeModelId,
prompt,
size,
aspectRatio: aspectRatio || undefined,
imageSizeLabel: imageSize,
imageUrls,
quality,
background,