diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 302797b96..6f12ba40e 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -59,6 +59,7 @@ import { Scissors, Expand, Move3D, + Layers, Upload, Video, Loader2, @@ -959,6 +960,21 @@ export function AdminSettingsClient() { timeout: 120, }); + const [layerSplitConfig, setLayerSplitConfig] = useState({ + enabled: false, + provider: "replicate" as "replicate", + replicateApiToken: "", + replicateModel: "qwen/qwen-image-layered", + layers: 4, + resolution: 640, + trueCfgScale: 4, + numInferenceSteps: 30, + cfgNormalize: true, + useEnPrompt: true, + negativePrompt: "", + timeoutMs: 300000, + }); + // 实时画布API配置状态 const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({ enabled: true, @@ -1134,6 +1150,40 @@ export function AdminSettingsClient() { } }, [angleEditConfigData]); + const { data: layerSplitConfigData } = useSWR("/api/admin/settings?key=layer_split_config", fetcher); + useEffect(() => { + if (layerSplitConfigData?.setting?.value) { + const config = layerSplitConfigData.setting.value as { + enabled?: boolean; + provider?: "replicate"; + replicateApiToken?: string; + replicateModel?: string; + layers?: number; + resolution?: number; + trueCfgScale?: number; + numInferenceSteps?: number; + cfgNormalize?: boolean; + useEnPrompt?: boolean; + negativePrompt?: string; + timeoutMs?: number; + }; + setLayerSplitConfig({ + enabled: config.enabled === true, + provider: "replicate", + replicateApiToken: config.replicateApiToken || "", + replicateModel: config.replicateModel || "qwen/qwen-image-layered", + layers: Math.max(2, Math.min(12, Number(config.layers) || 4)), + resolution: Math.max(256, Math.min(1536, Number(config.resolution) || 640)), + trueCfgScale: Math.max(1, Math.min(12, Number(config.trueCfgScale) || 4)), + numInferenceSteps: Math.max(1, Math.min(80, Number(config.numInferenceSteps) || 30)), + cfgNormalize: config.cfgNormalize !== false, + useEnPrompt: config.useEnPrompt !== false, + negativePrompt: config.negativePrompt || "", + timeoutMs: Math.max(30000, Math.min(900000, Number(config.timeoutMs) || 300000)), + }); + } + }, [layerSplitConfigData]); + // 加载实时画布API配置 const { data: realtimeCanvasConfigData } = useSWR( "/api/admin/settings?key=realtime_canvas_config", @@ -2242,6 +2292,51 @@ export function AdminSettingsClient() { } }; + const handleSaveLayerSplitConfig = async () => { + try { + if (layerSplitConfig.enabled) { + if (!layerSplitConfig.replicateApiToken.trim()) { + toast.error("启用图层拆分需要配置 Replicate API Token"); + return; + } + if (!layerSplitConfig.replicateModel.trim()) { + toast.error("请填写 Replicate 模型"); + return; + } + } + + const response = await fetch("/api/admin/settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + key: "layer_split_config", + value: { + enabled: layerSplitConfig.enabled, + provider: "replicate", + replicateApiToken: layerSplitConfig.replicateApiToken.trim(), + replicateModel: layerSplitConfig.replicateModel.trim() || "qwen/qwen-image-layered", + layers: Math.max(2, Math.min(12, Number(layerSplitConfig.layers) || 4)), + resolution: Math.max(256, Math.min(1536, Number(layerSplitConfig.resolution) || 640)), + trueCfgScale: Math.max(1, Math.min(12, Number(layerSplitConfig.trueCfgScale) || 4)), + numInferenceSteps: Math.max( + 1, + Math.min(80, Number(layerSplitConfig.numInferenceSteps) || 30) + ), + cfgNormalize: layerSplitConfig.cfgNormalize, + useEnPrompt: layerSplitConfig.useEnPrompt, + negativePrompt: layerSplitConfig.negativePrompt.trim(), + timeoutMs: Math.max(30000, Math.min(900000, Number(layerSplitConfig.timeoutMs) || 300000)), + }, + }), + }); + + if (!response.ok) throw new Error("保存失败"); + toast.success("图层拆分API配置保存成功"); + } catch (error) { + toast.error("保存图层拆分API配置失败"); + } + }; + // --- 实时画布API配置逻辑 --- const handleSaveRealtimeCanvasConfig = async () => { try { @@ -9141,6 +9236,220 @@ export function AdminSettingsClient() { + + + + + 图层拆分API配置 + + + 配置 Replicate 上的 Qwen-Image-Layered 模型,用于将单张图片拆分为多个可编辑图层并回写到无限画布。 + + + +
+
+ +
+ 开启后,用户在无限画布中选中图片可使用“图层拆分”工具。 +
+
+
+ + setLayerSplitConfig({ ...layerSplitConfig, enabled: checked }) + } + /> + + {layerSplitConfig.enabled ? "已开启" : "已关闭"} + +
+
+ + + +
+ + + setLayerSplitConfig({ ...layerSplitConfig, replicateApiToken: e.target.value }) + } + placeholder="r8_xxxxxxxxxxxxx" + disabled={!layerSplitConfig.enabled} + /> +
+ +
+ + + setLayerSplitConfig({ ...layerSplitConfig, replicateModel: e.target.value }) + } + placeholder="qwen/qwen-image-layered" + disabled={!layerSplitConfig.enabled} + /> +
+ +
+
+ + + setLayerSplitConfig({ + ...layerSplitConfig, + layers: Math.max(2, Math.min(12, Number(e.target.value) || 4)), + }) + } + disabled={!layerSplitConfig.enabled} + /> +
+
+ + + setLayerSplitConfig({ + ...layerSplitConfig, + resolution: Math.max(256, Math.min(1536, Number(e.target.value) || 640)), + }) + } + disabled={!layerSplitConfig.enabled} + /> +
+
+ + + setLayerSplitConfig({ + ...layerSplitConfig, + trueCfgScale: Math.max(1, Math.min(12, Number(e.target.value) || 4)), + }) + } + disabled={!layerSplitConfig.enabled} + /> +
+
+ + + setLayerSplitConfig({ + ...layerSplitConfig, + numInferenceSteps: Math.max(1, Math.min(80, Number(e.target.value) || 30)), + }) + } + disabled={!layerSplitConfig.enabled} + /> +
+
+ +
+
+
+ +
保持前景/背景透明边缘更稳定
+
+ + setLayerSplitConfig({ ...layerSplitConfig, cfgNormalize: checked }) + } + disabled={!layerSplitConfig.enabled} + /> +
+ +
+
+ +
使用英文提示增强兼容性
+
+ + setLayerSplitConfig({ ...layerSplitConfig, useEnPrompt: checked }) + } + disabled={!layerSplitConfig.enabled} + /> +
+ +
+ + + setLayerSplitConfig({ + ...layerSplitConfig, + timeoutMs: Math.max(30000, Math.min(900000, Number(e.target.value) || 300000)), + }) + } + disabled={!layerSplitConfig.enabled} + /> +
+
+ +
+ +