完成素材无限画布最终审计与恢复安全收口
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Failing after 19s
Project CI / Native shell tests (pull_request) Successful in 12m10s

同步资源管理最新稳定的 manifest relay、Runner 重挂与 MCP 域名修复
统一网站与 Tauri 的共享画布 history、缩放恢复和宿主边界
移除生成账本中的临时上传凭证与 Provider 敏感字段
补齐事务快照发布前故障恢复和同幂等身份重放
更新最终 PRD、技术方案、decision-log 与 pitfalls
补齐前端、Tauri Rust、竞态、锁、恢复和原生壳门禁
This commit is contained in:
2026-08-06 10:12:09 +08:00
parent c776f9225b
commit 050d06ddb5
45 changed files with 1227 additions and 270 deletions
@@ -1,6 +1,5 @@
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
import type { CanvasTool } from '@genarrative/image-canvas-core';
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
export type CanvasViewportProps = HTMLAttributes<HTMLDivElement> & {
children: ReactNode;
@@ -1,9 +1,8 @@
import type { HTMLAttributes, ReactNode } from 'react';
import {
CANVAS_WORLD_SIZE,
type CanvasViewport,
} from '@genarrative/image-canvas-core';
import type { HTMLAttributes, ReactNode } from 'react';
export function CanvasWorld({
children,
@@ -1,6 +1,5 @@
import type { HTMLAttributes, KeyboardEventHandler, ReactNode } from 'react';
import type { CanvasLayer } from '@genarrative/image-canvas-core';
import type { HTMLAttributes, KeyboardEventHandler, ReactNode } from 'react';
export function LayerRenderer({
as = 'button',
+1 -2
View File
@@ -1,6 +1,5 @@
import type { PointerEventHandler, Ref } from 'react';
import type { StageMinimapModel } from '@genarrative/image-canvas-core';
import type { PointerEventHandler, Ref } from 'react';
export function Minimap({
className = '',
@@ -1,10 +1,9 @@
import type { ReactNode } from 'react';
import {
canvasDisplayScaleToViewportScale,
formatCanvasDisplayScalePercent,
type CanvasViewport,
formatCanvasDisplayScalePercent,
} from '@genarrative/image-canvas-core';
import type { ReactNode } from 'react';
export type CanvasZoomActions = {
displayPercent: string;
+1 -1
View File
@@ -4,8 +4,8 @@ export * from './CanvasWorld';
export * from './LayerRenderer';
export * from './Minimap';
export * from './SelectionOverlay';
export * from './ZoomControls';
export * from './useCanvasHistory';
export * from './useImageCanvasStageController';
export * from './useImageCanvasStageInteractions';
export * from './useImageCanvasViewportControls';
export * from './ZoomControls';
@@ -1,11 +1,10 @@
/* @vitest-environment jsdom */
import type { CanvasLayer } from '@genarrative/image-canvas-core';
import { act, renderHook } from '@testing-library/react';
import { createRef } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { CanvasLayer } from '@genarrative/image-canvas-core';
import { useImageCanvasViewportControls } from './useImageCanvasViewportControls';
function layer(id: string, x: number): CanvasLayer {
@@ -1,5 +1,3 @@
import { type RefObject, useCallback, useRef, useState } from 'react';
import type {
CanvasGenerationDialogState,
CanvasHistoryAction,
@@ -11,27 +9,28 @@ import type {
GenerateDialogState,
} from '@genarrative/image-canvas-core';
import {
MAX_HISTORY_STEPS,
canRestoreCanvasHistorySnapshotWithoutContentLoss,
isProtectedCanvasHistoryAction,
MAX_HISTORY_STEPS,
mergeCanvasHistorySnapshotForRestore,
} from '@genarrative/image-canvas-core';
import { type RefObject, useCallback, useRef, useState } from 'react';
type CanvasHistoryRefs = {
layersRef: RefObject<CanvasLayer[]>;
viewportRef: RefObject<CanvasViewport>;
generateDialogRef: RefObject<GenerateDialogState | null>;
inactiveGenerateDialogsRef: RefObject<CanvasGenerationDialogState[]>;
selectedLayerIdRef: RefObject<string | null>;
generateDialogRef?: RefObject<GenerateDialogState | null>;
inactiveGenerateDialogsRef?: RefObject<CanvasGenerationDialogState[]>;
selectedLayerIdRef?: RefObject<string | null>;
selectedLayerIdsRef: RefObject<string[]>;
};
type CanvasHistorySetters = {
setLayers: (layers: CanvasLayer[]) => void;
setViewport: (viewport: CanvasViewport) => void;
setGenerateDialog: (dialog: GenerateDialogState | null) => void;
setInactiveGenerateDialogs: (dialogs: CanvasGenerationDialogState[]) => void;
setSelectedLayerId: (layerId: string | null) => void;
setGenerateDialog?: (dialog: GenerateDialogState | null) => void;
setInactiveGenerateDialogs?: (dialogs: CanvasGenerationDialogState[]) => void;
setSelectedLayerId?: (layerId: string | null) => void;
setSelectedLayerIds: (layerIds: string[]) => void;
};
@@ -69,11 +68,13 @@ export function useCanvasHistory({
setters,
resetters,
effects,
allowContentRemovalOnRestore = false,
}: {
refs: CanvasHistoryRefs;
setters: CanvasHistorySetters;
resetters: CanvasHistoryResetters;
resetters?: CanvasHistoryResetters;
effects?: CanvasHistoryEffects;
allowContentRemovalOnRestore?: boolean;
}) {
const undoStackRef = useRef<CanvasHistoryEntry[]>([]);
const redoStackRef = useRef<CanvasHistoryEntry[]>([]);
@@ -84,13 +85,13 @@ export function useCanvasHistory({
(): CanvasHistorySnapshot => ({
layers: refs.layersRef.current.map((layer) => ({ ...layer })),
viewport: { ...refs.viewportRef.current },
generateDialog: refs.generateDialogRef.current
generateDialog: refs.generateDialogRef?.current
? cloneGenerateDialog(refs.generateDialogRef.current)
: null,
inactiveGenerateDialogs: refs.inactiveGenerateDialogsRef.current.map(
cloneCanvasGenerationDialog,
),
selectedLayerId: refs.selectedLayerIdRef.current,
inactiveGenerateDialogs: (
refs.inactiveGenerateDialogsRef?.current ?? []
).map(cloneCanvasGenerationDialog),
selectedLayerId: refs.selectedLayerIdRef?.current ?? null,
selectedLayerIds: [...refs.selectedLayerIdsRef.current],
}),
[refs],
@@ -112,22 +113,28 @@ export function useCanvasHistory({
? new Set(action.layerIds)
: undefined
: new Set(),
layerSizeLayerIds:
action?.type === 'resize-image'
? action.layerIds?.length
? new Set(action.layerIds)
: new Set(snapshot.layers.map((layer) => layer.id))
: new Set(),
});
setters.setLayers(mergedSnapshot.layers.map((layer) => ({ ...layer })));
setters.setViewport({ ...mergedSnapshot.viewport });
setters.setGenerateDialog(
setters.setGenerateDialog?.(
mergedSnapshot.generateDialog
? cloneGenerateDialog(mergedSnapshot.generateDialog)
: null,
);
setters.setInactiveGenerateDialogs(
setters.setInactiveGenerateDialogs?.(
mergedSnapshot.inactiveGenerateDialogs.map(cloneCanvasGenerationDialog),
);
setters.setSelectedLayerId(mergedSnapshot.selectedLayerId);
setters.setSelectedLayerId?.(mergedSnapshot.selectedLayerId);
setters.setSelectedLayerIds([...mergedSnapshot.selectedLayerIds]);
resetters.setHoveredLayerId(null);
resetters.setMetadataLayer(null);
resetters.resetCanvasInteractionState();
resetters?.setHoveredLayerId(null);
resetters?.setMetadataLayer(null);
resetters?.resetCanvasInteractionState();
if (action?.type === 'change-asset-kind') {
const currentLayerById = new Map(
@@ -215,11 +222,12 @@ export function useCanvasHistory({
}
const currentSnapshot = getCanvasHistorySnapshot();
if (
isProtectedCanvasHistoryAction(previousEntry.action) ||
!canRestoreCanvasHistorySnapshotWithoutContentLoss({
current: currentSnapshot,
target: previousEntry.snapshot,
})
!allowContentRemovalOnRestore &&
(isProtectedCanvasHistoryAction(previousEntry.action) ||
!canRestoreCanvasHistorySnapshotWithoutContentLoss({
current: currentSnapshot,
target: previousEntry.snapshot,
}))
) {
return {
status: 'blocked',
@@ -244,7 +252,11 @@ export function useCanvasHistory({
);
setHistoryVersion((version) => version + 1);
return { status: 'success', action: previousEntry.action };
}, [getCanvasHistorySnapshot, restoreCanvasHistorySnapshot]);
}, [
allowContentRemovalOnRestore,
getCanvasHistorySnapshot,
restoreCanvasHistorySnapshot,
]);
const redoCanvasChange = useCallback((): CanvasHistoryApplyResult => {
const nextEntry = redoStackRef.current.at(-1);
@@ -253,6 +265,7 @@ export function useCanvasHistory({
}
const currentSnapshot = getCanvasHistorySnapshot();
if (
!allowContentRemovalOnRestore &&
!canRestoreCanvasHistorySnapshotWithoutContentLoss({
current: currentSnapshot,
target: nextEntry.snapshot,
@@ -281,7 +294,17 @@ export function useCanvasHistory({
);
setHistoryVersion((version) => version + 1);
return { status: 'success', action: nextEntry.action };
}, [getCanvasHistorySnapshot, restoreCanvasHistorySnapshot]);
}, [
allowContentRemovalOnRestore,
getCanvasHistorySnapshot,
restoreCanvasHistorySnapshot,
]);
const resetCanvasHistory = useCallback(() => {
undoStackRef.current = [];
redoStackRef.current = [];
setHistoryVersion((version) => version + 1);
}, []);
return {
canUndo: undoStackRef.current.length > 0,
@@ -294,5 +317,6 @@ export function useCanvasHistory({
discardCanvasHistoryEntriesById,
undoCanvasChange,
redoCanvasChange,
resetCanvasHistory,
};
}
@@ -1,11 +1,3 @@
import {
type Dispatch,
type MouseEvent as ReactMouseEvent,
type SetStateAction,
useCallback,
useMemo,
} from 'react';
import type {
CanvasContextMenuState,
CanvasGenerationDialogState,
@@ -13,6 +5,13 @@ import type {
CanvasViewport,
ImageContextMenuState,
} from '@genarrative/image-canvas-core';
import {
type Dispatch,
type MouseEvent as ReactMouseEvent,
type SetStateAction,
useCallback,
useMemo,
} from 'react';
function getBrowserViewportBounds() {
return typeof window === 'undefined'
@@ -1,15 +1,3 @@
import {
type Dispatch,
type MouseEvent as ReactMouseEvent,
type PointerEvent as ReactPointerEvent,
type RefObject,
type SetStateAction,
useCallback,
useMemo,
useRef,
useState,
} from 'react';
import type {
CanvasGenerationDialogState,
CanvasHistoryAction,
@@ -44,6 +32,17 @@ import {
updateGenerateDialogForLayerPointerDown,
updateMinimapDragMovement,
} from '@genarrative/image-canvas-core';
import {
type Dispatch,
type MouseEvent as ReactMouseEvent,
type PointerEvent as ReactPointerEvent,
type RefObject,
type SetStateAction,
useCallback,
useMemo,
useRef,
useState,
} from 'react';
const CLICK_COLLAPSE_MOVEMENT_THRESHOLD_PX = 3;
@@ -1,22 +1,11 @@
import {
type Dispatch,
type RefObject,
type SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import type {
CanvasLayer,
CanvasViewport,
DragState,
} from '@genarrative/image-canvas-core';
import {
DEFAULT_CANVAS_SIZE,
createMinimapModel,
DEFAULT_CANVAS_SIZE,
fitViewportToLayers,
getCanvasDropPoint as resolveCanvasDropPoint,
getCanvasPointFromClient as resolveCanvasPointFromClient,
@@ -27,6 +16,16 @@ import {
scaleViewportFromScreenPoint,
zoomViewportFromWheel,
} from '@genarrative/image-canvas-core';
import {
type Dispatch,
type RefObject,
type SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
export const DEFAULT_IMAGE_CANVAS_VIEWPORT: CanvasViewport = {
x: -260,