完成素材无限画布最终审计与恢复安全收口
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,14 +1,15 @@
import type {
EditorAssetLibrarySnapshot,
EditorCharacterAnimationGenerationResult,
EditorProjectLayerSnapshot,
} from '../../services/image-editor/editorProjectClient';
import {
canvasDisplayScaleToViewportScale,
clamp,
resolveSnappedItemPosition,
viewportScaleToCanvasDisplayScale,
} from '@genarrative/image-canvas-core';
import type {
EditorAssetLibrarySnapshot,
EditorCharacterAnimationGenerationResult,
EditorProjectLayerSnapshot,
} from '../../services/image-editor/editorProjectClient';
import type {
CanvasAssetKind,
CanvasContextMenuState,
@@ -26,23 +27,23 @@ export {
CANVAS_DISPLAY_SCALE_BASE,
CANVAS_WORLD_ORIGIN,
CANVAS_WORLD_SIZE,
canvasDisplayScaleToViewportScale,
clamp,
DEFAULT_CANVAS_SIZE,
findNearestSnap,
FIT_VIEW_PADDING,
formatCanvasDisplayScalePercent,
formatPercent,
getLayerBounds,
MAX_HISTORY_STEPS,
MAX_SCALE,
MIN_SCALE,
MINIMAP_DRAG_SENSITIVITY,
MINIMAP_PADDING,
MINIMAP_SIZE,
resolveSnappedItemPosition,
SNAP_DISTRIBUTION_OVERLAP_TOLERANCE,
SNAP_THRESHOLD_SCREEN_PX,
canvasDisplayScaleToViewportScale,
clamp,
findNearestSnap,
formatCanvasDisplayScalePercent,
formatPercent,
getLayerBounds,
resolveSnappedItemPosition,
viewportScaleToCanvasDisplayScale,
} from '@genarrative/image-canvas-core';
@@ -1,5 +1,5 @@
import type { ReactNode } from 'react';
import { CanvasPortal } from '@genarrative/image-canvas-react';
import type { ReactNode } from 'react';
import { useAuthUi } from '../auth/AuthUiContext';
@@ -1,3 +1,13 @@
import type {
CanvasAssetKind,
CanvasGenerationInputs,
CanvasHistoryAction,
CanvasLayer as SharedCanvasLayer,
CanvasMediaType,
CanvasSourceType,
CanvasViewport,
} from '@genarrative/image-canvas-core';
import type {
EditorAssetSnapshot,
EditorCharacterAnimationFrameCount,
@@ -11,26 +21,6 @@ import type {
EditorVideoResolution,
EditorVideoSoundMode,
} from '../../services/image-editor/editorProjectClient';
import type {
CanvasAssetKind,
CanvasContextMenuState,
CanvasGenerationInputField,
CanvasGenerationInputReference,
CanvasGenerationInputs,
CanvasHistoryAction,
CanvasHistoryActionType,
CanvasHistoryApplyResult,
CanvasLayer as SharedCanvasLayer,
CanvasMarqueeState,
CanvasMediaType,
CanvasSnapItem,
CanvasSourceType,
CanvasTool,
CanvasViewport,
DragState,
SnapCandidate,
SnapGuide,
} from '@genarrative/image-canvas-core';
export type {
CanvasAssetKind,
@@ -199,6 +199,19 @@ describe('ImageCanvasHistoryModel', () => {
});
});
it('restores layer size only for layers targeted by a resize action', () => {
const targetLayer = createLayer({ width: 320, height: 240 });
const currentLayer = createLayer({ width: 640, height: 480 });
const merged = mergeCanvasHistorySnapshotForRestore({
current: createSnapshot([currentLayer]),
target: createSnapshot([targetLayer]),
layerSizeLayerIds: new Set([targetLayer.id]),
});
expect(merged.layers[0]).toMatchObject({ width: 320, height: 240 });
});
it('restores the complete target layer after that layer was deleted', () => {
const targetLayer = createLayer({
resourceId: 'deleted-resource',
@@ -1,3 +1,4 @@
import { Minimap, ZoomControls } from '@genarrative/image-canvas-react';
import {
ImagePlus,
Layers,
@@ -14,7 +15,6 @@ import type {
PointerEvent as ReactPointerEvent,
} from 'react';
import { useEffect, useRef } from 'react';
import { Minimap, ZoomControls } from '@genarrative/image-canvas-react';
import {
PlatformFloatingMenu,
@@ -1,3 +1,4 @@
import { CanvasViewport as SharedCanvasViewport } from '@genarrative/image-canvas-react';
import type {
CSSProperties,
DragEvent as ReactDragEvent,
@@ -9,7 +10,6 @@ import type {
import type { ExternalGenerationTaskRecord } from '@/packages/shared/src';
import { EditorAgentConversationPanelView } from '@/src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.tsx';
import { CanvasViewport as SharedCanvasViewport } from '@genarrative/image-canvas-react';
import { PlatformRuntimeStatusToast } from '../common/PlatformRuntimeStatusToast';
import { ImageCanvasBottomToolbarView } from './ImageCanvasBottomToolbarView';
@@ -1,3 +1,8 @@
import {
CanvasWorld as SharedCanvasWorld,
LayerRenderer,
SelectionOverlay,
} from '@genarrative/image-canvas-react';
import {
AppWindow,
Clapperboard,
@@ -24,11 +29,6 @@ import type {
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useResolvedAssetReadUrl } from '../../hooks/useResolvedAssetReadUrl';
import {
CanvasWorld as SharedCanvasWorld,
LayerRenderer,
SelectionOverlay,
} from '@genarrative/image-canvas-react';
import {
PlatformFloatingMenu,
PlatformFloatingMenuItem,
@@ -128,6 +128,9 @@ function HistoryHarness({
<span data-testid="layers">
{layers.map((layer) => `${layer.id}:${layer.x}`).join(',')}
</span>
<span data-testid="layer-size">
{layers.map((layer) => `${layer.width}x${layer.height}`).join(',')}
</span>
<span data-testid="layer-relations">
{layers
.map(
@@ -171,6 +174,14 @@ function HistoryHarness({
>
capture
</button>
<button
type="button"
onClick={() => {
history.captureCanvasHistory({ type: 'resize-image', count: 1 });
}}
>
capture-resize
</button>
<button
type="button"
onClick={() => {
@@ -212,6 +223,20 @@ function HistoryHarness({
<button type="button" onClick={() => setLayers([])}>
delete
</button>
<button
type="button"
onClick={() =>
setLayers((currentLayers) =>
currentLayers.map((layer) => ({
...layer,
width: 240,
height: 160,
})),
)
}
>
resize
</button>
<button
type="button"
onClick={() =>
@@ -310,6 +335,26 @@ function HistoryHarness({
}
describe('useCanvasHistory', () => {
it('restores shared layer bounds for resize history', () => {
render(<HistoryHarness onClearDrag={() => {}} />);
act(() => {
screen.getByRole('button', { name: 'capture-resize' }).click();
screen.getByRole('button', { name: 'resize' }).click();
});
expect(screen.getByTestId('layer-size').textContent).toBe('240x160');
act(() => {
screen.getByRole('button', { name: 'undo' }).click();
});
expect(screen.getByTestId('layer-size').textContent).toBe('100x80');
act(() => {
screen.getByRole('button', { name: 'redo' }).click();
});
expect(screen.getByTestId('layer-size').textContent).toBe('240x160');
});
it('captures, undoes, and redoes safe canvas history snapshots', () => {
const clearDragState = vi.fn();
render(<HistoryHarness onClearDrag={clearDragState} />);