From 731aa9725a45a034123fafb1ed4ce8cda1be0d81 Mon Sep 17 00:00:00 2001 From: 134 Date: Tue, 11 Aug 2026 15:28:11 +0800 Subject: [PATCH] fix: preserve main behavior after Claw merge --- .../[id]/CanvasEditorLegacyPageClient.tsx | 16 +- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 2 +- components/canvas/CanvasChatPanel.tsx | 7 +- components/canvas/nodes/ImageNode.tsx | 214 +++++++----------- scripts/check-secrets.mjs | 2 +- scripts/check-test-hygiene.mjs | 6 +- tests/routes/canvas-overlay-density.test.ts | 6 +- tests/routes/clawd-request-lifecycle.test.ts | 4 +- .../routes/hicad-embedded-performance.test.ts | 8 +- tsconfig.json | 17 +- 10 files changed, 121 insertions(+), 161 deletions(-) diff --git a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx index ff4ec3457..8ebf45e4f 100644 --- a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx +++ b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx @@ -192,6 +192,11 @@ import { getAgentWorkflowPageFeatureSettings, sanitizeAgentWorkflowSelections, } from "@/lib/agents/workflow-ui-config"; +import { + loadCanvasChatPanelModule, + preloadCanvasChatPanel, +} from "@/components/canvas/canvas-chat-panel-loader"; +import { preloadCanvasChatData } from "@/lib/client/canvas-chat-preload"; import { cn, fetcher } from "@/lib/utils"; import { buildVideoWorkflowCanvasNode, @@ -313,7 +318,7 @@ const CanvasWorkspace = dynamic( const CanvasChatPanel = dynamic( () => - import("@/components/canvas/CanvasChatPanel").then( + loadCanvasChatPanelModule().then( (module) => module.CanvasChatPanel ), { @@ -3350,6 +3355,15 @@ export default function CanvasEditorLegacyPage({ }; }, [checkCanvasAuthSession]); + useEffect(() => { + preloadCanvasChatPanel(); + preloadCanvasChatData(id); + + return scheduleCanvasIdleTask(() => { + void import("@/components/canvas/CanvasFileListPanel"); + }, 700); + }, [id]); + useEffect(() => { const timeoutId = window.setTimeout(() => { setAreSecondaryPanelsReady(true); diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 7ff5e81ff..078326cc6 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -1238,7 +1238,7 @@ function DetailResultLocalEditDialog({ ); if (unchangedRegion) { setActiveRegionId(unchangedRegion.id); - toast.error(`区域 ${unchangedRegion.index} 的目标框大小变化不明显,请重新框选`); + toast.error(`区域 ${unchangedRegion.index} 的目标框大小和位置均未变化`); return; } } else { diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 156b734f3..f2fc5d00b 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -12534,9 +12534,10 @@ export function CanvasChatPanel({ void mutateClawdHistory(); hasHydratedHistoryRef.current = true; } catch (error) { - const wasCanceled = - canceledClawdMessageIdsRef.current.has(userMessageId) || - (error instanceof Error && error.name === "AbortError"); + const wasCanceled = shouldRestoreClawdComposerAfterFailure({ + explicitlyCanceled: + canceledClawdMessageIdsRef.current.has(userMessageId), + }); if (wasCanceled) { const shouldRestoreRequest = pendingClawdRequestRestoresRef.current.get(requestChatId) === diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 459256e3a..938a60fa8 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -97,6 +97,7 @@ import { createLocalEditRegionId, getLocalEditRegionsFromMetadata, getNormalizedRectOverlayStyle, + hasScaleAdjustTargetChanged, isHttpImageUrl, LOCAL_EDIT_COLORS, LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION, @@ -219,6 +220,9 @@ import { } from "@/lib/ai-wash/config"; import { normalizeOptionalHexColor } from "@/lib/canvas/text-edit-colors"; import { isCanvasImageUploadPending } from "@/lib/canvas/local-upload-state"; +import { createCanvasCapabilityClientCommand } from "@/lib/canvas/capabilities/client-command"; +import { getCanvasCapabilityByClientAction } from "@/lib/canvas/capabilities/registry"; +import type { CanvasCapabilityClientAction } from "@/lib/canvas/capabilities/types"; import { cn, fetcher } from "@/lib/utils"; import type { CanvasTextEditPayloadItem, @@ -2049,6 +2053,7 @@ export function ImageNode({ const selectNode = useCanvasStore((state) => state.selectNode); const projectId = useCanvasStore((state) => state.projectId); const getCanvasState = useCanvasStore((state) => state.getCanvasState); + const loadCanvas = useCanvasStore((state) => state.loadCanvas); const addPendingChatAttachment = useCanvasStore( (state) => state.addPendingChatAttachment ); @@ -4418,15 +4423,11 @@ export function ImageNode({ toast.error(`请为区域 ${missingTargetRegion.index} 框选青色目标框`); return; } - const unchangedRegion = validRegions.find((region) => { - const regionScalePercent = deriveCanvasScalePercentFromRects( - region.rect, - region.targetRect - ); - return Math.abs(regionScalePercent) < 1; - }); + const unchangedRegion = validRegions.find( + (region) => !hasScaleAdjustTargetChanged(region.rect, region.targetRect) + ); if (unchangedRegion) { - toast.error(`请调整区域 ${unchangedRegion.index} 的青色目标框大小`); + toast.error(`请调整区域 ${unchangedRegion.index} 的青色目标框大小或位置`); return; } } else { @@ -6658,8 +6659,58 @@ export function ImageNode({ updateNode, ]); - // 处理图片编辑操作 - 集成我们现有的 API - const handleAction = async (action: string) => { + const executeSharedToolbarCapability = async ( + action: "rmbg" | "watermark" | "enhance" | "extend" | "layer-split" + ) => { + if (!projectId) { + toast.error("画布还在加载,请稍后再试"); + return; + } + const capability = getCanvasCapabilityByClientAction(action); + if (!capability) { + toast.error("画布能力未注册"); + return; + } + + setIsCreatingDetachedResultNode(false); + setIsProcessing(true); + try { + const command = createCanvasCapabilityClientCommand({ + capabilityId: capability.id, + nodeIds: [node.id], + projectId, + source: "toolbar", + }); + const response = await fetch(`/api/canvas/${projectId}/capabilities`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(command), + }); + const payload = await response.json().catch(() => ({})); + if (!response.ok || payload?.success !== true) { + throw new Error(payload?.error || `${capability.title}失败`); + } + + const projectResult = await canvasService.getProject(projectId); + if (!projectResult.success || !projectResult.data?.canvasData) { + throw new Error(projectResult.error || "结果已生成,但画布同步失败"); + } + loadCanvas(projectResult.data.canvasData); + toast.success(`${capability.title}完成`); + } catch (error) { + const message = + error instanceof Error ? error.message : `${capability.title}失败`; + toast.error(message); + } finally { + setIsCreatingDetachedResultNode(false); + setIsProcessing(false); + } + }; + + // 处理图片编辑操作 + const handleAction = async ( + action: CanvasCapabilityClientAction | "scene-fusion" | "annotate" + ) => { if (!data.src) { toast.error("请先上传图片"); return; @@ -6762,34 +6813,7 @@ export function ImageNode({ toast.info("图层拆分功能未开启"); return; } - setIsCreatingDetachedResultNode(true); - setIsProcessing(true); - try { - const result = await imageGenerationService.splitImageLayers(data.src); - const layers = result.data?.layers || []; - - if (!result.success || layers.length === 0) { - toast.error(result.error || "图层拆分失败"); - return; - } - - const addedLayerCount = handleLayerSplitComplete( - layers.map((layer) => ({ - originalUrl: layer.originalUrl, - title: layer.title, - layerIndex: layer.layerIndex, - totalLayers: layer.totalLayers, - bounds: layer.bounds ?? null, - })) - ); - toast.success(`图层拆分完成,已新增 ${addedLayerCount} 个图层结果`); - } catch (error) { - console.error("图层拆分失败:", error); - toast.error("图层拆分失败"); - } finally { - setIsCreatingDetachedResultNode(false); - setIsProcessing(false); - } + await executeSharedToolbarCapability("layer-split"); return; } if (action === "box-cutout") { @@ -6851,102 +6875,13 @@ export function ImageNode({ return; } - const actionName = getActionName(action); - - setImageAdjustPanelOpen(false); - setCropPanelOpen(false); - setCropInteraction(null); - setFlipRotatePanelOpen(false); - setAnglePanelOpen(false); - setIsCreatingDetachedResultNode(true); - setIsProcessing(true); - // 先创建一个"处理中"状态的节点,显示在原图右侧 - const newNodeId = addNode({ - type: "image", - position: { - x: node.position.x + node.size.width + 20, - y: node.position.y, - }, - size: { width: node.size.width, height: node.size.height }, - data: { - src: "", // 暂时没有图片 - status: "generating", - prompt: `${actionName}处理中...`, - title: `${displayTitle}-${actionName}`, - originalSrc: data.src, - }, - connections: [node.id], - }); - - let result; - - try { - switch (action) { - case "rmbg": - result = await imageGenerationService.removeBackground(data.src); - break; - case "watermark": - result = await imageGenerationService.removeWatermark(data.src); - break; - case "enhance": - result = await imageGenerationService.enhanceImage(data.src); - break; - case "extend": - result = await imageGenerationService.extendImage(data.src); - break; - default: - // 删除创建的节点 - if (newNodeId) deleteNode(newNodeId); - return; - } - - if (result?.success && result.data?.url) { - // 更新节点为完成状态 - if (newNodeId) { - updateNode(newNodeId, { - data: { - src: result.data.url, - status: "completed", - prompt: `${actionName}结果`, - title: `${displayTitle}-${actionName}`, - originalSrc: data.src, - }, - }); - } - toast.success(`${actionName}处理完成`); - } else { - // 更新节点为失败状态 - if (newNodeId) { - updateNode(newNodeId, { - data: { - src: "", - status: "failed", - prompt: `${actionName}失败`, - title: `${displayTitle}-${actionName}-失败`, - originalSrc: data.src, - }, - }); - } - toast.error(result?.error || "处理失败"); - } - } catch (error) { - console.error(`${action} 处理失败:`, error); - // 更新节点为失败状态 - if (newNodeId) { - updateNode(newNodeId, { - data: { - src: "", - status: "failed", - prompt: `${actionName}失败`, - title: `${displayTitle}-${actionName}-失败`, - originalSrc: data.src, - }, - }); - } - toast.error("处理失败,请重试"); - } finally { - setIsCreatingDetachedResultNode(false); - setIsProcessing(false); + if ( + action === "rmbg" || + action === "watermark" || + action === "enhance" || + action === "extend" + ) { + await executeSharedToolbarCapability(action); } }; @@ -6990,7 +6925,16 @@ export function ImageNode({ const action = customEvent.detail?.action; if (!targetNodeId || !action || targetNodeId !== node.id) return; - void handleAction(action); + if (action === "scene-fusion" || action === "annotate") { + void handleAction(action); + return; + } + const capability = getCanvasCapabilityByClientAction( + action as CanvasCapabilityClientAction + ); + if (capability) { + void handleAction(capability.clientAction); + } }; window.addEventListener( diff --git a/scripts/check-secrets.mjs b/scripts/check-secrets.mjs index 2bdc3ae47..acfc2cae4 100644 --- a/scripts/check-secrets.mjs +++ b/scripts/check-secrets.mjs @@ -29,7 +29,7 @@ const skippedExtensions = new Set([ const skippedFiles = new Set(["pnpm-lock.yaml"]); const secretPatterns = [ - { pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{24,}/, reason: "OpenAI-style API key" }, + { pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{24,}/, reason: "OpenAI-style API key" }, { pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" }, { pattern: /AKIA[0-9A-Z]{16}/, reason: "AWS access key id" }, { pattern: /xox[baprs]-[A-Za-z0-9-]{24,}/, reason: "Slack token" }, diff --git a/scripts/check-test-hygiene.mjs b/scripts/check-test-hygiene.mjs index b2ac67809..1bd884fe6 100644 --- a/scripts/check-test-hygiene.mjs +++ b/scripts/check-test-hygiene.mjs @@ -26,7 +26,11 @@ expectIncludes(testHelpersScript, "playwright test --config=playwright.helpers.c expectIncludes(qualityScript, "pnpm test:helpers", "helper tests in quality gate"); expectIncludes(helpersConfig, 'testDir: "./tests"', "tests directory"); -expectIncludes(helpersConfig, "testMatch: /routes\\/canvas-chat-helpers\\.test\\.ts/", "single pure helper test target"); +expectIncludes( + helpersConfig, + "/routes\\/canvas-chat-helpers\\.test\\.ts/", + "canvas helper test target" +); expectIncludes(helpersConfig, 'reporter: process.env.CI ? "github" : "list"', "fast non-html reporter"); expectIncludes(helpersConfig, "timeout: 60 * 1000", "bounded helper test timeout"); diff --git a/tests/routes/canvas-overlay-density.test.ts b/tests/routes/canvas-overlay-density.test.ts index 6850083d6..fbfe1f453 100644 --- a/tests/routes/canvas-overlay-density.test.ts +++ b/tests/routes/canvas-overlay-density.test.ts @@ -11,8 +11,12 @@ test.describe("canvas overlay density", () => { const source = readSource("components/canvas/CanvasChatPanel.tsx"); expect(source).toContain( - "max-h-[360px] w-[292px] max-w-[calc(100vw-24px)]" + "max-h-[360px] max-w-[calc(100vw-24px)]" ); + expect(source).toMatch( + /style=\{\{ width: `\$\{modelSelectorWidth\}px` \}\}/ + ); + expect(source).toContain("DEFAULT_CANVAS_MODEL_SELECTOR_WIDTH = 360"); expect(source).toContain( "h-9 rounded-[var(--ui-radius-control)] px-2.5 text-[13px]" ); diff --git a/tests/routes/clawd-request-lifecycle.test.ts b/tests/routes/clawd-request-lifecycle.test.ts index e501f02ae..a8370ba74 100644 --- a/tests/routes/clawd-request-lifecycle.test.ts +++ b/tests/routes/clawd-request-lifecycle.test.ts @@ -37,7 +37,9 @@ test.describe("Claw request lifecycle", () => { expect(source).not.toContain('request.signal.addEventListener("abort"'); expect(source).not.toContain("cancelClawdRequest(cancelKey)"); - expect(source).toContain("registerActiveClawdRequest(cancelKey"); + expect(source).toMatch( + /registerActiveClawdRequest\(\s*(?:params\.)?cancelKey/ + ); }); test("does not classify every AbortError as an explicit stop action", () => { diff --git a/tests/routes/hicad-embedded-performance.test.ts b/tests/routes/hicad-embedded-performance.test.ts index 3f6ac4df0..acf1c80f3 100644 --- a/tests/routes/hicad-embedded-performance.test.ts +++ b/tests/routes/hicad-embedded-performance.test.ts @@ -25,11 +25,11 @@ test.describe("HiCAD embedded startup", () => { expect(vendorScript).toContain( 'const brandedFaviconUrl = "/api/favicon?surface=hicad"' ); - expect(vendorScript).toContain( - '` `' + expect(vendorScript).toMatch( + /` {2}`/ ); - expect(vendorScript).toContain( - '` `' + expect(vendorScript).toMatch( + /` {2}`/ ); expect(vendorScript).toMatch( /manifest\.icons = \[\s*\{\s*src: brandedFaviconUrl,\s*sizes: "any",/ diff --git a/tsconfig.json b/tsconfig.json index 1d64d579f..d3eb39861 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ESNext", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -23,9 +19,7 @@ } ], "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] }, "strictNullChecks": true }, @@ -45,10 +39,7 @@ ".next-build-hicad-20260728-140320/types/**/*.ts", ".next-build-hicad-20260728-140320/dev/types/**/*.ts", ".next-candidate-home-banner-20260804-bg/types/**/*.ts", - ".next-candidate-home-banner-20260804-bg/dev/types/**/*.ts", + ".next-candidate-home-banner-20260804-bg/dev/types/**/*.ts" ], - "exclude": [ - "node_modules", - "services/pascal-editor" - ] + "exclude": ["node_modules", "services/pascal-editor"] }