Merge branch 'master' into opt/design-simplify
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m20s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m56s
Project CI / Backend tests (pull_request) Successful in 3m57s
Project CI / Frontend tests (pull_request) Successful in 2m35s
Project CI / Native shell tests (pull_request) Successful in 6m27s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m36s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m0s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m31s
Project CI / Repository checks (pull_request) Successful in 2m5s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m20s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m56s
Project CI / Backend tests (pull_request) Successful in 3m57s
Project CI / Frontend tests (pull_request) Successful in 2m35s
Project CI / Native shell tests (pull_request) Successful in 6m27s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m36s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m0s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m31s
Project CI / Repository checks (pull_request) Successful in 2m5s
This commit is contained in:
@@ -3144,27 +3144,16 @@ async fn generate_direct_taonier_art_asset_at(
|
||||
};
|
||||
let runtime_context =
|
||||
direct_taonier_art_generation_runtime_context(root, output_path, asset_kind)?;
|
||||
let generation = if retain_runtime_state {
|
||||
generate_platform_art_asset_with_retained_runtime_options_at(
|
||||
root,
|
||||
prompt,
|
||||
&[],
|
||||
&options,
|
||||
require_slices,
|
||||
&runtime_context,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
generate_platform_art_asset_with_runtime_options_at(
|
||||
root,
|
||||
prompt,
|
||||
&[],
|
||||
&options,
|
||||
require_slices,
|
||||
&runtime_context,
|
||||
)
|
||||
.await
|
||||
};
|
||||
let generation = generate_art_package_asset_with_runtime_options_at(
|
||||
root,
|
||||
prompt,
|
||||
&[],
|
||||
&options,
|
||||
require_slices,
|
||||
retain_runtime_state,
|
||||
&runtime_context,
|
||||
)
|
||||
.await;
|
||||
let generated = match generation {
|
||||
Ok(generated) => generated,
|
||||
Err(error)
|
||||
|
||||
@@ -18,8 +18,7 @@ pub(crate) use canvas_generation::submit_external_generation_request;
|
||||
pub(in crate::agent) use canvas_generation::{
|
||||
admit_platform_art_generation_at, commit_prepared_platform_art_asset_at,
|
||||
commit_prepared_platform_art_asset_strict_slices_at, generate_admitted_platform_art_asset_at,
|
||||
generate_platform_art_asset_with_retained_runtime_options_at,
|
||||
generate_platform_art_asset_with_runtime_options_at,
|
||||
generate_art_package_asset_with_runtime_options_at,
|
||||
platform_art_generation_error_result_unknown, recover_persisted_visual_generation_options,
|
||||
register_existing_platform_art_slices_at, request_platform_art_asset_with_runtime_options_at,
|
||||
restore_platform_art_asset_bytes_at,
|
||||
|
||||
@@ -2267,6 +2267,35 @@ fn decode_platform_art_image_bytes_with_limits(
|
||||
.map_err(|error| format!("{label}无法在安全内存边界内解码:{error}"))
|
||||
}
|
||||
|
||||
fn normalize_art_package_png(download: &mut CanvasResourceDownload) -> Result<(), String> {
|
||||
if download.bytes.len() > PLATFORM_ART_SPRITESHEET_SINGLE_DOWNLOAD_BYTES {
|
||||
return Err("美术包图片超过 20 MiB 本地校验上限".to_string());
|
||||
}
|
||||
let format = image::guess_format(&download.bytes)
|
||||
.map_err(|error| format!("美术包图片无法识别格式:{error}"))?;
|
||||
if !matches!(
|
||||
format,
|
||||
image::ImageFormat::Png | image::ImageFormat::Jpeg | image::ImageFormat::WebP
|
||||
) {
|
||||
return Err("美术包图片仅支持 PNG、JPEG、WebP".to_string());
|
||||
}
|
||||
// PNG 也必须完整解码;保留原字节以兼容既有摘要和恢复记录。
|
||||
let decoded = decode_platform_art_image_with_limits(download, "美术包图片")?;
|
||||
if format != image::ImageFormat::Png {
|
||||
let mut output = std::io::Cursor::new(Vec::new());
|
||||
decoded
|
||||
.write_to(&mut output, image::ImageFormat::Png)
|
||||
.map_err(|error| format!("美术包图片转为 PNG 失败:{error}"))?;
|
||||
let bytes = output.into_inner();
|
||||
if bytes.len() > PLATFORM_ART_SPRITESHEET_SINGLE_DOWNLOAD_BYTES {
|
||||
return Err("美术包图片转为 PNG 后超过 20 MiB 本地校验上限".to_string());
|
||||
}
|
||||
download.bytes = bytes;
|
||||
}
|
||||
download.media_type = "image/png".to_string();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(in crate::agent) struct ValidatedPlatformArtPng {
|
||||
pub(in crate::agent) width: u32,
|
||||
@@ -2555,6 +2584,7 @@ pub(in crate::agent) async fn generate_admitted_platform_art_asset_at(
|
||||
false,
|
||||
&context,
|
||||
Some(admission.guard),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -2735,20 +2765,19 @@ pub(in crate::agent) async fn generate_platform_art_asset_with_runtime_options_a
|
||||
false,
|
||||
runtime_context,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Installs a recovered or newly generated asset while retaining the durable
|
||||
/// request state. Package-level workflows use this to compensate local files
|
||||
/// after a later stage fails without losing the accepted operation identity or
|
||||
/// issuing another billable POST when the package is retried.
|
||||
pub(in crate::agent) async fn generate_platform_art_asset_with_retained_runtime_options_at(
|
||||
/// 美术包统一输出 PNG;重生成时保留账本,以便后续阶段失败后复用已付费结果。
|
||||
pub(in crate::agent) async fn generate_art_package_asset_with_runtime_options_at(
|
||||
root: &Path,
|
||||
prompt: &str,
|
||||
briefs: &[AgentGroupBrief],
|
||||
options: &PlatformArtAssetGenerationOptions,
|
||||
require_slices: bool,
|
||||
retain_runtime_state: bool,
|
||||
runtime_context: &PlatformArtGenerationRuntimeContext,
|
||||
) -> Result<GeneratedPlatformArtAsset, String> {
|
||||
generate_platform_art_asset_with_runtime_options_and_retention_at(
|
||||
@@ -2757,9 +2786,10 @@ pub(in crate::agent) async fn generate_platform_art_asset_with_retained_runtime_
|
||||
briefs,
|
||||
options,
|
||||
require_slices,
|
||||
true,
|
||||
retain_runtime_state,
|
||||
runtime_context,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -2909,6 +2939,7 @@ async fn generate_platform_art_asset_with_runtime_options_and_retention_at(
|
||||
retain_runtime_state: bool,
|
||||
runtime_context: &PlatformArtGenerationRuntimeContext,
|
||||
admitted_guard: Option<super::external_generation_state::DurablePlatformArtGenerationGuard>,
|
||||
normalize_package_png: bool,
|
||||
) -> Result<GeneratedPlatformArtAsset, String> {
|
||||
if require_slices && options.asset_kind != GameCreationAppAssetKind::IconSpritesheet {
|
||||
return Err("严格游戏切片生成只允许 icon-spritesheet 资产类型".to_string());
|
||||
@@ -2980,7 +3011,7 @@ async fn generate_platform_art_asset_with_runtime_options_and_retention_at(
|
||||
)?;
|
||||
recover_interrupted_strict_platform_art_transaction_locked_at(root, &recovery_lock)?;
|
||||
}
|
||||
let prepared = request_platform_art_asset_with_runtime_options_at(
|
||||
let mut prepared = request_platform_art_asset_with_runtime_options_at(
|
||||
root,
|
||||
prompt,
|
||||
briefs,
|
||||
@@ -2988,6 +3019,21 @@ async fn generate_platform_art_asset_with_runtime_options_and_retention_at(
|
||||
Some(runtime_context),
|
||||
)
|
||||
.await?;
|
||||
if normalize_package_png {
|
||||
normalize_art_package_png(&mut prepared.download)?;
|
||||
prepared.extension = "png".to_string();
|
||||
if options.asset_kind == GameCreationAppAssetKind::IconSpritesheet
|
||||
&& prepared.download.bytes.len()
|
||||
+ prepared
|
||||
.slices
|
||||
.iter()
|
||||
.map(|slice| slice.download.bytes.len())
|
||||
.sum::<usize>()
|
||||
> PLATFORM_ART_SPRITESHEET_TOTAL_DOWNLOAD_BYTES
|
||||
{
|
||||
return Err("美术包图集转码后与切片累计超过 32 MiB,已拒绝同步".to_string());
|
||||
}
|
||||
}
|
||||
prepared.validate_frozen_session()?;
|
||||
let lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||
root,
|
||||
@@ -10322,6 +10368,81 @@ mod canvas_generation_tests {
|
||||
rgba_test_png_with_quality(alpha, CompressionType::Fast, FilterType::Adaptive)
|
||||
}
|
||||
|
||||
fn art_package_test_image(format: image::ImageFormat) -> CanvasResourceDownload {
|
||||
let image = image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(
|
||||
2,
|
||||
2,
|
||||
image::Rgb([12, 34, 56]),
|
||||
));
|
||||
let mut output = std::io::Cursor::new(Vec::new());
|
||||
image.write_to(&mut output, format).expect("encode fixture");
|
||||
CanvasResourceDownload {
|
||||
bytes: output.into_inner(),
|
||||
media_type: format.to_mime_type().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn art_package_png_normalization_preserves_png_bytes_and_real_alpha() {
|
||||
let mut png = rgba_test_png(120);
|
||||
let original = png.bytes.clone();
|
||||
png.media_type = "application/octet-stream".to_string();
|
||||
normalize_art_package_png(&mut png).expect("validate existing PNG");
|
||||
assert_eq!(png.bytes, original);
|
||||
assert_eq!(png.media_type, "image/png");
|
||||
|
||||
let mut webp = std::io::Cursor::new(Vec::new());
|
||||
image::load_from_memory(&original)
|
||||
.unwrap()
|
||||
.write_to(&mut webp, image::ImageFormat::WebP)
|
||||
.unwrap();
|
||||
let mut webp = CanvasResourceDownload {
|
||||
bytes: webp.into_inner(),
|
||||
media_type: "image/webp".to_string(),
|
||||
};
|
||||
normalize_art_package_png(&mut webp).unwrap();
|
||||
let validated = validate_platform_art_png_bytes_with_limits(&webp.bytes, "test").unwrap();
|
||||
assert!(validated.has_transparent_pixels && validated.has_visible_pixels);
|
||||
assert_eq!(
|
||||
validated.pixel_sha256,
|
||||
validate_platform_art_png_bytes_with_limits(&original, "test")
|
||||
.unwrap()
|
||||
.pixel_sha256
|
||||
);
|
||||
|
||||
let mut jpeg = art_package_test_image(image::ImageFormat::Jpeg);
|
||||
normalize_art_package_png(&mut jpeg).unwrap();
|
||||
let (transparent, visible, _) = platform_art_spritesheet_alpha_contract(&jpeg).unwrap();
|
||||
assert!(!transparent, "JPEG 转码不能补造透明像素");
|
||||
assert!(visible);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn art_package_png_normalization_rejects_invalid_or_oversized_images() {
|
||||
let mut oversized = Vec::new();
|
||||
PngEncoder::new(&mut oversized)
|
||||
.write_image(
|
||||
&vec![0; (PLATFORM_ART_SPRITESHEET_MAX_DIMENSION as usize + 1) * 4],
|
||||
PLATFORM_ART_SPRITESHEET_MAX_DIMENSION + 1,
|
||||
1,
|
||||
ColorType::Rgba8.into(),
|
||||
)
|
||||
.unwrap();
|
||||
for bytes in [
|
||||
b"GIF89a".to_vec(),
|
||||
b"\x89PNG\r\n\x1a\n".to_vec(),
|
||||
vec![0xff, 0xd8, 0xff],
|
||||
oversized,
|
||||
vec![0; PLATFORM_ART_SPRITESHEET_SINGLE_DOWNLOAD_BYTES + 1],
|
||||
] {
|
||||
let mut download = CanvasResourceDownload {
|
||||
bytes,
|
||||
media_type: "image/png".to_string(),
|
||||
};
|
||||
assert!(normalize_art_package_png(&mut download).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
fn rgba_test_png_with_quality(
|
||||
alpha: u8,
|
||||
compression: CompressionType,
|
||||
@@ -11007,6 +11128,16 @@ mod canvas_generation_tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn retained_runtime_generation_retries_a_completed_stage_without_posting_again() {
|
||||
for format in [
|
||||
image::ImageFormat::Png,
|
||||
image::ImageFormat::Jpeg,
|
||||
image::ImageFormat::WebP,
|
||||
] {
|
||||
assert_art_package_recovers_completed_stage(format).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn assert_art_package_recovers_completed_stage(format: image::ImageFormat) {
|
||||
let temporary = tempfile::tempdir().expect("create retained runtime project");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "retained-runtime", "整包阶段恢复")
|
||||
@@ -11014,12 +11145,14 @@ mod canvas_generation_tests {
|
||||
let listener =
|
||||
std::net::TcpListener::bind("127.0.0.1:0").expect("bind retained download fixture");
|
||||
let base_url = format!("http://{}", listener.local_addr().expect("fixture address"));
|
||||
let image_url = format!("{base_url}/retained.png");
|
||||
let extension = format.extensions_str()[0];
|
||||
let image_url = format!("{base_url}/retained.{extension}");
|
||||
let signed_image_url = image_url.clone();
|
||||
let png = rgba_test_png(u8::MAX).bytes;
|
||||
let source = art_package_test_image(format);
|
||||
let source_bytes = source.bytes.clone();
|
||||
let (request_sender, request_receiver) = std::sync::mpsc::channel();
|
||||
let server = std::thread::spawn(move || {
|
||||
for _ in 0..4 {
|
||||
for request_index in 0..6 {
|
||||
let (mut stream, _) = listener.accept().expect("accept retained download");
|
||||
let request = read_test_http_request(&mut stream);
|
||||
request_sender
|
||||
@@ -11038,12 +11171,19 @@ mod canvas_generation_tests {
|
||||
)
|
||||
.expect("write retained signed URL");
|
||||
} else {
|
||||
// 首次下载有合法签名但内容损坏,重试必须仍使用同一已完成账本。
|
||||
let bytes = if request_index == 1 {
|
||||
&source.bytes[..16]
|
||||
} else {
|
||||
&source.bytes
|
||||
};
|
||||
write!(
|
||||
stream,
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
png.len()
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
source.media_type,
|
||||
bytes.len()
|
||||
)
|
||||
.and_then(|_| stream.write_all(&png))
|
||||
.and_then(|_| stream.write_all(bytes))
|
||||
.expect("write retained image");
|
||||
}
|
||||
}
|
||||
@@ -11102,7 +11242,7 @@ mod canvas_generation_tests {
|
||||
"resource": {
|
||||
"resourceId": "retained-resource",
|
||||
"projectId": "retained-canvas-project",
|
||||
"objectKey": "retained/spec.png",
|
||||
"objectKey": format!("retained/spec.{extension}"),
|
||||
"imageSrc": image_url
|
||||
},
|
||||
"taskId": "retained-task",
|
||||
@@ -11118,21 +11258,76 @@ mod canvas_generation_tests {
|
||||
"retained-runtime-key".to_string(),
|
||||
),
|
||||
async {
|
||||
for _ in 0..2 {
|
||||
generate_platform_art_asset_with_retained_runtime_options_at(
|
||||
let ledger_before =
|
||||
read_platform_art_generation_runtime_state(root, &runtime_context)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let failure = generate_art_package_asset_with_runtime_options_at(
|
||||
root,
|
||||
prompt,
|
||||
&[],
|
||||
&options,
|
||||
false,
|
||||
false,
|
||||
&runtime_context,
|
||||
)
|
||||
.await;
|
||||
assert!(failure.is_err());
|
||||
assert!(!root.join("assets/art-spec.png").exists());
|
||||
assert!(read_manifest_for_project(root).unwrap().assets.is_empty());
|
||||
let ledger_after =
|
||||
read_platform_art_generation_runtime_state(root, &runtime_context)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
platform_art_generation_runtime_idempotency_key(&ledger_before),
|
||||
platform_art_generation_runtime_idempotency_key(&ledger_after)
|
||||
);
|
||||
assert_eq!(
|
||||
platform_art_generation_runtime_retained_result(&ledger_before),
|
||||
platform_art_generation_runtime_retained_result(&ledger_after)
|
||||
);
|
||||
let mut installed_bytes = None;
|
||||
let mut installed_asset_id = None;
|
||||
for retain in [true, false] {
|
||||
generate_art_package_asset_with_runtime_options_at(
|
||||
root,
|
||||
prompt,
|
||||
&[],
|
||||
&options,
|
||||
false,
|
||||
retain,
|
||||
&runtime_context,
|
||||
)
|
||||
.await
|
||||
.expect("recover retained completed stage");
|
||||
assert!(
|
||||
let bytes = fs::read(root.join("assets/art-spec.png")).unwrap();
|
||||
let validated =
|
||||
validate_platform_art_png_bytes_with_limits(&bytes, "recovered").unwrap();
|
||||
assert_eq!((validated.width, validated.height), (2, 2));
|
||||
if format == image::ImageFormat::Png {
|
||||
assert_eq!(bytes, source_bytes);
|
||||
}
|
||||
if let Some(previous) = installed_bytes.replace(bytes) {
|
||||
assert_eq!(installed_bytes.as_ref().unwrap(), &previous);
|
||||
}
|
||||
let manifest = read_manifest_for_project(root).unwrap();
|
||||
assert_eq!(manifest.assets.len(), 1);
|
||||
let asset = &manifest.assets[0];
|
||||
assert_eq!(asset.local_path, "assets/art-spec.png");
|
||||
assert_eq!(asset.media_type, "image/png");
|
||||
assert_eq!(
|
||||
asset.source.resource_id.as_deref(),
|
||||
Some("retained-resource")
|
||||
);
|
||||
if let Some(previous) = installed_asset_id.replace(asset.id.clone()) {
|
||||
assert_eq!(asset.id, previous);
|
||||
}
|
||||
assert_eq!(
|
||||
read_platform_art_generation_runtime_state(root, &runtime_context)
|
||||
.expect("read retained stage ledger")
|
||||
.is_some()
|
||||
.is_some(),
|
||||
retain
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -11145,13 +11340,13 @@ mod canvas_generation_tests {
|
||||
.ok()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(requests.len(), 4);
|
||||
assert_eq!(requests.len(), 6);
|
||||
assert_eq!(
|
||||
requests
|
||||
.iter()
|
||||
.filter(|request| request.starts_with("GET /retained.png "))
|
||||
.filter(|request| request.starts_with(&format!("GET /retained.{extension} ")))
|
||||
.count(),
|
||||
2
|
||||
3
|
||||
);
|
||||
assert!(requests.iter().all(|request| request.starts_with("GET ")));
|
||||
assert!(requests.iter().all(|request| !request.starts_with("POST ")));
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 踩坑与排障记录
|
||||
|
||||
## 美术包固定 PNG 路径与平台返回格式不一致
|
||||
|
||||
- 平台生成成功仍可能返回 JPEG/WebP;直接按 `assets/art-spec.png` 等固定路径保存会在扩展名校验时失败,图片尚未落盘但已付费结果账本仍存在,不能据 manifest 为空认定没有生成结果。
|
||||
- 美术包专用入口在本地提交前有界解码并归一化为 PNG,原 PNG 字节不变;扩展名、MIME、内容摘要和恢复比较必须使用同一份最终字节。普通图片工具和独立切片的格式合同不随之放宽,转码也不能代替真实透明度校验。
|
||||
- 失败后保留原账本并按冻结意图恢复,不能删除账本后重新生成。详情见 [AGC 美术包合同](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#2026-08-23-direct-codex-美术包显式重生成与切片投影)。
|
||||
|
||||
## 2026-09-24 AGC 生成路由迁移必须核对账号队列契约
|
||||
|
||||
- AGC 普通账号会把 external editor 路由映射到站内入口;只验证 API Key 路由不足以证明客户端链路可用。
|
||||
|
||||
@@ -1493,6 +1493,9 @@ game-project/
|
||||
|
||||
## 2026-08-23 Direct Codex 美术包显式重生成与切片投影
|
||||
|
||||
- 标准美术包在客户端将规范图、背景图和主图集统一保存为 PNG:下载仍校验来源、声明类型与文件签名,随后按真实内容接受 PNG/JPEG/WebP,在已有 20 MiB、4096 像素单边和 64 MiB 解码内存限制内完整解码。有效 PNG 原样保留,JPEG/WebP 编码成 PNG,最终内容也不得超过 20 MiB;本地媒体类型固定为 `image/png`。转码不补造透明度,主图集与独立切片继续执行真实 alpha、可见像素、尺寸和唯一性合同;平台独立切片仍须为 PNG。此行为仅属于美术包,普通图片工具的指定扩展名合同不变。
|
||||
- 美术包的转码在项目提交锁和本地写入之前完成,文件摘要、已安装结果识别、替换恢复和 manifest 登记均使用最终 PNG。转码失败保留原生成账本与平台身份,同冻结意图重试重新读取已有结果,不提交新的付费生成;不改变请求快照、幂等身份、固定资源路径或旧 PNG 包的复用方式,无数据迁移。验收覆盖 JPEG/WebP 转码、PNG 字节不变、损坏/超限拒绝、真实透明度以及已有结果重复恢复零生成 POST;客户端真实 Provider 的整包验证单独记录。
|
||||
- 转码自动化验收由 `canvas_generation_tests` 的格式/边界用例和 `retained_runtime_generation_retries_a_completed_stage_without_posting_again` 本地 HTTP 夹具覆盖:PNG/JPEG/WebP 均先下载损坏内容,再从同一已完成账本恢复两次,核对最终 PNG、稳定本地 asset ID、Canvas 来源身份、账本保留/清理及零生成 POST;替换与补偿继续由现有图集事务和 Direct 重生成用例覆盖。真实 Provider 的客户端整包效果不由这些夹具替代。
|
||||
- `agc_tools.taonier_prepare_game_art` 的请求模式固定为 `reuse-or-create | regenerate`。缺省使用 `reuse-or-create`,完整且可信的本地包继续零付费复用;只有用户显式要求重做、替换或切换视觉风格时使用 `regenerate`,并绕过完整包短路,按规范图、背景图、透明图集顺序生成和原位替换。`regenerate` 的旧包前置门只要求规范图和背景图已经可下载、可解码、来源一致且存在可信 manifest 登记,使两项旧字节与登记可以完整 rollback;历史主图集、私有回执、公开清单或 canonical 切片可以缺失。客户端必须把八个严格路径的实际存在性和摘要,以及其中受管顶层 asset identity,逐项冻结为 `Present/Some` 或 `Missing/None`,不能把缺失状态伪造成空文件或虚假登记。规范图或背景图任一缺失或身份无效时才失败关闭并提示先用 `reuse-or-create` 修复基础素材。
|
||||
- 显式重生成不放宽 External Editor 幂等与未知态边界。固定阶段已有 `prepared / accepted` 账本时,本次生成 prompt 必须与账本冻结 prompt 一致才可恢复;不一致返回 `platform-generation-result-unknown` 并保留原 `Idempotency-Key / operationId` 对账,禁止把旧结果解释为新意图,也禁止另起付费 POST。
|
||||
- 整包重生成在首个付费阶段前建立客户端私有 v4 workflow,状态固定为 `resetting / in-progress / compensating / completed`,并同时绑定意图摘要和客户端稳定 `clientTurnId`。专用 `direct-codex-art` 跨进程执行锁覆盖整个付费重生成生命周期,但不持有通用项目写锁等待网络。规范图和背景图替换后立即持久化旧字节、旧 manifest entry 与本轮双 CAS 锚点;任一后续阶段失败时进入 `compensating`,可在进程重启后继续恢复旧文件及旧登记。已成功阶段的生成账本继续保留;`completed` 持久化经脱敏和数量 / 长度限制的完整工具结果,同一 `clientTurnId` 回包丢失时必须等值重放且零新 POST。新的显式用户回合先持久化目标回合所有的 `resetting` workflow,再清理上一轮三阶段账本并转回 `in-progress`,任一崩溃点都不得出现无 workflow 窗口。只有尚无任何阶段账本且无替换锚点的孤立 `in-progress` 空壳允许被新回合原子接管;其余身份冲突、未知版本以及缺少新恢复字段的旧 v2/v3 workflow 均失败关闭,不能用 serde 缺省值把旧状态升级成可执行状态。
|
||||
|
||||
Reference in New Issue
Block a user