From 3101f5966da9ca34291857aa6e8fefea2e2ab9f3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 23 Apr 2026 13:25:05 +0800 Subject: [PATCH] feat: enhance canvas agent UI customization --- .../settings/admin-settings-client.tsx | 445 ++++++++++- app/api/admin/settings/route.ts | 22 +- app/api/canvas/detail-plan/route.ts | 99 ++- app/api/canvas/detail-review/route.ts | 42 +- app/api/canvas/ui-screen/review/route.ts | 62 ++ app/api/site-settings/route.ts | 52 ++ .../[id]/CanvasEditorLegacyPageClient.tsx | 359 +++++++-- components/canvas/CanvasChatPanel.tsx | 418 ++++++++-- components/canvas/CanvasToolbarIcon.tsx | 44 + components/canvas/CanvasWorkspace.tsx | 81 +- components/canvas/RealtimeCanvasDialog.tsx | 530 ++++++++---- components/canvas/nodes/ImageNode.tsx | 697 ++++++++++------ components/canvas/nodes/TextNode.tsx | 23 +- components/canvas/nodes/UiScreenNode.tsx | 409 ++++++++++ components/canvas/nodes/VideoNode.tsx | 94 ++- components/canvas/nodes/WorkflowNode.tsx | 26 +- components/canvas/ui/UiScreenRenderer.tsx | 362 +++++++++ lib/ai/context-memory/target-resolution.ts | 107 +-- lib/ai/detail-agent-kimi-profile.ts | 152 ++++ lib/ai/detail-agent-model-config.ts | 25 +- lib/ai/detail-agent-model-runtime.ts | 2 + lib/ai/dynamic-provider.ts | 13 + lib/canvas-toolbar-config.ts | 752 ++++++++++++++++++ lib/detail-ui-design-skill.ts | 231 ++++++ lib/detail-ui-design-tokens.ts | 6 +- lib/detail-ui-style-board.ts | 492 ++++++++++++ lib/ui-screen/detail-adapter.ts | 515 ++++++++++++ lib/ui-screen/patch.ts | 274 +++++++ lib/ui-screen/schema.ts | 308 +++++++ lib/ui-screen/validation.ts | 469 +++++++++++ types/canvas/index.ts | 31 + 31 files changed, 6449 insertions(+), 693 deletions(-) create mode 100644 app/api/canvas/ui-screen/review/route.ts create mode 100644 components/canvas/CanvasToolbarIcon.tsx create mode 100644 components/canvas/nodes/UiScreenNode.tsx create mode 100644 components/canvas/ui/UiScreenRenderer.tsx create mode 100644 lib/ai/detail-agent-kimi-profile.ts create mode 100644 lib/canvas-toolbar-config.ts create mode 100644 lib/detail-ui-design-skill.ts create mode 100644 lib/detail-ui-style-board.ts create mode 100644 lib/ui-screen/detail-adapter.ts create mode 100644 lib/ui-screen/patch.ts create mode 100644 lib/ui-screen/schema.ts create mode 100644 lib/ui-screen/validation.ts diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 803dae960..5f43152c5 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -30,6 +30,7 @@ import { } from "@/components/ui/select"; import { DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL, + DETAIL_AGENT_LOCAL_MOONSHOT_DISPLAY_NAME, DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID, DETAIL_AGENT_LOCAL_MOONSHOT_OPTION_ID, DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY, @@ -84,6 +85,16 @@ import { Settings, Wrench, Bot, + Box, + Brain, + CheckCircle2, + Copy, + Crosshair, + Download, + Eraser, + ExternalLink, + FolderOpen, + Globe2, HardDrive, Plus, Edit, @@ -92,29 +103,57 @@ import { Menu, Key, MessageSquare, + MousePointer, Bell, Image as ImageIcon, + ImagePlus, BarChart3, Search, + Scan, Calendar, TrendingUp, Users, Clock, Sparkles, RefreshCw, + ArrowLeft, ArrowUp, ArrowDown, Scissors, Expand, + Maximize2, + MessageSquarePlus, Move3D, + MoreHorizontal, + MoreVertical, + Hand, + GitBranch, + Grid3X3, Layers, + Paperclip, + PenTool, + Redo2, + Tag, + Type, + Undo2, Upload, Video, + Pause, + Play, + Square, + StickyNote, Loader2, Check, Paintbrush, Wand2, + Waypoints, + Volume2, + VolumeX, + X, + ZoomIn, + ZoomOut, Database, + type LucideIcon, } from "lucide-react"; import { Dialog, @@ -155,6 +194,14 @@ import { normalizeCanvasSkillTags, type CanvasSkillTag, } from "@/lib/canvas-skill-tags"; +import { + DEFAULT_CANVAS_TOOLBAR_ITEMS, + defaultCanvasToolbarConfig, + getDefaultCanvasToolbarItem, + normalizeCanvasToolbarConfig, + type CanvasToolbarConfig, + type CanvasToolbarItemKey, +} from "@/lib/canvas-toolbar-config"; import { defaultAgentWorkflowUiConfig, normalizeAgentWorkflowUiConfig, @@ -733,6 +780,121 @@ type RealtimeCanvasModeIconConfig = { type RealtimeCanvasModeIconKey = "realtimeIconUrl" | "enhanceIconUrl"; +const CANVAS_TOOLBAR_FALLBACK_ICONS: Record = { + selectTool: MousePointer, + panTool: Hand, + saveCanvas: Save, + cloudFiles: FolderOpen, + markerPanel: Tag, + workflowConnection: GitBranch, + styleMode: Wand2, + batchProcessing: Layers, + addImage: ImagePlus, + addImageInput: ImagePlus, + addText: Type, + addScriptNode: Type, + addVideoInput: Video, + workflowNodeLibrary: Grid3X3, + realtimeCanvas: PenTool, + enhanceCanvas: Sparkles, + model3d: Box, + exportVideoCanvas: Download, + importVideoCanvas: Upload, + newCanvas: Plus, + newVideoCanvas: Plus, + undo: Undo2, + redo: Redo2, + zoomOut: ZoomOut, + resetZoom: RefreshCw, + zoomIn: ZoomIn, + downloadFolder: Download, + imageNodeMore: MoreVertical, + imageAddToChat: MessageSquarePlus, + imagePickObject: Crosshair, + imageEnhance: Sparkles, + imageRemoveBackground: Scissors, + imageBoxCutout: Scan, + imageRemoveWatermark: Eraser, + imageResize: Maximize2, + imageTextEdit: Type, + imageMultiAngle: Box, + imageGenerate3dModel: Box, + imageGenerate3dWorld: Globe2, + imageLayerSplit: Layers, + imageExtend: Expand, + imagePromptRegenerate: RefreshCw, + imageNote: StickyNote, + imageRename: Edit, + imageDownload: Download, + imageDelete: Trash2, + imageCompare: Scan, + imageConfirmCutout: CheckCircle2, + imageCancelCutout: X, + textDelete: Trash2, + uiScreenReview: Wand2, + uiScreenDelete: Trash2, + uiScreenSaveText: Check, + videoUpload: Video, + videoPlay: Play, + videoPause: Pause, + videoMute: Volume2, + videoUnmute: VolumeX, + videoMore: MoreVertical, + videoDownload: Download, + videoDelete: Trash2, + workflowDelete: Trash2, + model3dOpen3d: ExternalLink, + model3dOpenWorld: Globe2, + model3dMore: MoreVertical, + model3dOpenMesh: Box, + model3dOpenImage: ImageIcon, + model3dDownload: Download, + model3dCopy: Copy, + model3dDelete: Trash2, + world3dViewSplat: Sparkles, + world3dViewImage: ImageIcon, + world3dViewMesh: Box, + world3dFirstPerson: Waypoints, + world3dHotspot: Crosshair, + world3dMore: MoreHorizontal, + world3dOpenMesh: Box, + world3dOpenImage: ImageIcon, + world3dDownload: Download, + world3dCopy: Copy, + world3dDelete: Trash2, + realtimeReturnCanvas: ArrowLeft, + realtimeModeRealtime: Paintbrush, + realtimeModeEnhance: Wand2, + realtimeLayerPanel: Layers, + realtimeLayerSplitImport: Layers, + realtimeAddImageLayer: Upload, + realtimeDownloadPreview: Download, + realtimeSelectTool: MousePointer, + realtimeBrushTool: Paintbrush, + realtimeEraserTool: Eraser, + realtimeShapeTool: Square, + realtimeSaveProject: Save, + realtimeProjectList: FolderOpen, + realtimeNewBlank: Plus, + realtimeGenerate: Sparkles, + realtimeSendToCanvas: ArrowUp, + chatModeAgentActive: Sparkles, + chatModePresetAgent: Bot, + chatModeAgentMenu: Sparkles, + chatModeDetailActive: Grid3X3, + chatModeDetailMenu: Grid3X3, + chatModeImageActive: ImagePlus, + chatModeImageMenu: ImagePlus, + chatModeVideoActive: Video, + chatModeVideoMenu: Video, + chatAttach: Paperclip, + chatDeepThinking: Brain, + chatModelSelector: Bot, + chatPoints: Sparkles, + chatSend: ArrowUp, + chatStartGeneration: Sparkles, +}; + type SiteSettings = { siteName: string; siteLogo: string | null; @@ -774,6 +936,7 @@ type SiteSettings = { agentWorkflowUiConfig: AgentWorkflowUiConfig; realtimeCanvasEnabled: boolean; realtimeCanvasModeIconConfig: RealtimeCanvasModeIconConfig; + canvasToolbarConfig: CanvasToolbarConfig; aiDetailGenerationEnabled: boolean; canvasDefaultModelId: string | null; canvasAutoModeDefaultModelId: string | null; @@ -1344,6 +1507,7 @@ export function AdminSettingsClient() { agentWorkflowUiConfig: defaultAgentWorkflowUiConfig, realtimeCanvasEnabled: true, realtimeCanvasModeIconConfig: defaultRealtimeCanvasModeIconConfig, + canvasToolbarConfig: defaultCanvasToolbarConfig, aiDetailGenerationEnabled: false, canvasDefaultModelId: null, canvasAutoModeDefaultModelId: null, @@ -1488,6 +1652,9 @@ export function AdminSettingsClient() { realtimeCanvasModeIconConfig: normalizeRealtimeCanvasModeIconConfig( (siteSettingsData as any).realtimeCanvasModeIconConfig ), + canvasToolbarConfig: normalizeCanvasToolbarConfig( + (siteSettingsData as any).canvasToolbarConfig + ), aiDetailGenerationEnabled: siteSettingsData.aiDetailGenerationEnabled ?? false, canvasDefaultModelId: siteSettingsData.canvasDefaultModelId || null, @@ -1827,7 +1994,7 @@ export function AdminSettingsClient() { baseUrl: DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL, apiKey: "", modelId: DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID, - displayName: "官方 Kimi K2.5", + displayName: DETAIL_AGENT_LOCAL_MOONSHOT_DISPLAY_NAME, isEnabled: true, }); const [thirdPartySqlServerConfig, setThirdPartySqlServerConfig] = useState({ @@ -2490,6 +2657,8 @@ export function AdminSettingsClient() { >(null); const [realtimeModeIconUploading, setRealtimeModeIconUploading] = useState(null); + const [canvasToolbarIconUploading, setCanvasToolbarIconUploading] = + useState(null); const { data: canvasSkillTagsData, mutate: mutateCanvasSkillTags } = useSWR<{ enabled?: boolean; title?: string; @@ -2670,6 +2839,65 @@ export function AdminSettingsClient() { } }; + const updateCanvasToolbarItem = ( + key: CanvasToolbarItemKey, + updates: Partial<{ label: string; iconUrl: string }> + ) => { + setSiteSettings((prev) => { + const normalized = normalizeCanvasToolbarConfig(prev.canvasToolbarConfig); + return { + ...prev, + canvasToolbarConfig: { + items: { + ...normalized.items, + [key]: { + ...normalized.items[key], + ...updates, + }, + }, + }, + }; + }); + }; + + const handleCanvasToolbarIconUpload = async ( + key: CanvasToolbarItemKey, + file: File + ) => { + const isSvg = + file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg"); + if (!isSvg) { + toast.error("请上传 SVG 图标文件"); + return; + } + + if (file.size > 512 * 1024) { + toast.error("SVG 图标不能超过 512KB"); + return; + } + + try { + setCanvasToolbarIconUploading(key); + const formData = new FormData(); + formData.append("file", file); + formData.append("purpose", "admin-svg-icon"); + + const response = await fetchUploadWithRetry(formData); + const result = await response.json(); + + if (!response.ok || !result?.url) { + throw new Error(result?.error || "上传失败"); + } + + updateCanvasToolbarItem(key, { iconUrl: result.url }); + toast.success("工具栏图标上传成功,请保存配置后生效"); + } catch (error: any) { + toast.error(error?.message || "工具栏图标上传失败"); + } finally { + setCanvasToolbarIconUploading(null); + } + }; + const handleAddCanvasSkillTag = () => { setCanvasSkillTags((prev) => [...prev, createEmptyCanvasSkillTag()]); }; @@ -5471,7 +5699,8 @@ export function AdminSettingsClient() { moonshotDetailAgentConfig.modelId.trim() || DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID; const trimmedDisplayName = - moonshotDetailAgentConfig.displayName.trim() || "官方 Kimi K2.5"; + moonshotDetailAgentConfig.displayName.trim() || + DETAIL_AGENT_LOCAL_MOONSHOT_DISPLAY_NAME; const trimmedProviderName = moonshotDetailAgentConfig.providerName.trim() || "Moonshot AI (Official)"; @@ -5484,7 +5713,7 @@ export function AdminSettingsClient() { } if (!trimmedModelId) { - toast.error("请填写官方 Kimi K2.5 的 API 模型 ID"); + toast.error("请填写官方 Kimi K2.6 的 API 模型 ID"); return; } @@ -5544,7 +5773,7 @@ export function AdminSettingsClient() { mutateMoonshotDetailAgentSetting(); mutateSiteSettings(); - toast.success("Moonshot 官方 Kimi K2.5 已绑定为 AI详情 Agent 专用模型"); + toast.success("Moonshot 官方 Kimi K2.6 已绑定为 AI详情 Agent 专用模型"); } catch (error) { console.error("[Moonshot Detail Agent Config] Error:", error); toast.error( @@ -10490,6 +10719,200 @@ export function AdminSettingsClient() { )} +
+
+
+
+ 无限画布工具栏名称与 SVG 图标 +
+
+ 统一管理画布底部主工具栏、画布快捷工具条、节点工具栏、实时画布工具栏、对话框底部工具栏的前端显示名称和按钮图标。上传 SVG 后会按按钮状态自动切换黑白色。 +
+
+ +
+ {Array.from( + new Set( + DEFAULT_CANVAS_TOOLBAR_ITEMS.map( + (item) => item.group + ) + ) + ).map((group) => { + const normalizedToolbarConfig = + normalizeCanvasToolbarConfig( + siteSettings.canvasToolbarConfig + ); + const groupItems = DEFAULT_CANVAS_TOOLBAR_ITEMS.filter( + (item) => item.group === group + ); + + return ( +
+
+ {group} +
+
+ {groupItems.map((item) => { + const itemConfig = + normalizedToolbarConfig.items[item.key]; + const defaultItem = + getDefaultCanvasToolbarItem(item.key); + const FallbackIcon = + CANVAS_TOOLBAR_FALLBACK_ICONS[item.key]; + const uploadId = `canvas-toolbar-icon-${item.key}`; + const isUploading = + canvasToolbarIconUploading === item.key; + + return ( +
+
+
+
+ {itemConfig.iconUrl ? ( + {`${itemConfig.label}图标预览`} + ) : ( + + )} +
+
+
+ {itemConfig.label} +
+
+ {item.description} +
+
+
+ +
+
+ + + updateCanvasToolbarItem(item.key, { + label: event.target.value.slice( + 0, + 40 + ), + }) + } + placeholder={defaultItem.label} + value={itemConfig.label} + /> +
+
+ + updateCanvasToolbarItem(item.key, { + iconUrl: event.target.value, + }) + } + placeholder="SVG 图标 URL(可选)" + value={itemConfig.iconUrl} + /> + + {itemConfig.iconUrl ? ( + + ) : null} + { + const file = + event.target.files?.[0]; + if (file) { + await handleCanvasToolbarIconUpload( + item.key, + file + ); + } + event.currentTarget.value = ""; + }} + /> +
+
+ ); + })} +
+
+ ); + })} +
+
@@ -14767,10 +15190,10 @@ export function AdminSettingsClient() {
- Moonshot 官方 Kimi K2.5 配置 + Moonshot 官方 Kimi K2.6 配置 - 为 AI详情生成 单独配置 Moonshot 官方 Kimi K2.5 + 为 AI详情生成 单独配置 Moonshot 官方 Kimi K2.6 模型。保存后会自动绑定到“AI详情生成 - Agent专用模型”。
@@ -14845,7 +15268,7 @@ export function AdminSettingsClient() {
- +
已按你提供的 Moonshot 官方文档默认填写 - `kimi-k2.5`;保存后系统会自动创建/更新对应模型记录。 + `kimi-k2.6`;保存后系统会自动创建/更新对应模型记录。
@@ -14874,7 +15297,7 @@ export function AdminSettingsClient() { displayName: e.target.value, })) } - placeholder="官方 Kimi K2.5" + placeholder={DETAIL_AGENT_LOCAL_MOONSHOT_DISPLAY_NAME} /> @@ -14917,7 +15340,7 @@ export function AdminSettingsClient() {
diff --git a/app/api/admin/settings/route.ts b/app/api/admin/settings/route.ts index aee21f78d..fc3b98536 100644 --- a/app/api/admin/settings/route.ts +++ b/app/api/admin/settings/route.ts @@ -13,7 +13,12 @@ import { normalizeRealtimeCanvasConfigForStorage, resolveRealtimeProviders, } from "@/lib/realtime/config"; -import { DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY } from "@/lib/ai/detail-agent-model-config"; +import { + DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL, + DETAIL_AGENT_LOCAL_MOONSHOT_DISPLAY_NAME, + DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID, + DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY, +} from "@/lib/ai/detail-agent-model-config"; import { normalizeProductPlanningApiConfig, PRODUCT_PLANNING_API_CONFIG_KEY, @@ -57,8 +62,7 @@ const moonshotDetailAgentConfigSchema = z.object({ isEnabled: z.boolean().optional(), }); -const MOONSHOT_OFFICIAL_BASE_URL = "https://api.moonshot.cn/v1"; -const MOONSHOT_KIMI_K25_MODEL_ID = "kimi-k2.5"; +const MOONSHOT_OFFICIAL_BASE_URL = DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL; function isMoonshotOfficialBaseUrl(value: string) { return String(value || "").trim().replace(/\/+$/, "") === MOONSHOT_OFFICIAL_BASE_URL; @@ -114,8 +118,16 @@ export async function POST(request: Request) { : {}; const config = moonshotDetailAgentConfigSchema.parse(value || {}); const normalizedBaseUrl = String(config.baseUrl || "").trim() || MOONSHOT_OFFICIAL_BASE_URL; - const normalizedModelId = String(config.modelId || "").trim() || MOONSHOT_KIMI_K25_MODEL_ID; - const normalizedDisplayName = String(config.displayName || "").trim() || "官方 Kimi K2.5"; + const rawModelId = String(config.modelId || "").trim(); + const rawDisplayName = String(config.displayName || "").trim(); + const normalizedModelId = + !rawModelId || rawModelId === "kimi-k2.5" + ? DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID + : rawModelId; + const normalizedDisplayName = + !rawDisplayName || rawDisplayName === "官方 Kimi K2.5" + ? DETAIL_AGENT_LOCAL_MOONSHOT_DISPLAY_NAME + : rawDisplayName; const normalizedProviderName = String(config.providerName || "").trim() || "Moonshot AI (Official)"; const normalizedApiKey = diff --git a/app/api/canvas/detail-plan/route.ts b/app/api/canvas/detail-plan/route.ts index 3cc6c77d9..81957434a 100644 --- a/app/api/canvas/detail-plan/route.ts +++ b/app/api/canvas/detail-plan/route.ts @@ -12,6 +12,10 @@ import { } from "@/lib/db/ai-model-flags"; import { getLanguageModelById } from "@/lib/ai/dynamic-provider"; import { resolveDetailAgentLocalMoonshotModel } from "@/lib/ai/detail-agent-model-runtime"; +import { + buildDetailKimiGenerateOptions, + type DetailKimiGenerateOptions, +} from "@/lib/ai/detail-agent-kimi-profile"; import { inlineVisionImages } from "@/lib/ai/vision-inline"; import { filterReachableRemoteUrls } from "@/lib/net/url-access"; import { @@ -23,8 +27,18 @@ import { normalizeDetailUiDslV2Document, type DetailUiDslV2Document, } from "@/lib/detail-ui-dsl-v2"; +import { + buildDetailUiDesignSkillFlowStep, + buildDetailUiDesignSkillPrompt, + buildDetailUiDesignSkillSpec, + summarizeDetailUiDesignSkill, + type DetailUiDesignSkillSpec, + type DetailUiDesignSkillSummary, +} from "@/lib/detail-ui-design-skill"; +import { buildDetailBrandStyleBoard } from "@/lib/detail-ui-style-board"; import { validateDetailUiProtocol } from "@/lib/detail-ui-validator"; import type { UiSurfaceSnapshot } from "@/lib/ui-surface-protocol"; +import type { UiScreenDocument } from "@/lib/ui-screen/schema"; export const runtime = "nodejs"; export const maxDuration = 180; @@ -227,6 +241,7 @@ type DetailPlanResponse = { version: string; steps: string[]; }; + designSkill?: DetailUiDesignSkillSummary; qualityChecks: { passed: boolean; issues: string[]; @@ -246,6 +261,7 @@ type DetailPlanResponse = { uiProtocol?: DetailUiProtocol; uiSurface?: UiSurfaceSnapshot; uiDslV2Screens?: DetailUiDslV2Document[]; + brandStyleBoard?: UiScreenDocument; model?: { id: string; displayName: string; @@ -800,6 +816,17 @@ function buildDetailSkillFlow(language: "zh" | "en") { }; } +function applyDesignSkillToFlow( + flow: DetailPlanResponse["skillFlow"], + designSkill: DetailUiDesignSkillSpec +): DetailPlanResponse["skillFlow"] { + return { + ...flow, + version: "1.2", + steps: [...flow.steps, buildDetailUiDesignSkillFlowStep(designSkill)], + }; +} + function clampCount(value: unknown): number { const num = Number(value); if (!Number.isFinite(num)) return 14; @@ -1187,6 +1214,7 @@ function buildFallbackPlan(params: { language: "zh" | "en"; brief: DetailBrief; coreFocus: DetailCoreFocusContext; + designSkill: DetailUiDesignSkillSpec; referenceImageNotes: string[]; hasModelImage: boolean; hasBundleImages: boolean; @@ -1194,7 +1222,8 @@ function buildFallbackPlan(params: { }): DetailPlanResponse { const { count, language } = params; const globalStyle = buildDefaultGlobalStyle(language); - const skillFlow = buildDetailSkillFlow(language); + const skillFlow = applyDesignSkillToFlow(buildDetailSkillFlow(language), params.designSkill); + const designSkillSummary = summarizeDetailUiDesignSkill(params.designSkill); const fallbackScreens = buildFallbackScreens({ ...params, globalStyle, @@ -1237,6 +1266,7 @@ function buildFallbackPlan(params: { }, globalStyle, skillFlow, + designSkill: designSkillSummary, qualityChecks: { passed: qualityChecks.passed && uiValidation.passed, issues: mergedIssues, @@ -1268,6 +1298,13 @@ function buildFallbackPlan(params: { }, }) : undefined, + brandStyleBoard: buildDetailBrandStyleBoard({ + language, + prompt: params.prompt, + brief: params.brief, + globalStyle, + designSkill: designSkillSummary, + }), }; } @@ -1575,9 +1612,12 @@ function evaluatePlanQuality(params: { async function runDualDirectorPlanPreflight(params: { model: any; isLocalMoonshot?: boolean; + kimiOptions?: DetailKimiGenerateOptions; + timeoutMs?: number; language: "zh" | "en"; brief: DetailBrief; coreFocus: DetailCoreFocusContext; + designSkillPrompt: string; globalStyle: DetailPlanResponse["globalStyle"]; screens: DetailPlanScreen[]; hasModelImage: boolean; @@ -1633,6 +1673,8 @@ async function runDualDirectorPlanPreflight(params: { params.hasBundleImages ? "Bundle refs provided: keep exactly one bundle showcase frame." : "No bundle refs: bundle showcase must be false in all frames.", + "Apply this UI design skill as a hard visual preflight contract:", + params.designSkillPrompt, "If not pass, rewrite globalStyle and all screens directly to a production-ready version.", "Return strict JSON only:", "{\"pass\":boolean,\"designDirectorScore\":0-100,\"marketingDirectorScore\":0-100,\"issues\":[\"...\"],\"revisedGlobalStyle\":{...},\"revisedScreens\":[...]}", @@ -1657,6 +1699,8 @@ async function runDualDirectorPlanPreflight(params: { params.hasBundleImages ? "已提供搭配图:必须且仅保留1屏搭配展示页。" : "未提供搭配图:所有分镜 bundleShowcase=false。", + "必须把以下 UI 设计技能作为硬性视觉预审契约:", + params.designSkillPrompt, "若不通过,直接重写 globalStyle 与全量 screens 到可生产状态。", "只返回严格JSON:", "{\"pass\":boolean,\"designDirectorScore\":0-100,\"marketingDirectorScore\":0-100,\"issues\":[\"...\"],\"revisedGlobalStyle\":{...},\"revisedScreens\":[...]}", @@ -1680,7 +1724,8 @@ async function runDualDirectorPlanPreflight(params: { : "你同时扮演高转化电商详情页的平面设计总监与营销总监。仅输出JSON。", temperature: params.isLocalMoonshot ? 1 : 0.1, maxTokens: 2000, - }, DETAIL_PLAN_PREFLIGHT_TIMEOUT_MS); + ...(params.kimiOptions || {}), + }, params.timeoutMs || DETAIL_PLAN_PREFLIGHT_TIMEOUT_MS); const parsed = extractJsonBlock<{ pass?: unknown; @@ -1795,6 +1840,17 @@ export async function POST(request: Request) { ); } const coreFocus = buildCoreFocusContext(brief, language); + const designSkill = buildDetailUiDesignSkillSpec({ + language, + generationMode, + screenCount: count, + hasReferenceImages: referenceImages.length > 0, + hasModelImage, + hasBundleImages, + productName: brief.productName, + brandTone: brief.brandTone, + }); + const designSkillPrompt = buildDetailUiDesignSkillPrompt(designSkill); const fallback = buildFallbackPlan({ prompt, count, @@ -1802,6 +1858,7 @@ export async function POST(request: Request) { language, brief, coreFocus, + designSkill, referenceImageNotes: effectiveReferenceImageNotes, hasModelImage, hasBundleImages, @@ -1863,6 +1920,20 @@ export async function POST(request: Request) { type: plannerConfig!.provider.type, baseUrl: plannerConfig!.provider.baseUrl, }; + const plannerKimiOptions = buildDetailKimiGenerateOptions({ + purpose: "plan", + generationMode, + modelId: selectedModelRef.modelId, + providerBaseUrl: selectedProviderRef.baseUrl, + isLocalMoonshot: Boolean(localDetailAgentResolved), + }); + const preflightKimiOptions = buildDetailKimiGenerateOptions({ + purpose: "preflight", + generationMode, + modelId: selectedModelRef.modelId, + providerBaseUrl: selectedProviderRef.baseUrl, + isLocalMoonshot: Boolean(localDetailAgentResolved), + }); console.log("[Canvas Detail Plan] Planner model selected:", { id: selectedModelRef.id, @@ -1887,6 +1958,12 @@ export async function POST(request: Request) { defaultParams: selectedModelRef.defaultParams, }), hasReferenceImages: referenceImages.length > 0, + kimiDetailProfile: plannerKimiOptions.headers + ? { + thinking: plannerKimiOptions.headers["x-detail-agent-kimi-thinking"], + maxTokens: plannerKimiOptions.maxTokens, + } + : null, }); const codingModelId = localDetailAgentResolved @@ -1910,6 +1987,7 @@ export async function POST(request: Request) { `输出语言:${language === "en" ? "English" : "中文"}`, `生成路线:${generationMode === "agent_layout" ? "AGENT排版(画面层+程序文字层)" : "传统生图(整页出图)"}`, "任务类型:电商详情页“AI详情生成”", + `详情 UI 设计技能:\n${designSkillPrompt}`, "必须遵循“详情升级文案方案”:定位卡 -> 卖点到买点转译 -> 黄金五屏 -> FABE闭环。", `核心卖点锚点(整套必须围绕):${coreFocus.anchor}`, `首屏标题参考(必须承接核心卖点):${coreFocus.firstScreenTitle}`, @@ -2015,12 +2093,14 @@ export async function POST(request: Request) { : `14) 未提供搭配产品图:所有分镜均设置 bundleShowcase=false。\n`) + (generationMode === "agent_layout" ? `15) 当前为 AGENT 详情模式:除 screens 外,还必须返回 dslV2Screens。每个 dslV2Screens[i] 都是可直接渲染的 detail-ui-dsl/v2 文档,必须包含 version、screenIndex、layoutVariant、region、nodes。nodes 仅允许这些 kind:panel、stack、row、badge、heading、body、feature-card、spec-table、compare、proof、cta。\n` + + `15.1) dslV2Screens 必须执行“详情 UI 设计技能”:region 覆盖面积控制在画布45%以内;避开商品主体/Logo/人脸/动作;不得卡片套卡片;每屏最多1个主面板+1个辅助网格/证据条;所有 id 语义化且文字短句化。\n` : "") + `输出格式:\n` + `{"summary":"简述","globalStyle":{"designLanguage":"一句话风格方向","typography":{"fontFamily":"字体家族","title":"主标题规范","subtitle":"副标题规范","body":"正文规范"},"layout":{"grid":"栅格","safeArea":"安全区","rhythm":"留白节奏"},"palette":"配色规范"},"screens":[{"index":1,"screenType":"B","objective":"本屏目标","title":"首屏示例:甩甩杆,自拍不手抖(6-20字)","subtitle":"一句副标题","copy":"正文文案","layoutNote":"排版说明","useModel":false,"modelReason":"是否需要人物及原因","bundleShowcase":false,"bundleReason":"是否为搭配展示页及原因","fabe":{"b":"利益","f":"特征","a":"优势","e":"证据"},"visualPrompt":"用于生图的完整提示词,必须包含790x1300与统一字体排版"}],"dslV2Screens":[{"version":"detail-ui-dsl/v2","screenIndex":1,"layoutVariant":"hero-left-panel","region":{"x":0.06,"y":0.05,"width":0.88,"height":0.46},"nodes":[{"kind":"panel","id":"hero-panel","variant":"glass","tone":"light","direction":"column","gap":18,"children":[{"kind":"badge","id":"screen-badge","icon":"sparkle","text":"核心升级","tone":"primary"},{"kind":"heading","id":"hero-title","text":"护腰椅,久坐不腰酸","level":1,"emphasis":"hero"},{"kind":"body","id":"hero-subtitle","text":"在家里加班,也能护腰/舒适","emphasis":"strong"}]}]}]}`, temperature: 0.25, ...(localDetailAgentResolved ? { temperature: 1 } : {}), maxTokens: 1800, + ...plannerKimiOptions, }); let text = ""; @@ -2028,7 +2108,7 @@ export async function POST(request: Request) { const runPlanner = async (visionInputs: Array<{ type: "image"; image: string }>) => { return await generateTextWithTimeout( buildPlannerRequest(visionInputs), - DETAIL_PLAN_MAIN_TIMEOUT_MS + plannerKimiOptions.headers ? 120_000 : DETAIL_PLAN_MAIN_TIMEOUT_MS ); }; @@ -2135,9 +2215,12 @@ export async function POST(request: Request) { const preflight = await runDualDirectorPlanPreflight({ model, isLocalMoonshot: Boolean(localDetailAgentResolved), + kimiOptions: preflightKimiOptions, + timeoutMs: preflightKimiOptions.headers ? 30_000 : DETAIL_PLAN_PREFLIGHT_TIMEOUT_MS, language, brief, coreFocus, + designSkillPrompt, globalStyle: globalStyleInitial, screens: coreFocusedScreens, hasModelImage, @@ -2228,8 +2311,10 @@ export async function POST(request: Request) { width: 790, height: 1300, }, + designStyle: item!.designStyle || globalStyle, })) : undefined; + const designSkillSummary = summarizeDetailUiDesignSkill(designSkill); const response: DetailPlanResponse = { summary, @@ -2241,6 +2326,7 @@ export async function POST(request: Request) { }, globalStyle, skillFlow: fallback.skillFlow, + designSkill: designSkillSummary, qualityChecks: qualityChecks.passed ? { passed: qualityChecks.passed && uiValidation.passed, @@ -2282,6 +2368,13 @@ export async function POST(request: Request) { }) : undefined, uiDslV2Screens, + brandStyleBoard: buildDetailBrandStyleBoard({ + language, + prompt, + brief, + globalStyle, + designSkill: designSkillSummary, + }), model: { id: selectedModelRef.id, displayName: selectedModelRef.displayName, diff --git a/app/api/canvas/detail-review/route.ts b/app/api/canvas/detail-review/route.ts index e28e32942..66c5738dc 100644 --- a/app/api/canvas/detail-review/route.ts +++ b/app/api/canvas/detail-review/route.ts @@ -12,6 +12,7 @@ import { } from "@/lib/db/ai-model-flags"; import { getLanguageModelById } from "@/lib/ai/dynamic-provider"; import { resolveDetailAgentLocalMoonshotModel } from "@/lib/ai/detail-agent-model-runtime"; +import { buildDetailKimiGenerateOptions } from "@/lib/ai/detail-agent-kimi-profile"; import { inlineVisionImages } from "@/lib/ai/vision-inline"; import { buildDetailUiProtocol, @@ -287,6 +288,13 @@ export async function POST(request: Request) { type: reviewerConfig!.provider.type, baseUrl: reviewerConfig!.provider.baseUrl, }; + const reviewKimiOptions = buildDetailKimiGenerateOptions({ + purpose: "review", + generationMode, + modelId: selectedModelRef.modelId, + providerBaseUrl: selectedProviderRef.baseUrl, + isLocalMoonshot: Boolean(localDetailAgentResolved), + }); const codingModelId = localDetailAgentResolved ? null : getAiModelAgentCodingModelId(reviewerConfig!.model); @@ -312,6 +320,12 @@ export async function POST(request: Request) { : fallbackAgentConfig?.model.id === reviewerConfig?.model.id ? "agent-default" : "vision-fallback", + kimiDetailProfile: reviewKimiOptions.headers + ? { + thinking: reviewKimiOptions.headers["x-detail-agent-kimi-thinking"], + maxTokens: reviewKimiOptions.maxTokens, + } + : null, }); if (!model) { return NextResponse.json({ @@ -376,19 +390,32 @@ export async function POST(request: Request) { const reviewImageCandidates = [imageUrl, ...referenceImages].slice(0, 6); let effectiveReviewImages = reviewImageCandidates; - if ( - !localDetailAgentResolved && - reviewerConfig && - !isVolcanoResponsesReviewer(reviewerConfig) - ) { - const { inlineDataUrls } = await inlineVisionImages(reviewImageCandidates, { + const mustInlineForKimi = Boolean(reviewKimiOptions.headers); + const shouldInlineReviewImages = + mustInlineForKimi || + (!localDetailAgentResolved && + reviewerConfig && + !isVolcanoResponsesReviewer(reviewerConfig)); + if (shouldInlineReviewImages) { + const { inlineDataUrls, skipped } = await inlineVisionImages(reviewImageCandidates, { maxImages: 6, maxSide: 900, timeoutMs: 12000, retries: 2, loggerPrefix: "[Canvas Detail Review]", }); - effectiveReviewImages = inlineDataUrls.length > 0 ? inlineDataUrls : reviewImageCandidates; + effectiveReviewImages = + inlineDataUrls.length > 0 + ? inlineDataUrls + : mustInlineForKimi + ? [] + : reviewImageCandidates; + if (mustInlineForKimi && skipped.length > 0 && inlineDataUrls.length === 0) { + console.warn( + "[Canvas Detail Review] Kimi K2.6 image inputs skipped after inline failed:", + skipped + ); + } } const content: Array<{ type: "text"; text: string } | { type: "image"; image: string }> = [ @@ -412,6 +439,7 @@ export async function POST(request: Request) { : "你同时是高转化电商详情页的平面设计总监与营销总监。按严格专业标准评估,仅输出JSON。", temperature: localDetailAgentResolved ? 1 : 0.1, maxTokens: 800, + ...reviewKimiOptions, } as any); text = String(result?.text || ""); } catch (error) { diff --git a/app/api/canvas/ui-screen/review/route.ts b/app/api/canvas/ui-screen/review/route.ts new file mode 100644 index 000000000..6d3a8207d --- /dev/null +++ b/app/api/canvas/ui-screen/review/route.ts @@ -0,0 +1,62 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/app/(auth)/auth"; +import { + applyUiScreenPatch, + buildAutoUiScreenPatch, + type UiScreenPatch, +} from "@/lib/ui-screen/patch"; +import { normalizeUiScreenDocument } from "@/lib/ui-screen/schema"; +import { validateUiScreenDocument } from "@/lib/ui-screen/validation"; + +export const runtime = "nodejs"; +export const maxDuration = 60; + +type UiScreenReviewRequest = { + document?: unknown; + applyAutoPatch?: boolean; +}; + +export async function POST(request: Request) { + try { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ success: false, error: "未登录" }, { status: 401 }); + } + + const body = (await request.json()) as UiScreenReviewRequest; + const document = normalizeUiScreenDocument(body.document); + if (!document) { + return NextResponse.json( + { success: false, error: "缺少有效的 ui-screen/v1 文档" }, + { status: 400 } + ); + } + + const validation = validateUiScreenDocument(document); + let patch: UiScreenPatch | undefined; + let patchedDocument = document; + + if (body.applyAutoPatch) { + patch = buildAutoUiScreenPatch({ document, validation }); + patchedDocument = applyUiScreenPatch(document, patch); + } + + return NextResponse.json({ + success: true, + data: { + validation, + patch, + patchedDocument: body.applyAutoPatch ? patchedDocument : undefined, + }, + }); + } catch (error) { + console.error("[UiScreenReview] Failed:", error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : "UI Screen 质检失败", + }, + { status: 500 } + ); + } +} diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts index 7fe2410b5..0cf8ca868 100644 --- a/app/api/site-settings/route.ts +++ b/app/api/site-settings/route.ts @@ -82,6 +82,11 @@ import { defaultAgentPresetLibraryConfig, normalizeAgentPresetLibraryConfig, } from "@/lib/agents/preset-library-config"; +import { + CANVAS_TOOLBAR_CONFIG_SETTING_KEY, + defaultCanvasToolbarConfig, + normalizeCanvasToolbarConfig, +} from "@/lib/canvas-toolbar-config"; import { defaultOssImageMigrationConfig, normalizeOssImageMigrationConfig, @@ -452,6 +457,7 @@ export async function GET() { dulupayConfigSetting, realtimeCanvasConfigSetting, realtimeCanvasModeIconConfigSetting, + canvasToolbarConfigSetting, layerSplitConfigSetting, canvasAgentModeEnabledSetting, videoCanvasEnabledSetting, @@ -562,6 +568,12 @@ export async function GET() { .where(eq(systemSettings.key, REALTIME_CANVAS_MODE_ICON_CONFIG_KEY)) .limit(1) .then(rows => rows[0]), + db + .select() + .from(systemSettings) + .where(eq(systemSettings.key, CANVAS_TOOLBAR_CONFIG_SETTING_KEY)) + .limit(1) + .then(rows => rows[0]), db .select() .from(systemSettings) @@ -697,6 +709,9 @@ export async function GET() { const realtimeCanvasModeIconConfig = normalizeRealtimeCanvasModeIconConfig( realtimeCanvasModeIconConfigSetting?.value ); + const canvasToolbarConfig = normalizeCanvasToolbarConfig( + canvasToolbarConfigSetting?.value + ); const layerSplitEnabled = normalizeLayerSplitEnabled( layerSplitConfigSetting?.value ); @@ -805,6 +820,7 @@ export async function GET() { defaultAgentPsdTemplateCategoryTagIndex, realtimeCanvasEnabled: true, realtimeCanvasModeIconConfig: defaultRealtimeCanvasModeIconConfig, + canvasToolbarConfig: defaultCanvasToolbarConfig, realtimeCanvasServiceEnabled: true, layerSplitEnabled, storageFailoverConfig, @@ -866,6 +882,7 @@ export async function GET() { dulupayConfig, realtimeCanvasServiceEnabled, realtimeCanvasModeIconConfig, + canvasToolbarConfig, layerSplitEnabled, canvasAgentModeEnabled, videoCanvasEnabled, @@ -1222,6 +1239,18 @@ const updateSchema = z.object({ enhanceLabel: z.string().trim().max(32).optional(), }) .optional(), + canvasToolbarConfig: z + .object({ + items: z + .record( + z.object({ + label: z.string().trim().max(40).optional(), + iconUrl: z.string().trim().max(2000).optional(), + }) + ) + .optional(), + }) + .optional(), aiDetailGenerationEnabled: z.boolean().optional(), canvasDefaultModelId: z.string().nullable().optional(), canvasAutoModeDefaultModelId: z.string().nullable().optional(), @@ -1349,6 +1378,7 @@ export async function PUT(request: NextRequest) { storageFailoverConfig, ossImageMigrationConfig, realtimeCanvasModeIconConfig, + canvasToolbarConfig, canvasAgentModeEnabled, videoCanvasEnabled, videoCanvasSidebarEnabled, @@ -2018,6 +2048,13 @@ export async function PUT(request: NextRequest) { ); } + if (canvasToolbarConfig !== undefined) { + await persistSystemSetting( + CANVAS_TOOLBAR_CONFIG_SETTING_KEY, + normalizeCanvasToolbarConfig(canvasToolbarConfig) + ); + } + await persistHomeAnimationConfig(); return NextResponse.json({ @@ -2122,6 +2159,10 @@ export async function PUT(request: NextRequest) { realtimeCanvasModeIconConfig !== undefined ? normalizeRealtimeCanvasModeIconConfig(realtimeCanvasModeIconConfig) : defaultRealtimeCanvasModeIconConfig, + canvasToolbarConfig: + canvasToolbarConfig !== undefined + ? normalizeCanvasToolbarConfig(canvasToolbarConfig) + : defaultCanvasToolbarConfig, canvasAgentModeEnabled: canvasAgentModeEnabled !== undefined ? normalizeCanvasAgentModeEnabled(canvasAgentModeEnabled) @@ -2625,6 +2666,13 @@ export async function PUT(request: NextRequest) { ); } + if (canvasToolbarConfig !== undefined) { + await persistSystemSetting( + CANVAS_TOOLBAR_CONFIG_SETTING_KEY, + normalizeCanvasToolbarConfig(canvasToolbarConfig) + ); + } + if (videoCanvasEnabled !== undefined) { const normalized = normalizeVideoCanvasEnabled(videoCanvasEnabled); await db @@ -2747,6 +2795,10 @@ export async function PUT(request: NextRequest) { realtimeCanvasModeIconConfig !== undefined ? normalizeRealtimeCanvasModeIconConfig(realtimeCanvasModeIconConfig) : defaultRealtimeCanvasModeIconConfig, + canvasToolbarConfig: + canvasToolbarConfig !== undefined + ? normalizeCanvasToolbarConfig(canvasToolbarConfig) + : defaultCanvasToolbarConfig, canvasAgentModeEnabled: canvasAgentModeEnabled !== undefined ? normalizeCanvasAgentModeEnabled(canvasAgentModeEnabled) diff --git a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx index ea9631f03..551e61a9b 100644 --- a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx +++ b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx @@ -88,6 +88,7 @@ import { } from "@/components/agents/AgentWorkflowPanelNew"; import { CanvasChatPanel } from "@/components/canvas/CanvasChatPanel"; import { CanvasFileListPanel } from "@/components/canvas/CanvasFileListPanel"; +import { CanvasToolbarIcon } from "@/components/canvas/CanvasToolbarIcon"; import { ObjectPickerOverlay } from "@/components/canvas/ObjectPickerOverlay"; import { SceneFusionDialog } from "@/components/scene-fusion-dialog"; import { Badge } from "@/components/ui/badge"; @@ -165,6 +166,14 @@ import { type VideoCanvasMarkerValue, type VideoCanvasSymbolMarkerId, } from "@/lib/canvas/video-canvas-markers"; +import { + getCanvasToolbarItem, + getDefaultCanvasToolbarItem, + normalizeCanvasToolbarConfig, + type CanvasToolbarConfig, + type CanvasToolbarItemConfig, + type CanvasToolbarItemKey, +} from "@/lib/canvas-toolbar-config"; import { buildAgentWorkflowComposerContext, getAgentWorkflowProgress, @@ -510,30 +519,21 @@ function normalizeRealtimeCanvasModeDisplayConfig( }; } -function RealtimeCanvasToolbarIcon({ - iconUrl, - fallback: FallbackIcon, - className, -}: { - iconUrl?: string; - fallback: LucideIcon; - className?: string; -}) { - if (iconUrl) { - return ( - - ); - } +function resolveCanvasToolbarItemWithFallback( + config: CanvasToolbarConfig, + key: CanvasToolbarItemKey, + fallback?: Partial +): CanvasToolbarItemConfig { + const item = getCanvasToolbarItem(config, key); + const defaultItem = getDefaultCanvasToolbarItem(key); - return ; + return { + label: + item.label !== defaultItem.label + ? item.label + : fallback?.label || item.label, + iconUrl: item.iconUrl || fallback?.iconUrl || "", + }; } const VIDEO_CANVAS_SIDEBAR_GROUPS: VideoCanvasSidebarGroup[] = [ @@ -1613,6 +1613,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { realtimeCanvasEnabled?: boolean; realtimeCanvasServiceEnabled?: boolean; realtimeCanvasModeIconConfig?: Partial; + canvasToolbarConfig?: CanvasToolbarConfig; agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig; agentWorkflowBrandModelConfig?: unknown; agentPsdTemplateCategoryTagIndex?: AgentPsdTemplateCategoryTagIndex; @@ -1671,6 +1672,10 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { ), [siteSettingsData?.realtimeCanvasModeIconConfig] ); + const canvasToolbarConfig = useMemo( + () => normalizeCanvasToolbarConfig(siteSettingsData?.canvasToolbarConfig), + [siteSettingsData?.canvasToolbarConfig] + ); const isAgentMode = isAgentWorkspaceEnabled && requestedAgentMode; const hasLoadedCurrentAgentProject = !requestedAgentMode || @@ -8734,6 +8739,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { > setIsPanelOpen(false)} @@ -9570,10 +9577,17 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { size="icon" variant={tool === "select" ? "secondary" : "ghost"} > - + - 选择 (V) + + {getCanvasToolbarItem(canvasToolbarConfig, "selectTool").label} (V) + @@ -9592,10 +9606,17 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { size="icon" variant={tool === "pan" ? "secondary" : "ghost"} > - + - 平移 (H) + + {getCanvasToolbarItem(canvasToolbarConfig, "panTool").label} (H) + {isVideoMode ? ( @@ -9611,11 +9632,17 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { {isSaving ? ( ) : ( - + )} - 保存到云端 + + {getCanvasToolbarItem(canvasToolbarConfig, "saveCanvas").label} + ) : null} @@ -9631,10 +9658,16 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { size="icon" variant="ghost" > - + - 云文件列表 + + {getCanvasToolbarItem(canvasToolbarConfig, "cloudFiles").label} + ) : null} @@ -9654,10 +9687,17 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { size="icon" variant="ghost" > - + - 标记 + + {getCanvasToolbarItem(canvasToolbarConfig, "markerPanel").label} + ) : null} @@ -9678,10 +9718,17 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { size="icon" variant={tool === "draw" ? "secondary" : "ghost"} > - + - 工作流连线 (L) + + {getCanvasToolbarItem(canvasToolbarConfig, "workflowConnection").label} (L) + ) : null} @@ -9702,10 +9749,17 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { size="icon" variant={isAgentMode ? "secondary" : "ghost"} > - + - 风格化模式 + + {getCanvasToolbarItem(canvasToolbarConfig, "styleMode").label} + ) : null} @@ -9722,10 +9776,16 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { size="icon" variant="ghost" > - + - 批量处理 + + {getCanvasToolbarItem(canvasToolbarConfig, "batchProcessing").label} + ) : null} @@ -9741,11 +9801,23 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { size="icon" variant="ghost" > - + - {isVideoMode ? "添加图片输入" : "添加图片"} + { + getCanvasToolbarItem( + canvasToolbarConfig, + isVideoMode ? "addImageInput" : "addImage" + ).label + } @@ -9761,11 +9833,23 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { size="icon" variant="ghost" > - + - {isVideoMode ? "添加脚本节点" : "添加文本"} + { + getCanvasToolbarItem( + canvasToolbarConfig, + isVideoMode ? "addScriptNode" : "addText" + ).label + } @@ -9778,10 +9862,16 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { size="icon" variant="ghost" > -