切换客户端全类型资源编辑至External API
移除WebView登录Token读取及Tauri命令透传 统一图片视频音效和背景音乐的External v1调用链 严格校验202受理语义并保护历史站内账本 补齐凭据隔离非破坏性派生和视频端到端测试 同步客户端创作合同与项目共享记忆
This commit is contained in:
@@ -462,40 +462,6 @@ pub(crate) async fn resolve_canvas_resource_download(
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_authenticated_canvas_resource_download(
|
||||
client: &reqwest::Client,
|
||||
api_base_url: &str,
|
||||
access_token: &str,
|
||||
resource: &serde_json::Value,
|
||||
) -> Result<Option<CanvasResourceDownload>, String> {
|
||||
resolve_authenticated_canvas_resource_download_with_limit(
|
||||
client,
|
||||
api_base_url,
|
||||
access_token,
|
||||
resource,
|
||||
20 * 1024 * 1024,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_authenticated_canvas_resource_download_with_limit(
|
||||
client: &reqwest::Client,
|
||||
api_base_url: &str,
|
||||
access_token: &str,
|
||||
resource: &serde_json::Value,
|
||||
max_bytes: usize,
|
||||
) -> Result<Option<CanvasResourceDownload>, String> {
|
||||
resolve_canvas_resource_download_with_limit_and_route(
|
||||
client,
|
||||
api_base_url,
|
||||
access_token,
|
||||
resource,
|
||||
max_bytes,
|
||||
"/api/assets/read-url",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn external_asset_url_same_origin(url: &url::Url, api_base_url: &str) -> bool {
|
||||
let Ok(api_base_url) = url::Url::parse(api_base_url) else {
|
||||
return false;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3
-11
@@ -15,7 +15,6 @@ import type {
|
||||
GameCreationAppAssetManifestEntry,
|
||||
GameCreationAppManifest,
|
||||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import { getStoredAuthAccessToken } from '../../services/clientApi';
|
||||
|
||||
const MAX_SAFE_REVISION = Number.MAX_SAFE_INTEGER;
|
||||
export const LOCAL_ASSET_COMMITTED_EVENT =
|
||||
@@ -165,6 +164,8 @@ function failure(error: unknown): ImageCanvasHostResult<never> {
|
||||
? 'reconciliation-required'
|
||||
: message.includes('project-identity-conflict')
|
||||
? 'project-identity-conflict'
|
||||
: normalized.includes('external editor api')
|
||||
? 'platform-service-configuration'
|
||||
: normalized.includes('authentication-required') ||
|
||||
message.includes('登录已失效')
|
||||
? 'authentication-required'
|
||||
@@ -268,10 +269,6 @@ export function createTauriImageCanvasHostAdapter(input: {
|
||||
async generateImage(generationInput) {
|
||||
let unlisten: (() => void) | undefined;
|
||||
try {
|
||||
const accessToken = getStoredAuthAccessToken();
|
||||
if (!accessToken) {
|
||||
throw new Error('authentication-required: 登录已失效,请重新登录后重试');
|
||||
}
|
||||
unlisten = await listenGenerationProgress(
|
||||
generationInput.scope,
|
||||
generationInput.generationId,
|
||||
@@ -300,7 +297,6 @@ export function createTauriImageCanvasHostAdapter(input: {
|
||||
assetKind: generationInput.assetKind,
|
||||
assetName: generationInput.assetName,
|
||||
referenceResourceIds: generationInput.referenceResourceIds,
|
||||
accessToken,
|
||||
},
|
||||
);
|
||||
return { status: 'ok', value: result };
|
||||
@@ -313,10 +309,6 @@ export function createTauriImageCanvasHostAdapter(input: {
|
||||
async recoverImages(recoverInput) {
|
||||
let unlisten: (() => void) | undefined;
|
||||
try {
|
||||
const accessToken = getStoredAuthAccessToken();
|
||||
if (!accessToken) {
|
||||
throw new Error('authentication-required: 登录已失效,请重新登录后重试');
|
||||
}
|
||||
unlisten = await listenGenerationProgress(
|
||||
recoverInput.scope,
|
||||
null,
|
||||
@@ -324,7 +316,7 @@ export function createTauriImageCanvasHostAdapter(input: {
|
||||
);
|
||||
const result = await invokeInput<{ resumedGenerationIds: string[] }>(
|
||||
'recover_local_project_asset_canvas_generations',
|
||||
{ ...baseScope(recoverInput.scope), accessToken },
|
||||
baseScope(recoverInput.scope),
|
||||
);
|
||||
return { status: 'ok', value: result };
|
||||
} catch (error) {
|
||||
|
||||
@@ -56,7 +56,6 @@ import {
|
||||
LocalGamePreviewFrame,
|
||||
resolveEmbeddedPreviewUrl,
|
||||
} from '../../features/project-workspace/LocalGamePreviewFrame';
|
||||
import { getStoredAuthAccessToken } from '../../services/clientApi';
|
||||
import {
|
||||
type ProjectManifestSnapshotMetadata,
|
||||
resolveResourceFocusIntent,
|
||||
@@ -1418,14 +1417,6 @@ export default function ProjectDevelopmentView({
|
||||
if (!route || !invoke) {
|
||||
throw new Error('资源编辑需要在客户端内打开');
|
||||
}
|
||||
const accessToken = route.capability.requiresAccessToken
|
||||
? getStoredAuthAccessToken()
|
||||
: '';
|
||||
if (route.capability.requiresAccessToken && !accessToken) {
|
||||
throw new Error(
|
||||
'authentication-required: 登录已失效,请重新登录后重试',
|
||||
);
|
||||
}
|
||||
let expectedProjectRevision = resourceEditorRevisionRef.current.get(
|
||||
route.operationId,
|
||||
);
|
||||
@@ -1466,7 +1457,6 @@ export default function ProjectDevelopmentView({
|
||||
sourceVersionId: resource.version?.versionId ?? null,
|
||||
prompt,
|
||||
assetName,
|
||||
accessToken: accessToken || null,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -27,7 +27,6 @@ export type ProjectResourceEditCapability =
|
||||
route: 'derive';
|
||||
editKind: LocalProjectResourceEditKind;
|
||||
sourceMediaType: string;
|
||||
requiresAccessToken: boolean;
|
||||
semanticNotice: string | null;
|
||||
};
|
||||
|
||||
@@ -126,7 +125,6 @@ export function resolveProjectResourceEditCapability(
|
||||
route: 'derive',
|
||||
editKind: 'version',
|
||||
sourceMediaType: 'application/vnd.genarrative.project-version+json',
|
||||
requiresAccessToken: false,
|
||||
semanticNotice: null,
|
||||
};
|
||||
}
|
||||
@@ -135,7 +133,6 @@ export function resolveProjectResourceEditCapability(
|
||||
route: 'derive',
|
||||
editKind: 'agent-result',
|
||||
sourceMediaType: 'text/markdown',
|
||||
requiresAccessToken: false,
|
||||
semanticNotice: null,
|
||||
};
|
||||
}
|
||||
@@ -161,7 +158,6 @@ export function resolveProjectResourceEditCapability(
|
||||
route: 'derive',
|
||||
editKind: 'svg',
|
||||
sourceMediaType: mediaType,
|
||||
requiresAccessToken: false,
|
||||
semanticNotice: null,
|
||||
};
|
||||
}
|
||||
@@ -170,7 +166,6 @@ export function resolveProjectResourceEditCapability(
|
||||
route: 'derive',
|
||||
editKind: 'image-reference',
|
||||
sourceMediaType: mediaType,
|
||||
requiresAccessToken: true,
|
||||
semanticNotice: null,
|
||||
};
|
||||
}
|
||||
@@ -179,7 +174,6 @@ export function resolveProjectResourceEditCapability(
|
||||
route: 'derive',
|
||||
editKind: 'video',
|
||||
sourceMediaType: mediaType,
|
||||
requiresAccessToken: true,
|
||||
semanticNotice: null,
|
||||
};
|
||||
}
|
||||
@@ -190,7 +184,6 @@ export function resolveProjectResourceEditCapability(
|
||||
? 'background-music'
|
||||
: 'sound-effect',
|
||||
sourceMediaType: mediaType,
|
||||
requiresAccessToken: true,
|
||||
semanticNotice: '音频将基于原资源语义派生重制,不会修改原音频波形。',
|
||||
};
|
||||
}
|
||||
@@ -198,7 +191,6 @@ export function resolveProjectResourceEditCapability(
|
||||
route: 'derive',
|
||||
editKind: 'text',
|
||||
sourceMediaType: mediaType,
|
||||
requiresAccessToken: false,
|
||||
semanticNotice: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1366,12 +1366,15 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
|
||||
expect(invokeSpy).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('Tauri adapter 只把登录 Token 临时传给生成命令,并在公开结果中移除', async () => {
|
||||
it('Tauri adapter 不向生成与恢复命令透传登录 Token 或 External API Key', async () => {
|
||||
const accessToken = 'session-token-must-stay-ephemeral';
|
||||
setStoredAuthAccessToken(accessToken);
|
||||
const manifest = manifestFixture(scope.projectId);
|
||||
const invokeSpy = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'recover_local_project_asset_canvas_generations') {
|
||||
return { resumedGenerationIds: [] };
|
||||
}
|
||||
if (command !== 'generate_local_project_asset_canvas_image') {
|
||||
throw new Error(`unexpected command: ${command}`);
|
||||
}
|
||||
@@ -1422,11 +1425,11 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
|
||||
idempotencyKey: '33333333-3333-4333-8333-333333333333',
|
||||
commitId: '44444444-4444-4444-8444-444444444444',
|
||||
commitIdempotencyKey: '55555555-5555-4555-8555-555555555555',
|
||||
prompt: '登录态图片生成',
|
||||
prompt: 'External 图片生成',
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '1K',
|
||||
assetKind: 'illustration',
|
||||
assetName: '登录态素材',
|
||||
assetName: 'External 素材',
|
||||
referenceResourceIds: [],
|
||||
});
|
||||
|
||||
@@ -1436,17 +1439,22 @@ describe('Tauri 素材创作无限画布独立 Surface', () => {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(commandInput.accessToken).toBe(accessToken);
|
||||
expect(commandInput).not.toHaveProperty('accessToken');
|
||||
expect(commandInput).not.toHaveProperty('apiKey');
|
||||
expect(JSON.stringify(result)).not.toContain(accessToken);
|
||||
|
||||
clearStoredAuthAccessToken();
|
||||
const missingAuth = await adapter.generation.recoverImages({ scope });
|
||||
expect(missingAuth).toEqual(
|
||||
expect.objectContaining({
|
||||
status: 'failed',
|
||||
code: 'authentication-required',
|
||||
}),
|
||||
);
|
||||
expect(invokeSpy).toHaveBeenCalledTimes(1);
|
||||
const recovery = await adapter.generation.recoverImages({ scope });
|
||||
expect(recovery).toEqual({
|
||||
status: 'ok',
|
||||
value: { resumedGenerationIds: [] },
|
||||
});
|
||||
const recoveryInput = invokeSpy.mock.calls[1]?.[1]?.input as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(recoveryInput).not.toHaveProperty('accessToken');
|
||||
expect(recoveryInput).not.toHaveProperty('apiKey');
|
||||
expect(invokeSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -532,6 +532,10 @@ describe('project resource live canvas integration', () => {
|
||||
await waitFor(() => expect(deriveCalls).toHaveLength(2));
|
||||
expect(deriveCalls[0]?.operationId).toBe(deriveCalls[1]?.operationId);
|
||||
expect(deriveCalls[0]?.idempotencyKey).toBe(deriveCalls[1]?.idempotencyKey);
|
||||
expect(deriveCalls[0]).not.toHaveProperty('accessToken');
|
||||
expect(deriveCalls[0]).not.toHaveProperty('apiKey');
|
||||
expect(deriveCalls[1]).not.toHaveProperty('accessToken');
|
||||
expect(deriveCalls[1]).not.toHaveProperty('apiKey');
|
||||
const operationId = String(deriveCalls[1]?.operationId);
|
||||
expect(
|
||||
await screen.findByRole('region', {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-08-10 Tauri 客户端远端资源编辑固定使用 External v1
|
||||
|
||||
- 产品边界:主站网页画布继续使用登录态 `/api/editor/*`、`/api/assets/*` 与 `/api/runtime/external-generation/jobs/*`;AI 游戏创作 Tauri 客户端没有网页画布宿主,图片、图片引用、视频、音效和背景音乐等远端媒体编辑固定使用 `/api/external/v1/*`。两条入口继续复用相同请求 DTO、owner 归属、统一生成队列和正式资产结果,不新建平行生成服务。
|
||||
- 凭据边界:客户端沿用发布 AppData 私有运行时配置中的 `editorApi.baseUrl/apiKey`,不实现登录后自动签发 Developer API Key,不把站内 Access Token 传给 Tauri 生成命令,也不把 API Key 打包进仓库、传入 WebView、写入项目、账本、日志或普通错误。Key 缺失投影安全配置错误,`401/403` 投影 External 凭据无效或权限不足,不再描述为站内登录失效。
|
||||
- 路由边界:图片编辑、视频、音效和 BGM 分别使用 `/api/external/v1/editor/images/edits`、`/api/external/v1/editor/videos/generations`、`/api/external/v1/editor/audios/sound-effects/generations` 与 `/api/external/v1/editor/audios/background-music/generations`;上传、确认、轮询和换签固定使用 External v1 对应端点。SVG、UTF-8 文档、代码、Agent 回执与项目版本仍是本地派生,不制造无意义的远端请求。
|
||||
- 可靠性边界:External POST 固定携带原稳定 `Idempotency-Key` 并只接受 `202 + operationId`;`prepared` 只重放原正文和原键,`accepted/running` 只查询 `/api/external/v1/generations/{operationId}`。账本绑定不含明文 Key 的 External 配置身份指纹;升级前已保存的站内 endpoint 只保留为待对账状态,禁止拿 External Key 自动重放。本地参考媒体继续严格执行 ticket → OSS form → confirm,结果只消费稳定 `objectKey/resource/asset`,旧资源保留且派生资源追加。
|
||||
- 关联:`apps/ai-game-creator-shell/src/features/asset-canvas/tauriImageCanvasHostAdapter.ts`、`apps/ai-game-creator-shell/src/view/project-development/resourceEditModel.ts`、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs`、`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`。
|
||||
|
||||
## 2026-08-10 客户端参考媒体直传复用已授权私有前缀
|
||||
|
||||
- 根因:客户端素材画布和全类型资源编辑把内部用途目录 `asset-canvas-references`、`resource-editor-references` 直接作为 `legacyPrefix`;api-server 只接受 `platform-oss` 权威白名单,因此请求在票据阶段返回 `400`,OSS 上传、对象确认、生成提交和扣费都没有发生。
|
||||
|
||||
@@ -4509,6 +4509,14 @@
|
||||
- 验证:客户端端到端测试必须断言 confirm 早于生成 POST、请求使用精确前缀与 pathSegments、账本只持久化稳定对象身份;票据失败时断言 `operationId/requestBodyJson` 为空且 manifest 只有源资产。api-server 测试应断言生成的 key 位于 `generated-character-drafts/editor/...` 且 access 为 private。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs`、`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`、`server-rs/crates/api-server/src/assets.rs`。
|
||||
|
||||
## Tauri 有平台登录 Token 不代表应调用主站画布 API(2026-08-10)
|
||||
|
||||
- 现象:客户端素材画布和全类型资源编辑从 WebView 读取平台 Access Token,把它传给 Tauri command,再调用 `/api/editor/*`、`/api/assets/*` 和 `/api/runtime/external-generation/jobs/*`;代码同时保留 External 分支,导致真实 UI、Runtime 和测试使用不同路径,发布客户端还错误依赖网页画布登录态。
|
||||
- 原因:把“客户端壳有账号登录能力”误当成“客户端画布属于主站网页宿主”。主站和 Tauri 虽复用相同请求 DTO 与后端生成服务,但对外边界不同:主站使用站内认证路由,Tauri 远端媒体能力使用 Developer API Key 和 External v1 路由。
|
||||
- 处理:Tauri 前端不读取、透传或持久化站内 Access Token;Rust 只从发布 AppData 私有 `editorApi.baseUrl/apiKey` 解析 External 凭据。图片、视频、音效、BGM 的项目/素材库、上传、确认、生成、轮询与换签全部留在 `/api/external/v1`,不访问内部 job 查询或账号/profile 接口。账本只绑定 External 配置身份指纹,升级前遗留的站内 endpoint 必须进入待对账状态,不能拿 External Key 自动重放。Key 缺失或无权限只返回安全配置错误,不打印 Key、Authorization、Provider 正文或私有路径。
|
||||
- 验证:前端测试断言 command input 不含 `accessToken/apiKey`;Rust mock 服务器拒绝任何 `/api/editor/*`、`/api/assets/*` 和 `/api/runtime/external-generation/jobs/*` 请求,并覆盖 External `202`、原 operation 轮询、换签、非破坏性本地提交和账本零凭据。主站路由与 OpenAPI 未发生契约变化时不得为了客户端切换修改后端接口。
|
||||
- 关联:`apps/ai-game-creator-shell/src/features/asset-canvas/tauriImageCanvasHostAdapter.ts`、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs`、`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`。
|
||||
|
||||
## prepared journal 之前同样存在正式事务崩溃窗口(2026-08-05)
|
||||
|
||||
- 现象:事务依次安装 before/after 快照后才写 journal;若进程在首个快照、全部快照或 journal 已写但 ledger 未写时退出,重启扫描看到 transaction 目录却无法进入原先只覆盖 prepared 之后的恢复状态机,可能留下孤儿目录或阻塞项目后续提交。
|
||||
|
||||
@@ -971,8 +971,8 @@ game-project/
|
||||
|
||||
- 阶段五只接图片生成与图片精修,通过阶段二共享 `ImageCanvasGenerationPort` 调用客户端 Tauri adapter,并复用现有编辑器项目、统一生成队列、资产上传/换签、稳定引用解析和受控下载能力;不复制网站 `useImageCanvasGenerationWorkflow`,不新增角色动画、视频、音频、图集或平行生成系统。
|
||||
- 生成面板和确认摘要只操作本地状态;用户点击独立“确认生成”后才通过 `canvas.asset_generate` confirm 权限边界。本地生成意图建立时即分配稳定 `intentId/generationId/idempotencyKey`,取消确认则直接丢弃且零 External 调用;一旦提交,重复点击、重启和未知结果保持原身份,不能自动确认或换键重扣。
|
||||
- 普通客户模式只使用当前平台登录态调用 `/api/editor/*`、`/api/assets/*` 与 `/api/runtime/external-generation/jobs/*`;后端从登录身份解析 owner、按现有统一生成队列预扣/退款泥点,并只在服务端读取供应商配置。客户 UI 不显示 Base URL / API Key,也不得把根目录 `.env*`、供应商 Key 或开发者 Key 打包给客户端。`/api/external/v1` 与开发者 API Key 继续保留给第三方 Agent、CLI 和内部验收,不能用开发者 Key 替代客户身份或计费归属。
|
||||
- 调用模式、精确相对 endpoint、精确请求、prompt、idempotency key、operationId 和稳定产物引用只进入 `.agent/workbench/asset-canvas/generations/` 私有账本。本地参考图 direct-upload ticket 的 host/formFields/policy/signature/临时 Authorization 只存在内存;恢复账本只允许保存稳定 bucket/objectKey。登录 Access Token、API Key、Authorization、Provider URL、签名 URL和绝对路径不进入账本或项目事实;登录 Token 刷新不得触发 `configuration-changed`。草稿/manifest/event/日志只投影安全阶段与本地身份,远端 taskId 不写 manifest `source.taskId`。
|
||||
- 主站网页画布继续使用当前平台登录态调用 `/api/editor/*`、`/api/assets/*` 与 `/api/runtime/external-generation/jobs/*`。AI 游戏创作 Tauri 客户端的图片、图片引用、视频、音效和背景音乐远端编辑固定调用 `/api/external/v1/*`,凭据只读取发布 AppData 私有 `editorApi.baseUrl/apiKey`;不实现登录后自动签发 Developer API Key,不把站内 Access Token 传给 Tauri 生成命令,也不把开发者 Key 打包进客户端或传入 WebView。两条入口继续由后端解析 owner、沿统一生成队列预扣/退款泥点并只在服务端读取供应商配置。
|
||||
- 调用模式、精确相对 endpoint、精确请求、prompt、idempotency key、operationId 和稳定产物引用只进入 `.agent/workbench/asset-canvas/generations/` 私有账本。本地参考图 direct-upload ticket 的 host/formFields/policy/signature/临时 Authorization 只存在内存;恢复账本只允许保存稳定 bucket/objectKey。API Key、Authorization、Provider URL、签名 URL和绝对路径不进入账本或项目事实;External 配置指纹变化不得借旧 Key 身份自动重放。草稿/manifest/event/日志只投影安全阶段与本地身份,远端 taskId 不写 manifest `source.taskId`。
|
||||
- 固定状态顺序为 `generation-accepted -> generation-running -> remote-completed -> media-downloaded -> asset-durable-committed -> manifest-projected -> layout-ready -> selected`。远端 completed 后必须先验证稳定引用,再经 `/assets/read-url` 下载并写入阶段三 staging;随后用账本中的固定本地 commit 身份进入既有 journal/ledger 事务。正式提交失败只恢复本地事务,不重新生成或下载。
|
||||
- 应用重启时 `accepted/running` 只恢复 GET,`prepared` 只以私有账本原请求字节和原键恢复同一 POST。用户停止等待只失效当前 focus generation;不可取消的远端操作继续 reconcile,迟到 completed 可以更新原项目但不能抢焦点。配置缺失、结果引用不稳定、下载/提交失败和未知结果都不得显示资源创建成功。
|
||||
- 最终审计后 Tauri 撤销/重做直接使用与网站相同的共享 `useCanvasHistory`,共享 history 同时恢复 viewport、选择、图层位置和缩放边界;Tauri 自身只保留 Pointer 事件接线,平移、缩放、选择、图层变换和 renderer 算法继续来自共享 core/react。
|
||||
|
||||
@@ -952,13 +952,13 @@ confirmation-required
|
||||
|
||||
### 14.4 首版请求范围与恢复
|
||||
|
||||
- 普通客户生成使用 `POST /api/editor/images/generations`,refine 使用 `POST /api/editor/images/edits`;两者携带当前登录 Bearer Token 和稳定 `Idempotency-Key`,由后端将 owner 与幂等身份绑定并沿统一生成队列扣费。提交响应中的 `queueState.operationId` 是轮询身份,状态使用 `GET /api/runtime/external-generation/jobs/{operationId}`;completed 必须返回与 External 通道一致语义的 compact `result`,供客户端解析稳定引用。
|
||||
- 开发者模式才使用对应 `/api/external/v1/editor/images/*`;两种模式支持相同 prompt、`1:1 | 2:3 | 3:2 | 9:16 | 16:9`、`0.5K | 1K | 2K`、合法 `assetKind` 与参考资源约束。refine 的 `sourceImageSrc` 必须来自源资产的 objectKey、服务端项目 resourceId 或素材 assetId;`assetObjectId` 本身不是该字段接受的引用。本地独有图片在用户确认后先走现有 direct-upload ticket → OSS form → object confirm,确认后的 objectKey 才可进入生成请求。`sourceResourceId` 只在源资产确有服务端 resourceId 时发送,不能用本地 `local-asset:*`、objectKey 或 assetObjectId 冒充。额外参考最多 8 个。
|
||||
- 主站网页画布继续使用 `POST /api/editor/images/generations`、`POST /api/editor/images/edits` 与 `/api/runtime/external-generation/jobs/{operationId}`。AI 游戏创作 Tauri 客户端固定使用对应 `/api/external/v1/editor/images/*`,携带发布 AppData 私有 `editorApi.apiKey` 和稳定 `Idempotency-Key`,状态只查询 `GET /api/external/v1/generations/{operationId}`。两条入口复用相同请求 DTO、owner 归属和统一生成队列;客户端不读取或透传主站 Access Token,也不实现登录后自动签发 Developer API Key。
|
||||
- External 图片生成和编辑支持相同 prompt、`1:1 | 2:3 | 3:2 | 9:16 | 16:9`、`0.5K | 1K | 2K`、合法 `assetKind` 与参考资源约束。refine 的 `sourceImageSrc` 必须来自源资产的 objectKey、服务端项目 resourceId 或素材 assetId;`assetObjectId` 本身不是该字段接受的引用。本地独有图片在用户确认后先走 External direct-upload ticket → OSS form → object confirm,确认后的 objectKey 才可进入生成请求。`sourceResourceId` 只在源资产确有服务端 resourceId 时发送,不能用本地 `local-asset:*`、objectKey 或 assetObjectId 冒充。额外参考最多 8 个。
|
||||
- 客户端参考媒体直传固定复用 `legacyPrefix=generated-character-drafts`,不得把内部用途目录作为新 legacy prefix,也不得扩大服务端白名单。图片画布的 `pathSegments` 固定为 `editor / asset-canvas-references / <projectId> / <draftId> / <generationId>`;全类型资源编辑的本地视频、音频等源媒体固定为 `editor / resource-editor-references / <projectId> / <operationId>`。两条路径都只持久化 confirm 后的稳定 objectKey,不持久化 ticket 或签名 URL。
|
||||
- 图片参考资源准备失败按阶段投影安全错误码:本地读取/校验为 `reference-material-invalid`,票据为 `reference-ticket-failed`,OSS 表单上传为 `reference-object-upload-failed`,对象确认为 `reference-confirm-failed`;`401/403` 仍为 `authentication-required`。任一阶段失败都必须保持 External `operationId=null`、生成 endpoint/request body 未建立,不得进入扣费或生成提交。普通错误不得包含 ticket host、formFields、policy、signature、Token、API Key、Provider 响应正文或本机绝对路径。
|
||||
- `accepted/running` 重启只查询原 operation;`prepared` 只以账本中的原 endpoint、原始字节和原键恢复同一 POST。轮询超时、临时鉴权/网关错误或结果未知保持可恢复,不自动转 failed 或换键扣费。
|
||||
- 用户“停止等待”只撤销当前 session 的聚焦意图,不伪装远端取消。后台 command 可以继续安全 reconcile;应用退出后由项目恢复扫描继续原 operation。迟到完成可以更新原项目权威缓存和 manifest,但不能切回旧画布、清搜索或抢焦点。
|
||||
- 登录失效、余额不足、平台生成服务配置错误、远端 terminal failed、稳定引用缺失、下载失败、staging/正式提交失败都不得返回资源创建成功。客户模式不得把缺少开发者 API Key 显示为失败原因。只有 terminal failed 才允许用户通过新的独立确认建立新 intent;其它未知或对账状态必须保留原身份。
|
||||
- External API Key 缺失、无效或权限不足,余额不足、平台生成服务配置错误、远端 terminal failed、稳定引用缺失、下载失败、staging/正式提交失败都不得返回资源创建成功。错误只说明需要配置或检查 External Editor 凭据,不回显 Key、Authorization 或上游正文。只有 terminal failed 才允许用户通过新的独立确认建立新 intent;其它未知或对账状态必须保留原身份。
|
||||
|
||||
## 15. 2026-08-06 画布 UI 对齐阶段三至五收口
|
||||
|
||||
@@ -978,16 +978,16 @@ confirmation-required
|
||||
- 资源总览的“新增资源”保留为明确禁用态,入口处理函数也必须拒绝 create,不能只依赖按钮外观阻止进入空白画布。
|
||||
- 资源聚焦态的所有现役资源均提供“编辑资源”,覆盖 manifest asset、已完成任务产物、已导入附件、Agent 文本回执和项目版本。后端必须按 manifest、任务完成态、上传登记或回执身份重新核验来源;没有唯一来源身份的本地媒体不得仅凭前端路径进入编辑。
|
||||
- 静态 PNG / JPEG / WebP 继续使用 `AssetCanvasSurface + intent=refine`,自动加载唯一源图片,并把源资源身份作为图片编辑请求的必选引用。任务产物或附件中的静态图片必须先正规化为正式 manifest asset,再进入现有图片画布。
|
||||
- 普通客户确认编辑后固定调用 `POST /api/editor/images/edits`;提示词、比例、尺寸、资源用途、登录态、泥点计费和原 operation 恢复继续复用现有生成合同。
|
||||
- 客户端确认编辑后固定调用 `POST /api/external/v1/editor/images/edits`;提示词、比例、尺寸、资源用途、泥点计费和原 operation 恢复继续复用现有生成合同,凭据只来自发布 AppData 私有 `editorApi.apiKey`。
|
||||
- SVG、UTF-8 文档、代码和 Agent 文本回执使用文本差异派生:把源内容当作不可信数据交给当前客户端 LLM,响应必须是完整、唯一的结构化内容 envelope;JSON、SVG 等可校验格式必须在落盘前重新校验。结果写入新的本地路径和 manifest asset,不能直接写回源文件。Agent 回执原记录不转写、不删除,新 asset 以回执资源身份登记血缘。
|
||||
- 视频使用现役 `POST /api/editor/videos/generations`。有稳定远端引用时直接作为 `referenceVideoSrcs`,只有本地文件时先走 direct-upload ticket、OSS 表单上传和 confirm,再提交同一逻辑生成;结果必须下载到新的本地文件并登记远端稳定身份。
|
||||
- 音效和背景音乐分别使用现役 `POST /api/editor/audios/sound-effects/generations` 与 `POST /api/editor/audios/background-music/generations`。当前接口没有源音频引用字段,因此产品语义固定为“基于原资源语义的派生重制”,界面不得描述为对源波形的裁剪、变声或局部修改;结果仍必须引用源资源身份并保留原音频。
|
||||
- 视频使用 `POST /api/external/v1/editor/videos/generations`。有稳定远端引用时直接作为 `referenceVideoSrcs`,只有本地文件时先走 External direct-upload ticket、OSS 表单上传和 confirm,再提交同一逻辑生成;结果必须下载到新的本地文件并登记远端稳定身份。
|
||||
- 音效和背景音乐分别使用 `POST /api/external/v1/editor/audios/sound-effects/generations` 与 `POST /api/external/v1/editor/audios/background-music/generations`。当前接口没有源音频引用字段,因此产品语义固定为“基于原资源语义的派生重制”,界面不得描述为对源波形的裁剪、变声或局部修改;结果仍必须引用源资源身份并保留原音频。
|
||||
- 项目版本编辑固定追加 `parentVersionId` 指向源版本的子版本,继承源版本资源绑定并记录本轮编辑提示;已有版本数组元素不可修改、删除或重排。
|
||||
- 所有编辑结果固定创建新的本地 asset、文件路径、版本 ID 或资源身份。源 asset、源文件、Agent 回执与源版本不得删除、覆盖或复用;新 asset 的 `referenceResourceIds` 必须包含源资源规范身份,资源总览同时保留新旧资源。
|
||||
- 各类型统一使用稳定 `operationId / Idempotency-Key`。远端 `202` 只表示受理,必须轮询原 operation;提交结果未知或登录失效时保留原身份供恢复,不能换键重提。签名 URL 不得写入 manifest,manifest 只保存稳定 objectKey 对应的资源 / 资产身份。
|
||||
- 登录态图片 refine、视频、音效和背景音乐请求统一在 `generationInputs.source` 写入专用消费身份 `game-creator-resource-editor`;视频和音频登录态路由必须实际读取并传递同一稳定 `Idempotency-Key`,不能只由客户端发送后在路由层丢弃。队列完成态只向该消费身份返回经过裁剪的稳定 `objectKey / resource / asset` 引用和必要媒体元数据,不暴露 provider、worker、队列内部字段或临时签名 URL。
|
||||
- External 图片 refine、视频、音效和背景音乐请求统一在 `generationInputs.source` 写入专用消费身份 `game-creator-resource-editor`;所有 External 路由必须实际读取并传递同一稳定 `Idempotency-Key`。队列完成态只向该消费身份返回经过裁剪的稳定 `objectKey / resource / asset` 引用和必要媒体元数据,不暴露 provider、worker、队列内部字段或临时签名 URL。
|
||||
- 生成公开状态每次写入草稿都必须推进草稿 revision,并把最新 revision 同步到私有生成账本、进度事件、staging 与正式 commit;旧 UI 快照不得覆盖 accepted/running/reconciliation 状态。
|
||||
- operation 已受理或首次提交结果未知后遇到 401/403,不得写成 terminal failed。私有账本保留原 operation、原请求字节和原幂等身份,公开状态进入可恢复对账态;登录刷新后只继续同一 operation。
|
||||
- operation 已受理或首次提交结果未知后遇到 401/403,不得写成 terminal failed。私有账本保留原 operation、原请求字节和原幂等身份,公开状态进入可恢复对账态;修正 External Editor API Key 配置后只继续同一 operation。
|
||||
- 返回资源总览时,clean 草稿直接保留并退出;dirty 草稿必须使用独立确认面板提供“保留草稿并退出”和“放弃草稿”,默认保留。保留前必须等待当前保存或主动 flush,放弃才允许调用 discard。
|
||||
- 图层选择与图层缩放必须同时提供指针和键盘路径;不得嵌套 button/role=button。Enter/Space 可选择图层,缩放手柄使用原生 button 并提供方向键离散缩放。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user