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"]
}