修复美术包缺失图集的续生成与完成判定
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m18s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m33s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m33s
Project CI / Backend tests (pull_request) Successful in 8m53s
Project CI / Frontend tests (pull_request) Successful in 3m32s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m11s
Project CI / Native shell tests (pull_request) Successful in 9m59s
Project CI / Repository checks (pull_request) Successful in 8m34s

复用有效规范图和背景图,补齐缺失图集并优先恢复原阶段请求
统一要求三张主图有效才返回完成,失败保留基础素材并返回原因
增加补齐、恢复幂等、失败保留与不完整包拒绝的关键回归
同步美术包规范、验收证据和共享排障记忆
This commit is contained in:
2026-10-06 12:01:35 +01:00
parent ede5243191
commit 67225824e7
6 changed files with 585 additions and 136 deletions
@@ -3,6 +3,8 @@ use base64::Engine as _;
use std::io::Write;
use std::path::{Path, PathBuf};
#[cfg(test)]
mod art_package_tests;
mod user_input;
#[cfg(not(test))]
pub(crate) use user_input::enqueue_direct_codex_turn;
@@ -3250,35 +3252,31 @@ async fn generate_direct_taonier_art_asset_at(
fn direct_taonier_art_package_result(
root: &Path,
asset_paths: Vec<String>,
warnings: Vec<String>,
mut slice_warnings: Vec<String>,
) -> DirectTaonierArtPackage {
let includes_spritesheet = asset_paths
.iter()
.any(|path| path == DIRECT_CODEX_SPRITESHEET_ASSET_PATH);
if includes_spritesheet {
if let Err(error) = register_existing_platform_art_slices_at(root) {
slice_warnings.push(format!("现有图集切片补登记失败:{error}"));
}
) -> Result<DirectTaonierArtPackage, String> {
if !direct_taonier_art_package_is_valid(root) {
return Err("陶泥儿美术包未完成:规范图、背景图或核心图集缺失或不可验证".to_string());
}
let slice_paths = if includes_spritesheet && direct_taonier_art_package_is_valid(root) {
direct_registered_taonier_slice_paths(root)
} else {
Vec::new()
};
if includes_spritesheet && slice_paths.is_empty() {
if let Err(error) = register_existing_platform_art_slices_at(root) {
slice_warnings.push(format!("现有图集切片补登记失败:{error}"));
}
let slice_paths = direct_registered_taonier_slice_paths(root);
if slice_paths.is_empty() {
slice_warnings.push(
"当前图集没有可验证的独立切片;请查看总图后决定使用或进一步处理,不得猜测坐标或伪造素材身份"
.to_string(),
);
}
DirectTaonierArtPackage {
asset_paths,
Ok(DirectTaonierArtPackage {
asset_paths: DIRECT_CODEX_ART_ASSET_PATHS
.iter()
.map(|path| (*path).to_string())
.collect(),
slice_paths,
warnings,
slice_warnings,
}
})
}
pub(crate) async fn ensure_direct_taonier_art_package_at(
@@ -3305,15 +3303,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
emit_direct_game_creator_progress(root, "art.prepare", "正在检查陶泥儿美术包");
if !mode.regenerates_existing() && direct_taonier_art_package_is_valid(root) {
emit_direct_game_creator_progress(root, "art.ready", "陶泥儿美术包已就绪");
return Ok(direct_taonier_art_package_result(
root,
DIRECT_CODEX_ART_ASSET_PATHS
.iter()
.map(|path| (*path).to_string())
.collect(),
Vec::new(),
Vec::new(),
));
return direct_taonier_art_package_result(root, Vec::new(), Vec::new());
}
let mut regeneration_workflow = if mode.regenerates_existing() {
let invocation_id = active_turn_id_at(root)?;
@@ -3364,9 +3354,6 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
)
})
.flatten();
let existing_art_spec_and_background = existing_art_spec.as_ref().is_some_and(|art_spec| {
direct_taonier_background_asset_identity(root, Some(art_spec)).is_some()
});
let art_spec = match existing_art_spec {
Some(identity) => identity,
None => {
@@ -3499,51 +3486,23 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
}
if !mode.regenerates_existing() && direct_taonier_art_package_is_valid(root) {
emit_direct_game_creator_progress(root, "art.ready", "陶泥儿美术包已就绪");
return Ok(direct_taonier_art_package_result(
root,
DIRECT_CODEX_ART_ASSET_PATHS
.iter()
.map(|path| (*path).to_string())
.collect(),
warnings,
slice_warnings,
));
return direct_taonier_art_package_result(root, warnings, slice_warnings);
}
// 补齐缺失图集时优先恢复原请求,不能用远端资源查找绕过阶段账本。
let spritesheet_request_exists = game_creator_agent_runtime_external_generation_exists(
root,
DIRECT_CODEX_ART_AGENT_ID,
"art-spritesheet",
);
if !mode.regenerates_existing()
&& recover_direct_taonier_spritesheet_read_only_at(root, &art_spec).await?
&& !spritesheet_request_exists
&& recover_direct_taonier_spritesheet_read_only_at(root, &art_spec)
.await
.map_err(|error| format!("{error};规范图和背景图已保留,核心图集未完成"))?
{
if direct_taonier_art_package_is_valid(root) {
emit_direct_game_creator_progress(root, "art.ready", "陶泥儿美术包已就绪");
return Ok(direct_taonier_art_package_result(
root,
DIRECT_CODEX_ART_ASSET_PATHS
.iter()
.map(|path| (*path).to_string())
.collect(),
warnings,
slice_warnings,
));
}
}
if existing_art_spec_and_background {
if !direct_taonier_art_base_is_valid(root) {
return Err("当前项目已有的陶泥儿规范图或背景图不再满足可下载、可解码和来源一致性要求,已终止代码生成".to_string());
}
emit_direct_game_creator_progress(
root,
"art.ready",
"核心图集未能安全恢复,已使用现有陶泥儿规范图和背景图继续创作",
);
slice_warnings.push("未能安全恢复核心图集,已仅返回规范图和背景图".to_string());
return Ok(direct_taonier_art_package_result(
root,
vec![
DIRECT_CODEX_ART_SPEC_ASSET_PATH.to_string(),
DIRECT_CODEX_BACKGROUND_ASSET_PATH.to_string(),
],
warnings,
slice_warnings,
));
let package = direct_taonier_art_package_result(root, warnings, slice_warnings)?;
emit_direct_game_creator_progress(root, "art.ready", "陶泥儿美术包已就绪");
return Ok(package);
}
if let Some(workflow) = regeneration_workflow.as_mut() {
if let Err(error) = begin_direct_taonier_strict_spritesheet_commit_at(root, workflow) {
@@ -3571,6 +3530,10 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
{
Ok(outcome) => outcome,
Err(error) => {
// 补齐缺失图集失败时不回滚或重做已经有效的基础素材,保留原错误前缀。
if !mode.regenerates_existing() {
return Err(format!("{error};规范图和背景图已保留,核心图集未完成"));
}
return Err(direct_taonier_strict_spritesheet_stage_failure(
root,
regeneration_rollback.as_ref(),
@@ -3590,15 +3553,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
"{DIRECT_TAONIER_RESULT_UNKNOWN_PREFIX} 严格图集提交后整包身份未通过回读;已保留新结果、workflow 与三阶段账本,禁止局部补偿或自动重发"
));
}
let package = direct_taonier_art_package_result(
root,
DIRECT_CODEX_ART_ASSET_PATHS
.iter()
.map(|path| (*path).to_string())
.collect(),
warnings,
slice_warnings,
);
let package = direct_taonier_art_package_result(root, warnings, slice_warnings)?;
let package = if let Some(workflow) = regeneration_workflow.as_ref() {
complete_direct_taonier_regeneration_workflow_at(root, workflow, package)?
} else {
@@ -6432,16 +6387,13 @@ mod tests {
let completed_result = direct_taonier_art_package_result(
root.path(),
DIRECT_CODEX_ART_ASSET_PATHS
.iter()
.map(|path| (*path).to_string())
.collect(),
vec![format!(
"平台告警包含 {} 与 https://secret.example/path",
root.path().display()
)],
vec!["切片告警保持原样".to_string()],
);
)
.expect("complete package result");
let completed_result = complete_direct_taonier_regeneration_workflow_at(
root.path(),
&workflow,
@@ -6513,15 +6465,8 @@ mod tests {
.expect("record completed predecessor background identity");
persist_direct_taonier_regeneration_rollback_at(root.path(), &mut workflow, &rollback)
.expect("persist completed predecessor identities");
let package = direct_taonier_art_package_result(
root.path(),
DIRECT_CODEX_ART_ASSET_PATHS
.iter()
.map(|path| (*path).to_string())
.collect(),
Vec::new(),
Vec::new(),
);
let package = direct_taonier_art_package_result(root.path(), Vec::new(), Vec::new())
.expect("complete package result");
write_direct_taonier_spritesheet_stage_result_fixture(
root.path(),
"taonier-resource-icon-spec",
@@ -8128,15 +8073,8 @@ mod tests {
}
register_existing_platform_art_slices_at(root).unwrap();
assert!(direct_taonier_strict_art_package_is_valid(root));
let result = direct_taonier_art_package_result(
root,
DIRECT_CODEX_ART_ASSET_PATHS
.iter()
.map(|p| (*p).to_string())
.collect(),
vec![],
vec![],
);
let result = direct_taonier_art_package_result(root, vec![], vec![])
.expect("complete package result");
assert_eq!(
result.slice_paths,
(0..count)
@@ -8189,7 +8127,7 @@ mod tests {
}
}
fn write_direct_taonier_spritesheet_stage_result_fixture(
pub(super) fn write_direct_taonier_spritesheet_stage_result_fixture(
root: &Path,
art_spec_resource_id: &str,
spritesheet_resource_id: &str,
@@ -8403,7 +8341,7 @@ mod tests {
.expect("register replacement fixture");
}
fn register_direct_taonier_art_base_fixture(root: &Path) {
pub(super) fn register_direct_taonier_art_base_fixture(root: &Path) {
std::fs::create_dir_all(root.join("assets")).expect("assets dir");
register_direct_taonier_art_asset_fixture(
root,
@@ -8473,7 +8411,7 @@ mod tests {
bytes
}
fn tiny_strict_slice_png(index: usize) -> Vec<u8> {
pub(super) fn tiny_strict_slice_png(index: usize) -> Vec<u8> {
let red = 40_u8.saturating_add((index as u8).saturating_mul(40));
let mut pixels = image::RgbaImage::new(2, 1);
pixels.put_pixel(0, 0, image::Rgba([red, 96, 224, 255]));
@@ -8777,15 +8715,8 @@ mod tests {
register_direct_taonier_art_slice_entries_fixture(root.path());
std::fs::write(root.path().join("game/index.html"), "<!doctype html>").expect("index");
std::fs::write(root.path().join("game/style.css"), "body {} ").expect("style");
let package = direct_taonier_art_package_result(
root.path(),
DIRECT_CODEX_ART_ASSET_PATHS
.iter()
.map(|path| (*path).to_string())
.collect(),
Vec::new(),
Vec::new(),
);
let package = direct_taonier_art_package_result(root.path(), Vec::new(), Vec::new())
.expect("complete package result");
assert_eq!(package.slice_paths.len(), 4);
std::fs::write(
@@ -8812,15 +8743,9 @@ mod tests {
)
.expect("opaque slice");
assert!(direct_game_output_completion_error(root.path()).is_some());
let invalid_package = direct_taonier_art_package_result(
root.path(),
DIRECT_CODEX_ART_ASSET_PATHS
.iter()
.map(|path| (*path).to_string())
.collect(),
Vec::new(),
Vec::new(),
);
let invalid_package =
direct_taonier_art_package_result(root.path(), Vec::new(), Vec::new())
.expect("valid main images with unusable slices still complete");
assert!(invalid_package.slice_paths.is_empty());
assert!(invalid_package
.slice_warnings
@@ -8922,15 +8847,8 @@ mod tests {
.expect("remove slice registrations");
assert!(direct_taonier_art_package_is_valid(root.path()));
let package = direct_taonier_art_package_result(
root.path(),
DIRECT_CODEX_ART_ASSET_PATHS
.iter()
.map(|path| (*path).to_string())
.collect(),
Vec::new(),
Vec::new(),
);
let package = direct_taonier_art_package_result(root.path(), Vec::new(), Vec::new())
.expect("valid main images without slices still complete");
assert!(package.slice_paths.is_empty());
assert!(package.warnings.is_empty());
assert_eq!(package.slice_warnings.len(), 1);
@@ -1,6 +1,6 @@
# AGC 图集实际产物与语义识别实施计划
- Version: 1
- Version: 2
- Status: Implemented, Awaiting Acceptance
- Date: 2026-10-05
- Parent Spec: [里程碑](./【里程碑】AGC图集实际产物与语义识别-2026-10-05.md)
@@ -26,3 +26,18 @@
- 回环 HTTP 测试在允许监听端口的环境运行;真实付费 Provider 与视觉试玩未运行。
具体条款与证据见对应里程碑。
## 2026-10-06 验收缺口修正
- 用户已确认本轮只修复第二次调用的续生成与完成判据。范围为 Direct 美术包编排、结果构造、对应 Rust 测试和文档;第三次显式重生成的身份比较问题单独处理。
- 先删除已有基础图时返回两图的分支;已有图集阶段账本优先进入原恢复逻辑,没有账本才查询可恢复资源,查询错误直接返回。
- 美术包结果构造返回 Result,固定返回三张通过现有校验的主图;保持零切片告警和原有错误传递,补齐失败明确基础图保留状态。
- 新增五项回归,仅覆盖只生成缺失图集、prepared/accepted/已保留结果恢复、身份冲突或账本损坏、查询或生成失败,以及两图包不能返回完成;验证实际 POST 数量、冻结请求与基础图文件/登记不变。零片/多片和图片校验边界复用已有覆盖。
- 定向运行 Direct runtime、canvas generation、tool bridge / MCP 测试及 Rust 检查;运行格式、文档索引、编码和 diff 检查。每段约 30 分钟检查一次范围,不调用真实付费 Provider、不修改用户项目现场。回滚仅涉及本次代码与文档,无持久化格式迁移。
### 本次验证证据
- `cargo test --locked --offline --bin genarrative-ai-game-creator-shell direct_runtime -- --test-threads=1`:106 项通过,含五项新增回归。
- 同一测试可执行文件的 `canvas_generation_tests`、`direct_tool_bridge`、`mcp` 分组:118 / 44 / 53 项通过。
- `cargo check --locked --offline --bin genarrative-ai-game-creator-shell` 通过;现有编译告警未在本轮扩大处理。
- 修改文件的 rustfmt、编码、文档索引与 diff 检查通过。HTTP 夹具只访问本机回环端口;未调用真实 Provider,也未修改用户项目现场。
@@ -1,6 +1,6 @@
# AGC 图集实际产物与语义识别
- Version: 1
- Version: 2
- Status: Implemented, Awaiting Acceptance
- Date: 2026-10-05
- Parent Spec: [AGC 实施计划](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#2026-08-23-direct-codex-美术包显式重生成与切片投影)
@@ -11,6 +11,8 @@
必须移除客户端数量参数及按顺序赋义,按实际产物返回完整身份和可查看图片。风险项为动态集合事务、旧活动账本恢复与零切片告警;可选的自动语义分类、补切工作流不实施。服务端 API 与 OpenAPI 不变。
2026-10-06 用户复核发现复用两张基础图时跳过缺失图集、并将不完整包返回 completed;用户已评审补齐流程与完成判据方案并指示修复提交。本次为同一里程碑验收缺口修正,不处理显式重生成的参考资源 ID 比较错误,不改输入长度校验。
## 验收
1. 美术包及普通新请求完全不含 sliceCount;工具不接受该参数,保留模式和网格参数。
@@ -19,6 +21,8 @@
4. Agent 说明明确内容要求不是严格数量或用途保证,先看图再处理;本地产物通过既有工具登记。
5. 完成账本保留切分声明,历史零切片账本缺少声明时带告警保留总图;事务与重生成恢复核对动态集合;旧固定事务、旧用途文件和旧已付费请求继续可恢复且不增加生成 POST。
6. 定向测试、Rust 检查、提示词与 skill 包、文档索引、编码及 diff 检查通过;真实 Provider 未验证部分明确记录。
7. 已有规范图和背景图但缺图集时,仅补齐图集;已有请求优先恢复原身份,无请求才只读查找并按需生成。查询失败、身份冲突或账本损坏不能触发新的生成。
8. 整包只有三张主图有效才返回 completed;图集失败保留已有基础图并返回明确错误,零切片和实际多切片仍按原合同交付。恢复用例证明无新增付费请求,基础图的文件和登记保持不变。
## 验收证据
@@ -30,6 +34,8 @@
| 4 Agent 说明 | 工具与随包 skill 改为看图识别、按实际内容处理;skill-pack:check 与提示词测试通过 |
| 5 恢复与兼容 | 新旧请求三种状态及旧输出槽 HTTP 夹具保留原正文/幂等身份,已受理任务无新生成 POST;动态事务中断恢复、路径白名单、原固定合同与 Direct 重生成回归通过;动态包复用和快照漂移检测覆盖 0/2/4/6 片 |
| 6 工程检查 | 生成组 118、Direct 运行时 93、工具桥 44、MCP 32、账本 6、提示词 24,共 317 项 Rust 测试通过;cargo check、rustfmt、skill 包、文档索引、变更文件编码及 diff 检查通过 |
| 7 补齐与恢复(2026-10-06) | 五项新增回归覆盖只生成缺失图集、三种请求状态恢复、查询/生成失败、身份冲突/账本损坏和完成判据;断言只提交所需图集请求,已受理或已有结果不新增生成 POST,基础图文件与登记不变 |
| 8 完成与保留(2026-10-06) | 两图包结果构造返回错误;图集失败返回原原因及基础图保留状态;已有零片/多片、图片校验与重生成测试继续通过。此次 Direct 106、生成 118、工具桥 44、MCP 相关 53 项通过,Rust check、格式、编码、文档索引和 diff 检查通过 |
使用本地 HTTP 夹具验证生产入口,没有调用真实付费 Provider,也没有客户端视觉试玩;这些测试不证明素材语义正确。用户安装 PyYAML 6.0.3 后,使用 `python3` 重跑通用 skill `quick_validate.py`,随包 `taonier-art-assets` 校验通过(`Skill is valid!`);仓库 skill 包完整性检查和提示词/随包 skill 回归也已通过。原无参数全仓编码命令受沙箱子进程限制,按真实变更文件列表重跑通过。
@@ -6548,6 +6548,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 图集请求中的 sliceCount 不影响模型提示词,只会约束服务端后处理;客户端不再暴露或发送它,服务端 API 仍保留现状。四类内容需求不等于四个连通域,也不能按返回次序或历史 player 等文件名分配用途。
- 新切片按源资源摘要隔离、以中性序号保存;普通清单记录实际总图路径,美术包回执按实际集合验证。工具返回全部身份与路径标注预览,Agent 看图识别后使用或处理;零片仍交付有效总图与告警。
- 动态集合必须同时进入事务 journal、重生成快照、完整性检查和工具投影。旧数量请求需先定位原动作槽并使用原请求体、幂等键和 operation,候选不唯一则对账;不能删掉指纹字段后直接发起新付费请求。完成结果要保留 sliceMode/gridX/gridY,否则重放会丢失服务端回显。
- 美术包仅有规范图和背景图时,`reuse-or-create` 必须继续补齐图集,不能把两图包返回为 completed。补齐优先恢复现有阶段请求;无请求才只读查找并按需生成,查询失败、身份冲突或账本损坏不能当作资源不存在而重发。补齐失败保留有效基础图,三张主图齐全且可验证后才交付;零切片告警不等于缺失总图。
## 2026-10-05 在线游玩「一直黑屏」:加载面缺失 + 发行网关不压不发 ETag
@@ -1602,6 +1602,8 @@ game-project/
- 客户端图集入口不暴露或发送 `sliceCount`;保留 `sliceMode` 与网格参数。服务端 API、OpenAPI 及服务端数量限制保持现状。四类内容只是生成需求,不能推导四个连通域、切片数量或返回次序对应的语义。
- 美术包和普通图集按实际返回的 0–256 张切片保存、登记与返回。有效透明总图没有独立切片时仍完成并返回切片告警;Agent 必须看图确认实体、状态、用途及缺失内容,再决定使用、进一步本地处理或补充生成。已有授权、付费幂等与资产登记规则继续适用,不能虚构切片、坐标或 Canvas 身份。
- 美术包 `reuse-or-create` 复用有效规范图和背景图,继续补齐缺失图集。补齐时若图集阶段有持久化请求,优先恢复原请求、原幂等键和操作身份,不以远端资源查找绕过账本;没有请求时才尝试只读恢复已有图集,确认没有可恢复产物后才发起图集生成。查询失败、账本损坏或身份冲突返回错误并保留现场,不把失败解释为不存在而新建请求,也不重生成已有效的前两张图。完整本地包仍按已有规则直接复用。
- 美术包成功结果统一要求规范图、背景图和总图通过已有文件、来源与登记校验,并返回这三张主图及实际可验证切片;缺失任一主图时返回工具错误,不返回 `completed`。有效总图没有独立切片仍为完成并附切片告警。补齐图集失败时保留已有规范图、背景图和可恢复请求,明确告知图集未完成及真实原因;游戏工程本身允许复用已有基础素材的验收规则不随之收紧。
- 新切片使用源图集身份隔离的目录与中性序号,不再生成按 player 等用途命名的别名。普通图集的清单记录实际总图路径并保存在自己的目录;美术包公共清单与私有回执只证明来源、完整性和实际产物集合,不表达语义分类。历史四用途路径仅白名单兼容读取与恢复,文件名不能证明用途。
- 本地事务按固定合同文件与本次实际切片集合冻结快照、提交及恢复,保留摘要、真实 alpha、可见像素、唯一身份、项目归属、资源预算和 CAS 校验。重生成的旧快照包含实际旧切片;历史固定集合事务仍可恢复,不重写旧账本或请求体。普通调用升级前携带数量的活动请求必须在新 POST 前被定位并继续使用原身份;多个可能候选时失败关闭,不能任选或重复付费。
- 图集完成结果账本保留响应的 `sliceMode/gridX/gridY`,以支持同身份重放。历史账本已丢失切分声明且返回零切片时,声明无法用于证明切片方式;保留有效总图并明确告警,不因此重发生成。存在实际切片时仍要求切分声明一致。