fix: preserve main behavior after Claw merge

This commit is contained in:
134 2026-08-11 15:28:11 +08:00
parent 1dd489100d
commit 731aa9725a
10 changed files with 121 additions and 161 deletions

View File

@ -192,6 +192,11 @@ import {
getAgentWorkflowPageFeatureSettings, getAgentWorkflowPageFeatureSettings,
sanitizeAgentWorkflowSelections, sanitizeAgentWorkflowSelections,
} from "@/lib/agents/workflow-ui-config"; } 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 { cn, fetcher } from "@/lib/utils";
import { import {
buildVideoWorkflowCanvasNode, buildVideoWorkflowCanvasNode,
@ -313,7 +318,7 @@ const CanvasWorkspace = dynamic(
const CanvasChatPanel = dynamic( const CanvasChatPanel = dynamic(
() => () =>
import("@/components/canvas/CanvasChatPanel").then( loadCanvasChatPanelModule().then(
(module) => module.CanvasChatPanel (module) => module.CanvasChatPanel
), ),
{ {
@ -3350,6 +3355,15 @@ export default function CanvasEditorLegacyPage({
}; };
}, [checkCanvasAuthSession]); }, [checkCanvasAuthSession]);
useEffect(() => {
preloadCanvasChatPanel();
preloadCanvasChatData(id);
return scheduleCanvasIdleTask(() => {
void import("@/components/canvas/CanvasFileListPanel");
}, 700);
}, [id]);
useEffect(() => { useEffect(() => {
const timeoutId = window.setTimeout(() => { const timeoutId = window.setTimeout(() => {
setAreSecondaryPanelsReady(true); setAreSecondaryPanelsReady(true);

View File

@ -1238,7 +1238,7 @@ function DetailResultLocalEditDialog({
); );
if (unchangedRegion) { if (unchangedRegion) {
setActiveRegionId(unchangedRegion.id); setActiveRegionId(unchangedRegion.id);
toast.error(`区域 ${unchangedRegion.index} 的目标框大小变化不明显,请重新框选`); toast.error(`区域 ${unchangedRegion.index} 的目标框大小和位置均未变化`);
return; return;
} }
} else { } else {

View File

@ -12534,9 +12534,10 @@ export function CanvasChatPanel({
void mutateClawdHistory(); void mutateClawdHistory();
hasHydratedHistoryRef.current = true; hasHydratedHistoryRef.current = true;
} catch (error) { } catch (error) {
const wasCanceled = const wasCanceled = shouldRestoreClawdComposerAfterFailure({
canceledClawdMessageIdsRef.current.has(userMessageId) || explicitlyCanceled:
(error instanceof Error && error.name === "AbortError"); canceledClawdMessageIdsRef.current.has(userMessageId),
});
if (wasCanceled) { if (wasCanceled) {
const shouldRestoreRequest = const shouldRestoreRequest =
pendingClawdRequestRestoresRef.current.get(requestChatId) === pendingClawdRequestRestoresRef.current.get(requestChatId) ===

View File

@ -97,6 +97,7 @@ import {
createLocalEditRegionId, createLocalEditRegionId,
getLocalEditRegionsFromMetadata, getLocalEditRegionsFromMetadata,
getNormalizedRectOverlayStyle, getNormalizedRectOverlayStyle,
hasScaleAdjustTargetChanged,
isHttpImageUrl, isHttpImageUrl,
LOCAL_EDIT_COLORS, LOCAL_EDIT_COLORS,
LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION, LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION,
@ -219,6 +220,9 @@ import {
} from "@/lib/ai-wash/config"; } from "@/lib/ai-wash/config";
import { normalizeOptionalHexColor } from "@/lib/canvas/text-edit-colors"; import { normalizeOptionalHexColor } from "@/lib/canvas/text-edit-colors";
import { isCanvasImageUploadPending } from "@/lib/canvas/local-upload-state"; 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 { cn, fetcher } from "@/lib/utils";
import type { import type {
CanvasTextEditPayloadItem, CanvasTextEditPayloadItem,
@ -2049,6 +2053,7 @@ export function ImageNode({
const selectNode = useCanvasStore((state) => state.selectNode); const selectNode = useCanvasStore((state) => state.selectNode);
const projectId = useCanvasStore((state) => state.projectId); const projectId = useCanvasStore((state) => state.projectId);
const getCanvasState = useCanvasStore((state) => state.getCanvasState); const getCanvasState = useCanvasStore((state) => state.getCanvasState);
const loadCanvas = useCanvasStore((state) => state.loadCanvas);
const addPendingChatAttachment = useCanvasStore( const addPendingChatAttachment = useCanvasStore(
(state) => state.addPendingChatAttachment (state) => state.addPendingChatAttachment
); );
@ -4418,15 +4423,11 @@ export function ImageNode({
toast.error(`请为区域 ${missingTargetRegion.index} 框选青色目标框`); toast.error(`请为区域 ${missingTargetRegion.index} 框选青色目标框`);
return; return;
} }
const unchangedRegion = validRegions.find((region) => { const unchangedRegion = validRegions.find(
const regionScalePercent = deriveCanvasScalePercentFromRects( (region) => !hasScaleAdjustTargetChanged(region.rect, region.targetRect)
region.rect,
region.targetRect
); );
return Math.abs(regionScalePercent) < 1;
});
if (unchangedRegion) { if (unchangedRegion) {
toast.error(`请调整区域 ${unchangedRegion.index} 的青色目标框大小`); toast.error(`请调整区域 ${unchangedRegion.index} 的青色目标框大小或位置`);
return; return;
} }
} else { } else {
@ -6658,8 +6659,58 @@ export function ImageNode({
updateNode, updateNode,
]); ]);
// 处理图片编辑操作 - 集成我们现有的 API const executeSharedToolbarCapability = async (
const handleAction = async (action: string) => { 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) { if (!data.src) {
toast.error("请先上传图片"); toast.error("请先上传图片");
return; return;
@ -6762,34 +6813,7 @@ export function ImageNode({
toast.info("图层拆分功能未开启"); toast.info("图层拆分功能未开启");
return; return;
} }
setIsCreatingDetachedResultNode(true); await executeSharedToolbarCapability("layer-split");
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);
}
return; return;
} }
if (action === "box-cutout") { if (action === "box-cutout") {
@ -6851,102 +6875,13 @@ export function ImageNode({
return; return;
} }
const actionName = getActionName(action); if (
action === "rmbg" ||
setImageAdjustPanelOpen(false); action === "watermark" ||
setCropPanelOpen(false); action === "enhance" ||
setCropInteraction(null); action === "extend"
setFlipRotatePanelOpen(false); ) {
setAnglePanelOpen(false); await executeSharedToolbarCapability(action);
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);
} }
}; };
@ -6990,7 +6925,16 @@ export function ImageNode({
const action = customEvent.detail?.action; const action = customEvent.detail?.action;
if (!targetNodeId || !action || targetNodeId !== node.id) return; if (!targetNodeId || !action || targetNodeId !== node.id) return;
if (action === "scene-fusion" || action === "annotate") {
void handleAction(action); void handleAction(action);
return;
}
const capability = getCanvasCapabilityByClientAction(
action as CanvasCapabilityClientAction
);
if (capability) {
void handleAction(capability.clientAction);
}
}; };
window.addEventListener( window.addEventListener(

View File

@ -29,7 +29,7 @@ const skippedExtensions = new Set([
const skippedFiles = new Set(["pnpm-lock.yaml"]); const skippedFiles = new Set(["pnpm-lock.yaml"]);
const secretPatterns = [ 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: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
{ pattern: /AKIA[0-9A-Z]{16}/, reason: "AWS access key id" }, { pattern: /AKIA[0-9A-Z]{16}/, reason: "AWS access key id" },
{ pattern: /xox[baprs]-[A-Za-z0-9-]{24,}/, reason: "Slack token" }, { pattern: /xox[baprs]-[A-Za-z0-9-]{24,}/, reason: "Slack token" },

View File

@ -26,7 +26,11 @@ expectIncludes(testHelpersScript, "playwright test --config=playwright.helpers.c
expectIncludes(qualityScript, "pnpm test:helpers", "helper tests in quality gate"); expectIncludes(qualityScript, "pnpm test:helpers", "helper tests in quality gate");
expectIncludes(helpersConfig, 'testDir: "./tests"', "tests directory"); 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, 'reporter: process.env.CI ? "github" : "list"', "fast non-html reporter");
expectIncludes(helpersConfig, "timeout: 60 * 1000", "bounded helper test timeout"); expectIncludes(helpersConfig, "timeout: 60 * 1000", "bounded helper test timeout");

View File

@ -11,8 +11,12 @@ test.describe("canvas overlay density", () => {
const source = readSource("components/canvas/CanvasChatPanel.tsx"); const source = readSource("components/canvas/CanvasChatPanel.tsx");
expect(source).toContain( 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( expect(source).toContain(
"h-9 rounded-[var(--ui-radius-control)] px-2.5 text-[13px]" "h-9 rounded-[var(--ui-radius-control)] px-2.5 text-[13px]"
); );

View File

@ -37,7 +37,9 @@ test.describe("Claw request lifecycle", () => {
expect(source).not.toContain('request.signal.addEventListener("abort"'); expect(source).not.toContain('request.signal.addEventListener("abort"');
expect(source).not.toContain("cancelClawdRequest(cancelKey)"); 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", () => { test("does not classify every AbortError as an explicit stop action", () => {

View File

@ -25,11 +25,11 @@ test.describe("HiCAD embedded startup", () => {
expect(vendorScript).toContain( expect(vendorScript).toContain(
'const brandedFaviconUrl = "/api/favicon?surface=hicad"' 'const brandedFaviconUrl = "/api/favicon?surface=hicad"'
); );
expect(vendorScript).toContain( expect(vendorScript).toMatch(
'` <link rel="icon" href="${brandedFaviconUrl}" />`' /` {2}<link rel="icon" href="\$\{brandedFaviconUrl\}" \/>`/
); );
expect(vendorScript).toContain( expect(vendorScript).toMatch(
'` <link rel="apple-touch-icon" href="${brandedFaviconUrl}" />`' /` {2}<link rel="apple-touch-icon" href="\$\{brandedFaviconUrl\}" \/>`/
); );
expect(vendorScript).toMatch( expect(vendorScript).toMatch(
/manifest\.icons = \[\s*\{\s*src: brandedFaviconUrl,\s*sizes: "any",/ /manifest\.icons = \[\s*\{\s*src: brandedFaviconUrl,\s*sizes: "any",/

View File

@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@ -23,9 +19,7 @@
} }
], ],
"paths": { "paths": {
"@/*": [ "@/*": ["./*"]
"./*"
]
}, },
"strictNullChecks": true "strictNullChecks": true
}, },
@ -45,10 +39,7 @@
".next-build-hicad-20260728-140320/types/**/*.ts", ".next-build-hicad-20260728-140320/types/**/*.ts",
".next-build-hicad-20260728-140320/dev/types/**/*.ts", ".next-build-hicad-20260728-140320/dev/types/**/*.ts",
".next-candidate-home-banner-20260804-bg/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": [ "exclude": ["node_modules", "services/pascal-editor"]
"node_modules",
"services/pascal-editor"
]
} }