diff --git a/backend/internal/service/protocol_registry.go b/backend/internal/service/protocol_registry.go index d953d061d..a2818032c 100644 --- a/backend/internal/service/protocol_registry.go +++ b/backend/internal/service/protocol_registry.go @@ -70,11 +70,13 @@ func (s *Service) PluginProviderCatalog(scope, capability string, includeUnavail continue } item := PluginProviderCatalogItem{ID: provider.ID, Version: plugin.Manifest.Version, Name: provider.Label, Vendor: plugin.Manifest.Author, Categories: provider.Capabilities, Scopes: provider.Scopes, BaseURL: provider.BaseURL, Enabled: plugin.Status == "enabled", UnavailableReason: plugin.Error, Workflows: workflowsForProvider(plugin.Manifest.Contributes.Workflows, provider.ID)} - item.Create = operationSummary(provider.Create) - if provider.Poll != nil { - item.Poll = operationSummary(*provider.Poll) + item.Create, item.Poll, item.ContentType = operationSummary(provider.Create), operationSummaryPtr(provider.Poll), provider.Create.ContentType + // The registry metadata is the canonical provider projection. This keeps + // host-backed dispatch paths out of every user-facing catalog consumer. + if adapter, ok := canonicalProviderAdapter(s.protocolRegistry(), provider.ID); ok { + metadata := adapter.Metadata() + item.Create, item.Poll, item.ContentType = metadata.Create, metadata.Poll, metadata.ContentType } - item.ContentType = provider.Create.ContentType if includeUnavailable || item.Enabled { items = append(items, item) } @@ -83,6 +85,13 @@ func (s *Service) PluginProviderCatalog(scope, capability string, includeUnavail return items } +func canonicalProviderAdapter(registry *protocol.Registry, id string) (protocol.Adapter, bool) { + if adapter, ok := protocol.Builtins().Resolve(id); ok { + return adapter, true + } + return registry.Resolve(id) +} + func containsPluginSurface(items []protocol.Surface, want protocol.Surface) bool { for _, item := range items { if item == want { @@ -114,6 +123,13 @@ func operationSummary(operation protocol.ManifestOperation) string { return strings.ToUpper(operation.Method) + " " + path } +func operationSummaryPtr(operation *protocol.ManifestOperation) string { + if operation == nil { + return "" + } + return operationSummary(*operation) +} + func (s *Service) protocolRegistry() *protocol.Registry { if s.pluginRuntime != nil { if registry := s.pluginRuntime.registrySnapshot(); registry != nil { diff --git a/backend/internal/service/protocol_runtime_test.go b/backend/internal/service/protocol_runtime_test.go index 474fce80b..b5d6010fb 100644 --- a/backend/internal/service/protocol_runtime_test.go +++ b/backend/internal/service/protocol_runtime_test.go @@ -35,6 +35,24 @@ func TestPluginViewIncludesDocumentationForEveryBundledProtocol(t *testing.T) { } } +func TestBundledProviderCatalogExposesUpstreamOperation(t *testing.T) { + center, err := newPluginRuntime(t.TempDir()) + if err != nil { + t.Fatal(err) + } + catalog := (&Service{pluginRuntime: center}).PluginProviderCatalog(string(protocol.SurfaceAdminSystemChannel), string(protocol.CapabilityVideo), false) + for _, item := range catalog { + if item.ID != "xai-video" { + continue + } + if item.Create != "POST /v1/videos/generations" || strings.Contains(item.Create, "__host__") { + t.Fatalf("xAI catalog create operation = %q", item.Create) + } + return + } + t.Fatal("xAI bundled provider missing from administrator catalog") +} + func TestPluginRuntimeIsTheProtocolSourceOfTruth(t *testing.T) { dataDir := t.TempDir() center, err := newPluginRuntime(dataDir) diff --git a/web/src/components/canvas/canvas-config-composer.tsx b/web/src/components/canvas/canvas-config-composer.tsx index 194766a25..b3cd4cb77 100644 --- a/web/src/components/canvas/canvas-config-composer.tsx +++ b/web/src/components/canvas/canvas-config-composer.tsx @@ -161,13 +161,13 @@ export function CanvasConfigComposer({ value, inputs, skillReferences = [], gene return (
event.stopPropagation()} > -
+
{simpleMode ? "快速生成" : "组装提示词"}
{simpleMode ? "已连接素材会自动带入" : workflowVideoReferenceMode ? "已连接媒体会按工作流字段顺序自动带入" : "@ 引用已连接素材或已激活技能,发送前自动组装"}
@@ -178,7 +178,7 @@ export function CanvasConfigComposer({ value, inputs, skillReferences = [], gene
{generationMode === "video" && onMetadataChange && !simpleMode ? ( -
+
) : null} -
- {!value.trim() ?
输入提示词,按 @ 引用连接素材或技能
: null} +
+ {!value.trim() ?
输入提示词,按 @ 引用连接素材或技能
: null}
{ if (!composingRef.current) syncFromEditor(); diff --git a/web/src/components/canvas/canvas-config-node-panel.tsx b/web/src/components/canvas/canvas-config-node-panel.tsx index 1cba87dd1..6404d600d 100644 --- a/web/src/components/canvas/canvas-config-node-panel.tsx +++ b/web/src/components/canvas/canvas-config-node-panel.tsx @@ -1,21 +1,15 @@ import { useEffect, useState, type CSSProperties, type ReactNode } from "react"; -import { ChevronDown, Dice5, Image as ImageIcon, LoaderCircle, MessageSquare, Music2, Play, Settings2, Sparkles, Video, Workflow as WorkflowIcon } from "lucide-react"; +import { ChevronDown, Dice5, Image as ImageIcon, LoaderCircle, MessageSquare, Music2, Play, Sparkles, Video, Workflow as WorkflowIcon } from "lucide-react"; import { Button, Input, InputNumber, Segmented, Select, Slider, Switch, Tooltip } from "antd"; -import { ModelPicker } from "@/components/model-picker"; import { configuredModelMatchesCapability, defaultConfig, modelOptionName, normalizeRunningHubCapability, resolveModelChannel, useEffectiveConfig, type AiConfig, type RunningHubCapability, type RunningHubWorkflow, type RunningHubWorkflowKind } from "@/stores/use-config-store"; -import { CreditSymbol, requestCreditCost } from "@/constant/credits"; import { canvasThemes } from "@/lib/canvas-theme"; import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options"; import { defaultModelCapabilityConfig, modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue, workflowFieldChoiceValues, workflowFieldCurrentValue, workflowFieldKey, workflowFieldNumberBounds, workflowFieldRandomKey, workflowFieldSubmissionValue, workflowFieldValueError, workflowImageCapabilityConfig, workflowOutputSizeValue, workflowParameterFields, workflowVideoCapabilityConfig, workflowVideoFieldsFromJson, type WorkflowVideoFieldLike } from "@/lib/model-capabilities"; import { defaultImageParamsForModel, modelCompatibilityError, modelRequestOptions, resolveCompatibleModel, resolveModelGenerationDefaults, type ModelRequirements } from "@/lib/model-selection"; import { resolveCanvasWorkflowProvider } from "@/lib/canvas/canvas-workflow"; -import { navigateToSettings } from "@/lib/settings-navigation"; +import type { CanvasAudioSettingKey } from "./canvas-audio-settings-popover"; import { useThemeStore } from "@/stores/use-theme-store"; -import { useUserStore } from "@/stores/use-user-store"; -import { CanvasImageSettingsPopover } from "./canvas-image-settings-popover"; -import { CanvasAudioSettingsPopover, type CanvasAudioSettingKey } from "./canvas-audio-settings-popover"; -import { CanvasVideoSettingsPopover } from "./canvas-video-settings-popover"; import type { CanvasGenerationMode, CanvasNodeData, CanvasNodeMetadata, CanvasVideoEditOperation, CanvasWorkspaceMode } from "@/types/canvas"; type CanvasConfigNodePanelProps = { @@ -65,10 +59,10 @@ function runningHubWorkflowEntryKey(workflow: RunningHubWorkflow): string { export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigChange, onGenerate, onComposerToggle, workspaceMode = "professional" }: CanvasConfigNodePanelProps) { const globalConfig = useEffectiveConfig(); const theme = canvasThemes[useThemeStore((state) => state.theme)]; - const creditsEnabled = useUserStore((state) => state.features.creditsEnabled); - const mode = node.metadata?.generationMode || "image"; + const mode = node.metadata?.generationMode === "video" || node.metadata?.generationMode === "audio" ? node.metadata.generationMode : "image"; const simpleMode = workspaceMode === "simple"; - const workflowProvider = mode === "text" ? "model" : resolveCanvasWorkflowProvider(node.metadata); + const resolvedProvider = resolveCanvasWorkflowProvider(node.metadata); + const workflowProvider = resolvedProvider === "comfyui" ? "comfyui" : "runninghub"; const workflowCapability: RunningHubCapability = mode === "video" ? "video" : mode === "audio" ? "audio" : "image"; const requirements: ModelRequirements = { capability: mode, @@ -104,7 +98,6 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC : []; const dynamicWorkflowFields = workflowParameterFields(selectedWorkflowFields); useEffect(() => { - if (mode === "text") return; if (workflowProvider === "runninghub" && !node.metadata?.runningHubWorkflowId?.trim()) { const capability = normalizeRunningHubCapability(globalConfig.runningHub.capability); // 模式切换后优先按当前能力选择工作流,不能沿用全局默认的视频工作流, @@ -150,36 +143,27 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC .filter((item) => item.capability === workflowCapability || item.workflowId.trim() === node.metadata?.comfyBridgeWorkflowId?.trim()) .map((item) => ({ label: item.title || item.workflowId, value: item.workflowId, kind: "workflow" as const, title: `${item.title || item.workflowId} · ${capabilityLabel(item.capability)}` })); const comfyBridgeOptions: WorkflowSelectOption[] = comfyBridgeEntries.length ? [{ label: , options: comfyBridgeEntries }] : []; - const videoProfile = mode === "video" ? (workflowProvider === "model" ? modelCapabilityConfigFor(config, config.model).video! : undefined) : undefined; - const operationOptions = videoProfile ? videoOperationOptions.filter((item) => videoProfile.operations.includes(item.value) || item.value === "concat") : videoOperationOptions; - const count = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1))); - const textCountValue = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(node.metadata?.textCount) || 1)))); - const priceChannel = resolveModelChannel(config, config.model); - const credits = requestCreditCost({ channelMode: priceChannel.scope === "system" ? "remote" : "local", modelCosts: priceChannel.modelCosts, model: modelOptionName(config.model), count: mode === "image" ? count : 1, seconds: mode === "video" ? config.videoSeconds : 1 }); - const hasPrice = workflowProvider === "model" && creditsEnabled && credits !== null; - const chipStyle = { background: theme.node.fill, borderColor: theme.node.stroke, color: theme.node.text }; + const chipStyle = { background: theme.node.fill, color: theme.node.text }; const hasAnyInput = Boolean(inputSummary.textCount || inputSummary.imageCount || inputSummary.videoCount || inputSummary.audioCount || inputSummary.characterCount); const hasComposerContent = Boolean((node.metadata?.composerContent ?? node.metadata?.prompt ?? "").trim()); - const workflowParameterError = workflowProvider === "model" ? "" : firstWorkflowParameterError(dynamicWorkflowFields, node.metadata?.workflowParameters || {}); + const workflowParameterError = firstWorkflowParameterError(dynamicWorkflowFields, node.metadata?.workflowParameters || {}); const capabilityError = workflowParameterError || (workflowProvider === "runninghub" ? (!globalConfig.runningHub.enabled ? "请先在设置中启用 RunningHub" : !node.metadata?.runningHubWorkflowId ? `请选择${capabilityLabel(workflowCapability)}工作流或 App` : !selectedRunningHubWorkflow ? "当前画布引用的 RunningHub 条目已不存在,请重新选择" : selectedRunningHubCapability !== workflowCapability ? `当前条目用途为${capabilityLabel(selectedRunningHubCapability || "image")},请切换画布模式或重新选择条目` : undefined) : workflowProvider === "comfyui" ? (!globalConfig.comfyBridge.enabled ? "请先在设置中启用 ComfyUI Bridge" : !globalConfig.comfyBridge.bridgeId ? "请选择在线 Bridge" : !node.metadata?.comfyBridgeWorkflowId ? `请选择${capabilityLabel(workflowCapability)}工作流` : !selectedComfyBridgeWorkflow ? "当前画布引用的 ComfyUI 条目已不存在,请重新选择" : selectedComfyBridgeWorkflow.capability !== workflowCapability ? `当前条目用途为${capabilityLabel(selectedComfyBridgeWorkflow.capability)},请切换画布模式或重新选择条目` : undefined) - : modelCompatibilityError(config, config.model, requirements)); + : undefined); const canGenerate = (hasComposerContent || (mode === "audio" ? inputSummary.textCount > 0 : hasAnyInput)) && !capabilityError; return ( -
event.stopPropagation()}> -
-
{simpleMode ? "快速生成" : workflowProvider === "model" ? "生成配置" : "工作流生成"}
+
event.stopPropagation()}> +
+
{simpleMode ? "快速生成" : "工作流生成"}
{simpleMode ? 自动配置 :
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}> onConfigChange(node.id, workflowProvider === "model" - ? { generationMode: value as CanvasGenerationMode } - : { generationMode: value as CanvasGenerationMode, runningHubWorkflowId: undefined, runningHubWorkflowKind: undefined, comfyBridgeWorkflowId: undefined, workflowParameters: {} })} + onChange={(value) => onConfigChange(node.id, { generationMode: value as CanvasGenerationMode, runningHubWorkflowId: undefined, runningHubWorkflowKind: undefined, comfyBridgeWorkflowId: undefined, workflowParameters: {} })} options={[ { value: "image", @@ -190,15 +174,6 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC ), }, - { - value: "text", - label: ( - - - 文本 - - ), - }, { value: "video", label: ( @@ -222,70 +197,58 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC
}
-
+
{inputSummary.characterCount ? : null} -
+ + {mode === "video" && !simpleMode ? ( -
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}> - {workflowProvider === "model" ? ( - onDraftChange(event.target.value)} - onFocus={(event) => event.currentTarget.select()} - onBlur={onCommit} - onKeyDown={(event) => { - if (event.key === "Enter") event.currentTarget.blur(); - if (event.key === "Escape") onCancel(); - }} - aria-label="节点名称" - /> - ) : editable ? ( - - ) : ( - {node.title} - )} +
+ + {editing ? ( + onDraftChange(event.target.value)} + onFocus={(event) => event.currentTarget.select()} + onBlur={onCommit} + onKeyDown={(event) => { + if (event.key === "Enter") event.currentTarget.blur(); + if (event.key === "Escape") onCancel(); + }} + aria-label="节点名称" + /> + ) : editable ? ( + + ) : ( + {node.title} + )} +
+ {dimensionLabel ? {dimensionLabel} : null}
); } diff --git a/web/src/components/canvas/canvas-toolbar.tsx b/web/src/components/canvas/canvas-toolbar.tsx index a6ae3a36d..d2ba71204 100644 --- a/web/src/components/canvas/canvas-toolbar.tsx +++ b/web/src/components/canvas/canvas-toolbar.tsx @@ -244,7 +244,7 @@ function AddNodeMenu({ x, theme, commands }: { commands: CanvasCreateCommand[]; }) { return ( - + event.stopPropagation()}> diff --git a/web/src/constant/canvas.ts b/web/src/constant/canvas.ts index 299a0fa45..91793c88d 100644 --- a/web/src/constant/canvas.ts +++ b/web/src/constant/canvas.ts @@ -14,8 +14,8 @@ export const NODE_DEFAULT_SIZE = { [CanvasNodeType.Drawing]: { width: 440, height: 300, title: "绘图" }, [CanvasNodeType.Script]: { width: 920, height: 360, title: "分镜脚本" }, [CanvasNodeType.Skill]: { width: 360, height: 220, title: "技能" }, - // 配置节点同时承载模式、渠道、工作流和参数;预留稳定空间,避免视频工作流控件互相贴合。 - [CanvasNodeType.Config]: { width: 380, height: 340, title: "生成配置" }, + // 配置节点同时承载模式、渠道、工作流和参数;预留稳定空间,避免控件和错误状态互相挤压。 + [CanvasNodeType.Config]: { width: 480, height: 390, title: "生成配置" }, [CanvasNodeType.Video]: { width: 720, height: 405, title: "视频" }, [CanvasNodeType.Audio]: { width: 340, height: 120, title: "Audio" }, [CanvasNodeType.Frame]: { width: 760, height: 520, title: "未命名背板" }, diff --git a/web/src/lib/canvas/canvas-project-generation.ts b/web/src/lib/canvas/canvas-project-generation.ts index dccfd4442..0321e87c9 100644 --- a/web/src/lib/canvas/canvas-project-generation.ts +++ b/web/src/lib/canvas/canvas-project-generation.ts @@ -401,7 +401,9 @@ export function generationWorkflowMetadata(config: AiConfig): Pick { const mediaNode = ensureMediaNodeMinimumSize(node); const resizedNode = - mediaNode.type === CanvasNodeType.Config && mediaNode.height < configHeight - ? { ...mediaNode, height: configHeight } + mediaNode.type === CanvasNodeType.Config && (mediaNode.width < configWidth || mediaNode.height < configHeight) + ? { ...mediaNode, width: Math.max(mediaNode.width, configWidth), height: Math.max(mediaNode.height, configHeight) } : mediaNode.type === CanvasNodeType.Script && mediaNode.height < NODE_DEFAULT_SIZE[CanvasNodeType.Script].height ? { ...mediaNode, height: NODE_DEFAULT_SIZE[CanvasNodeType.Script].height } : mediaNode; diff --git a/web/src/lib/canvas/tool-registry/definitions/add-node-menu-tools.tsx b/web/src/lib/canvas/tool-registry/definitions/add-node-menu-tools.tsx index 2dbbb7919..88c31fdcc 100644 --- a/web/src/lib/canvas/tool-registry/definitions/add-node-menu-tools.tsx +++ b/web/src/lib/canvas/tool-registry/definitions/add-node-menu-tools.tsx @@ -1,24 +1,14 @@ -import { Clapperboard, Folder, FolderOpen, Image as ImageIcon, Layers3, Music2, Palette, PanelTop, Pencil, Type, UploadCloud, UserRound, Video, Workflow } from "lucide-react"; +import { Folder, FolderOpen, Layers3, Palette, UploadCloud, UserRound, Workflow } from "lucide-react"; import { getNodeIcon, getNodeLabel } from "@/lib/canvas/node-registry"; import { registerAddNodeMenuCommands, type AddNodeMenuCommand } from "@/lib/canvas/tool-registry"; -import { PortraitClearanceIcon } from "@/components/canvas/portrait-clearance/portrait-clearance-icon"; -import { PORTRAIT_CLEARANCE_NODE_TYPE, PORTRAIT_CLEARANCE_PLUGIN_ID } from "@/lib/portrait-clearance/contracts"; -import { CanvasNodeType, type CanvasNodeTypeId } from "@/types/canvas"; +import { CanvasNodeType } from "@/types/canvas"; /** 真正创建节点的命令,文案与图标统一取自节点注册表。 */ function nodeCommand(type: CanvasNodeType, rest: Omit): AddNodeMenuCommand { return { id: type, label: getNodeLabel(type), icon: getNodeIcon(type), section: "node", ...rest }; } -/** - * 扩展节点命令:落在独立的「展示与加工」分区,不挤占上方调好的四列创作网格。 - * 创建逻辑一律是 createNode(type),所以统一走 onAddExtensionNode 而不是各开一个 handler。 - */ -function extensionCommand(type: CanvasNodeTypeId, defaultOrder: number): AddNodeMenuCommand { - return { id: type, label: getNodeLabel(type), icon: getNodeIcon(type), section: "extension", defaultOrder, run: (ctx) => ctx.handlers.onAddExtensionNode(type) }; -} - export const addNodeMenuCommands: AddNodeMenuCommand[] = [ // 项目级动作不占用节点网格,保证 8 个节点稳定保持四列两行。 { id: "style", label: "项目画风", icon: , section: "project", defaultOrder: 10, applicable: (ctx) => !ctx.isProjectLinked, run: (ctx) => ctx.handlers.onChooseStyle() }, @@ -33,25 +23,8 @@ export const addNodeMenuCommands: AddNodeMenuCommand[] = [ // 导演台落在节点分区,但它开的是导演工作台、不是某种画布节点,故不走注册表。 { id: "director", label: "导演台", icon: , badge: "3D", section: "node", defaultOrder: 70, applicable: (ctx) => ctx.workspaceMode !== "simple", run: (ctx) => ctx.handlers.onOpenDirector() }, nodeCommand(CanvasNodeType.Audio, { defaultOrder: 80, applicable: (ctx) => ctx.workspaceMode !== "simple", run: (ctx) => ctx.handlers.onAddAudio() }), - // 展示与加工(扩展节点) - extensionCommand(CanvasNodeType.Markdown, 10), - extensionCommand(CanvasNodeType.Svg, 20), - extensionCommand(CanvasNodeType.Html, 30), - extensionCommand(CanvasNodeType.Panorama, 40), - extensionCommand(CanvasNodeType.Compare, 50), - extensionCommand(CanvasNodeType.Chart, 60), - extensionCommand(CanvasNodeType.ColorGrade, 70), // 云端和本地工作流共用独立配置节点,不进入基础模型节点的渠道选择。 { id: "workflow", label: "工作流", icon: , section: "workflow", defaultOrder: 10, applicable: (ctx) => ctx.workspaceMode !== "simple", run: (ctx) => ctx.handlers.onAddWorkflow() }, - { - id: PORTRAIT_CLEARANCE_NODE_TYPE, - label: getNodeLabel(PORTRAIT_CLEARANCE_NODE_TYPE), - icon: , - section: "extension", - defaultOrder: 80, - applicable: (ctx) => Boolean(ctx.enabledPluginIds?.has(PORTRAIT_CLEARANCE_PLUGIN_ID)), - run: (ctx) => ctx.handlers.onAddExtensionNode(PORTRAIT_CLEARANCE_NODE_TYPE), - }, // 导入资源 { id: "upload", label: "上传文件", icon: , section: "resource", defaultOrder: 10, run: (ctx) => ctx.handlers.onUpload() }, { id: "project-character", label: "添加角色卡", icon: , section: "resource", defaultOrder: 20, applicable: (ctx) => ctx.isProjectLinked, run: (ctx) => ctx.handlers.onOpenProjectCharacters() }, diff --git a/web/src/lib/canvas/tool-registry/tool-definition.ts b/web/src/lib/canvas/tool-registry/tool-definition.ts index 4c83a135e..ea9a0dab5 100644 --- a/web/src/lib/canvas/tool-registry/tool-definition.ts +++ b/web/src/lib/canvas/tool-registry/tool-definition.ts @@ -185,7 +185,7 @@ export type AddNodeMenuCommand = { icon: ReactNode; badge?: string; // extension:展示与加工类扩展节点。单独一区,避免挤散 node 区调好的四列网格。 - section: "node" | "extension" | "workflow" | "project" | "resource"; + section: "node" | "workflow" | "project" | "resource"; defaultOrder: number; applicable?: (ctx: AddNodeMenuContext) => boolean; run: (ctx: AddNodeMenuContext) => void; diff --git a/web/src/pages/admin/components/channel-model-manager.tsx b/web/src/pages/admin/components/channel-model-manager.tsx index d4db3c77f..965440246 100644 --- a/web/src/pages/admin/components/channel-model-manager.tsx +++ b/web/src/pages/admin/components/channel-model-manager.tsx @@ -510,7 +510,7 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel: ) : null} {modelCapability === "text" || modelCapability === "image" || modelCapability === "video" ? ( -
+
+
svg { - color: var(--admin-status-info); + color: var(--admin-text-primary); } .admin-shell .app-workspace-nav-link:focus-visible, @@ -10490,14 +10490,14 @@ html:not(.dark) .creation-media-preview-modal .ant-modal-container { background: .admin-shell .admin-sidebar { background: var(--admin-sidebar); border-radius: 0 !important; - border-right: 1px solid var(--admin-divider); + border-right: 0; box-shadow: none; } /* 管理后台侧栏不复用通用 Tailwind 宽度:通用 sidebar 规则在后面会覆盖 w-*,导致视觉收缩但布局仍占展开宽度。 */ .admin-shell .admin-sidebar { - width: 224px; - flex-basis: 224px; + width: var(--workspace-sidebar-width); + flex-basis: var(--workspace-sidebar-width); } .admin-shell .admin-sidebar.is-collapsed { @@ -10507,8 +10507,8 @@ html:not(.dark) .creation-media-preview-modal .ant-modal-container { background: .admin-shell .admin-sidebar-header { height: 68px; - border-bottom: 1px solid var(--admin-divider); - background: linear-gradient(160deg, color-mix(in srgb, var(--foreground) 3%, var(--admin-sidebar)), var(--admin-sidebar)); + border-bottom: 0; + background: var(--admin-sidebar); } .admin-shell .admin-sidebar-brand-mark { @@ -10528,7 +10528,7 @@ html:not(.dark) .creation-media-preview-modal .ant-modal-container { background: margin-top: 1px; color: var(--admin-text-muted); font-size: 10px; - letter-spacing: 0.06em; + letter-spacing: 0; line-height: 15px; } @@ -10566,8 +10566,8 @@ html:not(.dark) .creation-media-preview-modal .ant-modal-container { background: } .admin-shell .admin-sidebar-footer { - border-top: 1px solid var(--admin-divider); - background: linear-gradient(0deg, color-mix(in srgb, var(--foreground) 2%, var(--admin-sidebar)), var(--admin-sidebar)); + border-top: 0; + background: var(--admin-sidebar); padding: 9px 10px 10px; } @@ -10634,14 +10634,14 @@ html:not(.dark) .creation-media-preview-modal .ant-modal-container { background: .admin-page-header { min-height: 58px; - border-bottom: 1px solid var(--admin-divider); - padding-bottom: 14px; + border-bottom: 0; + padding-bottom: 10px; } .admin-page-title { color: var(--admin-text-primary); font-size: 20px; - letter-spacing: -0.02em; + letter-spacing: 0; line-height: 1.35; } @@ -11100,8 +11100,8 @@ html:not(.dark) .creation-media-preview-modal .ant-modal-container { background: .admin-settings-section { width: 100%; overflow: hidden; - border: 1px solid var(--admin-card-border); - border-radius: 12px; + border: 0; + border-radius: var(--r-lg); background: var(--admin-layer-1); box-shadow: var(--admin-card-shadow); } @@ -11111,16 +11111,16 @@ html:not(.dark) .creation-media-preview-modal .ant-modal-container { background: } .admin-settings-section-summary { - background: linear-gradient(145deg, color-mix(in srgb, var(--foreground) 4%, var(--admin-layer-1)), var(--admin-layer-1) 72%); + background: var(--admin-layer-1); } .admin-settings-section-content { - background: color-mix(in srgb, var(--admin-layer-1) 96%, var(--admin-layer-2)); + background: var(--admin-layer-1); } .admin-settings-section-footer { - border-top: 1px solid var(--admin-divider); - background: color-mix(in srgb, var(--admin-layer-2) 56%, var(--admin-layer-1)); + border-top: 0; + background: var(--admin-layer-2); } .admin-operation-card { @@ -11241,6 +11241,11 @@ html:not(.dark) .creation-media-preview-modal .ant-modal-container { background: padding: 20px 22px; } +.admin-model-editor-section-stacked { + grid-template-columns: minmax(0, 1fr); + gap: 14px; +} + .admin-model-editor-section-heading, .admin-model-editor-section-content { min-width: 0; @@ -11880,19 +11885,34 @@ html:not(.dark) .creation-media-preview-modal .ant-modal-container { background: .admin-page-frame-scrollable > :not(.admin-page-header) { width: 100%; - max-width: 1120px; - margin-inline: auto; + max-width: none; + margin-inline: 0; } .admin-settings-section:not(.is-stacked) .admin-settings-section-content { - border-left: 1px solid var(--admin-divider); + border-left: 0; } .admin-settings-section.is-stacked .admin-settings-section-summary { - border-bottom: 1px solid var(--admin-divider); + border-bottom: 0; } } +.canvas-config-prompt-button { + border: 0; + transition: background-color var(--motion-instant) ease-out, transform var(--motion-instant) ease-out; +} + +.canvas-config-prompt-button:hover { + filter: brightness(1.04); +} + +.canvas-config-prompt-button:focus-visible, +.canvas-config-composer-editor:focus-within { + outline: 1px solid color-mix(in srgb, currentColor 24%, transparent); + outline-offset: 1px; +} + .admin-shell .admin-monospace { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; font-size: 12px;