修复开放Issue 115、118、127和128
补齐Anthropic strict能力门控、传输schema、缓存usage和真实端点验收 隔离可选MCP服务目录失败并安全重定位局部schema引用 修复PowerShell 7环境下Windows私有ACL检查 统一角色与图标素材落库的用户prompt语义 合并权威画布快照与本地待保存或在途布局并补齐回归 同步更新技术文档、项目记忆和验证门禁
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ import { ApiClientError } from '../../services/apiClient';
|
||||
import { uploadEditorMediaAssetObjectFile } from '../../services/image-editor/editorMediaAssetUploadClient';
|
||||
import {
|
||||
createEditorProjectResource,
|
||||
type EditorProjectLayerSnapshot,
|
||||
type EditorProjectResourceSnapshot,
|
||||
type EditorProjectSnapshot,
|
||||
loadEditorProject,
|
||||
@@ -75,6 +76,12 @@ type PendingProjectLayoutSave = {
|
||||
transportRetries?: number;
|
||||
};
|
||||
|
||||
type ActiveProjectLayoutSaveAttempt = {
|
||||
save: PendingProjectLayoutSave;
|
||||
authorityEpoch: number;
|
||||
ownerUserId: string | null | undefined;
|
||||
};
|
||||
|
||||
type CachedEditorProjectSnapshot = {
|
||||
project: EditorProjectSnapshot;
|
||||
cachedAt: number;
|
||||
@@ -87,6 +94,102 @@ type ApplyProjectSnapshotOptions = {
|
||||
allowProjectSwitch?: boolean;
|
||||
};
|
||||
|
||||
function canvasLayoutItemId(item: EditorProjectLayerSnapshot) {
|
||||
return typeof item.layerId === 'string' ? item.layerId : null;
|
||||
}
|
||||
|
||||
function mergePendingCanvasLayerLayout(
|
||||
authoritative: EditorProjectLayerSnapshot,
|
||||
pending: EditorProjectLayerSnapshot,
|
||||
): EditorProjectLayerSnapshot {
|
||||
if (pending.itemType === 'canvas-settings') {
|
||||
return pending;
|
||||
}
|
||||
if (
|
||||
authoritative.itemType === 'generation-dialog' &&
|
||||
pending.itemType === 'generation-dialog'
|
||||
) {
|
||||
const authoritativeDialog = (
|
||||
authoritative as { dialog?: CanvasGenerationDialogState }
|
||||
).dialog;
|
||||
const pendingDialog = (pending as { dialog?: CanvasGenerationDialogState })
|
||||
.dialog;
|
||||
if (authoritativeDialog && pendingDialog) {
|
||||
return {
|
||||
...authoritative,
|
||||
dialog: {
|
||||
...authoritativeDialog,
|
||||
...pendingDialog,
|
||||
status: authoritativeDialog.status,
|
||||
generatedLayerId: authoritativeDialog.generatedLayerId,
|
||||
errorMessage: authoritativeDialog.errorMessage,
|
||||
generationStartedAt: authoritativeDialog.generationStartedAt,
|
||||
generationFinishedAt: authoritativeDialog.generationFinishedAt,
|
||||
characterAnimationResult:
|
||||
authoritativeDialog.characterAnimationResult,
|
||||
},
|
||||
};
|
||||
}
|
||||
return pending;
|
||||
}
|
||||
return {
|
||||
...authoritative,
|
||||
// pendingItems 来自 serializeCanvasLayout,这些布局字段始终存在;直接
|
||||
// 覆盖才能保留 false / undefined 代表的解锁、取消分组和取消翻转。
|
||||
title: pending.title,
|
||||
x: pending.x,
|
||||
y: pending.y,
|
||||
width: pending.width,
|
||||
height: pending.height,
|
||||
originalWidth: pending.originalWidth,
|
||||
originalHeight: pending.originalHeight,
|
||||
zIndex: pending.zIndex,
|
||||
groupId: pending.groupId,
|
||||
hidden: pending.hidden,
|
||||
locked: pending.locked,
|
||||
flipX: pending.flipX,
|
||||
flipY: pending.flipY,
|
||||
};
|
||||
}
|
||||
|
||||
export function mergeAuthoritativeCanvasLayoutWithPendingLocalLayout({
|
||||
authoritativeItems,
|
||||
pendingItems,
|
||||
previousAuthoritativeItemIds,
|
||||
}: {
|
||||
authoritativeItems: EditorProjectLayerSnapshot[];
|
||||
pendingItems: EditorProjectLayerSnapshot[];
|
||||
previousAuthoritativeItemIds: ReadonlySet<string>;
|
||||
}) {
|
||||
const authoritativeById = new Map(
|
||||
authoritativeItems.flatMap((item) => {
|
||||
const id = canvasLayoutItemId(item);
|
||||
return id ? [[id, item] as const] : [];
|
||||
}),
|
||||
);
|
||||
const pendingIds = new Set<string>();
|
||||
const merged = pendingItems.flatMap((pendingItem) => {
|
||||
const id = canvasLayoutItemId(pendingItem);
|
||||
if (!id) {
|
||||
return [pendingItem];
|
||||
}
|
||||
pendingIds.add(id);
|
||||
const authoritativeItem = authoritativeById.get(id);
|
||||
if (authoritativeItem) {
|
||||
return [mergePendingCanvasLayerLayout(authoritativeItem, pendingItem)];
|
||||
}
|
||||
return previousAuthoritativeItemIds.has(id) ? [] : [pendingItem];
|
||||
});
|
||||
for (const authoritativeItem of authoritativeItems) {
|
||||
const id = canvasLayoutItemId(authoritativeItem);
|
||||
if (!id || pendingIds.has(id) || previousAuthoritativeItemIds.has(id)) {
|
||||
continue;
|
||||
}
|
||||
merged.push(authoritativeItem);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
type ImageCanvasProjectPersistenceRefs = {
|
||||
layersRef: RefObject<CanvasLayer[]>;
|
||||
viewportRef: RefObject<CanvasViewport>;
|
||||
@@ -394,6 +497,7 @@ export function useImageCanvasProjectPersistence({
|
||||
const authoritativeProjectIdRef = useRef<string | null>(null);
|
||||
const hasAuthoritativeProjectSnapshotRef = useRef(false);
|
||||
const acceptedAuthoritativeSnapshotSequenceRef = useRef(0);
|
||||
const authoritativeLayoutItemIdsRef = useRef<Set<string>>(new Set());
|
||||
const lastAuthoritativeOwnerUserIdRef = useRef<string | null | undefined>(
|
||||
undefined,
|
||||
);
|
||||
@@ -418,6 +522,8 @@ export function useImageCanvasProjectPersistence({
|
||||
);
|
||||
const isProjectLayoutSaveRunningRef = useRef(false);
|
||||
const activeProjectLayoutSavePromiseRef = useRef<Promise<void> | null>(null);
|
||||
const activeProjectLayoutSaveAttemptRef =
|
||||
useRef<ActiveProjectLayoutSaveAttempt | null>(null);
|
||||
const skipNextProjectLayoutSaveRef = useRef(false);
|
||||
const saveTimerRef = useRef<number | null>(null);
|
||||
const projectTitleRef = useRef('未命名画布');
|
||||
@@ -478,6 +584,12 @@ export function useImageCanvasProjectPersistence({
|
||||
...pendingSave,
|
||||
attemptExpectedRevision: expectedRevision,
|
||||
};
|
||||
const activeAttempt: ActiveProjectLayoutSaveAttempt = {
|
||||
save: attemptedSave,
|
||||
authorityEpoch: saveAuthorityEpoch,
|
||||
ownerUserId: saveOwnerUserId,
|
||||
};
|
||||
activeProjectLayoutSaveAttemptRef.current = activeAttempt;
|
||||
const saveStillBelongsToCurrentAuthority = () =>
|
||||
projectAuthorityEpochRef.current === saveAuthorityEpoch &&
|
||||
currentUserIdRef.current === saveOwnerUserId &&
|
||||
@@ -492,11 +604,15 @@ export function useImageCanvasProjectPersistence({
|
||||
if (
|
||||
saveStillBelongsToCurrentAuthority() &&
|
||||
result &&
|
||||
typeof result.revision === 'number'
|
||||
typeof result.revision === 'number' &&
|
||||
result.revision > (projectRevisionRef.current ?? -1)
|
||||
) {
|
||||
projectRevisionRef.current = Math.max(
|
||||
projectRevisionRef.current ?? 0,
|
||||
result.revision,
|
||||
projectRevisionRef.current = result.revision;
|
||||
authoritativeLayoutItemIdsRef.current = new Set(
|
||||
attemptedSave.input.layers.flatMap((item) => {
|
||||
const id = canvasLayoutItemId(item);
|
||||
return id ? [id] : [];
|
||||
}),
|
||||
);
|
||||
}
|
||||
runNextSave = Boolean(pendingProjectLayoutSaveRef.current);
|
||||
@@ -518,6 +634,7 @@ export function useImageCanvasProjectPersistence({
|
||||
return;
|
||||
}
|
||||
applyProjectSnapshotRef.current?.(latestProject);
|
||||
runNextSave = Boolean(pendingProjectLayoutSaveRef.current);
|
||||
};
|
||||
const scheduleAuthoritativeReload = (retryCount: number) => {
|
||||
if (
|
||||
@@ -588,6 +705,9 @@ export function useImageCanvasProjectPersistence({
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (activeProjectLayoutSaveAttemptRef.current === activeAttempt) {
|
||||
activeProjectLayoutSaveAttemptRef.current = null;
|
||||
}
|
||||
if (activeProjectLayoutSavePromiseRef.current === savePromise) {
|
||||
activeProjectLayoutSavePromiseRef.current = null;
|
||||
}
|
||||
@@ -1049,6 +1169,20 @@ export function useImageCanvasProjectPersistence({
|
||||
}
|
||||
acceptedAuthoritativeSnapshotSequenceRef.current += 1;
|
||||
}
|
||||
const pendingSave = pendingProjectLayoutSaveRef.current;
|
||||
const activeSaveAttempt = activeProjectLayoutSaveAttemptRef.current;
|
||||
const pendingLocalLayout = authoritative
|
||||
? pendingSave?.projectId === project.projectId
|
||||
? pendingSave
|
||||
: activeSaveAttempt?.save.projectId === project.projectId &&
|
||||
activeSaveAttempt.authorityEpoch ===
|
||||
projectAuthorityEpochRef.current &&
|
||||
activeSaveAttempt.ownerUserId === currentUserIdRef.current
|
||||
? activeSaveAttempt.save
|
||||
: null
|
||||
: null;
|
||||
const previousAuthoritativeItemIds =
|
||||
authoritativeLayoutItemIdsRef.current;
|
||||
clearPendingProjectLayoutSave();
|
||||
skipNextProjectLayoutSaveRef.current = true;
|
||||
if (projectIdRef.current !== project.projectId) {
|
||||
@@ -1075,7 +1209,25 @@ export function useImageCanvasProjectPersistence({
|
||||
projectTitleRef.current = nextProjectTitle;
|
||||
setProjectTitle(nextProjectTitle);
|
||||
setProjectRenameValue(nextProjectTitle);
|
||||
const nextViewport = canvasDisplayViewportToViewport(project.viewport);
|
||||
const appliedLayoutItems = pendingLocalLayout
|
||||
? mergeAuthoritativeCanvasLayoutWithPendingLocalLayout({
|
||||
authoritativeItems: project.layers,
|
||||
pendingItems: pendingLocalLayout.input.layers,
|
||||
previousAuthoritativeItemIds,
|
||||
})
|
||||
: project.layers;
|
||||
if (authoritative) {
|
||||
authoritativeLayoutItemIdsRef.current = new Set(
|
||||
project.layers.flatMap((item) => {
|
||||
const id = canvasLayoutItemId(item);
|
||||
return id ? [id] : [];
|
||||
}),
|
||||
);
|
||||
}
|
||||
const appliedViewport = pendingLocalLayout
|
||||
? pendingLocalLayout.input.viewport
|
||||
: project.viewport;
|
||||
const nextViewport = canvasDisplayViewportToViewport(appliedViewport);
|
||||
setViewport(nextViewport);
|
||||
refs.viewportRef.current = nextViewport;
|
||||
const resourcesById = new Map<string, CanvasLayerResourceMetadata>(
|
||||
@@ -1105,7 +1257,11 @@ export function useImageCanvasProjectPersistence({
|
||||
]),
|
||||
);
|
||||
const { layerItems, generationDialogs, canvasBackgroundColor } =
|
||||
splitCanvasLayoutItems(project.layers, resourcesById, currentUserId);
|
||||
splitCanvasLayoutItems(
|
||||
appliedLayoutItems,
|
||||
resourcesById,
|
||||
currentUserId,
|
||||
);
|
||||
const hydratedLayers = layerItems
|
||||
.map((layer) => hydrateLayer(layer, resourcesById))
|
||||
.filter((layer): layer is CanvasLayer => Boolean(layer));
|
||||
@@ -1124,10 +1280,22 @@ export function useImageCanvasProjectPersistence({
|
||||
authoritativeProjectIdRef.current === project.projectId;
|
||||
if (projectIsAuthoritative) {
|
||||
writeEditorProjectSessionCache(
|
||||
project,
|
||||
pendingLocalLayout
|
||||
? {
|
||||
...project,
|
||||
viewport: appliedViewport,
|
||||
layers: appliedLayoutItems,
|
||||
}
|
||||
: project,
|
||||
currentUserId,
|
||||
project.canvas?.revision,
|
||||
);
|
||||
if (pendingLocalLayout) {
|
||||
queueProjectLayoutSave(project.projectId, {
|
||||
viewport: appliedViewport,
|
||||
layers: appliedLayoutItems,
|
||||
});
|
||||
}
|
||||
}
|
||||
return projectIsAuthoritative;
|
||||
},
|
||||
@@ -1135,6 +1303,7 @@ export function useImageCanvasProjectPersistence({
|
||||
applyCanvasBackgroundColor,
|
||||
clearPendingProjectLayoutSave,
|
||||
currentUserId,
|
||||
queueProjectLayoutSave,
|
||||
refs,
|
||||
restoreCanvasGenerationDialogs,
|
||||
selectSingleLayer,
|
||||
@@ -1171,6 +1340,8 @@ export function useImageCanvasProjectPersistence({
|
||||
hasAuthoritativeProjectSnapshotRef.current = false;
|
||||
authoritativeProjectIdRef.current = null;
|
||||
projectRevisionRef.current = null;
|
||||
authoritativeLayoutItemIdsRef.current = new Set();
|
||||
activeProjectLayoutSaveAttemptRef.current = null;
|
||||
pendingCreatedProjectResourceLayersRef.current = [];
|
||||
clearPendingProjectLayoutSave();
|
||||
setIsProjectReady(false);
|
||||
@@ -1179,6 +1350,8 @@ export function useImageCanvasProjectPersistence({
|
||||
hasAuthoritativeProjectSnapshotRef.current = false;
|
||||
authoritativeProjectIdRef.current = null;
|
||||
projectRevisionRef.current = null;
|
||||
authoritativeLayoutItemIdsRef.current = new Set();
|
||||
activeProjectLayoutSaveAttemptRef.current = null;
|
||||
clearPendingProjectLayoutSave();
|
||||
setIsProjectReady(false);
|
||||
let cancelled = false;
|
||||
|
||||
Reference in New Issue
Block a user