1302 lines
47 KiB
TypeScript
1302 lines
47 KiB
TypeScript
"use client";
|
||
|
||
import {
|
||
Database,
|
||
Eye,
|
||
EyeOff,
|
||
Image as ImageIcon,
|
||
Loader2,
|
||
Paintbrush,
|
||
Save,
|
||
Sparkles,
|
||
} from "lucide-react";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
import { Switch } from "@/components/ui/switch";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
import {
|
||
DEFAULT_CANVAS_IMAGE_BATCH_DISPATCH_INTERVAL_MS,
|
||
DEFAULT_CANVAS_IMAGE_TASK_CONCURRENCY,
|
||
MAX_CANVAS_IMAGE_BATCH_DISPATCH_INTERVAL_MS,
|
||
MAX_CANVAS_IMAGE_TASK_CONCURRENCY,
|
||
MIN_CANVAS_IMAGE_BATCH_DISPATCH_INTERVAL_MS,
|
||
MIN_CANVAS_IMAGE_TASK_CONCURRENCY,
|
||
normalizeCanvasImageTaskQueueConfig,
|
||
} from "@/lib/canvas/image-task-config";
|
||
import type {
|
||
AdminAiModel,
|
||
AdminAiProvider,
|
||
LocalAiRuntimeSectionsProps,
|
||
} from "../../_lib/types";
|
||
import {
|
||
LocalAiGroupHeading,
|
||
LocalAiSectionShell,
|
||
LocalAiSectionSummary,
|
||
} from "../local-ai-section-shell";
|
||
|
||
export function LocalAiRuntimeSections({
|
||
thirdPartySqlServerConfig,
|
||
setThirdPartySqlServerConfig,
|
||
isThirdPartySqlServerFieldsVisible,
|
||
setIsThirdPartySqlServerFieldsVisible,
|
||
isTestingThirdPartySqlServer,
|
||
realtimeCanvasConfig,
|
||
setRealtimeCanvasConfig,
|
||
canvasImageTaskQueueConfig,
|
||
setCanvasImageTaskQueueConfig,
|
||
sceneFusionConfig,
|
||
setSceneFusionConfig,
|
||
hasThirdPartySqlServerConfiguredValue,
|
||
modelsData,
|
||
providersData,
|
||
isLocalAiSectionExpanded,
|
||
toggleLocalAiSection,
|
||
DEFAULT_BFL_BASE_URL,
|
||
DEFAULT_BFL_REALTIME_MODEL,
|
||
handleSaveThirdPartySqlServerConfig,
|
||
handleTestThirdPartySqlServerConfig,
|
||
handleSaveRealtimeCanvasConfig,
|
||
handleSaveCanvasImageTaskQueueConfig,
|
||
handleSaveSceneFusionConfig,
|
||
}: LocalAiRuntimeSectionsProps) {
|
||
return (
|
||
<>
|
||
<LocalAiGroupHeading
|
||
description="把外部数据库、实时画布、并发队列和批量处理放在运行控制区,方便上线前集中检查风险项。"
|
||
eyebrow="Runtime"
|
||
title="数据源、实时画布与任务队列"
|
||
/>
|
||
|
||
<LocalAiSectionShell
|
||
description={
|
||
<>
|
||
配置后可供后续业务功能直接访问外部 SQL Server
|
||
数据库。当前仅提供配置保存与连通性测试。
|
||
</>
|
||
}
|
||
expanded={isLocalAiSectionExpanded("third-party-sqlserver")}
|
||
id="local-ai-section-third-party-sqlserver"
|
||
onToggle={() => toggleLocalAiSection("third-party-sqlserver")}
|
||
summary={
|
||
<LocalAiSectionSummary
|
||
items={[
|
||
{
|
||
label: thirdPartySqlServerConfig.enabled
|
||
? "连接启用"
|
||
: "连接关闭",
|
||
tone: thirdPartySqlServerConfig.enabled
|
||
? "enabled"
|
||
: "disabled",
|
||
},
|
||
{
|
||
label: hasThirdPartySqlServerConfiguredValue
|
||
? "连接信息已保存"
|
||
: "连接信息待配置",
|
||
tone: hasThirdPartySqlServerConfiguredValue
|
||
? "enabled"
|
||
: "warning",
|
||
},
|
||
{
|
||
label: isThirdPartySqlServerFieldsVisible
|
||
? "字段已显示"
|
||
: "字段已隐藏",
|
||
tone: "neutral",
|
||
},
|
||
]}
|
||
/>
|
||
}
|
||
title={
|
||
<span className="flex items-center gap-2">
|
||
<Database className="h-5 w-5" />
|
||
第三方 SQL Server 数据库连接
|
||
</span>
|
||
}
|
||
>
|
||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||
<div className="space-y-0.5">
|
||
<Label className="text-base">启用第三方数据库连接</Label>
|
||
<div className="text-muted-foreground text-sm">
|
||
关闭后保留配置但不作为后续功能默认外部数据源。
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Switch
|
||
checked={thirdPartySqlServerConfig.enabled}
|
||
onCheckedChange={(checked) =>
|
||
setThirdPartySqlServerConfig((prev) => ({
|
||
...prev,
|
||
enabled: checked,
|
||
}))
|
||
}
|
||
/>
|
||
<span className="min-w-[3rem] font-medium text-muted-foreground text-sm">
|
||
{thirdPartySqlServerConfig.enabled ? "已开启" : "已关闭"}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-3 rounded-lg border border-amber-200 bg-amber-50 p-4 md:flex-row md:items-center md:justify-between">
|
||
<div className="space-y-1">
|
||
<div className="font-medium text-amber-900 text-sm">
|
||
数据库连接信息默认隐藏
|
||
</div>
|
||
<div className="text-amber-800 text-xs">
|
||
{hasThirdPartySqlServerConfiguredValue
|
||
? "已保存的主机地址、数据库、账号和密码不会默认显示,只有主动点击后才会回显并允许编辑。"
|
||
: "当前未展开连接字段。需要新增或编辑配置时,再手动显示连接信息。"}
|
||
</div>
|
||
</div>
|
||
<Button
|
||
onClick={() =>
|
||
setIsThirdPartySqlServerFieldsVisible((prev) => !prev)
|
||
}
|
||
type="button"
|
||
variant="outline"
|
||
>
|
||
{isThirdPartySqlServerFieldsVisible ? (
|
||
<EyeOff className="mr-2 h-4 w-4" />
|
||
) : (
|
||
<Eye className="mr-2 h-4 w-4" />
|
||
)}
|
||
{isThirdPartySqlServerFieldsVisible
|
||
? "隐藏连接信息"
|
||
: "显示并编辑连接信息"}
|
||
</Button>
|
||
</div>
|
||
|
||
{isThirdPartySqlServerFieldsVisible ? (
|
||
<>
|
||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="sqlserverHost">主机地址</Label>
|
||
<Input
|
||
id="sqlserverHost"
|
||
onChange={(e) =>
|
||
setThirdPartySqlServerConfig((prev) => ({
|
||
...prev,
|
||
host: e.target.value,
|
||
}))
|
||
}
|
||
placeholder="例如:db.example.com 或 10.0.0.8"
|
||
value={thirdPartySqlServerConfig.host}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="sqlserverPort">端口</Label>
|
||
<Input
|
||
id="sqlserverPort"
|
||
max={65_535}
|
||
min={1}
|
||
onChange={(e) =>
|
||
setThirdPartySqlServerConfig((prev) => ({
|
||
...prev,
|
||
port: Math.max(
|
||
1,
|
||
Math.min(65_535, Number(e.target.value) || 1433)
|
||
),
|
||
}))
|
||
}
|
||
placeholder="1433"
|
||
type="number"
|
||
value={thirdPartySqlServerConfig.port}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="sqlserverDatabase">数据库名称</Label>
|
||
<Input
|
||
id="sqlserverDatabase"
|
||
onChange={(e) =>
|
||
setThirdPartySqlServerConfig((prev) => ({
|
||
...prev,
|
||
database: e.target.value,
|
||
}))
|
||
}
|
||
placeholder="请输入数据库名称"
|
||
value={thirdPartySqlServerConfig.database}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="sqlserverUsername">账号</Label>
|
||
<Input
|
||
id="sqlserverUsername"
|
||
onChange={(e) =>
|
||
setThirdPartySqlServerConfig((prev) => ({
|
||
...prev,
|
||
username: e.target.value,
|
||
}))
|
||
}
|
||
placeholder="请输入数据库账号"
|
||
value={thirdPartySqlServerConfig.username}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="sqlserverPassword">密码</Label>
|
||
<Input
|
||
id="sqlserverPassword"
|
||
onChange={(e) =>
|
||
setThirdPartySqlServerConfig((prev) => ({
|
||
...prev,
|
||
password: e.target.value,
|
||
}))
|
||
}
|
||
placeholder="请输入 SQL Server 密码"
|
||
type="password"
|
||
value={thirdPartySqlServerConfig.password}
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="sqlserverConnectionTimeout">
|
||
连接超时(毫秒)
|
||
</Label>
|
||
<Input
|
||
id="sqlserverConnectionTimeout"
|
||
max={120_000}
|
||
min={1000}
|
||
onChange={(e) =>
|
||
setThirdPartySqlServerConfig((prev) => ({
|
||
...prev,
|
||
connectionTimeoutMs: Math.max(
|
||
1000,
|
||
Math.min(120_000, Number(e.target.value) || 15_000)
|
||
),
|
||
}))
|
||
}
|
||
step={1000}
|
||
type="number"
|
||
value={thirdPartySqlServerConfig.connectionTimeoutMs}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="sqlserverRequestTimeout">
|
||
请求超时(毫秒)
|
||
</Label>
|
||
<Input
|
||
id="sqlserverRequestTimeout"
|
||
max={120_000}
|
||
min={1000}
|
||
onChange={(e) =>
|
||
setThirdPartySqlServerConfig((prev) => ({
|
||
...prev,
|
||
requestTimeoutMs: Math.max(
|
||
1000,
|
||
Math.min(120_000, Number(e.target.value) || 30_000)
|
||
),
|
||
}))
|
||
}
|
||
step={1000}
|
||
type="number"
|
||
value={thirdPartySqlServerConfig.requestTimeoutMs}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||
<div className="flex items-center justify-between rounded-lg border p-3">
|
||
<div className="space-y-0.5">
|
||
<Label className="text-sm">启用加密</Label>
|
||
<div className="text-muted-foreground text-xs">
|
||
现代云数据库通常需要开启;传统内网 SQL Server 可关闭。
|
||
</div>
|
||
</div>
|
||
<Switch
|
||
checked={thirdPartySqlServerConfig.encrypt}
|
||
onCheckedChange={(checked) =>
|
||
setThirdPartySqlServerConfig((prev) => ({
|
||
...prev,
|
||
encrypt: checked,
|
||
}))
|
||
}
|
||
/>
|
||
</div>
|
||
<div className="flex items-center justify-between rounded-lg border p-3">
|
||
<div className="space-y-0.5">
|
||
<Label className="text-sm">信任服务器证书</Label>
|
||
<div className="text-muted-foreground text-xs">
|
||
对自签名证书或传统 SQL Server 更友好。
|
||
</div>
|
||
</div>
|
||
<Switch
|
||
checked={thirdPartySqlServerConfig.trustServerCertificate}
|
||
onCheckedChange={(checked) =>
|
||
setThirdPartySqlServerConfig((prev) => ({
|
||
...prev,
|
||
trustServerCertificate: checked,
|
||
}))
|
||
}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="rounded-lg border border-dashed p-4 text-muted-foreground text-sm">
|
||
{hasThirdPartySqlServerConfiguredValue
|
||
? "当前已保存的 SQL Server 连接字段处于隐藏状态。点击上方按钮后才会展示主机、数据库、账号和密码。"
|
||
: "当前未展示 SQL Server 连接字段。点击上方按钮开始填写连接信息。"}
|
||
</div>
|
||
)}
|
||
|
||
<div className="space-y-1 rounded-lg border border-blue-200 bg-blue-50 p-4 text-blue-800 text-xs">
|
||
<p>• 当前支持 SQL Server 标准 TCP 连接。</p>
|
||
<p>• 配置会保存到系统设置,供后续外部数据查询/同步功能复用。</p>
|
||
<p>
|
||
• 测试连接会尝试建立连接并执行基础查询,不会修改外部数据库数据。
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap justify-end gap-3">
|
||
<Button
|
||
disabled={isTestingThirdPartySqlServer}
|
||
onClick={handleTestThirdPartySqlServerConfig}
|
||
variant="outline"
|
||
>
|
||
{isTestingThirdPartySqlServer ? (
|
||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||
) : (
|
||
<Database className="mr-2 h-4 w-4" />
|
||
)}
|
||
测试连接
|
||
</Button>
|
||
<Button onClick={handleSaveThirdPartySqlServerConfig}>
|
||
<Save className="mr-2 h-4 w-4" /> 保存 SQL Server 配置
|
||
</Button>
|
||
</div>
|
||
</LocalAiSectionShell>
|
||
|
||
{/* 实时画布API配置 */}
|
||
<LocalAiSectionShell
|
||
description={
|
||
<>
|
||
配置实时画布功能的供应商、模型与 API Key。当前已接入 BFL / Replicate
|
||
/ FAL 供应商;FAL 的 WebSocket
|
||
真流式实时属于第二阶段,不是这次上线内容。
|
||
</>
|
||
}
|
||
expanded={isLocalAiSectionExpanded("realtime-canvas")}
|
||
id="local-ai-section-realtime-canvas"
|
||
onToggle={() => toggleLocalAiSection("realtime-canvas")}
|
||
summary={
|
||
<LocalAiSectionSummary
|
||
items={[
|
||
{
|
||
label: realtimeCanvasConfig.enabled ? "服务启用" : "服务关闭",
|
||
tone: realtimeCanvasConfig.enabled ? "enabled" : "disabled",
|
||
},
|
||
{
|
||
label: `主供应商 ${realtimeCanvasConfig.providerType.toUpperCase()}`,
|
||
tone: "neutral",
|
||
},
|
||
{
|
||
label: realtimeCanvasConfig.fallbackEnabled
|
||
? "备用链路启用"
|
||
: "无备用链路",
|
||
tone: realtimeCanvasConfig.fallbackEnabled
|
||
? "enabled"
|
||
: "neutral",
|
||
},
|
||
]}
|
||
/>
|
||
}
|
||
title={
|
||
<span className="flex items-center gap-2">
|
||
<Paintbrush className="h-5 w-5" />
|
||
实时画布配置
|
||
</span>
|
||
}
|
||
>
|
||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||
<div className="space-y-0.5">
|
||
<Label className="text-base">启用实时画布服务</Label>
|
||
<div className="text-muted-foreground text-sm">
|
||
关闭后前端“实时画布”入口仍可显示,但会提示服务未启用。
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Switch
|
||
checked={realtimeCanvasConfig.enabled}
|
||
onCheckedChange={(checked) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
enabled: checked,
|
||
})
|
||
}
|
||
/>
|
||
<span className="min-w-[3rem] font-medium text-muted-foreground text-sm">
|
||
{realtimeCanvasConfig.enabled ? "已开启" : "已关闭"}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>主服务供应商</Label>
|
||
<Select
|
||
onValueChange={(value: "bfl" | "replicate" | "fal") =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
providerType: value,
|
||
})
|
||
}
|
||
value={realtimeCanvasConfig.providerType}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="bfl">BFL / 兼容接口</SelectItem>
|
||
<SelectItem value="replicate">Replicate</SelectItem>
|
||
<SelectItem value="fal">
|
||
FAL(WebSocket 真流式实时为二阶段)
|
||
</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
{realtimeCanvasConfig.providerType === "replicate" ? (
|
||
<>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeReplicateApiToken">
|
||
Replicate API Token <span className="text-destructive">*</span>
|
||
</Label>
|
||
<Input
|
||
disabled={!realtimeCanvasConfig.enabled}
|
||
id="realtimeReplicateApiToken"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
replicateApiToken: e.target.value,
|
||
})
|
||
}
|
||
placeholder="r8_xxxxxxxxxxxxx"
|
||
type="password"
|
||
value={realtimeCanvasConfig.replicateApiToken}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeReplicateModel">
|
||
Replicate 模型 <span className="text-destructive">*</span>
|
||
</Label>
|
||
<Input
|
||
disabled={!realtimeCanvasConfig.enabled}
|
||
id="realtimeReplicateModel"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
replicateModel: e.target.value,
|
||
})
|
||
}
|
||
placeholder="black-forest-labs/flux-2-klein-4b"
|
||
value={realtimeCanvasConfig.replicateModel}
|
||
/>
|
||
</div>
|
||
</>
|
||
) : realtimeCanvasConfig.providerType === "fal" ? (
|
||
<>
|
||
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-amber-800 text-sm">
|
||
当前 FAL 接入的是“供应商能力接入阶段”,会通过现有
|
||
`/api/realtime/generate` 服务端链路调用 FAL。 前端直连 FAL
|
||
WebSocket 的“真流式实时”属于第二阶段,当前尚未启用。
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeFalApiKey">
|
||
FAL API Key <span className="text-destructive">*</span>
|
||
</Label>
|
||
<Input
|
||
disabled={!realtimeCanvasConfig.enabled}
|
||
id="realtimeFalApiKey"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
falApiKey: e.target.value,
|
||
})
|
||
}
|
||
placeholder="FAL_KEY_ID:FAL_KEY_SECRET 或 token"
|
||
type="password"
|
||
value={realtimeCanvasConfig.falApiKey}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeFalModel">
|
||
FAL 模型 ID <span className="text-destructive">*</span>
|
||
</Label>
|
||
<Input
|
||
disabled={!realtimeCanvasConfig.enabled}
|
||
id="realtimeFalModel"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
falModel: e.target.value,
|
||
})
|
||
}
|
||
placeholder="fal-ai/your-image-edit-model"
|
||
value={realtimeCanvasConfig.falModel}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeFalBaseUrl">FAL Base URL</Label>
|
||
<Input
|
||
disabled={!realtimeCanvasConfig.enabled}
|
||
id="realtimeFalBaseUrl"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
falBaseUrl: e.target.value,
|
||
})
|
||
}
|
||
placeholder="https://fal.run"
|
||
value={realtimeCanvasConfig.falBaseUrl}
|
||
/>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeBflApiKey">
|
||
BFL API Key <span className="text-destructive">*</span>
|
||
</Label>
|
||
<Input
|
||
disabled={!realtimeCanvasConfig.enabled}
|
||
id="realtimeBflApiKey"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
bflApiKey: e.target.value,
|
||
})
|
||
}
|
||
placeholder="bfl_xxxxxxxxxxxxx"
|
||
type="password"
|
||
value={realtimeCanvasConfig.bflApiKey}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeBflBaseUrl">BFL API Base URL</Label>
|
||
<Input
|
||
disabled={!realtimeCanvasConfig.enabled}
|
||
id="realtimeBflBaseUrl"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
bflBaseUrl: e.target.value,
|
||
})
|
||
}
|
||
placeholder={DEFAULT_BFL_BASE_URL}
|
||
value={realtimeCanvasConfig.bflBaseUrl}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeBflModel">
|
||
FLUX.2 模型 ID <span className="text-destructive">*</span>
|
||
</Label>
|
||
<Input
|
||
disabled={!realtimeCanvasConfig.enabled}
|
||
id="realtimeBflModel"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
bflModel: e.target.value,
|
||
})
|
||
}
|
||
placeholder={DEFAULT_BFL_REALTIME_MODEL}
|
||
value={realtimeCanvasConfig.bflModel}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
<div className="space-y-4 rounded-lg border border-dashed p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div className="space-y-0.5">
|
||
<Label className="text-base">备用实时服务</Label>
|
||
<div className="text-muted-foreground text-sm">
|
||
主实时服务失败时自动切换到备用 BFL 服务。
|
||
</div>
|
||
</div>
|
||
<Switch
|
||
checked={realtimeCanvasConfig.fallbackEnabled}
|
||
disabled={!realtimeCanvasConfig.enabled}
|
||
onCheckedChange={(checked) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
fallbackEnabled: checked,
|
||
})
|
||
}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>备用供应商</Label>
|
||
<Select
|
||
onValueChange={(value: "bfl" | "replicate" | "fal") =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
fallbackProviderType: value,
|
||
})
|
||
}
|
||
value={realtimeCanvasConfig.fallbackProviderType}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="bfl">BFL / 兼容接口</SelectItem>
|
||
<SelectItem value="replicate">Replicate</SelectItem>
|
||
<SelectItem value="fal">
|
||
FAL(WebSocket 真流式实时为二阶段)
|
||
</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
{realtimeCanvasConfig.fallbackProviderType === "replicate" ? (
|
||
<>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeFallbackReplicateApiToken">
|
||
备用 Replicate API Token
|
||
</Label>
|
||
<Input
|
||
disabled={
|
||
!realtimeCanvasConfig.enabled ||
|
||
!realtimeCanvasConfig.fallbackEnabled
|
||
}
|
||
id="realtimeFallbackReplicateApiToken"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
fallbackReplicateApiToken: e.target.value,
|
||
})
|
||
}
|
||
placeholder="r8_xxxxxxxxxxxxx"
|
||
type="password"
|
||
value={realtimeCanvasConfig.fallbackReplicateApiToken}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeFallbackReplicateModel">
|
||
备用 Replicate 模型
|
||
</Label>
|
||
<Input
|
||
disabled={
|
||
!realtimeCanvasConfig.enabled ||
|
||
!realtimeCanvasConfig.fallbackEnabled
|
||
}
|
||
id="realtimeFallbackReplicateModel"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
fallbackReplicateModel: e.target.value,
|
||
})
|
||
}
|
||
placeholder="black-forest-labs/flux-2-klein-4b"
|
||
value={realtimeCanvasConfig.fallbackReplicateModel}
|
||
/>
|
||
</div>
|
||
</>
|
||
) : realtimeCanvasConfig.fallbackProviderType === "fal" ? (
|
||
<>
|
||
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-amber-800 text-sm">
|
||
备用 FAL 目前同样走现有服务端生成链路,不是前端直连 WebSocket
|
||
真流式实时。
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeFallbackFalApiKey">
|
||
备用 FAL API Key
|
||
</Label>
|
||
<Input
|
||
disabled={
|
||
!realtimeCanvasConfig.enabled ||
|
||
!realtimeCanvasConfig.fallbackEnabled
|
||
}
|
||
id="realtimeFallbackFalApiKey"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
fallbackFalApiKey: e.target.value,
|
||
})
|
||
}
|
||
placeholder="FAL_KEY_ID:FAL_KEY_SECRET 或 token"
|
||
type="password"
|
||
value={realtimeCanvasConfig.fallbackFalApiKey}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeFallbackFalModel">
|
||
备用 FAL 模型 ID
|
||
</Label>
|
||
<Input
|
||
disabled={
|
||
!realtimeCanvasConfig.enabled ||
|
||
!realtimeCanvasConfig.fallbackEnabled
|
||
}
|
||
id="realtimeFallbackFalModel"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
fallbackFalModel: e.target.value,
|
||
})
|
||
}
|
||
placeholder="fal-ai/your-image-edit-model"
|
||
value={realtimeCanvasConfig.fallbackFalModel}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeFallbackFalBaseUrl">
|
||
备用 FAL Base URL
|
||
</Label>
|
||
<Input
|
||
disabled={
|
||
!realtimeCanvasConfig.enabled ||
|
||
!realtimeCanvasConfig.fallbackEnabled
|
||
}
|
||
id="realtimeFallbackFalBaseUrl"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
fallbackFalBaseUrl: e.target.value,
|
||
})
|
||
}
|
||
placeholder="https://fal.run"
|
||
value={realtimeCanvasConfig.fallbackFalBaseUrl}
|
||
/>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeFallbackBflApiKey">
|
||
备用 BFL API Key
|
||
</Label>
|
||
<Input
|
||
disabled={
|
||
!realtimeCanvasConfig.enabled ||
|
||
!realtimeCanvasConfig.fallbackEnabled
|
||
}
|
||
id="realtimeFallbackBflApiKey"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
fallbackBflApiKey: e.target.value,
|
||
})
|
||
}
|
||
placeholder="bfl_xxxxxxxxxxxxx"
|
||
type="password"
|
||
value={realtimeCanvasConfig.fallbackBflApiKey}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeFallbackBflBaseUrl">
|
||
备用 BFL API Base URL
|
||
</Label>
|
||
<Input
|
||
disabled={
|
||
!realtimeCanvasConfig.enabled ||
|
||
!realtimeCanvasConfig.fallbackEnabled
|
||
}
|
||
id="realtimeFallbackBflBaseUrl"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
fallbackBflBaseUrl: e.target.value,
|
||
})
|
||
}
|
||
placeholder={DEFAULT_BFL_BASE_URL}
|
||
value={realtimeCanvasConfig.fallbackBflBaseUrl}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="realtimeFallbackBflModel">
|
||
备用 FLUX.2 模型 ID
|
||
</Label>
|
||
<Input
|
||
disabled={
|
||
!realtimeCanvasConfig.enabled ||
|
||
!realtimeCanvasConfig.fallbackEnabled
|
||
}
|
||
id="realtimeFallbackBflModel"
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
fallbackBflModel: e.target.value,
|
||
})
|
||
}
|
||
placeholder={DEFAULT_BFL_REALTIME_MODEL}
|
||
value={realtimeCanvasConfig.fallbackBflModel}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>实时渲染模式</Label>
|
||
<Select
|
||
onValueChange={(
|
||
value: "strict-structure" | "balanced" | "stylize"
|
||
) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
renderMode: value,
|
||
})
|
||
}
|
||
value={realtimeCanvasConfig.renderMode}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="strict-structure">结构优先</SelectItem>
|
||
<SelectItem value="balanced">平衡模式</SelectItem>
|
||
<SelectItem value="stylize">风格优先</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
<p className="text-muted-foreground text-xs">
|
||
推荐:BFL 实时草图模型使用 `flux-2-klein-4b`;FAL
|
||
需填写支持图像编辑输入的模型 ID。
|
||
</p>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>发送节流(ms)</Label>
|
||
<Input
|
||
max={260}
|
||
min={60}
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
throttleInterval: Number.parseInt(e.target.value, 10) || 100,
|
||
})
|
||
}
|
||
type="number"
|
||
value={realtimeCanvasConfig.throttleInterval}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>最大缓冲帧</Label>
|
||
<Input
|
||
max={6}
|
||
min={1}
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
maxBuffering: Number.parseInt(e.target.value, 10) || 2,
|
||
})
|
||
}
|
||
type="number"
|
||
value={realtimeCanvasConfig.maxBuffering}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>输入分辨率上限(px)</Label>
|
||
<Input
|
||
max={1536}
|
||
min={512}
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
maxInputResolution:
|
||
Number.parseInt(e.target.value, 10) || 1024,
|
||
})
|
||
}
|
||
type="number"
|
||
value={realtimeCanvasConfig.maxInputResolution}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>JPEG质量</Label>
|
||
<Input
|
||
max={0.82}
|
||
min={0.35}
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
jpegQuality: Number.parseFloat(e.target.value) || 0.55,
|
||
})
|
||
}
|
||
step={0.01}
|
||
type="number"
|
||
value={realtimeCanvasConfig.jpegQuality}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-3 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>预览分支(MP)</Label>
|
||
<Input
|
||
max={0.8}
|
||
min={0.2}
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
previewMegapixels: Number.parseFloat(e.target.value) || 0.4,
|
||
})
|
||
}
|
||
step={0.01}
|
||
type="number"
|
||
value={realtimeCanvasConfig.previewMegapixels}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>细节分支(MP)</Label>
|
||
<Input
|
||
max={1.2}
|
||
min={0.6}
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
detailMegapixels: Number.parseFloat(e.target.value) || 1,
|
||
})
|
||
}
|
||
step={0.01}
|
||
type="number"
|
||
value={realtimeCanvasConfig.detailMegapixels}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>返回JPEG质量</Label>
|
||
<Input
|
||
max={90}
|
||
min={35}
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
outputJpegQuality: Number.parseInt(e.target.value, 10) || 62,
|
||
})
|
||
}
|
||
step={1}
|
||
type="number"
|
||
value={realtimeCanvasConfig.outputJpegQuality}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>推理步数</Label>
|
||
<Input
|
||
max={6}
|
||
min={1}
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
steps: Number.parseInt(e.target.value, 10) || 3,
|
||
})
|
||
}
|
||
type="number"
|
||
value={realtimeCanvasConfig.steps}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>反馈强度</Label>
|
||
<Input
|
||
max={1}
|
||
min={0.7}
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
feedback: Number.parseFloat(e.target.value) || 0.95,
|
||
})
|
||
}
|
||
step={0.01}
|
||
type="number"
|
||
value={realtimeCanvasConfig.feedback}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>schedule_mu</Label>
|
||
<Input
|
||
max={3.5}
|
||
min={1.5}
|
||
onChange={(e) =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
scheduleMu: Number.parseFloat(e.target.value) || 2.3,
|
||
})
|
||
}
|
||
step={0.1}
|
||
type="number"
|
||
value={realtimeCanvasConfig.scheduleMu}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>输出尺寸</Label>
|
||
<Select
|
||
onValueChange={(value: "square" | "square_hd") =>
|
||
setRealtimeCanvasConfig({
|
||
...realtimeCanvasConfig,
|
||
imageSize: value,
|
||
})
|
||
}
|
||
value={realtimeCanvasConfig.imageSize}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="square">square</SelectItem>
|
||
<SelectItem value="square_hd">square_hd</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<Button onClick={handleSaveRealtimeCanvasConfig}>
|
||
<Save className="mr-2 h-4 w-4" /> 保存配置
|
||
</Button>
|
||
</LocalAiSectionShell>
|
||
|
||
<LocalAiSectionShell
|
||
description="生图任务直接发起,图片先显示,后台转存到存储桶。"
|
||
expanded={isLocalAiSectionExpanded("canvas-image-task-queue")}
|
||
id="local-ai-section-canvas-image-task-queue"
|
||
onToggle={() => toggleLocalAiSection("canvas-image-task-queue")}
|
||
summary={<LocalAiSectionSummary items={[{ label: "并发不限", tone: "enabled" }, { label: "即时发起", tone: "enabled" }]} />}
|
||
title={<span className="flex items-center gap-2"><ImageIcon className="h-5 w-5" />生图任务执行</span>}
|
||
>
|
||
<p className="text-muted-foreground text-sm">已取消模型及单用户并发限制,任务无需等待其他图片生成完成。任务状态、超时处理和失败重试继续保留。</p>
|
||
</LocalAiSectionShell>
|
||
|
||
{/* 批量处理配置 */}
|
||
<LocalAiSectionShell
|
||
description={
|
||
<>
|
||
配置批量处理功能的默认提示词和参数。批量处理可将产品图批量融合到多个场景中。
|
||
</>
|
||
}
|
||
expanded={isLocalAiSectionExpanded("batch-process")}
|
||
id="local-ai-section-batch-process"
|
||
onToggle={() => toggleLocalAiSection("batch-process")}
|
||
summary={
|
||
<LocalAiSectionSummary
|
||
items={[
|
||
{
|
||
label: sceneFusionConfig.enabled ? "功能启用" : "功能关闭",
|
||
tone: sceneFusionConfig.enabled ? "enabled" : "disabled",
|
||
},
|
||
{
|
||
label: sceneFusionConfig.preRmbgEnabled
|
||
? "预抠图开启"
|
||
: "预抠图关闭",
|
||
tone: sceneFusionConfig.preRmbgEnabled ? "enabled" : "neutral",
|
||
},
|
||
{
|
||
label: `最多 ${sceneFusionConfig.maxScenes} 场景`,
|
||
tone: "neutral",
|
||
},
|
||
]}
|
||
/>
|
||
}
|
||
title={
|
||
<span className="flex items-center gap-2">
|
||
<Sparkles className="h-5 w-5" />
|
||
批量处理配置
|
||
</span>
|
||
}
|
||
>
|
||
{/* 功能开关 */}
|
||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||
<div className="space-y-0.5">
|
||
<Label className="text-base">启用批量处理功能</Label>
|
||
<div className="text-muted-foreground text-sm">
|
||
开启后用户可在图片编辑器中使用批量处理功能
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Switch
|
||
checked={sceneFusionConfig.enabled}
|
||
onCheckedChange={(checked) =>
|
||
setSceneFusionConfig({
|
||
...sceneFusionConfig,
|
||
enabled: checked,
|
||
})
|
||
}
|
||
/>
|
||
<span className="min-w-[3rem] font-medium text-muted-foreground text-sm">
|
||
{sceneFusionConfig.enabled ? "已开启" : "已关闭"}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||
<div className="space-y-0.5">
|
||
<Label className="text-base">场景裂变前自动抠图</Label>
|
||
<div className="text-muted-foreground text-sm">
|
||
先对产品图做抠图预处理,再进行裂变融合;抠图失败将自动降级为原图继续生成
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Switch
|
||
checked={sceneFusionConfig.preRmbgEnabled}
|
||
disabled={!sceneFusionConfig.enabled}
|
||
onCheckedChange={(checked) =>
|
||
setSceneFusionConfig({
|
||
...sceneFusionConfig,
|
||
preRmbgEnabled: checked,
|
||
})
|
||
}
|
||
/>
|
||
<span className="min-w-[3rem] font-medium text-muted-foreground text-sm">
|
||
{sceneFusionConfig.preRmbgEnabled ? "已开启" : "已关闭"}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 默认提示词 */}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="sceneFusionPrompt">默认提示词 *</Label>
|
||
<Textarea
|
||
disabled={!sceneFusionConfig.enabled}
|
||
id="sceneFusionPrompt"
|
||
onChange={(e) =>
|
||
setSceneFusionConfig({
|
||
...sceneFusionConfig,
|
||
defaultPrompt: e.target.value,
|
||
})
|
||
}
|
||
placeholder="输入默认的融合提示词..."
|
||
rows={4}
|
||
value={sceneFusionConfig.defaultPrompt}
|
||
/>
|
||
<div className="text-muted-foreground text-xs">
|
||
该提示词将作为批量处理的基础指令,用户可以在此基础上添加修改要求
|
||
</div>
|
||
</div>
|
||
|
||
{/* 最大场景数量 */}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="maxScenes">最大场景数量</Label>
|
||
<Input
|
||
disabled={!sceneFusionConfig.enabled}
|
||
id="maxScenes"
|
||
max={10}
|
||
min={1}
|
||
onChange={(e) =>
|
||
setSceneFusionConfig({
|
||
...sceneFusionConfig,
|
||
maxScenes: Math.max(
|
||
1,
|
||
Math.min(10, Number.parseInt(e.target.value) || 10)
|
||
),
|
||
})
|
||
}
|
||
type="number"
|
||
value={sceneFusionConfig.maxScenes}
|
||
/>
|
||
<div className="text-muted-foreground text-xs">
|
||
用户单次裂变可上传的最大场景图数量(1-10张)
|
||
</div>
|
||
</div>
|
||
|
||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||
<div className="font-medium text-blue-900 text-sm">
|
||
生成模式:多样性模式(固定)
|
||
</div>
|
||
<div className="mt-1 text-blue-800 text-xs">
|
||
系统会在所有可用生图模型中轮换分配任务,并在单模型失败时自动迁移到其他模型。
|
||
</div>
|
||
</div>
|
||
|
||
{/* 生图模型选择 */}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="sceneFusionModel">首选生图模型(可选)</Label>
|
||
<Select
|
||
disabled={!sceneFusionConfig.enabled}
|
||
onValueChange={(value) =>
|
||
setSceneFusionConfig({
|
||
...sceneFusionConfig,
|
||
modelId: value === "auto" ? "" : value,
|
||
})
|
||
}
|
||
value={sceneFusionConfig.modelId || "auto"}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="选择首选模型(不选则随机轮换)" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="auto">不指定首选(完全随机轮换)</SelectItem>
|
||
{(modelsData?.models || [])
|
||
.filter((m: AdminAiModel) => {
|
||
const provider = (providersData?.providers || []).find(
|
||
(p: AdminAiProvider) => p.id === m.providerId
|
||
);
|
||
return Boolean(provider && m.supportsImageGen);
|
||
})
|
||
.map((m: AdminAiModel) => {
|
||
const provider = (providersData?.providers || []).find(
|
||
(p: AdminAiProvider) => p.id === m.providerId
|
||
);
|
||
return (
|
||
<SelectItem key={m.id} value={m.id}>
|
||
{m.displayName} ({provider?.name || provider?.type})
|
||
</SelectItem>
|
||
);
|
||
})}
|
||
{/* 如果没有找到生图模型,显示提示 */}
|
||
{(modelsData?.models || []).filter((m: AdminAiModel) => {
|
||
const provider = (providersData?.providers || []).find(
|
||
(p: AdminAiProvider) => p.id === m.providerId
|
||
);
|
||
return Boolean(provider && m.supportsImageGen);
|
||
}).length === 0 && (
|
||
<SelectItem disabled value="no-model">
|
||
暂无可用的生图模型,请先配置生图供应商
|
||
</SelectItem>
|
||
)}
|
||
</SelectContent>
|
||
</Select>
|
||
<div className="text-muted-foreground text-xs">
|
||
仅用于设置“优先尝试”的模型;实际生成会自动在多模型间轮换并故障迁移
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2 rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||
<div className="font-medium text-blue-900 text-sm">功能说明</div>
|
||
<div className="space-y-1 text-blue-800 text-xs">
|
||
<p>• 用户上传1张产品图和多张场景参考图</p>
|
||
<p>• 系统自动将产品图融合到每个场景中</p>
|
||
<p>• 固定采用多样性模式:多模型轮换 + 自动故障迁移</p>
|
||
<p>• 生成的图片会自动保存到用户相册</p>
|
||
</div>
|
||
</div>
|
||
|
||
<Button onClick={handleSaveSceneFusionConfig}>
|
||
<Save className="mr-2 h-4 w-4" /> 保存配置
|
||
</Button>
|
||
</LocalAiSectionShell>
|
||
</>
|
||
);
|
||
}
|