修复AGC抠图结果下载登记与任务恢复

将抠图接入资源编辑账本,受理后轮询并下载透明PNG登记本地资源
保留任务身份及模式颜色参数,中断后恢复原任务并修正账号和开发者路由
补充完整链路、失败处理和恢复幂等测试,同步技能包指纹及技术文档
This commit is contained in:
2026-09-17 18:40:55 +08:00
parent 47d7c55058
commit 8f33dc9b78
10 changed files with 1077 additions and 245 deletions
@@ -14,7 +14,7 @@ Let the client derive projections from real disk changes and trusted tool result
3. Keep read scopes separate: `asset.list` is the current project manifest, `asset.library.list` is the signed-in account library, and the web project's canvas resource read model is the authoritative canvas list. The account library is not the complete canvas list.
4. Use `canvas.asset_import` for safe account/canvas asset IDs or project-relative local paths. The client rechecks ownership and validates bytes; host absolute paths require native UI file-picker authorization.
5. When the user explicitly asks to create or derive video, character animation, sound effect, or background music, call `agc_create_or_derive_resource`. Use `create` only for video/audio without a source and `derive` with a registered `sourceLocalAssetId`; character animation is always derived from an image.
6. When the user explicitly asks to remove an image background, call `agc_remove_background` with a registered image `sourceLocalAssetId` and `assetName`. Optional `backgroundMode` is `complex` (semantic foreground segmentation; default) or `flat` (solid-colour background removal). Prefer `flat` when the background is known to be solid. Only `flat` accepts optional `screenColor`: `auto`, `#RRGGBB`, or omitted for automatic detection by the service. Do not select a colour on behalf of `auto`. The client requires the signed-in account, owns canvas/folder context and task identity, and returns only bounded queue state.
6. When the user explicitly asks to remove an image background, call `agc_remove_background` with a registered image `sourceLocalAssetId` and `assetName`. Optional `backgroundMode` is `complex` (semantic foreground segmentation; default) or `flat` (solid-colour background removal). Prefer `flat` when the background is known to be solid. Only `flat` accepts optional `screenColor`: `auto`, `#RRGGBB`, or omitted for automatic detection by the service. Do not select a colour on behalf of `auto`. The client requires the signed-in account, owns canvas/folder context and task identity, waits for the accepted operation, downloads and registers the completed local asset, and preserves the operation for recovery when the remote result is not yet known.
7. Preserve existing relative paths when a small edit is sufficient so client resource identities remain stable.
8. Do not edit `.agent/manifest.json`, revision counters, version records, resource IDs, canvas identities, source provenance, generation ledgers, or browser receipts by hand.
9. Do not create a version when no game file changed. The client compares content fingerprints and advances revision only after an actual source change.
@@ -14,4 +14,6 @@ Read scopes remain separate: `asset.list` is the current project's local manifes
`agc_create_or_derive_resource` accepts only semantic intent. The client resolves `sourceLocalAssetId`, creates stable request identities, recovers matching pending operations, serializes paid submissions, writes supported media into the current canvas and same-name asset folder, validates downloaded bytes, commits the local manifest transaction, and returns redacted warnings. A tool error or timeout is not permission to generate again with a new identity.
`agc_remove_background` accepts a registered image `sourceLocalAssetId`, `assetName`, and optional `backgroundMode` and `screenColor`. `complex` uses semantic segmentation to identify the foreground; `flat` removes a solid-colour background. Prefer `flat` when the background is known to be solid; omitting the mode selects `complex`. Only `flat` accepts a colour: `auto`, `#RRGGBB`, or omitted for automatic service detection. Never infer a concrete colour for `auto`. Empty or invalid values and colour without `flat` are rejected. The client resolves the formal source resource, canvas/folder context, stable operation identity, idempotency key, and authenticated request. Ordinary account mode maps the External v1 shaped route to `/api/editor/images/background-removals`; ExternalDeveloper mode uses `/api/external/v1/editor/images/background-removals`. Mode and colour are part of request identity. Its result is bounded queue state; Codex must not poll internal workers, construct source URLs, or retry with a new identity after an uncertain response.
`agc_remove_background` accepts a registered image `sourceLocalAssetId`, `assetName`, and optional `backgroundMode` and `screenColor`. `complex` uses semantic segmentation to identify the foreground; `flat` removes a solid-colour background. Prefer `flat` when the background is known to be solid; omitting the mode selects `complex`. Only `flat` accepts a colour: `auto`, `#RRGGBB`, or omitted for automatic service detection. Never infer a concrete colour for `auto`. Empty or invalid values and colour without `flat` are rejected. The client resolves the formal source resource, canvas/folder context, stable operation identity, idempotency key, and authenticated request. Ordinary account mode maps the External v1 shaped route to `/api/editor/images/background-removals`; ExternalDeveloper mode uses `/api/external/v1/editor/images/background-removals`. Mode and colour are part of request identity. After acceptance, the client polls the authenticated generation status route, downloads the completed media, and commits it to the local manifest. If completion is unknown, it retains the same local operation for recovery; it never retries with a new identity or exposes internal worker details.
After an interrupted call, inspect `agc_list_registered_assets.pendingOperations`. Calling `agc_remove_background` again with the same source, name, mode, and colour resumes the matching pending operation. A submission marked `reconciliation-required` needs client-side reconciliation and cannot be automatically resumed. Do not change parameters to bypass a pending task. A queued receipt, fixed progress value, or absent local file does not establish that the background-removal provider is waiting in a queue; report only the observed state.
@@ -1,6 +1,6 @@
{
"schemaVersion": "agc-skill-pack.v1",
"version": "2026-08-26.18",
"version": "2026-08-26.21",
"skills": [
{
"name": "agc-game-production-workflow",
@@ -123,7 +123,7 @@
"agents/openai.yaml",
"references/projection-contract.md"
],
"sha256": "b6fa326e07ba48aa542fe3a1841e0fbc425656c6a41be4d575cf2e5c353a87b2"
"sha256": "8f4fb3de5601220436194a4e6eb7b3c5a2ef36ef91b50e2188f1e25c65fe02ff"
}
]
}
@@ -1282,7 +1282,10 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
.map(|asset| bridge_registered_resource(asset, include_sequence_frames))
.collect::<Vec<_>>();
let next_offset = (offset + resources.len() < total).then_some(offset + resources.len());
let pending = list_pending_local_project_resource_edits_at(
let platform_session = (editor_api_mode() == EditorApiMode::PlatformAccount)
.then(current_platform_session)
.flatten();
let pending = list_pending_local_project_resource_edits_for_session_at(
ListPendingLocalProjectResourceEditsInput {
project_path: root
.to_str()
@@ -1290,6 +1293,7 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
.to_string(),
expected_project_id: manifest.project_id,
},
platform_session.as_ref(),
)?
.into_iter()
.map(|edit| {
@@ -1299,6 +1303,8 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
"mode": edit.generation_mode,
"sourceResourceId": edit.source_resource_id,
"assetName": edit.asset_name,
"backgroundMode": edit.background_mode,
"screenColor": edit.screen_color,
"phase": edit.phase,
"createdAt": edit.created_at,
})
@@ -1760,8 +1766,8 @@ async fn bridge_import_account_assets(state: &DirectToolBridgeState, arguments:
fn bridge_completed_resource_result(
root: &Path,
kind: DirectResourceGenerationKind,
mode: DirectResourceGenerationMode,
kind: &str,
mode: &str,
result: DeriveLocalProjectResourceResult,
) -> Result<Value, String> {
let asset = result
@@ -1773,8 +1779,8 @@ fn bridge_completed_resource_result(
Ok(json!({
"status": "completed",
"operationId": result.operation_id,
"kind": kind.as_str(),
"mode": mode.as_str(),
"kind": kind,
"mode": mode,
"sourceResourceId": result.source_resource_id,
"committedProjectRevision": result.committed_project_revision,
"resource": bridge_registered_resource(asset, true),
@@ -1869,10 +1875,17 @@ async fn bridge_create_or_derive_resource(
source_version_id: None,
prompt: input.prompt.clone(),
asset_name: input.asset_name.clone(),
background_mode: None,
screen_color: None,
};
with_direct_editor_api_credentials(derive_local_project_resource_at(request)).await?
};
bridge_completed_resource_result(&state.root, input.kind, input.mode, completed)
bridge_completed_resource_result(
&state.root,
input.kind.as_str(),
input.mode.as_str(),
completed,
)
}
.await;
match result {
@@ -1886,6 +1899,7 @@ async fn bridge_create_or_derive_resource(
}
async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Value) -> Value {
let _generation_guard = state.resource_generation_gate.lock().await;
let result = with_direct_editor_api_credentials(async {
super::direct_tools_mcp::validate_remove_background_arguments(arguments)?;
enforce_project_permission_policy(&state.root, "canvas.asset_generate")?;
@@ -1907,69 +1921,71 @@ async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Val
if !source_asset.media_type.starts_with("image/") {
return Err("抠图工具只接受当前项目已登记的图片资源".to_string());
}
let source_resource_id = source_asset
.source
.resource_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty() && !value.starts_with("local-asset:"))
.ok_or_else(|| "图片资源缺少可供抠图服务使用的正式 resourceId".to_string())?
.to_string();
let (api_base_url, api_key, session) = resolve_canvas_sync_api_credentials(None, None)?;
let access = ExternalEditorBindingAccess::new(&api_base_url, &api_key, session.as_ref())?;
let client = crate::http_client::agc_main_site_client_builder()
.build()
.map_err(|_| "创建抠图服务连接失败".to_string())?;
let context =
prepare_external_canvas_generation_context(&state.root, &client, &access).await?;
let background_mode = background_mode.unwrap_or("complex").to_string();
let source_resource_id = bridge_asset_canonical_resource_id(source_asset);
let fingerprint = background_removal_request_fingerprint(
&source_asset_id,
&asset_name,
background_mode,
Some(background_mode.as_str()),
screen_color,
);
let (_operation_id, idempotency_key) = state.resource_request_ids(&fingerprint)?;
let route = access.api_route("/api/external/v1/editor/images/background-removals");
let request_body = background_removal_request_body(
&source_resource_id,
&context.project_id,
&source_asset.kind,
&context.asset_folder_id,
&asset_name,
background_mode,
screen_color,
);
let response = crate::http_client::with_agc_main_site_marker(
client
.post(format!("{}{}", api_base_url, route))
.bearer_auth(access.bearer_token())
.header("Idempotency-Key", idempotency_key)
.json(&request_body),
)
.send()
.await
.map_err(|error| format!("抠图服务提交失败:{error}"))?;
let status = response.status();
let payload = response
.json::<Value>()
.await
.map_err(|error| format!("抠图服务响应无法解析:{error}"))?;
if !status.is_success() {
if status == reqwest::StatusCode::UNAUTHORIZED {
return Err("authentication-required: 抠图服务提交失败:HTTP 401".to_string());
}
return Err(format!("抠图服务提交失败:HTTP {}", status.as_u16()));
let (_, _, platform_session) = resolve_canvas_sync_api_credentials(None, None)?;
let pending = list_pending_local_project_resource_edits_for_session_at(
ListPendingLocalProjectResourceEditsInput {
project_path: state.root.to_string_lossy().into_owned(),
expected_project_id: manifest.project_id.clone(),
},
platform_session.as_ref(),
)?;
let matching_pending = pending
.into_iter()
.filter(|pending| {
pending.edit_kind == LocalProjectResourceEditKind::BackgroundRemoval
&& (pending.source_asset_id.as_deref() == Some(source_asset_id.as_str())
|| pending.source_resource_id == format!("local-asset:{source_asset_id}"))
&& pending.asset_name == asset_name
&& pending.background_mode.as_deref().unwrap_or("complex")
== background_mode.as_str()
&& pending.screen_color.as_deref() == screen_color
})
.collect::<Vec<_>>();
if matching_pending.len() > 1 {
return Err("存在多个相同抠图 operation,必须先在客户端完成对账".to_string());
}
let queue_state =
background_removal_queue_state(&payload, access.frozen_platform_session().is_some())?;
Ok::<_, String>(json!({
"status": "queued",
"sourceLocalAssetId": source_asset_id,
"assetName": asset_name,
"projectId": context.project_id,
"assetFolderId": context.asset_folder_id,
"queueState": bridge_safe_queue_state(queue_state),
}))
let completed = if let Some(pending) = matching_pending.into_iter().next() {
resume_local_project_resource_edit_at(ResumeLocalProjectResourceEditInput {
project_path: state.root.to_string_lossy().into_owned(),
expected_project_id: manifest.project_id.clone(),
operation_id: pending.operation_id,
})
.await?
} else {
let (operation_id, idempotency_key) = state.resource_request_ids(&fingerprint)?;
let revision = read_game_creator_agent_runtime_project_revision(&state.root)?.revision;
let request = DeriveLocalProjectResourceInput {
project_path: state.root.to_string_lossy().into_owned(),
expected_project_id: manifest.project_id.clone(),
expected_project_revision: revision,
operation_id,
idempotency_key,
edit_kind: LocalProjectResourceEditKind::BackgroundRemoval,
generation_mode: LocalProjectResourceGenerationMode::Derive,
source_resource_id,
source_asset_id: Some(source_asset_id.clone()),
source_path: Some(source_asset.local_path.clone()),
source_media_type: Some(source_asset.media_type.clone()),
source_subtype: Some(source_asset.kind.clone()),
producer_task_id: source_asset.source.task_id.clone(),
source_version_id: None,
prompt: "去除背景".to_string(),
asset_name: asset_name.clone(),
background_mode: Some(background_mode),
screen_color: screen_color.map(str::to_string),
};
derive_local_project_resource_at(request).await?
};
emit_game_creator_manifest_invalidated(&state.root, "direct-background-removal");
bridge_completed_resource_result(&state.root, "background-removal", "derive", completed)
})
.await;
match result {
@@ -1996,57 +2012,6 @@ fn background_removal_request_fingerprint(
}
}
fn background_removal_request_body(
source_resource_id: &str,
remote_project_id: &str,
asset_kind: &str,
asset_folder_id: &str,
asset_name: &str,
background_mode: Option<&str>,
screen_color: Option<&str>,
) -> Value {
let mut body = json!({
"sourceImageSrc": source_resource_id,
"projectId": remote_project_id,
"assetKind": asset_kind,
"assetFolderId": asset_folder_id,
"assetLabel": asset_name,
"sourceResourceId": source_resource_id,
});
if background_mode == Some("flat") {
body["backgroundMode"] = json!("flat");
}
if let Some(color) = screen_color {
body["screenColor"] = json!(color);
}
body
}
fn background_removal_queue_state(
payload: &Value,
platform_account_route: bool,
) -> Result<Value, String> {
let data = external_editor_response_data(payload);
if platform_account_route {
return data
.get("queueState")
.cloned()
.ok_or_else(|| "抠图服务响应缺少 queueState".to_string());
}
Ok(data.clone())
}
fn bridge_safe_queue_state(value: Value) -> Value {
let object = value.as_object();
json!({
"operationId": object.and_then(|value| value.get("operationId")).and_then(Value::as_str),
"status": object.and_then(|value| value.get("status")).and_then(Value::as_str),
"phaseLabel": object.and_then(|value| value.get("phaseLabel")).and_then(Value::as_str),
"progress": object.and_then(|value| value.get("progress")).and_then(Value::as_u64),
"updatedAtMicros": object.and_then(|value| value.get("updatedAtMicros")).and_then(Value::as_u64),
})
}
fn bridge_art_resources(
root: &Path,
asset_paths: &[String],
@@ -3624,69 +3589,4 @@ mod tests {
);
}
}
#[test]
fn bridge_background_removal_queue_projection_is_bounded() {
let projection = bridge_safe_queue_state(json!({
"operationId": "background-removal-1",
"status": "queued",
"phaseLabel": "排队中",
"progress": 0,
"updatedAtMicros": 1,
"error": "private provider detail",
"signedUrl": "https://private.invalid/result"
}));
assert_eq!(projection["operationId"], "background-removal-1");
assert!(projection.get("error").is_none());
assert!(projection.get("signedUrl").is_none());
}
#[test]
fn background_removal_queue_state_matches_authentication_route_envelope() {
let platform_payload = json!({
"data": {
"queueState": {
"operationId": "platform-operation-1",
"status": "queued"
}
}
});
assert_eq!(
background_removal_queue_state(&platform_payload, true).expect("platform queue state"),
json!({
"operationId": "platform-operation-1",
"status": "queued"
})
);
let external_payload = json!({
"data": {
"operationId": "external-operation-1",
"status": "queued"
}
});
assert_eq!(
background_removal_queue_state(&external_payload, false).expect("external queue state"),
json!({
"operationId": "external-operation-1",
"status": "queued"
})
);
}
#[test]
fn background_removal_request_uses_bound_remote_project_id() {
let body = background_removal_request_body(
"editor-resource-source",
"remote-project-1",
"character",
"remote-folder-1",
"雄狮-透明底",
Some("complex"),
None,
);
assert_eq!(body["projectId"], "remote-project-1");
assert_ne!(body["projectId"], "local-project-1");
assert_eq!(body["assetFolderId"], "remote-folder-1");
}
}
File diff suppressed because it is too large Load Diff
@@ -419,6 +419,8 @@ type PendingLocalProjectResourceEdit = {
editKind: string;
sourceResourceId: string;
assetName: string;
backgroundMode?: string | null;
screenColor?: string | null;
phase: string;
createdAt: number;
};
@@ -461,6 +463,7 @@ type ResourceEditServiceIdentityConfirmation = {
function pendingResourceEditKindLabel(editKind: string) {
if (editKind === 'image') return '图片编辑';
if (editKind === 'background-removal') return '图片抠图';
if (editKind === 'text') return '文本编辑';
if (editKind === 'agent-result') return '智能体结果编辑';
return '资源编辑';
@@ -8826,3 +8826,10 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 边界:Deploy 阶段在远端 dev / release agent 执行,不受该上限约束。调整只动这两处:`systemctl set-property / revert jenkins.service``docker update --cpus=<n> gitea-runner` 加同步 compose(备份 `/opt/gitea-stack/compose.yml.bak-<时间戳>`)。
- 验证:限速后 `Genarrative-Full-Build-And-Deploy` #289 / #290 SUCCESS;采样期 Jenkins 峰值 10.2~10.5 核、限流不足 2s(可忽略),runner 峰值 12.07 核且持续出现 throttling,整机回落到 2.6%~19.8%。
- 关联文档:[开发运维](../../【开发运维】本地开发验证与生产运维-2026-05-15.md)。
## 2026-09-17 AGC 抠图接入本地资源编辑恢复闭环
- 背景:`agc_remove_background` 原先只提交 `/api/editor/images/background-removals` 并返回 `queued`,没有轮询远端任务、下载完成媒体或写入本地 manifestBgFilter 已成功处理但 Agent 因此永远只能看到受理回执。
- 决策:抠图作为 `LocalProjectResourceEditKind::BackgroundRemoval` 接入现有资源编辑账本,模式和背景色写入 operation 身份;提交后复用同一套轮询、结果下载、staging、manifest 提交和恢复逻辑。已有账本优先恢复,禁止在未知结果时换 operation/idempotency 重发。
- 边界:主站异步队列、BgFilter 和 SpacetimeDB schema 不变;Agent 只获得本地完成资源和安全身份投影,不接触内部 worker 或凭据。
- 恢复:已受理任务中断后按原 operation 续查;提交结果不确定时保留账本并人工对账。升级前无账本的 queued 回执不自动迁移或重发,已有远端成果通过正式素材导入恢复。
@@ -61,12 +61,25 @@ OpenAPI 与客户端工具的对外说明只描述模式用途、参数约束和
}
```
客户端保留旧参数调用;新字段不填时不改变旧调用语义。客户端不读取图片、不自动选色、不把 `auto` 改写为具体颜色,使用原有 Bearer 认证、幂等键和队列返回模型
客户端保留旧参数调用;新字段不填时不改变模式语义。客户端不自动选色、不把 `auto` 改写为具体颜色;源资源校验、认证、异步任务查询、结果下载和本地登记由客户端负责
工具 schema、桥接参数校验和随包 `agc-client-projection` Skill/契约说明必须保持一致。模式与颜色属于请求意图,必须参与客户端幂等指纹;同一图片与名称的不同模式不能复用同一次请求。缺省 complex 且没有颜色时保留既有指纹。主站在默认值归一化之前计算 External 请求指纹,缺失的新字段不序列化,避免旧请求重放发生冲突。
客户端提交前建立的本地项目绑定会返回主站远端 `projectId``assetFolderId`,抠图请求必须使用这两个远端身份;本地 manifest `projectId` 仅用于绑定和本地状态,不能直接提交给主站。
### 本地结果与恢复合同
`agc_remove_background` 复用本地资源编辑账本,类型为 `background-removal`。源图片保持不变,抠图结果作为新资源写入项目。模式和背景色随账本持久化并参与请求指纹,其它编辑类型的历史指纹保持不变。
1. 客户端建立源图片的正式资源绑定,在提交前保存 operation、幂等键和请求意图。普通登录态提交 `/api/editor/images/background-removals`,从 `data.queueState.operationId` 读取受理身份;开发者模式提交 External v1 对应路由,从 `data.operationId` 读取身份。
2. 受理后持续查询账号路由 `/api/runtime/external-generation/jobs/{operationId}` 或 External v1 对应状态路由。`queued``running` 只描述远端返回状态;固定进度值、本地文件缺失或 pending 清单为空均不能证明 BgFilter 排队。
3. 远端 completed 后按稳定资源身份换取有效下载 URL,校验结果为带 alpha 通道的有效 PNG,随后复用 staging、manifest 和 revision 提交。只在本地登记完成后向 Agent 返回 `completed``operationId``resource.localAssetId`、相对路径和安全告警,不暴露临时 URL 或凭据。
4. 轮询中断、超时或下载失败保留已受理 operation;`agc_list_registered_assets.pendingOperations` 与客户端恢复面板可见。相同源资源、结果名称、模式和颜色的后续调用优先恢复同一任务,不再次提交。不同账号不能恢复原账号任务;切回原账号后按既有恢复规则续接。
5. 远端 failed 明确失败;提交响应不确定且无法确认 operation 时进入人工对账状态,不自动换键重发。失败/未知均不得伪造透明图或自动切换本地抠图方式。
6. 升级前仅返回 queued、没有本地账本的任务不自动迁移;已有远端结果须通过正式资源查询和导入恢复,不据旧回执重新发起付费请求。
本修复只扩展 AGC 客户端现有工作流,不修改主站队列、BgFilter 或 SpacetimeDB schema。验收覆盖账号与开发者两种响应封装、queued/running/completed、已受理中断恢复不重复 POST、远端失败不登记结果,以及既有资源编辑回归。
## 实施任务
### 任务一:冻结 BgFilter 契约
@@ -95,6 +108,13 @@ OpenAPI 与客户端工具的对外说明只描述模式用途、参数约束和
## 验收证据
2026-09-17 客户端闭环验证:
- `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell project::resource_editor:: -- --test-threads=1` 通过。新增 HTTP fixture 覆盖开发者 queued/running/completed、账号 HTTP 200 queueState 与 `data.job`、换签下载、PNG alpha 校验、原图保留和新资源提交。
- 已受理任务的首次轮询失败后,pending 保留模式与颜色;恢复只查询原 operation,整个流程只 POST 一次,成功后清除 pending。远端 failed 不新增结果资源。既有账号隔离、提交原子性和崩溃恢复用例通过。
- `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell agent::direct_tool_bridge::tests -- --test-threads=1`、AGC 类型检查、技能包校验、Rust 格式、编码、文档索引与 diff 检查通过。
- 本轮未运行真实登录客户端 → 本地主站 → BgFilter 的端到端 smoke;当前本地后端已停止,自动化证据使用模拟 HTTP 服务。此前 BgFilter 成功日志只证明上游处理完成,不证明主站结果持久化或客户端导入成功。
2026-09-16 实测:
- 主站 `cargo test -p api-server background_removal`:36 项通过,覆盖非法请求入队前拒绝、缺省 complex、队列参数保留、旧请求指纹、父侧内部 RPC 和 provider multipart。
@@ -136,7 +136,7 @@ npm 游戏的可预览产物固定为对应 package 目录下的 `dist/index.htm
- 项目路径、projectId、当前 revision、源文件路径与媒体类型、operationId、Idempotency-Key、登录态、项目锁、付费提交、轮询恢复、下载校验与 manifest 事务全部由客户端持有。模型不能提交或覆盖这些字段。同一 Direct `clientTurnId + 规范语义参数` 生成稳定 UUID v4 身份;单回合同参重试复用原 operation,不同请求串行且最多四项。跨回合存在完全匹配的 pending 账本时优先恢复原 operation,不能换键重发。
- 资源查询同时投影未完成 operation 的安全状态。媒体工具成功只返回 operation、本地相对路径、资源类型、Canvas/resource/asset/task 身份、正式序列帧以及脱敏后的 `warnings / sliceWarnings`;错误继续使用统一脱敏边界。客户端资源账本持久化 completed 结果的两类告警,committed replay 不能把历史告警伪装成空集合。
- 角色动画、视频、音效和背景音乐在构造新的远端请求前统一准备当前项目同名画布与素材目录上下文,并在端点支持时携带 `projectId / assetFolderId / canvasCompletion`。角色动画 placeholder 使用源图片真实宽高,避免非方形角色进入画布时失真;正式 resource/asset 与序列帧继续直接复用 External 返回身份,不从首帧伪造重复资源。已有冻结 request body 或已受理 operation 保持不变,不因本次升级重建请求或重复扣费。
- 抠图通过新增 `agc_remove_background` 语义工具开放:模型只提交当前 manifest 的图片 `sourceLocalAssetId` 与结果名称;客户端解析稳定 `resourceId`,准备同名画布/素材目录并生成稳定 operation/idempotency 身份。普通登录态使用账号鉴权的 `/api/editor/images/background-removals`ExternalDeveloper 模式使用 External v1 `/api/external/v1/editor/images/background-removals`两者都只返回有界队列状态。抠图服务仍由客户端和服务端负责源校验、BgFilter、素材登记与画布事务,Codex 不获得内部 worker、凭据或任意 API 调用权。
- 抠图通过新增 `agc_remove_background` 语义工具开放:模型只提交当前 manifest 的图片 `sourceLocalAssetId` 与结果名称;客户端解析稳定 `resourceId`,准备同名画布/素材目录并生成稳定 operation/idempotency 身份。普通登录态使用账号鉴权的 `/api/editor/images/background-removals`ExternalDeveloper 模式使用 External v1 `/api/external/v1/editor/images/background-removals`客户端接收异步受理后轮询任务状态,下载完成媒体并登记到本地 manifest,未知结果保留同一 operation 供恢复。抠图服务仍由客户端和服务端负责源校验、BgFilter、素材登记与画布事务,Codex 不获得内部 worker、凭据或任意 API 调用权。
## 2026-08-23 AGC 资源生成补齐(视频 / 动画 / 音效 / 背景音乐)