1302 lines
47 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
</>
);
}