532c6d51fd
## 背景 本合并请求整合 Game Agent 资源管理的栏目分页自由画布,以及图片持续精修、候选生成和本地恢复事务。 当前远端比较基线: - base:`master` @ `b00a3f80576949567bf3320f0c6c92b4ed0a649d` - head:`codex/game-agent-resource-section-pages` @ `5795d2f9b8797c342dcf2920ccefec2a3ff3f472` - 相对 base:28 commits、33 files(`+3746 / -1333`) ## 主要改动 ### 资源管理分页自由画布 - 固定展示“设计文档 → 美术资源 → 音乐音效 → 游戏代码 → 项目版本”五个栏目;空栏目仍可从悬浮 Dock 打开空画布。 - `按依赖` 与 `按类型` 共用栏目分页画布;每个“排序模式 + 栏目”组合独立保存 viewport。 - 普通滚轮使用有界意图队列切换栏目;Ctrl/Meta + 滚轮以指针为锚点缩放;空白拖拽可沿 x/y 无限平移,不以资源 extent 作为导航边界。 - 资源卡支持拖拽布局与一次 CAS 持久化;切页、排序切换和卸载会统一取消拖拽及 pointer capture。 - 资源详情为非模态独立卡片:打开、切换或关闭详情不卸载背景画布、资源卡或依赖连线,也不重置 viewport、搜索或排序状态。 - 顶部已移除“生成视频 / 生成音效 / 生成背景音乐 / 新增 UI 设计”的手动入口;保留播放、未完成编辑恢复、排序和画布复位,以及资源详情中的既有编辑动作。 ### 图片持续精修与生成事务 - 图片资源可进入唯一活动精修草稿,支持原生批量导入、图片下方快速编辑卡、候选图层、任务侧栏和“设为最终图”。 - 生成成功只合并候选图层和 generation 权威事实,不以全量 hydrate 覆盖生成期间的本地编辑。 - 候选媒体、图层和公开 generation 记录写入并回读成功后,才推进私有 `candidate-ready`。 - 候选确认接入 Surface 保存 FIFO 与重新打开 hydrate;revision-sensitive 操作等待确认屏障。 - 失败归档和正式图提交采用可恢复中间态,避免中断后出现幽灵任务、重复 revision 或不一致的正式资产。 ### 合同与文档 - `acknowledgeCandidateLayers`、`importLocalImages`、`archiveFailedGeneration` 为必选 Host Port;不支持的宿主返回结构化 `unsupported-capability`。 - 同步更新 TypeScript / Rust 合同、Tauri command 可达性、PRD、技术方案与项目共享记忆。 - 明确资源栏目无限画布合同:普通平移不夹取;资源 extent、图片测量、布局变更和 resize 不得重置用户 viewport;仅首次可测量布局与显式复位按真实卡片包围盒适配内容。 ## 验证 CI run [#1290](https://git.genarrative.world/git/GenarrativeAI/Genarrative/actions/runs/1290) 针对当前 head 已完成且四项均成功: - [x] Repository checks - [x] Frontend tests(235 test files、3166 tests passed;App Surface 410 tests) - [x] Backend tests - [x] Native shell tests 另有本地定向检查: - [x] `npm run agc:typecheck` - [x] 资源画布、App Surface、Asset Canvas 与资源实时集成定向回归 - [x] Rust 候选确认、候选中断恢复、失败归档与恢复定向测试 - [x] `cargo fmt --check` - [x] `npm run check:encoding` - [x] `git diff --check` ## 评审 provenance - review #247、#248、#252 都针对旧 head,当前在 Gitea 中均标记为 stale;不将其写作“已处理 #247”或当前 head 的评审结论。 - review #253 针对旧 `5456a1d…` head,提出 PRD 退役入口、共享 pitfalls 的 extent 旧合同和 PR 正文事实更新三项 P2;对应文档已在 `5795d2f…` head 修复并推送。 - 早期由 PR #176 引入的候选持久化、归档、Host Port 与快速编辑卡问题,当前已在 base/head 中具备对应修复,不作为本轮重复阻断项。 - 当前 head 仍需新的、针对最新提交的审批;历史 stale review 不能替代当前 head approval。 ## 影响范围 - 修改 AI 游戏创作 App 的资源管理、图片精修、Tauri 本地持久化与恢复语义,以及共享合同和权威文档。 - 会调整本地资源布局、精修 draft 和私有 generation ledger 的状态机。 - 不修改 SpacetimeDB schema,不新增或变更 `/api/external/v1` 路由。 --------- Co-authored-by: kdletters <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/181 Co-authored-by: suzmii <suzmii@qq.com> Co-committed-by: suzmii <suzmii@qq.com>
371 lines
9.7 KiB
TypeScript
371 lines
9.7 KiB
TypeScript
export type ImageCanvasHostKind = 'web' | 'tauri';
|
|
|
|
export type ImageCanvasHostCapabilities = {
|
|
account: boolean;
|
|
wallet: boolean;
|
|
cloudAssetLibrary: boolean;
|
|
localProject: boolean;
|
|
externalEditorGeneration: boolean;
|
|
advancedBackgroundRemoval: boolean;
|
|
};
|
|
|
|
export type ImageCanvasHostScope = {
|
|
projectId: string;
|
|
draftId: string;
|
|
intent: 'create' | 'refine';
|
|
sourceAssetId: string | null;
|
|
};
|
|
|
|
export type ImageCanvasHostResult<T> =
|
|
| { status: 'ok'; value: T }
|
|
| {
|
|
status: 'unsupported-capability';
|
|
capability: keyof ImageCanvasHostCapabilities;
|
|
message: string;
|
|
}
|
|
| {
|
|
status: 'conflict';
|
|
conflictKind: 'project-identity' | 'host-revision' | 'draft-revision';
|
|
draft: ImageCanvasDraft | null;
|
|
hostRevision: string | null;
|
|
}
|
|
| { status: 'failed'; code: string; message: string };
|
|
|
|
export type ImageCanvasMediaRef =
|
|
| { kind: 'project-asset'; assetId: string }
|
|
| {
|
|
kind: 'draft-media';
|
|
mediaId: string;
|
|
mediaType: 'image/png' | 'image/jpeg' | 'image/webp';
|
|
sha256: string;
|
|
byteLength: number;
|
|
pixelWidth: number;
|
|
pixelHeight: number;
|
|
}
|
|
| { kind: 'host-resource'; resourceId: string };
|
|
|
|
export type ImageCanvasDraftLayer = {
|
|
layerId: string;
|
|
resourceId: string;
|
|
title: string;
|
|
mediaRef: ImageCanvasMediaRef;
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
originalWidth: number;
|
|
originalHeight: number;
|
|
zIndex: number;
|
|
groupId: string | null;
|
|
hidden: boolean;
|
|
locked: boolean;
|
|
flipX: boolean;
|
|
flipY: boolean;
|
|
};
|
|
|
|
export type ImageCanvasDraftCanvas = {
|
|
viewport: { x: number; y: number; scale: number };
|
|
backgroundColor: string;
|
|
layers: ImageCanvasDraftLayer[];
|
|
selectedLayerIds: string[];
|
|
primarySelectedLayerId: string | null;
|
|
};
|
|
|
|
export type ImageCanvasDraft = {
|
|
schemaVersion: 'game-creator-asset-canvas-draft.v1';
|
|
draftId: string;
|
|
projectId: string;
|
|
intent: 'create' | 'refine';
|
|
sourceAssetId: string | null;
|
|
sourceResourceId: string | null;
|
|
revision: number;
|
|
status:
|
|
| 'editing'
|
|
| 'generating'
|
|
| 'commit-prepared'
|
|
| 'committed'
|
|
| 'cancelled'
|
|
| 'reconciliation-required';
|
|
canvas: ImageCanvasDraftCanvas;
|
|
generations: ImageCanvasGenerationRecord[];
|
|
pendingCommit: {
|
|
commitId: string;
|
|
idempotencyKey?: string;
|
|
requestFingerprint: string;
|
|
} | null;
|
|
lastCommit: {
|
|
commitId: string;
|
|
idempotencyKey?: string;
|
|
assetId: string;
|
|
eventId: string;
|
|
committedProjectRevision: number;
|
|
sourceLayerId: string;
|
|
mediaSha256: string;
|
|
} | null;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
};
|
|
|
|
export type ImageCanvasGenerationPlaceholder = {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
};
|
|
|
|
export type ImageCanvasGenerationRecord = {
|
|
generationId: string;
|
|
intentId: string;
|
|
phase:
|
|
| 'generation-accepted'
|
|
| 'generation-running'
|
|
| 'remote-completed'
|
|
| 'media-downloaded'
|
|
| 'candidate-ready'
|
|
| 'asset-durable-committed'
|
|
| 'failed'
|
|
| 'reconciliation-required';
|
|
referenceResourceIds: string[];
|
|
outputAssetId: string | null;
|
|
sourceLayerId: string | null;
|
|
placeholder: ImageCanvasGenerationPlaceholder | null;
|
|
errorCode: string | null;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
};
|
|
|
|
export type ImageCanvasGenerationProgressPhase =
|
|
| 'confirmation-required'
|
|
| ImageCanvasGenerationRecord['phase']
|
|
| 'manifest-projected'
|
|
| 'layout-ready'
|
|
| 'selected';
|
|
|
|
export type ImageCanvasGenerationProgress = {
|
|
intentId: string;
|
|
generationId: string;
|
|
draftRevision?: number;
|
|
phase: ImageCanvasGenerationProgressPhase;
|
|
progress: number | null;
|
|
errorCode: string | null;
|
|
};
|
|
|
|
export type ImageCanvasGenerationServiceIdentityConfirmation = {
|
|
generationId: string;
|
|
operationId: string | null;
|
|
operationState: string;
|
|
serviceOrigin: string;
|
|
challenge: string;
|
|
expiresAt: number;
|
|
};
|
|
|
|
export type ImageCanvasGenerationFailureDisposition =
|
|
| 'failed'
|
|
| 'reconciliation-required'
|
|
| 'not-started'
|
|
| 'already-terminal';
|
|
|
|
export type ImageCanvasHostInputImage = {
|
|
name: string;
|
|
mediaType: 'image/png' | 'image/jpeg' | 'image/webp';
|
|
bytes: Uint8Array;
|
|
};
|
|
|
|
export type ImageCanvasHostImage = {
|
|
mediaRef: ImageCanvasMediaRef;
|
|
previewUrl: string;
|
|
resourceId: string | null;
|
|
};
|
|
|
|
export type ImageCanvasLocalImportResult =
|
|
| { status: 'cancelled' }
|
|
| {
|
|
status: 'imported';
|
|
draft: ImageCanvasDraft;
|
|
importedLayerIds: string[];
|
|
};
|
|
|
|
export interface ImageCanvasProjectPort {
|
|
loadDraft(
|
|
scope: ImageCanvasHostScope,
|
|
): Promise<ImageCanvasHostResult<ImageCanvasDraft | null>>;
|
|
createDraft(
|
|
scope: ImageCanvasHostScope,
|
|
): Promise<ImageCanvasHostResult<ImageCanvasDraft>>;
|
|
updateDraft(input: {
|
|
scope: ImageCanvasHostScope;
|
|
expectedDraftRevision: number;
|
|
status: 'editing' | 'generating' | 'cancelled';
|
|
canvas: ImageCanvasDraft['canvas'];
|
|
generations: ImageCanvasGenerationRecord[];
|
|
}): Promise<ImageCanvasHostResult<ImageCanvasDraft>>;
|
|
acknowledgeCandidateLayers(input: {
|
|
scope: ImageCanvasHostScope;
|
|
layerIds: string[];
|
|
}): Promise<ImageCanvasHostResult<ImageCanvasDraft>>;
|
|
discardDraft(input: {
|
|
scope: ImageCanvasHostScope;
|
|
expectedDraftRevision: number;
|
|
}): Promise<ImageCanvasHostResult<ImageCanvasDraft>>;
|
|
}
|
|
|
|
export interface ImageCanvasAssetPort {
|
|
importLocalImages(input: {
|
|
scope: ImageCanvasHostScope;
|
|
expectedDraftRevision: number;
|
|
viewportSize: { width: number; height: number };
|
|
}): Promise<ImageCanvasHostResult<ImageCanvasLocalImportResult>>;
|
|
importImages(input: {
|
|
scope: ImageCanvasHostScope;
|
|
expectedDraftRevision: number;
|
|
images: ImageCanvasHostInputImage[];
|
|
}): Promise<ImageCanvasHostResult<ImageCanvasHostImage[]>>;
|
|
exportImage(input: {
|
|
scope: ImageCanvasHostScope;
|
|
name: string;
|
|
mediaType: 'image/png' | 'image/jpeg' | 'image/webp';
|
|
quality: number | null;
|
|
bytes: Uint8Array;
|
|
}): Promise<
|
|
ImageCanvasHostResult<{
|
|
disposition: 'downloaded' | 'saved';
|
|
byteLength: number;
|
|
}>
|
|
>;
|
|
}
|
|
|
|
export interface ImageCanvasGenerationPort {
|
|
settleGenerationFailure(input: {
|
|
scope: ImageCanvasHostScope;
|
|
expectedDraftRevision: number;
|
|
generationId: string;
|
|
intentId: string;
|
|
errorCode: string;
|
|
reconciliationRequired: boolean;
|
|
}): Promise<
|
|
ImageCanvasHostResult<{
|
|
disposition: ImageCanvasGenerationFailureDisposition;
|
|
draft: ImageCanvasDraft;
|
|
}>
|
|
>;
|
|
archiveFailedGeneration(input: {
|
|
scope: ImageCanvasHostScope;
|
|
expectedDraftRevision: number;
|
|
generationId: string;
|
|
}): Promise<ImageCanvasHostResult<ImageCanvasDraft>>;
|
|
generateImage(input: {
|
|
scope: ImageCanvasHostScope;
|
|
expectedHostRevision: string;
|
|
expectedDraftRevision: number;
|
|
intentId: string;
|
|
generationId: string;
|
|
idempotencyKey: string;
|
|
commitId: string;
|
|
commitIdempotencyKey: string;
|
|
prompt: string;
|
|
aspectRatio: '1:1' | '2:3' | '3:2' | '9:16' | '16:9';
|
|
imageSize: '0.5K' | '1K' | '2K';
|
|
assetKind: string;
|
|
assetName: string;
|
|
referenceResourceIds: string[];
|
|
sourceLayerId: string | null;
|
|
placeholder: ImageCanvasGenerationPlaceholder | null;
|
|
onProgress?: (progress: ImageCanvasGenerationProgress) => void;
|
|
}): Promise<
|
|
ImageCanvasHostResult<{
|
|
generation: ImageCanvasGenerationRecord;
|
|
images: ImageCanvasHostImage[];
|
|
draft: ImageCanvasDraft;
|
|
}>
|
|
>;
|
|
recoverImages(input: {
|
|
scope: ImageCanvasHostScope;
|
|
onProgress?: (progress: ImageCanvasGenerationProgress) => void;
|
|
}): Promise<
|
|
ImageCanvasHostResult<{
|
|
resumedGenerationIds: string[];
|
|
serviceIdentityConfirmations: ImageCanvasGenerationServiceIdentityConfirmation[];
|
|
}>
|
|
>;
|
|
removeBackground(input: {
|
|
scope: ImageCanvasHostScope;
|
|
expectedDraftRevision: number;
|
|
generationId: string;
|
|
idempotencyKey: string;
|
|
sourceLayerId: string;
|
|
}): Promise<
|
|
ImageCanvasHostResult<{
|
|
generation: ImageCanvasGenerationRecord;
|
|
images: ImageCanvasHostImage[];
|
|
}>
|
|
>;
|
|
}
|
|
|
|
export interface ImageCanvasCompletionPort {
|
|
commitSelectedCandidate(input: {
|
|
scope: ImageCanvasHostScope;
|
|
expectedHostRevision: string;
|
|
expectedDraftRevision: number;
|
|
commitId: string;
|
|
idempotencyKey: string;
|
|
sourceLayerId: string;
|
|
name: string;
|
|
assetKind: string;
|
|
referenceResourceIds: string[];
|
|
}): Promise<
|
|
ImageCanvasHostResult<{
|
|
resourceId: string;
|
|
assetId?: string;
|
|
projectId?: string;
|
|
commitId?: string;
|
|
committedProjectRevision?: number;
|
|
draftRevision: number;
|
|
hostRevision: string;
|
|
commitStatus?: 'committed' | 'already-committed';
|
|
manifest?: unknown;
|
|
eventId?: string;
|
|
}>
|
|
>;
|
|
commitImage(input: {
|
|
scope: ImageCanvasHostScope;
|
|
expectedHostRevision: string;
|
|
expectedDraftRevision: number;
|
|
commitId: string;
|
|
idempotencyKey: string;
|
|
name: string;
|
|
assetKind: string;
|
|
referenceResourceIds: string[];
|
|
mediaType: 'image/png' | 'image/jpeg' | 'image/webp';
|
|
bytes: Uint8Array;
|
|
}): Promise<
|
|
ImageCanvasHostResult<{
|
|
resourceId: string;
|
|
assetId?: string;
|
|
projectId?: string;
|
|
commitId?: string;
|
|
committedProjectRevision?: number;
|
|
draftRevision: number;
|
|
hostRevision: string;
|
|
commitStatus?: 'committed' | 'already-committed';
|
|
manifest?: unknown;
|
|
eventId?: string;
|
|
}>
|
|
>;
|
|
}
|
|
|
|
export type ImageCanvasHostPort = {
|
|
readonly kind: ImageCanvasHostKind;
|
|
readonly capabilities: ImageCanvasHostCapabilities;
|
|
project: ImageCanvasProjectPort;
|
|
asset: ImageCanvasAssetPort;
|
|
generation: ImageCanvasGenerationPort;
|
|
completion: ImageCanvasCompletionPort;
|
|
};
|
|
|
|
export function unsupportedImageCanvasCapability(
|
|
capability: keyof ImageCanvasHostCapabilities,
|
|
message: string,
|
|
): ImageCanvasHostResult<never> {
|
|
return { status: 'unsupported-capability', capability, message };
|
|
}
|