Compare commits

..

17 Commits

Author SHA1 Message Date
lhk229 e8d5df4ccb 修复预览测试时序断言
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 6m51s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m6s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m39s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m12s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 6m1s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m38s
Project CI / Frontend tests (pull_request) Successful in 5m28s
Project CI / Repository checks (pull_request) Successful in 4m9s
Project CI / Backend tests (pull_request) Successful in 7m50s
Project CI / Native shell tests (pull_request) Successful in 7m49s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m47s
允许最后一个预览在被动 effect 调度中延后一轮提交
2026-09-17 01:13:24 +08:00
lhk229 965e80f374 Merge remote-tracking branch 'origin/master' into codex/ci-stability-20260917 2026-09-17 01:08:03 +08:00
lhk229 9b6ed8a4cf 整理抠图测试导入并明确对外说明边界
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m13s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m52s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m49s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m50s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 6m48s
Project CI / AI game creator shell Rust crates (push) Successful in 2m41s
Project CI / Frontend tests (push) Successful in 5m30s
Project CI / Repository checks (push) Successful in 4m22s
Project CI / Native shell tests (push) Successful in 9m3s
Project CI / Backend tests (push) Successful in 9m19s
Project CI / AI game creator shell web tests (push) Successful in 5m12s
将主站与客户端抠图测试模块的导入统一放到模块顶部
明确 provider 选择与兜底策略仅保留在内部技术方案中
2026-09-17 00:54:28 +08:00
lhk229 16deb0ad61 补齐抠图参数透传与验收
修复flat自动识别门禁并严格校验模式和颜色

保持旧请求幂等指纹并区分客户端抠图意图

同步工具说明和Skill契约并补充定向测试

记录真实BgFilter验证及本地数据库阻塞的待验收项
2026-09-17 00:54:28 +08:00
lhk229 13f56e644d 补充抠图模式调用说明
说明complex语义分割与flat纯色背景抠图

明确纯色背景优先使用flat并移除内部实现术语
2026-09-17 00:54:28 +08:00
lhk229 e08b031761 调整抠图模式参数说明 2026-09-17 00:54:28 +08:00
lhk229 70ad794b28 扩展AGC抠图客户端参数
支持complex与flat模式及auto背景色

保留旧客户端参数调用兼容性
2026-09-17 00:54:28 +08:00
lhk229 ca761e093c 接通抠图模式到BgFilter队列
透传backgroundMode和screenColor到内部worker

保留auto及省略颜色的下游识别语义
2026-09-17 00:54:28 +08:00
lhk229 a17367112f 增加主站抠图参数归一化
缺省模式兼容为complex并校验颜色格式

将auto颜色原样保留给BgFilter
2026-09-17 00:54:28 +08:00
lhk229 d7a0c3cffd 扩展主站抠图请求契约
增加backgroundMode和screenColor可选字段

同步External v1 OpenAPI抠图模式说明
2026-09-17 00:54:28 +08:00
lhk229 fa64819b6e 冻结BgFilter抠图服务契约
记录flat与complex模式及auto背景色行为

补充服务器认证头和当前运行版本
2026-09-17 00:54:28 +08:00
lhk229 f748d36d21 补充AGC抠图模式透传方案
新增主站与BgFilter模式及背景色契约

补充AGC客户端同步任务与兼容性规则

记录联调验收和发布回滚边界
2026-09-17 00:54:28 +08:00
lhk229 cd75a9e565 恢复CI测试并行度
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 6m45s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m52s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m9s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m43s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m51s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m34s
Project CI / Repository checks (pull_request) Successful in 4m30s
Project CI / Frontend tests (pull_request) Successful in 5m39s
Project CI / Native shell tests (pull_request) Successful in 8m46s
Project CI / Backend tests (pull_request) Successful in 9m38s
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
撤销全局单线程限制,避免无关前端测试变慢
2026-09-17 00:53:25 +08:00
lhk229 0388fa0912 稳定CI测试并发与工作流
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
为Project CI增加同分支并发取消,避免旧运行覆盖新结果

将Vitest默认线程数固定为1,降低异步UI测试竞态
2026-09-17 00:32:01 +08:00
lhk229 79b153e3fa 修复Direct活动回合重试类型错误
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 6m53s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m5s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m26s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m51s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m49s
Project CI / AI game creator shell Rust crates (push) Successful in 2m23s
Project CI / Frontend tests (push) Failing after 4m42s
Project CI / Repository checks (push) Successful in 4m44s
Project CI / Backend tests (push) Successful in 9m27s
Project CI / Native shell tests (push) Successful in 9m17s
Project CI / AI game creator shell web tests (push) Failing after 5m2s
重试等待 Promise 显式声明为 void,修复 shell typecheck 与原生构建失败。

补充卸载后取消读取重试的回归测试。
2026-09-16 21:53:19 +08:00
lhk229 362edcc49d 修复前端测试偶发超时与异步清理
Direct 活动回合读取失败后的重试定时器随组件卸载清理,避免测试环境销毁后访问 window。

后台素材查询大量缩略图测试合并可见性触发,并补充独立超时预算。

创作首页活动图与资源卡预览调度重载用例分别补充独立超时预算。

清单快照测试补齐 Direct 活动回合 IPC mock,避免未预期命令进入重试。
2026-09-16 21:53:19 +08:00
kdletters 1e434e0cb5 文档记录 CI 宿主 CPU 上限:Jenkins 16 核 / Gitea Actions runner 12 核
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 6m57s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m10s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m35s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m56s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m46s
Project CI / AI game creator shell Rust crates (push) Successful in 1m47s
Project CI / Repository checks (push) Successful in 3m34s
Project CI / Frontend tests (push) Successful in 4m25s
Project CI / Native shell tests (push) Successful in 7m56s
Project CI / AI game creator shell web tests (push) Successful in 4m32s
Project CI / Backend tests (push) Successful in 10m1s
- 在【开发运维】本地开发验证与生产运维新增「CI 宿主 CPU 上限」小节,写明 jenkins.service CPUQuota=1600% 与 gitea-stack runner cpus=12.0 的生效位置、核验命令和回滚方式
- 在 shared-memory/decision-log.md 补记该资源上限决策的背景、边界与验证证据
2026-09-16 21:17:30 +08:00
83 changed files with 1311 additions and 2160 deletions
-1
View File
@@ -164,7 +164,6 @@ module.exports = {
'server-rs/target-*',
'apps/desktop-shell/src-tauri/target',
'apps/ai-game-creator-shell/src/features/ui-editor/types/**',
'apps/ai-game-creator-shell/src/contracts/generated/**',
'apps/ai-game-creator-shell/src/features/project-workspace/generated/**',
'target',
'src/main.tsx',
+4
View File
@@ -7,6 +7,10 @@ on:
pull_request:
workflow_dispatch:
concurrency:
group: project-ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
-2
View File
@@ -4,8 +4,6 @@ node_modules
.codex-logs
public/Icons
apps/ai-game-creator-shell/src/features/ui-editor/types/
# ts-rs 直接落盘的生成产物:保持生成器原始格式,`cargo test export_bindings` 才可幂等复现。
apps/ai-game-creator-shell/src/contracts/generated/
apps/ai-game-creator-shell/src-tauri/resources/agc-skills/
media
*.log
@@ -38,6 +38,7 @@ vi.mock('../api/adminApiClient', () => ({
interface MockIntersectionObserverController {
enter: (target: Element) => void;
enterAll: (targets: Element[]) => void;
isObserved: (target: Element) => boolean;
}
@@ -106,6 +107,25 @@ function installIntersectionObserverMock(): MockIntersectionObserverController {
);
});
},
enterAll(targets) {
act(() => {
for (const target of targets) {
const record = observed.get(target);
if (!record) {
throw new Error('目标缩略图尚未进入 IntersectionObserver');
}
record.callback(
[
{
isIntersecting: true,
target,
} as IntersectionObserverEntry,
],
record.observer,
);
}
});
},
isObserved(target) {
return observed.has(target);
},
@@ -753,10 +773,10 @@ test('后台素材查询为大量同时可见的缩略图持续错峰换签', as
const thumbnails = entries.map((entry) =>
thumbnailElementForLabel(entry.label),
);
thumbnails.forEach((thumbnail) => {
for (const thumbnail of thumbnails) {
expect(observer.isObserved(thumbnail)).toBe(true);
observer.enter(thumbnail);
});
}
observer.enterAll(thumbnails);
await act(async () => {
await Promise.resolve();
});
@@ -776,7 +796,7 @@ test('后台素材查询为大量同时可见的缩略图持续错峰换签', as
await vi.advanceTimersByTimeAsync(200);
});
expect(getAdminAssetReadUrl).toHaveBeenCalledTimes(105);
});
}, 10_000);
test('后台素材查询读取更多后为新进入可视区域的素材换签', async () => {
const observer = installIntersectionObserverMock();
-2
View File
@@ -4900,8 +4900,6 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"tracing",
"ts-rs",
]
[[package]]
@@ -14,7 +14,7 @@ cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-i
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false, features = ["ts-bindings"] }
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
tauri-build = { version = "2.6.2", features = [] }
[dependencies]
@@ -50,7 +50,7 @@ portable-pty = "0.9"
percent-encoding = "2"
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "native-tls", "stream"] }
regex = "1"
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false, features = ["ts-bindings"] }
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
tauri = { version = "2.11.2", features = [] }
tauri-plugin-dialog = "2.7.1"
tauri-plugin-http = { version = "2.5.9", default-features = false, features = ["charset", "cookies", "http2", "rustls-tls"] }
@@ -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 an output name. 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, and returns only bounded queue state.
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,4 @@ 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` is the semantic image post-processing path. It accepts only a registered image `sourceLocalAssetId` and output name; the client resolves the formal source resource, canvas/folder context, stable operation identity, idempotency key, and authenticated External v1 `/api/external/v1/editor/images/background-removals` call. 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 External v1 `/api/external/v1/editor/images/background-removals` call. 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.
@@ -1,6 +1,6 @@
{
"schemaVersion": "agc-skill-pack.v1",
"version": "2026-08-26.16",
"version": "2026-08-26.17",
"skills": [
{
"name": "agc-game-production-workflow",
@@ -123,7 +123,7 @@
"agents/openai.yaml",
"references/projection-contract.md"
],
"sha256": "96b5bf9e2ed150bbe934a888867c1bb500b214a131f8b36c4830f51ca30267b6"
"sha256": "a929c27bc5b2b0bee0b7935e5c7b04ddbab1eb1804fe196f8c2537ad040ca5b1"
}
]
}
@@ -1,10 +1,6 @@
use super::model::{DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageItem};
use super::model::{DirectCodexUserContentPart, DirectCodexUserItem};
use super::validation::validate_direct_codex_user_item;
use crate::agent::{read_manifest_for_project, sanitize_attachment_local_path};
use crate::ui_editor::persistence::{
generate_ui_design_code_at, GenerateUiDesignCodeInput, UI_DESIGN_DOC_ASSET_KIND,
UI_DESIGN_DOC_MEDIA_TYPE,
};
use serde_json::Value;
use std::path::Path;
@@ -115,165 +111,29 @@ pub(crate) fn direct_codex_user_item_to_prompt(
item: &DirectCodexUserItem,
) -> Result<String, String> {
let wire = direct_codex_user_item_to_wire_input(root, item)?;
let DirectCodexUserItem::Message(message) = item;
let mut prompt = wire
.as_array()
wire.as_array()
.ok_or_else(|| "DirectProject user item wire input 不是数组".to_string())
.and_then(|parts| {
.map(|parts| {
parts
.iter()
.map(|part| {
part.get("text")
.and_then(Value::as_str)
.ok_or_else(|| "DirectProject user item wire part 缺少 text".to_string())
})
.collect::<Result<String, String>>()
})?;
if let Some(code_context) = render_ui_design_code_context(root, message)? {
prompt.push('\n');
prompt.push_str(&code_context);
}
if prompt.trim().is_empty() {
return Err("DirectProject user item 不能转换为空 prompt".to_string());
}
Ok(prompt)
}
/// 本轮 prompt 的 UI 设计文档引用上下文:复用 UI Editor 代码导出,把带文档注释的
/// JS 片段路径交给模型;生成失败只追加原始错误,不阻断本轮引用,其它引用继续处理。
///
/// 只在生成本轮 prompt 时展开:历史 item 回读走 `direct_codex_user_item_to_response_item`
/// 的纯投影,不得在这里产生项目写副作用。
fn render_ui_design_code_context(
root: &Path,
message: &DirectCodexUserMessageItem,
) -> Result<Option<String>, String> {
let referenced_ids = message
.content
.iter()
.filter_map(|part| match part {
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
Some(resource_id.trim())
}
_ => None,
.filter_map(|part| part.get("text").and_then(Value::as_str))
.collect::<String>()
})
.and_then(|prompt| {
if prompt.trim().is_empty() {
Err("DirectProject user item 不能转换为空 prompt".to_string())
} else {
Ok(prompt)
}
})
.collect::<Vec<_>>();
if referenced_ids.is_empty() {
return Ok(None);
}
let manifest = read_manifest_for_project(root)?;
let mut lines = Vec::new();
for resource_id in referenced_ids {
let is_ui_design_doc = manifest.assets.iter().any(|asset| {
asset.id == resource_id
&& asset.kind == UI_DESIGN_DOC_ASSET_KIND
&& asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE
});
if !is_ui_design_doc {
continue;
}
lines.push(
match generate_ui_design_code_at(GenerateUiDesignCodeInput {
project_path: root.to_string_lossy().into_owned(),
expected_project_id: manifest.project_id.clone(),
asset_id: resource_id.to_string(),
}) {
Ok(result) => format!("请先阅读生成的带有文档的代码片段: {}", result.relative_path),
Err(error) => format!("生成代码遇到错误{error}"),
},
);
}
if lines.is_empty() {
return Ok(None);
}
Ok(Some(lines.join("\n")))
}
#[cfg(test)]
mod tests {
use super::{direct_codex_user_item_to_prompt, direct_codex_user_item_to_response_item};
use crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE;
use super::direct_codex_user_item_to_response_item;
use serde_json::json;
use shared_contracts::game_creation_app::{
GameCreationAppAssetKind, GameCreationAppAssetSource, GameCreationAppAssetSourceKind,
};
use std::path::Path;
const PROMPT_CONTEXT_PROJECT_ID: &str = "direct-prompt-context-project";
fn prompt_context_project() -> tempfile::TempDir {
let directory = tempfile::tempdir().expect("temp project");
crate::init_local_game_project_at(
directory.path(),
PROMPT_CONTEXT_PROJECT_ID,
"Direct prompt 上下文测试",
)
.expect("init project");
directory
}
fn register_fixture_asset(
root: &Path,
relative_path: &str,
kind: GameCreationAppAssetKind,
media_type: &str,
) -> String {
let absolute_path = crate::resolve_local_project_path(root, relative_path)
.expect("resolve fixture asset path");
std::fs::create_dir_all(absolute_path.parent().expect("fixture asset parent"))
.expect("create fixture asset parent");
std::fs::write(&absolute_path, b"{}").expect("write fixture asset file");
crate::assets::register_local_asset_at(
root,
relative_path,
kind,
media_type,
"prompt-context-test",
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Generated,
canvas_project_id: None,
resource_id: Some("prompt-context".to_string()),
asset_object_id: None,
task_id: None,
prompt: None,
model: None,
generation_route: None,
generation_kind: None,
reference_resource_ids: Vec::new(),
},
)
.expect("register fixture asset")
.id
}
fn ui_design_doc_fixture(initialize_state: bool) -> (tempfile::TempDir, String) {
let directory = prompt_context_project();
let asset_id = register_fixture_asset(
directory.path(),
"ui/design.json",
GameCreationAppAssetKind::UiDesignDoc,
UI_DESIGN_DOC_MEDIA_TYPE,
);
if initialize_state {
crate::ui_editor::persistence::initialize_ui_design_state_at(
directory.path(),
PROMPT_CONTEXT_PROJECT_ID,
&asset_id,
)
.expect("initialize UI design state");
}
(directory, asset_id)
}
fn user_item_with_resource_reference(asset_id: &str) -> serde_json::Value {
json!({
"type": "message",
"role": "user",
"id": "turn-1:user",
"content": [{"type": "agc_resource_reference", "resourceId": asset_id}],
})
}
#[test]
fn standard_response_item_passes_through_without_agc_private_parts() {
let item = json!({
@@ -314,81 +174,4 @@ mod tests {
.expect_err("history item without type must fail");
assert!(error.contains("缺少 type"), "{error}");
}
#[test]
fn ui_design_doc_reference_appends_generated_code_context() {
let (project, asset_id) = ui_design_doc_fixture(true);
let item: super::DirectCodexUserItem =
serde_json::from_value(user_item_with_resource_reference(&asset_id))
.expect("canonical user item");
let prompt =
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
assert!(
prompt.contains(&format!(
"[素材引用 resourceId={asset_id};项目路径=ui/design.json]"
)),
"{prompt}"
);
assert!(
prompt.contains("请先阅读生成的带有文档的代码片段: ui/generated-"),
"{prompt}"
);
}
#[test]
fn history_projection_never_writes_generated_ui_design_code() {
let (project, asset_id) = ui_design_doc_fixture(true);
let item = user_item_with_resource_reference(&asset_id);
direct_codex_user_item_to_response_item(project.path(), &item).expect("history projection");
let generated_root =
crate::resolve_local_project_path(project.path(), "ui").expect("resolve ui directory");
let generated_files = std::fs::read_dir(generated_root)
.expect("read ui directory")
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.file_name()
.to_string_lossy()
.starts_with("generated-")
})
.count();
assert_eq!(
generated_files, 0,
"历史回读只做纯投影,不得生成 UI 设计代码"
);
}
#[test]
fn ui_design_generation_failure_keeps_reference_and_reports_error() {
let (project, asset_id) = ui_design_doc_fixture(false);
let item: super::DirectCodexUserItem =
serde_json::from_value(user_item_with_resource_reference(&asset_id))
.expect("canonical user item");
let prompt =
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
assert!(prompt.contains("生成代码遇到错误"), "{prompt}");
}
#[test]
fn other_asset_kind_reference_does_not_generate_ui_design_code() {
let project = prompt_context_project();
let asset_id = register_fixture_asset(
project.path(),
"assets/hero.png",
GameCreationAppAssetKind::Character,
"image/png",
);
let item: super::DirectCodexUserItem =
serde_json::from_value(user_item_with_resource_reference(&asset_id))
.expect("canonical user item");
let prompt =
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
assert!(
!prompt.contains("请先阅读生成的带有文档的代码片段"),
"{prompt}"
);
assert!(!prompt.contains("生成代码遇到错误"), "{prompt}");
}
}
@@ -69,31 +69,32 @@ const DIRECT_CODEX_ART_ASSET_PATHS: [&str; 3] = [
const DIRECT_CODEX_ART_AGENT_ID: &str = "direct-codex-art";
const DIRECT_CODEX_HOME_CREATE_PROJECT_MARKER: &str = "[[AGC_CREATE_PROJECT]]";
/// 直连 Codex 生成的游戏工程文件 → manifest 登记项。
///
/// 这些不是用户素材,而是游戏工程自身的源码/配置(`index.html`、`style.css`、`game.js`、
/// `package.json`、`package-lock.json`、`vite.config.js`)。它们的 manifest kind 统一是正式成员
/// `code``code` 与旧的 `game-*` / `image` 兜底一样派生 `unclassified`(「待归类」),
/// 不会把工程文件塞进内容栏目,同时写入侧不再产出非 canonical 的 kind。
fn direct_codex_game_outputs(root: &Path) -> Vec<(String, GameCreationAppAssetKind, &'static str)> {
fn direct_codex_game_outputs(root: &Path) -> Vec<(String, &'static str, &'static str)> {
let entry = agent_runtime_game_entry_relative_path(root);
let prefix = if entry == AGENT_RUNTIME_GAME_ENTRY_ROOT_PATH {
""
} else {
"game/"
};
let code = GameCreationAppAssetKind::Code;
vec![
(entry.to_string(), code, "text/html"),
(format!("{prefix}style.css"), code, "text/css"),
(format!("{prefix}game.js"), code, "text/javascript"),
(format!("{prefix}package.json"), code, "application/json"),
(entry.to_string(), "game-entry", "text/html"),
(format!("{prefix}style.css"), "game-style", "text/css"),
(format!("{prefix}game.js"), "game-script", "text/javascript"),
(
format!("{prefix}package-lock.json"),
code,
format!("{prefix}package.json"),
"game-package",
"application/json",
),
(format!("{prefix}vite.config.js"), code, "text/javascript"),
(
format!("{prefix}package-lock.json"),
"game-lockfile",
"application/json",
),
(
format!("{prefix}vite.config.js"),
"game-build-config",
"text/javascript",
),
]
}
@@ -937,7 +938,7 @@ fn direct_taonier_spritesheet_matches_retained_stage_result_at(
let Some(art_spec) = direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"/api/external/v1/editor/images/generations",
"spec",
None,
@@ -947,7 +948,7 @@ fn direct_taonier_spritesheet_matches_retained_stage_result_at(
let Some(spritesheet) = direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
GameCreationAppAssetKind::IconSpritesheet,
"art-spritesheet",
"/api/external/v1/editor/icon-spritesheets/generations",
"icon-spritesheet",
Some(&art_spec),
@@ -2500,7 +2501,7 @@ fn direct_taonier_art_generation_runtime_context(
fn direct_taonier_art_asset_identity(
root: &Path,
expected_path: &str,
expected_kind: GameCreationAppAssetKind,
expected_kind: &str,
expected_generation_route: &str,
expected_generation_kind: &str,
expected_reference_source: Option<&DirectTaonierArtAssetIdentity>,
@@ -2534,7 +2535,7 @@ fn direct_taonier_art_asset_identity(
local_asset_id: asset.id.clone(),
source_sha256: validated.content_sha256,
media_type: asset.media_type.clone(),
canonical_asset_kind: asset.kind.to_string(),
canonical_asset_kind: asset.kind.clone(),
resource_id: asset
.source
.resource_id
@@ -2608,7 +2609,7 @@ fn direct_taonier_art_base_is_valid(root: &Path) -> bool {
let Some(art_spec) = direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"/api/external/v1/editor/images/generations",
"spec",
None,
@@ -2618,7 +2619,7 @@ fn direct_taonier_art_base_is_valid(root: &Path) -> bool {
let Some(background) = direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"game-background",
"/api/external/v1/editor/images/generations",
"spec",
Some(&art_spec),
@@ -2633,7 +2634,7 @@ fn direct_taonier_art_package_is_valid(root: &Path) -> bool {
let Some(art_spec) = direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"/api/external/v1/editor/images/generations",
"spec",
None,
@@ -2646,7 +2647,7 @@ fn direct_taonier_art_package_is_valid(root: &Path) -> bool {
let Some(spritesheet) = direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
GameCreationAppAssetKind::IconSpritesheet,
"art-spritesheet",
"/api/external/v1/editor/icon-spritesheets/generations",
"icon-spritesheet",
Some(&art_spec),
@@ -2697,7 +2698,7 @@ fn direct_registered_taonier_slice_paths(root: &Path) -> Vec<String> {
let Some(art_spec) = direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"/api/external/v1/editor/images/generations",
"spec",
None,
@@ -2739,10 +2740,7 @@ fn direct_registered_taonier_slice_paths(root: &Path) -> Vec<String> {
return Vec::new();
};
if assets.next().is_some()
|| asset.kind != GameCreationAppAssetKind::Icon
|| !asset
.local_path
.starts_with("assets/art-spritesheet-slices/")
|| asset.kind != "art-spritesheet-slice"
|| asset.media_type != "image/png"
|| asset.source.kind != GameCreationAppAssetSourceKind::Canvas
|| asset.source.generation_route.as_deref()
@@ -3052,7 +3050,7 @@ fn snapshot_direct_spritesheet_recovery_manifest_for_install(
let current_art_spec = direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"/api/external/v1/editor/images/generations",
"spec",
None,
@@ -3071,7 +3069,7 @@ fn register_direct_recovered_spritesheet_at(
register_local_asset_at(
root,
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
GameCreationAppAssetKind::IconSpritesheet,
"art-spritesheet",
"image/png",
"platform-art-recovery",
source,
@@ -3108,7 +3106,7 @@ fn install_direct_recovered_spritesheet_binding_at(
&registered_asset.id,
content_sha256,
"image/png",
GameCreationAppAssetKind::IconSpritesheet.as_str(),
"art-spritesheet",
)?;
let binding = new_external_editor_resource_binding(
project_id,
@@ -3155,7 +3153,7 @@ async fn recover_direct_taonier_spritesheet_read_only_at(
&source_asset.id,
&format!("{:x}", Sha256::digest(&source_bytes)),
&source_asset.media_type,
source_asset.kind.as_str(),
&source_asset.kind,
)?;
let principal = external_editor_binding_principal(&access)?;
let Some(project_binding) =
@@ -3421,7 +3419,7 @@ async fn generate_direct_taonier_art_asset_at(
let art_spec = direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"/api/external/v1/editor/images/generations",
"spec",
None,
@@ -3581,7 +3579,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"/api/external/v1/editor/images/generations",
"spec",
None,
@@ -3592,7 +3590,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"game-background",
"/api/external/v1/editor/images/generations",
"spec",
Some(art_spec),
@@ -3658,7 +3656,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"/api/external/v1/editor/images/generations",
"spec",
None,
@@ -3676,7 +3674,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
|| direct_taonier_art_asset_identity(
root,
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"game-background",
"/api/external/v1/editor/images/generations",
"spec",
Some(&art_spec),
@@ -4416,7 +4414,7 @@ fn sync_direct_codex_project_file_projection_at(
register_local_asset_at(
root,
&local_path,
GameCreationAppAssetKind::Code,
"game-source",
media_type,
"direct-codex",
direct_codex_generated_source(),
@@ -7120,7 +7118,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"replacement-before-crash",
Vec::new(),
&replacement_spec,
@@ -7218,7 +7216,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"strict-crash-spec",
Vec::new(),
&new_spec,
@@ -7232,7 +7230,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"game-background",
"strict-crash-background",
vec!["strict-crash-spec".to_string()],
&new_background,
@@ -7448,7 +7446,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"replacement-before-crash",
Vec::new(),
&tiny_replacement_png(64, 96, 224),
@@ -7531,7 +7529,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"unknown-stage-replacement",
Vec::new(),
&tiny_replacement_png(224, 64, 32),
@@ -8130,7 +8128,7 @@ mod tests {
let art_spec = direct_taonier_art_asset_identity(
root.path(),
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"/api/external/v1/editor/images/generations",
"spec",
None,
@@ -8145,7 +8143,7 @@ mod tests {
let concurrent = register_local_asset_at(
root.path(),
concurrent_path,
GameCreationAppAssetKind::Image,
"illustration",
"image/png",
"uploaded",
GameCreationAppAssetSource {
@@ -8178,7 +8176,7 @@ mod tests {
let recovery = register_local_asset_at(
root.path(),
recovery_path,
GameCreationAppAssetKind::IconSpritesheet,
"art-spritesheet",
"image/png",
"platform-art-recovery",
GameCreationAppAssetSource {
@@ -8226,7 +8224,7 @@ mod tests {
let registered = register_local_asset_at(
root.path(),
local_path,
GameCreationAppAssetKind::IconSpritesheet,
"art-spritesheet",
"image/png",
"platform-art-recovery",
GameCreationAppAssetSource {
@@ -8343,18 +8341,14 @@ mod tests {
}));
}
fn register_direct_taonier_art_asset_fixture(
root: &Path,
local_path: &str,
kind: GameCreationAppAssetKind,
) {
fn register_direct_taonier_art_asset_fixture(root: &Path, local_path: &str, kind: &str) {
let (generation_route, generation_kind, reference_resource_ids) = match kind {
GameCreationAppAssetKind::IconSpritesheet => (
"art-spritesheet" => (
"/api/external/v1/editor/icon-spritesheets/generations",
"icon-spritesheet",
vec!["taonier-resource-icon-spec".to_string()],
),
GameCreationAppAssetKind::Scene => (
"game-background" => (
"/api/external/v1/editor/images/generations",
"spec",
vec!["taonier-resource-icon-spec".to_string()],
@@ -8365,14 +8359,6 @@ mod tests {
Vec::new(),
),
};
// 平台侧资源/对象/任务 id 按生成阶段命名(art-spec / game-background /
// art-spritesheet),与 manifest kind 是两套词汇;kind 收紧成枚举后不能让这些
// fixture 标识跟着改名。
let identity_slug = match kind {
GameCreationAppAssetKind::IconSpritesheet => "art-spritesheet",
GameCreationAppAssetKind::Scene => "game-background",
_ => kind.as_str(),
};
let mut png_bytes = Vec::new();
let image = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(
1,
@@ -8395,9 +8381,9 @@ mod tests {
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Canvas,
canvas_project_id: Some("taonier-project".to_string()),
resource_id: Some(format!("taonier-resource-{identity_slug}")),
asset_object_id: Some(format!("taonier-object-{identity_slug}")),
task_id: Some(format!("taonier-task-{identity_slug}")),
resource_id: Some(format!("taonier-resource-{kind}")),
asset_object_id: Some(format!("taonier-object-{kind}")),
task_id: Some(format!("taonier-task-{kind}")),
prompt: None,
model: Some("gpt-image-2".to_string()),
generation_route: Some(generation_route.to_string()),
@@ -8413,7 +8399,7 @@ mod tests {
register_direct_taonier_art_asset_fixture(
root,
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
GameCreationAppAssetKind::IconSpritesheet,
"art-spritesheet",
);
let main_bytes = std::fs::read(root.join(DIRECT_CODEX_SPRITESHEET_ASSET_PATH))
.expect("spritesheet bytes");
@@ -8490,7 +8476,7 @@ mod tests {
register_local_asset_at(
root,
path,
GameCreationAppAssetKind::Icon,
"art-spritesheet-slice",
"image/png",
"platform-art",
GameCreationAppAssetSource {
@@ -8602,7 +8588,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root,
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
GameCreationAppAssetKind::IconSpritesheet,
"art-spritesheet",
&spritesheet_resource_id,
vec![art_spec_resource_id.to_string()],
&spritesheet_bytes,
@@ -8689,20 +8675,19 @@ mod tests {
fn replace_direct_taonier_art_asset_fixture(
root: &Path,
local_path: &str,
kind: GameCreationAppAssetKind,
kind: &str,
resource_id: &str,
reference_resource_ids: Vec<String>,
bytes: &[u8],
) {
let (generation_route, generation_kind) =
if kind == GameCreationAppAssetKind::IconSpritesheet {
(
"/api/external/v1/editor/icon-spritesheets/generations",
"icon-spritesheet",
)
} else {
("/api/external/v1/editor/images/generations", "spec")
};
let (generation_route, generation_kind) = if kind == "art-spritesheet" {
(
"/api/external/v1/editor/icon-spritesheets/generations",
"icon-spritesheet",
)
} else {
("/api/external/v1/editor/images/generations", "spec")
};
std::fs::write(root.join(local_path), bytes).expect("replace fixture bytes");
register_local_asset_at(
root,
@@ -8731,12 +8716,12 @@ mod tests {
register_direct_taonier_art_asset_fixture(
root,
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
);
register_direct_taonier_art_asset_fixture(
root,
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"game-background",
);
}
@@ -9031,11 +9016,7 @@ mod tests {
init_local_game_project_at(root.path(), "direct-independent-image", "独立平台图片")
.expect("init project");
std::fs::create_dir_all(root.path().join("assets")).expect("assets dir");
register_direct_taonier_art_asset_fixture(
root.path(),
"assets/neon-mine.png",
GameCreationAppAssetKind::Image,
);
register_direct_taonier_art_asset_fixture(root.path(), "assets/neon-mine.png", "image");
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");
std::fs::write(
@@ -9243,7 +9224,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"replacement-spec",
Vec::new(),
&tiny_replacement_png(32, 64, 192),
@@ -9254,7 +9235,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"game-background",
"replacement-background",
vec!["replacement-spec".to_string()],
&tiny_replacement_png(16, 128, 224),
@@ -9342,7 +9323,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"durable-replacement-spec",
Vec::new(),
&tiny_replacement_png(32, 64, 192),
@@ -9355,7 +9336,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"game-background",
"durable-replacement-background",
vec!["durable-replacement-spec".to_string()],
&tiny_replacement_png(16, 128, 224),
@@ -9440,7 +9421,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"uncertain-committed-spec",
Vec::new(),
&replacement,
@@ -9500,7 +9481,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
GameCreationAppAssetKind::IconSpritesheet,
"art-spritesheet",
"uncertain-committed-spritesheet",
vec!["taonier-resource-icon-spec".to_string()],
&replacement,
@@ -9556,7 +9537,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"late-committed-spec",
Vec::new(),
&tiny_replacement_png(32, 64, 192),
@@ -9610,7 +9591,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"replacement-spec-before-background",
Vec::new(),
&tiny_replacement_png(32, 64, 192),
@@ -9629,7 +9610,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"game-background",
"late-committed-background",
vec!["replacement-spec-before-background".to_string()],
&tiny_replacement_png(16, 128, 224),
@@ -9674,7 +9655,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"replacement-spec",
Vec::new(),
&replacement_spec,
@@ -9685,7 +9666,7 @@ mod tests {
replace_direct_taonier_art_asset_fixture(
root.path(),
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
GameCreationAppAssetKind::Scene,
"game-background",
"replacement-background",
vec!["replacement-spec".to_string()],
&replacement_background,
@@ -1203,7 +1203,7 @@ fn bridge_registered_resource(
// (落盘值 + 按 kind 派生 + 读时自愈),这里走 Rust 的同构实现。
// 直接透传落盘 `asset.category` 会让 `kind:"ui"` 的资产在 UI 显示「UI 交互」、
// 在 Agent 侧读到 `unclassified`(真机 55 条分歧)。
"category": game_creation_app_asset_effective_category(asset.kind.as_str(), asset.category),
"category": game_creation_app_asset_effective_category(&asset.kind, asset.category),
"tags": asset.tags,
"canvasProjectId": asset.source.canvas_project_id,
"resourceId": asset.source.resource_id,
@@ -1244,8 +1244,7 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
arguments,
"kind",
DIRECT_TOOL_BRIDGE_MAX_RESOURCE_KIND_CHARS,
)?
.map(|kind| GameCreationAppAssetKind::parse_with_context(&kind, "asset.list.kind"));
)?;
let asset_id = bridge_optional_bounded_string(
arguments,
"assetId",
@@ -1265,7 +1264,7 @@ fn bridge_list_registered_assets(root: &Path, arguments: &Value) -> Value {
let mut assets = manifest
.assets
.iter()
.filter(|asset| kind.is_none_or(|kind| asset.kind == kind))
.filter(|asset| kind.as_ref().is_none_or(|kind| asset.kind == *kind))
.filter(|asset| {
asset_id
.as_ref()
@@ -1863,7 +1862,7 @@ async fn bridge_create_or_derive_resource(
source_asset_id: source_asset.as_ref().map(|asset| asset.id.clone()),
source_path: source_asset.as_ref().map(|asset| asset.local_path.clone()),
source_media_type: source_asset.as_ref().map(|asset| asset.media_type.clone()),
source_subtype: source_asset.as_ref().map(|asset| asset.kind.to_string()),
source_subtype: source_asset.as_ref().map(|asset| asset.kind.clone()),
producer_task_id: source_asset
.as_ref()
.and_then(|asset| asset.source.task_id.clone()),
@@ -1888,7 +1887,7 @@ async fn bridge_create_or_derive_resource(
async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Value) -> Value {
let result = async {
bridge_reject_unknown_fields(arguments, &["sourceLocalAssetId", "assetName"])?;
super::direct_tools_mcp::validate_remove_background_arguments(arguments)?;
enforce_project_permission_policy(&state.root, "canvas.asset_generate")?;
enforce_project_permission_policy(&state.root, "asset.register")?;
let source_asset_id = bridge_bounded_string(arguments, "sourceLocalAssetId", 80)?;
@@ -1897,6 +1896,8 @@ async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Val
"assetName",
DIRECT_TOOL_BRIDGE_MAX_RESOURCE_NAME_CHARS,
)?;
let background_mode = arguments.get("backgroundMode").and_then(Value::as_str);
let screen_color = arguments.get("screenColor").and_then(Value::as_str);
let manifest = read_existing_manifest_for_project(&state.root)?;
let source_asset = manifest
.assets
@@ -1921,22 +1922,34 @@ async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Val
.map_err(|_| "创建抠图服务连接失败".to_string())?;
let context =
prepare_external_canvas_generation_context(&state.root, &client, &access).await?;
let fingerprint = format!("{}\0{}", source_asset_id, asset_name);
let fingerprint = background_removal_request_fingerprint(
&source_asset_id,
&asset_name,
background_mode,
screen_color,
);
let (_operation_id, idempotency_key) = state.resource_request_ids(&fingerprint)?;
let route = "/api/external/v1/editor/images/background-removals";
let mut request_body = json!({
"sourceImageSrc": source_resource_id,
"projectId": manifest.project_id,
"assetKind": source_asset.kind,
"assetFolderId": context.asset_folder_id,
"assetLabel": asset_name,
"sourceResourceId": source_resource_id,
});
if background_mode == Some("flat") {
request_body["backgroundMode"] = json!("flat");
}
if let Some(color) = screen_color {
request_body["screenColor"] = json!(color);
}
let response = crate::http_client::with_agc_main_site_marker(
client
.post(format!("{}{}", api_base_url, route))
.bearer_auth(api_key)
.header("Idempotency-Key", idempotency_key)
.json(&json!({
"sourceImageSrc": source_resource_id,
"projectId": manifest.project_id,
"assetKind": source_asset.kind,
"assetFolderId": context.asset_folder_id,
"assetLabel": asset_name,
"sourceResourceId": source_resource_id,
})),
.json(&request_body),
)
.send()
.await
@@ -1973,6 +1986,20 @@ async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Val
}
}
fn background_removal_request_fingerprint(
source: &str,
name: &str,
mode: Option<&str>,
color: Option<&str>,
) -> String {
let mode = mode.unwrap_or("complex");
if mode == "complex" && color.is_none() {
format!("{source}\0{name}")
} else {
format!("{source}\0{name}\0{mode}\0{}", color.unwrap_or(""))
}
}
fn bridge_safe_queue_state(value: Value) -> Value {
let object = value.as_object();
json!({
@@ -2704,6 +2731,29 @@ pub(crate) async fn start_direct_tool_bridge(
#[cfg(test)]
mod tests {
#[test]
fn remove_background_identity_preserves_default_and_distinguishes_options() {
let legacy = "asset-1\0透明图";
assert_eq!(
background_removal_request_fingerprint("asset-1", "透明图", None, None),
legacy
);
assert_eq!(
background_removal_request_fingerprint("asset-1", "透明图", Some("complex"), None),
legacy
);
let mut identities = std::collections::HashSet::new();
identities.insert(legacy.to_string());
for color in [None, Some("auto"), Some("#CFEFFF"), Some("#112233")] {
let id =
background_removal_request_fingerprint("asset-1", "透明图", Some("flat"), color);
assert_eq!(
id,
background_removal_request_fingerprint("asset-1", "透明图", Some("flat"), color)
);
assert!(identities.insert(id));
}
}
use super::*;
use std::io::{Cursor, Read, Write};
@@ -3324,7 +3374,7 @@ mod tests {
fn bridge_art_resource_exposes_only_the_safe_identity_projection() {
let asset = GameCreationAppAssetManifestEntry {
id: "local-art-1".to_string(),
kind: GameCreationAppAssetKind::Icon,
kind: "art-spritesheet-slice".to_string(),
media_type: "image/png".to_string(),
local_path: "assets/art-spritesheet-slices/player.png".to_string(),
image_sequence_frames: None,
@@ -3381,7 +3431,7 @@ mod tests {
fn bridge_registered_resource_exposes_formal_sequence_without_private_generation_fields() {
let asset = GameCreationAppAssetManifestEntry {
id: "animation-1".to_string(),
kind: GameCreationAppAssetKind::CharacterAnimation,
kind: "character-animation".to_string(),
media_type: "video/mp4".to_string(),
local_path: "assets/edits/animation.mp4".to_string(),
image_sequence_frames: Some(vec![
@@ -3432,7 +3482,7 @@ mod tests {
fn bridge_registered_resource_keeps_explicit_manifest_classification() {
let asset = GameCreationAppAssetManifestEntry {
id: "spec-1".to_string(),
kind: GameCreationAppAssetKind::Spec,
kind: "spec".to_string(),
media_type: "application/json".to_string(),
local_path: "assets/specs/hero.json".to_string(),
image_sequence_frames: None,
@@ -3468,13 +3518,13 @@ mod tests {
///
/// UI 栏目走 TS 的 `gameCreationAppAssetCategory`(落盘值 + 按 kind 派生 + 读时自愈),
/// Agent 走 Rust 的同构实现 `game_creation_app_asset_effective_category`。直接透传落盘
/// `category` 会让 `ui-design` 的资产在 UI 显示「UI 交互」、在 Agent 侧读到
/// `unclassified`,两侧分类口径必须一致
/// `category` 会让 `kind:"ui"` 的资产在 UI 显示「UI 交互」、在 Agent 侧读到
/// `unclassified`——真机 122 条资产里有 55 条这样分叉
#[test]
fn bridge_registered_resource_projects_effective_category_not_raw_persisted_value() {
let asset = GameCreationAppAssetManifestEntry {
id: "ui-design-1".to_string(),
kind: GameCreationAppAssetKind::UiDesign,
id: "ui-1".to_string(),
kind: "ui".to_string(),
media_type: "application/json".to_string(),
local_path: "assets/UI 设计 1.json".to_string(),
image_sequence_frames: None,
@@ -3497,7 +3547,7 @@ mod tests {
};
let projection = bridge_registered_resource(&asset, false);
assert_eq!(projection["kind"], "ui-design");
assert_eq!(projection["kind"], "ui");
assert_eq!(projection["category"], "ui-interaction");
}
@@ -435,7 +435,7 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
}),
json!({
"name": "agc_remove_background",
"description": "为当前项目已登记的图片资源去除背景。客户端使用当前登录账号的抠图服务、项目画布和素材目录,模型只能提供已登记资源身份与结果名称;不会返回 Token、内部路由、宿主路径或临时签名 URL。",
"description": "为当前项目已登记的图片资源去除背景。complex 通过语义分割识别前景;flat 用于纯色背景抠图,确定背景为纯色时优先选择 flat。提供资源身份、结果名称及可选模式和背景色;客户端管理登录、项目画布和素材目录,不返回 Token、内部路由、宿主路径或临时签名 URL。",
"inputSchema": {
"type": "object",
"properties": {
@@ -449,6 +449,16 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
"type": "string",
"minLength": 1,
"maxLength": DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS
},
"backgroundMode": {
"type": "string",
"enum": ["complex", "flat"],
"description": "可选抠图模式:complex 用语义分割识别前景,flat 用纯色背景抠图;确定背景为纯色时优先使用 flat。省略时使用 complex"
},
"screenColor": {
"type": "string",
"pattern": "^(auto|#[0-9A-Fa-f]{6})$",
"description": "flat 模式可选背景色;传 auto 或 #RRGGBB,省略时由服务自动检测"
}
},
"required": ["sourceLocalAssetId", "assetName"],
@@ -881,14 +891,46 @@ fn validate_resource_generation_arguments(arguments: &Value) -> Result<(), Strin
}
}
fn validate_remove_background_arguments(arguments: &Value) -> Result<(), String> {
validate_tool_object_fields(arguments, &["sourceLocalAssetId", "assetName"])?;
pub(super) fn validate_remove_background_arguments(arguments: &Value) -> Result<(), String> {
validate_tool_object_fields(
arguments,
&[
"sourceLocalAssetId",
"assetName",
"backgroundMode",
"screenColor",
],
)?;
bounded_tool_string(arguments, "sourceLocalAssetId", 80)?;
bounded_tool_string(
arguments,
"assetName",
DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS,
)?;
if let Some(mode) = arguments.get("backgroundMode") {
let mode = mode
.as_str()
.ok_or_else(|| "backgroundMode 必须是 complex 或 flat".to_string())?;
if mode != "complex" && mode != "flat" {
return Err("backgroundMode 必须是 complex 或 flat".to_string());
}
}
if let Some(color) = arguments.get("screenColor") {
let color = color
.as_str()
.ok_or_else(|| "screenColor 必须是 auto 或 #RRGGBB".to_string())?;
let valid_hex = color.len() == 7
&& color.starts_with('#')
&& color[1..]
.chars()
.all(|character| character.is_ascii_hexdigit());
if color != "auto" && !valid_hex {
return Err("screenColor 必须是 auto 或 #RRGGBB".to_string());
}
if arguments.get("backgroundMode").and_then(Value::as_str) != Some("flat") {
return Err("complex 模式不能传 screenColor".to_string());
}
}
Ok(())
}
@@ -1849,6 +1891,51 @@ pub(crate) fn stop_game_creator_external_mcp() -> Result<(), String> {
mod tests {
use super::*;
#[test]
fn remove_background_arguments_enforce_mode_color_contract() {
for fields in [
json!({}),
json!({"backgroundMode":"complex"}),
json!({"backgroundMode":"flat"}),
json!({"backgroundMode":"flat","screenColor":"auto"}),
json!({"backgroundMode":"flat","screenColor":"#Ab12EF"}),
] {
let mut arguments = json!({"sourceLocalAssetId":"asset-1","assetName":"透明图"});
arguments
.as_object_mut()
.unwrap()
.extend(fields.as_object().unwrap().clone());
assert!(
validate_remove_background_arguments(&arguments).is_ok(),
"{fields}"
);
}
for fields in [
json!({"screenColor":"auto"}),
json!({"backgroundMode":"complex","screenColor":"auto"}),
json!({"backgroundMode":"flat","screenColor":""}),
json!({"backgroundMode":"flat","screenColor":" auto "}),
json!({"backgroundMode":"flat","screenColor":"AUTO"}),
json!({"backgroundMode":"flat","screenColor":"#GGGGGG"}),
json!({"backgroundMode":"flat","screenColor":null}),
json!({"backgroundMode":"flat","screenColor":12}),
json!({"backgroundMode":""}),
json!({"backgroundMode":"FLAT"}),
json!({"backgroundMode":" flat "}),
json!({"backgroundMode":null}),
] {
let mut arguments = json!({"sourceLocalAssetId":"asset-1","assetName":"透明图"});
arguments
.as_object_mut()
.unwrap()
.extend(fields.as_object().unwrap().clone());
assert!(
validate_remove_background_arguments(&arguments).is_err(),
"{fields}"
);
}
}
#[cfg(all(windows, feature = "cocos-editor-execute"))]
#[test]
fn builtin_mcp_process_probe() {
@@ -473,27 +473,6 @@ pub(crate) fn normalize_platform_art_asset_generation_kind(kind: &str) -> Option
})
}
/// 平台美术生成请求的 `assetKind` → manifest 资源 kind。
///
/// `PlatformArtAssetGenerationOptions::asset_kind` 是**平台请求词汇**`ui-prototype` /
/// `art-spritesheet` / `game-background` / `game-art` …),跟 manifest 的正式
/// `GameCreationAppAssetKind` 不是同一套词表。生成结果登记进 manifest 之前必须在这里显式
/// 转换,否则 manifest 会落进非 canonical 值,读侧只能靠 shared-contracts 的别名表兜底。
///
/// 词表外的值不猜:返回 `Unknown`,由调用方按既有未知输入口径处理。
pub(crate) fn platform_art_asset_manifest_kind(asset_kind: &str) -> GameCreationAppAssetKind {
match asset_kind.trim() {
"image" | "game-art" => GameCreationAppAssetKind::Image,
"character" | "character-art" => GameCreationAppAssetKind::Character,
"spec" | "icon-spec" => GameCreationAppAssetKind::IconSpec,
"ui-prototype" | "ui-design" => GameCreationAppAssetKind::UiDesign,
"art-spritesheet" | "icon-spritesheet" => GameCreationAppAssetKind::IconSpritesheet,
"publication-material" => GameCreationAppAssetKind::PublicationMaterial,
"game-background" | "scene" => GameCreationAppAssetKind::Scene,
_ => GameCreationAppAssetKind::Unknown,
}
}
pub(in crate::agent) fn recover_persisted_visual_generation_options(
root: &Path,
pending: &AgentRuntimePendingToolAction,
@@ -1635,7 +1614,7 @@ async fn canonical_art_spec_reference_at(
.iter()
.find(|asset| {
asset.local_path == AGENT_RUNTIME_ART_SPEC_PATH
&& asset.kind == GameCreationAppAssetKind::IconSpec
&& asset.kind == "icon-spec"
&& asset.media_type.starts_with("image/")
&& asset.source.kind == GameCreationAppAssetSourceKind::Canvas
})
@@ -1659,7 +1638,7 @@ async fn canonical_art_spec_reference_at(
&source.id,
&format!("{:x}", Sha256::digest(&bytes)),
&source.media_type,
source.kind.as_str(),
&source.kind,
)?;
if let Some(binding) = read_external_editor_resource_binding_at(
root,
@@ -6868,7 +6847,7 @@ fn register_platform_art_slice_manifest_entries_at(
.iter_mut()
.find(|asset| asset.local_path == registration.local_path)
{
existing.kind = GameCreationAppAssetKind::Icon;
existing.kind = "art-spritesheet-slice".to_string();
existing.media_type = registration.media_type.clone();
existing.image_sequence_frames = None;
existing.image_sequence_duration_ms = None;
@@ -6882,14 +6861,12 @@ fn register_platform_art_slice_manifest_entries_at(
);
manifest.assets.push(GameCreationAppAssetManifestEntry {
id: id.clone(),
kind: GameCreationAppAssetKind::Icon,
kind: "art-spritesheet-slice".to_string(),
media_type: registration.media_type.clone(),
local_path: registration.local_path.clone(),
image_sequence_frames: None,
image_sequence_duration_ms: None,
category: game_creation_app_asset_category_for_kind(
GameCreationAppAssetKind::Icon.as_str(),
),
category: game_creation_app_asset_category_for_kind("art-spritesheet-slice"),
tags: Vec::new(),
source,
});
@@ -6921,7 +6898,7 @@ fn existing_platform_art_slice_registrations_are_complete(
return Ok(false);
};
if candidates.next().is_some()
|| asset.kind != GameCreationAppAssetKind::Icon
|| asset.kind != "art-spritesheet-slice"
|| asset.media_type != "image/png"
|| asset.source.kind != GameCreationAppAssetSourceKind::Canvas
|| asset.source.generation_route.as_deref()
@@ -7010,12 +6987,10 @@ pub(in crate::agent) fn register_existing_platform_art_slices_at(
.clone()
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| "旧项目图集源资源缺少 sourceResourceId".to_string())?;
let has_partial_or_invalid_registration = manifest.assets.iter().any(|asset| {
asset.kind == GameCreationAppAssetKind::Icon
&& asset
.local_path
.starts_with("assets/art-spritesheet-slices/")
});
let has_partial_or_invalid_registration = manifest
.assets
.iter()
.any(|asset| asset.kind == "art-spritesheet-slice");
let receipt_path =
resolve_local_project_path(root, ".agent/runtime/art-spritesheet-contract.json")?;
@@ -7625,7 +7600,7 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
let registered = match register_local_asset_entry(
root,
&local_path,
platform_art_asset_manifest_kind(&options.asset_kind),
&options.asset_kind,
&download.media_type,
"platform-art",
GameCreationAppAssetSource {
@@ -11234,7 +11209,7 @@ mod canvas_generation_tests {
register_local_asset_at(
root,
"assets/art-spec.png",
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"image/png",
"changed-reference-test",
GameCreationAppAssetSource {
@@ -12804,7 +12779,7 @@ mod canvas_generation_tests {
let first_slice_ids = registered
.assets
.iter()
.filter(|asset| asset.kind == GameCreationAppAssetKind::Icon)
.filter(|asset| asset.kind == "art-spritesheet-slice")
.map(|asset| {
assert_eq!(asset.source.kind, GameCreationAppAssetSourceKind::Canvas);
assert_eq!(
@@ -12838,7 +12813,7 @@ mod canvas_generation_tests {
.clone();
mutate_manifest_at(root, |manifest| {
manifest.assets.retain(|asset| {
asset.kind != GameCreationAppAssetKind::Icon || asset.local_path == retained_path
asset.kind != "art-spritesheet-slice" || asset.local_path == retained_path
});
Ok(())
})
@@ -12852,7 +12827,7 @@ mod canvas_generation_tests {
let backfilled_slice_ids = backfilled
.assets
.iter()
.filter(|asset| asset.kind == GameCreationAppAssetKind::Icon)
.filter(|asset| asset.kind == "art-spritesheet-slice")
.map(|asset| (asset.local_path.clone(), asset.id.clone()))
.collect::<std::collections::BTreeMap<_, _>>();
assert_eq!(backfilled_slice_ids.len(), 4);
@@ -12901,7 +12876,7 @@ mod canvas_generation_tests {
let replaced_slice_ids = replaced
.assets
.iter()
.filter(|asset| asset.kind == GameCreationAppAssetKind::Icon)
.filter(|asset| asset.kind == "art-spritesheet-slice")
.map(|asset| {
assert_eq!(
asset.source.reference_resource_ids,
@@ -12934,7 +12909,7 @@ mod canvas_generation_tests {
register_local_asset_entry(
root,
AGENT_RUNTIME_ART_SPRITESHEET_PATH,
GameCreationAppAssetKind::IconSpritesheet,
"art-spritesheet",
"image/png",
"platform-art",
GameCreationAppAssetSource {
@@ -12995,12 +12970,12 @@ mod canvas_generation_tests {
assert!(after
.assets
.iter()
.all(|asset| asset.kind != GameCreationAppAssetKind::Icon));
.all(|asset| asset.kind != "art-spritesheet-slice"));
register_local_asset_entry(
root,
"assets/art-spritesheet-slices/player.png",
GameCreationAppAssetKind::Icon,
"art-spritesheet-slice",
"image/png",
"platform-art",
GameCreationAppAssetSource {
@@ -14556,10 +14531,7 @@ mod canvas_generation_tests {
let manifest = read_manifest_for_project(root).expect("read replacement manifest");
assert_eq!(manifest.assets.len(), 1);
assert_eq!(manifest.assets[0].local_path, "assets/art-spritesheet.png");
assert_eq!(
manifest.assets[0].kind,
GameCreationAppAssetKind::IconSpritesheet
);
assert_eq!(manifest.assets[0].kind, "art-spritesheet");
assert_eq!(
fs::read_dir(root.join("assets"))
.expect("read assets directory")
@@ -98,7 +98,7 @@ pub(crate) fn render_local_asset_prompt_context(root: &Path) -> Result<String, S
output.push_str("- ");
output.push_str(&asset.id);
output.push_str(": ");
output.push_str(asset.kind.as_str());
output.push_str(&asset.kind);
output.push_str(" / ");
output.push_str(&asset.media_type);
output.push_str(" / ");
@@ -2061,23 +2061,17 @@ mod tests {
.expect("resume first manifest task fixture");
for (local_path, kind) in [
("assets/art-spec.png", GameCreationAppAssetKind::IconSpec),
(
"assets/ui-prototype.png",
GameCreationAppAssetKind::UiDesign,
),
(
"assets/art-spritesheet.png",
GameCreationAppAssetKind::IconSpritesheet,
),
("assets/art-spec.png", "icon-spec"),
("assets/ui-prototype.png", "ui-prototype"),
("assets/art-spritesheet.png", "art-spritesheet"),
] {
let (generation_route, generation_kind, reference_resource_ids) = match kind {
GameCreationAppAssetKind::IconSpec => (
"icon-spec" => (
"/api/external/v1/editor/images/generations",
"spec",
Vec::new(),
),
GameCreationAppAssetKind::UiDesign => (
"ui-prototype" => (
"/api/external/v1/editor/images/generations",
"ui-design",
vec!["resource-icon-spec".to_string()],
@@ -2088,7 +2082,7 @@ mod tests {
vec!["resource-icon-spec".to_string()],
),
};
let bytes = if kind == GameCreationAppAssetKind::IconSpritesheet {
let bytes = if kind == "art-spritesheet" {
use image::ImageEncoder;
let mut bytes = Vec::new();
image::codecs::png::PngEncoder::new(&mut bytes)
@@ -1216,19 +1216,11 @@ pub(in crate::agent) fn visual_asset_completion_blocker_at_locked(
return None;
}
let (expected_path, expected_kind, label) = match agent_id {
"art-director" => (
AGENT_RUNTIME_ART_SPEC_PATH,
GameCreationAppAssetKind::IconSpec,
"统一视觉规范图",
),
"design-foundation" => (
"assets/ui-prototype.png",
GameCreationAppAssetKind::UiDesign,
"策划界面原型图",
),
"art-director" => (AGENT_RUNTIME_ART_SPEC_PATH, "icon-spec", "统一视觉规范图"),
"design-foundation" => ("assets/ui-prototype.png", "ui-prototype", "策划界面原型图"),
"art-asset-plan" => (
"assets/art-spritesheet.png",
GameCreationAppAssetKind::IconSpritesheet,
"art-spritesheet",
"首版美术素材图",
),
_ => return None,
@@ -1561,14 +1561,10 @@ mod orphaned_external_generation_recovery_tests {
fs::create_dir_all(workflow_path.parent().expect("workflow parent"))
.expect("workflow directory");
let rollback_assets = [
(
"assets/art-spec.png",
GameCreationAppAssetKind::IconSpec,
b"old-spec".as_slice(),
),
("assets/art-spec.png", "icon-spec", b"old-spec".as_slice()),
(
"assets/direct-game-background.png",
GameCreationAppAssetKind::Scene,
"game-background",
b"old-background".as_slice(),
),
]
@@ -451,7 +451,7 @@ mod asset_generation_task_tests {
init_local_game_project_at, read_manifest_for_project, write_project_permission_policy_at,
};
use shared_contracts::game_creation_app::{
GameCreationAppAssetKind, GameCreationAppAssetSource, GameCreationAppAssetSourceKind,
GameCreationAppAssetSource, GameCreationAppAssetSourceKind,
};
fn temp_project_root(label: &str) -> PathBuf {
@@ -484,7 +484,7 @@ mod asset_generation_task_tests {
register_local_asset_at(
root,
local_path,
GameCreationAppAssetKind::IconSpec,
"icon-spec",
"image/png",
"generated",
GameCreationAppAssetSource {
@@ -478,7 +478,7 @@ pub(crate) fn external_editor_api_credentials_override_is_active() -> bool {
/// 分类时才生效,`kind` 本身错时自愈只会把错值放大。
///
/// 判不出内容类型时返回中性的 `asset`(派生 `unclassified` → 「待归类」),**不猜具体类型**。
fn uploaded_asset_kind(file_name: &str, media_type: &str) -> GameCreationAppAssetKind {
fn uploaded_asset_kind(file_name: &str, media_type: &str) -> &'static str {
let media_type = media_type.trim().to_ascii_lowercase();
let extension = Path::new(file_name)
.extension()
@@ -492,20 +492,20 @@ fn uploaded_asset_kind(file_name: &str, media_type: &str) -> GameCreationAppAsse
"mp3" | "wav" | "ogg" | "m4a" | "aac" | "flac" | "opus"
)
{
GameCreationAppAssetKind::Audio
"audio"
} else if media_type.starts_with("video/") || matches!(extension, "mp4" | "webm" | "mov") {
GameCreationAppAssetKind::Video
"video"
} else if media_type.starts_with("font/")
|| matches!(extension, "ttf" | "otf" | "woff" | "woff2")
{
GameCreationAppAssetKind::Font
"document"
} else if media_type.starts_with("image/")
|| matches!(
extension,
"png" | "jpg" | "jpeg" | "webp" | "gif" | "svg" | "avif" | "bmp"
)
{
GameCreationAppAssetKind::Image
"image"
} else if matches!(media_type.as_str(), "text/html" | "text/css")
|| media_type.contains("javascript")
|| media_type.contains("typescript")
@@ -514,7 +514,7 @@ fn uploaded_asset_kind(file_name: &str, media_type: &str) -> GameCreationAppAsse
"html" | "htm" | "css" | "js" | "mjs" | "cjs" | "jsx" | "ts" | "tsx"
)
{
GameCreationAppAssetKind::Code
"code"
} else if media_type.starts_with("text/")
|| matches!(media_type.as_str(), "application/json" | "application/xml")
|| matches!(
@@ -532,9 +532,9 @@ fn uploaded_asset_kind(file_name: &str, media_type: &str) -> GameCreationAppAsse
| "xml"
)
{
GameCreationAppAssetKind::Document
"document"
} else {
GameCreationAppAssetKind::Unknown
"asset"
}
}
@@ -584,7 +584,7 @@ pub(crate) fn upload_local_asset_at(
pub(crate) fn register_local_asset_at(
root: &Path,
local_path: &str,
kind: GameCreationAppAssetKind,
kind: &str,
media_type: &str,
source_kind: &str,
source: GameCreationAppAssetSource,
@@ -647,7 +647,7 @@ pub(crate) fn register_design_artifacts_at(root: &Path) -> Result<bool, String>
let (_, asset_changed) = register_local_asset_entry_with_change(
root,
&relative,
GameCreationAppAssetKind::Document,
"document",
media_type,
"document",
GameCreationAppAssetSource {
@@ -686,7 +686,6 @@ pub(crate) fn import_canvas_asset_at(
if resource_id.is_none() && asset_object_id.is_none() {
return Err("画板资源 ID 和资产对象 ID 至少需要一个".to_string());
}
let kind = GameCreationAppAssetKind::parse_with_context(kind.trim(), "canvas.asset_import");
register_local_asset_at(
root,
@@ -919,12 +918,7 @@ pub(crate) async fn sync_canvas_project_assets_at(
&local_path,
json_string_field(resource, "assetKind")
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| {
GameCreationAppAssetKind::parse_with_context(value, "canvas.asset_sync")
})
.unwrap_or(GameCreationAppAssetKind::Image),
.unwrap_or("canvas-resource"),
&download.media_type,
"canvas",
GameCreationAppAssetSource {
@@ -1778,28 +1772,33 @@ pub(crate) fn normalized_zip_entry_name(name: &str) -> Result<String, String> {
/// 画板导出图层推断出的资源 kind。
///
/// 直接返回正式枚举成员:这个值会被 `register_local_asset_entry` 原样写入 manifest
/// 并据以派生落盘 `category`。分支字面量写成非 canonical 值在这里已经无法表达,
/// 不需要别名表兜底。
/// 返回值必须是 **canonical kind**:这个值会被 `register_local_asset_entry` 原样写入
/// manifest并据以派生落盘 `category`。历史上这里写过非 canonical
/// `ui` / `animation` / `asset`,只能靠别名表兜底,等于同时维护两套词汇;别名表只用于
/// 兼容存量数据,不作为新写入值的来源。
pub(crate) fn infer_canvas_export_asset_kind(
layer: &CanvasExportLayerMetadata,
file: &str,
) -> GameCreationAppAssetKind {
) -> &'static str {
let layer_type = layer.visible.layer_type.as_str();
if file.starts_with("sequences/") || contains_any(layer_type, &["序列", "动画", "动作"]) {
GameCreationAppAssetKind::CharacterAnimation
let inferred = if file.starts_with("sequences/")
|| contains_any(layer_type, &["序列", "动画", "动作"])
{
"character-animation"
} else if file.starts_with("media/") || contains_any(layer_type, &["音频", "音乐", "音效"])
{
GameCreationAppAssetKind::Audio
"audio"
} else if contains_any(layer_type, &["角色"]) {
GameCreationAppAssetKind::Character
"character"
} else if contains_any(layer_type, &["场景", "背景"]) {
GameCreationAppAssetKind::Scene
"scene"
} else if contains_any(layer_type, &["UI", "界面", "图标"]) {
GameCreationAppAssetKind::UiDesign
"ui-design"
} else {
GameCreationAppAssetKind::Image
}
"image"
};
// 防御性归一:分支字面量写错时由覆盖测试暴露,这里再兜一层。
shared_contracts::game_creation_app::canonical_game_creation_app_asset_kind(inferred)
}
pub(crate) fn infer_canvas_export_media_type(file: &str) -> &'static str {
@@ -1872,7 +1871,7 @@ pub(crate) fn build_canvas_project_url(
pub(crate) fn register_local_asset_entry(
root: &Path,
local_path: &str,
kind: GameCreationAppAssetKind,
kind: &str,
media_type: &str,
id_prefix: &str,
source: GameCreationAppAssetSource,
@@ -1884,7 +1883,7 @@ pub(crate) fn register_local_asset_entry(
fn register_local_asset_entry_with_change(
root: &Path,
local_path: &str,
kind: GameCreationAppAssetKind,
kind: &str,
media_type: &str,
id_prefix: &str,
source: GameCreationAppAssetSource,
@@ -1892,6 +1891,7 @@ fn register_local_asset_entry_with_change(
let normalized_path = normalize_relative_path(local_path)?;
let absolute_path = resolve_local_project_path(root, &normalized_path)?;
let manifest_path = root.join(".agent/manifest.json");
let kind = if kind.is_empty() { "asset" } else { kind };
let media_type = if media_type.is_empty() {
"application/octet-stream"
} else {
@@ -1914,8 +1914,8 @@ fn register_local_asset_entry_with_change(
|| existing.media_type != media_type
|| existing.source != source;
if existing.kind != kind {
existing.kind = kind;
existing.category = game_creation_app_asset_category_for_kind(kind.as_str());
existing.kind = kind.to_string();
existing.category = game_creation_app_asset_category_for_kind(kind);
}
existing.media_type = media_type.to_string();
existing.source = source;
@@ -1928,12 +1928,12 @@ fn register_local_asset_entry_with_change(
);
manifest.assets.push(GameCreationAppAssetManifestEntry {
id: id.clone(),
kind,
kind: kind.to_string(),
media_type: media_type.to_string(),
local_path: normalized_path.clone(),
image_sequence_frames: None,
image_sequence_duration_ms: None,
category: game_creation_app_asset_category_for_kind(kind.as_str()),
category: game_creation_app_asset_category_for_kind(kind),
tags: Vec::new(),
source,
});
@@ -1946,7 +1946,7 @@ fn register_local_asset_entry_with_change(
"recordType": record_type,
"assetId": id.clone(),
"localPath": normalized_path.clone(),
"kind": kind.as_str(),
"kind": kind,
"mediaType": media_type,
"source": source_for_record,
}),
@@ -2199,21 +2199,13 @@ mod tests {
}
// 每个分支的代表输入 → 期望的 canonical kind。
let cases: [(&str, &str, GameCreationAppAssetKind); 6] = [
(
"序列",
"sequences/01.png",
GameCreationAppAssetKind::CharacterAnimation,
),
(
"动画",
"layer.png",
GameCreationAppAssetKind::CharacterAnimation,
),
("音频", "layer.png", GameCreationAppAssetKind::Audio),
("角色", "layer.png", GameCreationAppAssetKind::Character),
("场景", "layer.png", GameCreationAppAssetKind::Scene),
("UI", "layer.png", GameCreationAppAssetKind::UiDesign),
let cases: [(&str, &str, &str); 6] = [
("序列", "sequences/01.png", "character-animation"),
("动画", "layer.png", "character-animation"),
("音频", "layer.png", "audio"),
("角色", "layer.png", "character"),
("场景", "layer.png", "scene"),
("UI", "layer.png", "ui-design"),
// 无匹配时落到 image。
];
for (layer_type, file, expected) in cases {
@@ -2223,20 +2215,20 @@ mod tests {
// media/ 前缀同样走音频分支。
assert_eq!(
infer_canvas_export_asset_kind(&layer("图层"), "media/bgm.mp3"),
GameCreationAppAssetKind::Audio
"audio"
);
// 兜底分支是 image 而不是旧的 "asset"。
assert_eq!(
infer_canvas_export_asset_kind(&layer("图层"), "layer.png"),
GameCreationAppAssetKind::Image
"image"
);
// 所有分支的返回值都必须是正式枚举成员,且不能是旧的非 canonical 写法。
// 所有分支的返回值都必须在 canonical 目录里,且不能是旧的非 canonical 写法。
for layer_type in ["序列", "音频", "角色", "场景", "UI", "图层", "其他"] {
let kind = infer_canvas_export_asset_kind(&layer(layer_type), "layer.png");
assert!(
shared_contracts::game_creation_app::GAME_CREATION_APP_CANONICAL_ASSET_KINDS
.contains(&kind.as_str()),
.contains(&kind),
"非 canonical kind: {kind}layer_type={layer_type}"
);
for legacy in ["ui", "animation", "asset"] {
@@ -2258,14 +2250,14 @@ mod tests {
assert_eq!(uploaded_asset_kind("theme.mp3", "audio/mpeg"), "audio");
assert_eq!(uploaded_asset_kind("intro.mp4", ""), "video");
assert_eq!(uploaded_asset_kind("rules.md", "text/markdown"), "document");
assert_eq!(uploaded_asset_kind("ui-font.ttf", "font/ttf"), "font");
assert_eq!(uploaded_asset_kind("ui-font.ttf", "font/ttf"), "document");
assert_eq!(uploaded_asset_kind("game.js", "text/javascript"), "code");
// 判不出内容类型 → `unknown`(派生 `unclassified` → 「待归类」),不猜具体类型。
// 判不出内容类型 → 中性 `asset`(派生 `unclassified` → 「待归类」),不猜具体类型。
assert_eq!(
uploaded_asset_kind("unknown.bin", "application/octet-stream"),
"unknown"
"asset"
);
assert_eq!(uploaded_asset_kind("no-extension", ""), "unknown");
assert_eq!(uploaded_asset_kind("no-extension", ""), "asset");
// 反查:正文里的"来源词"和"UI 默认值"都不得出现在返回值里。
for (file_name, media_type) in [
("hero.png", "image/png"),
@@ -2277,11 +2269,8 @@ mod tests {
assert_ne!(kind, "ui", "{file_name} 写回了非 canonical 的 `ui`");
assert!(
shared_contracts::game_creation_app::GAME_CREATION_APP_CANONICAL_ASSET_KINDS
.contains(&kind.as_str())
|| matches!(
kind,
GameCreationAppAssetKind::Font | GameCreationAppAssetKind::Unknown
),
.contains(&kind)
|| kind == "asset",
"非 canonical kind: {kind}{file_name}"
);
}
@@ -2167,7 +2167,7 @@ pub(crate) fn register_local_asset(
register_local_asset_at(
root,
local_path.trim(),
GameCreationAppAssetKind::parse_with_context(kind.trim(), "asset.register"),
kind.trim(),
media_type.trim(),
source_kind.trim(),
GameCreationAppAssetSource {
@@ -2200,10 +2200,7 @@ pub(crate) fn create_ui_design_resource(
let next_index = manifest
.assets
.iter()
.filter(|asset| {
asset.kind == crate::ui_editor::persistence::UI_DESIGN_DOC_ASSET_KIND
&& asset.media_type == crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE
})
.filter(|asset| asset.kind == "UI")
.count()
+ 1;
let resource_name = format!("UI 设计 {next_index}");
@@ -2237,8 +2234,8 @@ pub(crate) fn create_ui_design_resource(
let asset = match register_local_asset_at(
root,
&relative_path,
GameCreationAppAssetKind::UiDesignDoc,
crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE,
"UI",
"application/json",
"generated",
GameCreationAppAssetSource {
kind: GameCreationAppAssetSourceKind::Generated,
@@ -2728,7 +2725,7 @@ pub(crate) fn import_ui_editor_local_fonts(
let registered = register_local_asset_entry(
root,
&relative_path,
GameCreationAppAssetKind::Font,
"font",
validated.metadata.format.media_type(),
"font",
GameCreationAppAssetSource {
@@ -2840,7 +2837,7 @@ mod ui_editor_font_tests {
let registered = register_local_asset_entry(
root,
relative_path,
GameCreationAppAssetKind::Font,
"font",
"font/ttf",
"font",
GameCreationAppAssetSource {
@@ -2943,7 +2940,7 @@ mod ui_editor_font_tests {
symlink(fixture_font_path(), &target).expect("create font symlink");
let entry = GameCreationAppAssetManifestEntry {
id: "font-linked".to_string(),
kind: GameCreationAppAssetKind::Font,
kind: "font".to_string(),
media_type: "font/ttf".to_string(),
local_path: relative_path.to_string(),
image_sequence_frames: None,
@@ -3258,34 +3255,24 @@ mod agent_asset_import_tests {
/// 本用例必须变红(`character` / `scene` / 空值 / 未知值四组断言都会失败)。
#[test]
fn imported_platform_asset_kind_prefers_payload_type_over_ui_default() {
use GameCreationAppAssetKind as Kind;
assert_eq!(
imported_platform_asset_kind(Some("character")),
Kind::Character
);
assert_eq!(imported_platform_asset_kind(Some("scene")), Kind::Scene);
assert_eq!(imported_platform_asset_kind(Some("character")), "character");
assert_eq!(imported_platform_asset_kind(Some("scene")), "scene");
assert_eq!(
imported_platform_asset_kind(Some("character-animation")),
Kind::CharacterAnimation
);
assert_eq!(
imported_platform_asset_kind(Some("icon-spec")),
Kind::IconSpec
"character-animation"
);
assert_eq!(imported_platform_asset_kind(Some("icon-spec")), "icon-spec");
// 平台确实说它是 UI 设计稿时,才允许落 `ui-design`(→ 「UI 交互」);
// 大写 `UI` 与旧值 `ui` 都要归一到同一口径,但**不允许由我们替它默认**。
assert_eq!(imported_platform_asset_kind(Some("UI")), Kind::UiDesign);
assert_eq!(imported_platform_asset_kind(Some("ui")), Kind::UiDesign);
assert_eq!(imported_platform_asset_kind(Some("UI")), "ui-design");
assert_eq!(imported_platform_asset_kind(Some("ui")), "ui-design");
// 缺失 / 空白 / 未知值一律落 canonical `image` 兜底(→ 「待归类」)。
assert_eq!(imported_platform_asset_kind(None), Kind::Image);
assert_eq!(imported_platform_asset_kind(Some(" ")), Kind::Image);
assert_eq!(imported_platform_asset_kind(Some("mystery")), Kind::Image);
assert_eq!(imported_platform_asset_kind(None), "image");
assert_eq!(imported_platform_asset_kind(Some(" ")), "image");
assert_eq!(imported_platform_asset_kind(Some("mystery")), "image");
// `kind` 是外部输入,原型链键必须走 `image` 兜底,不能取到原型上的值。
assert_eq!(imported_platform_asset_kind(Some("__proto__")), Kind::Image);
assert_eq!(
imported_platform_asset_kind(Some("constructor")),
Kind::Image
);
assert_eq!(imported_platform_asset_kind(Some("__proto__")), "image");
assert_eq!(imported_platform_asset_kind(Some("constructor")), "image");
}
/// 反查门禁:两处平台素材导入必须继续用 `imported_platform_asset_kind` 解析 `kind`
@@ -3814,7 +3801,7 @@ pub(crate) async fn list_editor_assets_for_agent_at(
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct AgentLocalProjectFileType {
category: &'static str,
asset_kind: GameCreationAppAssetKind,
asset_kind: &'static str,
media_type: &'static str,
max_file_size: u64,
}
@@ -3869,14 +3856,14 @@ fn agent_local_project_file_type(
// 错值放大成显式的 `ui-interaction`。
media_type.map(|media_type| AgentLocalProjectFileType {
category: "image",
asset_kind: GameCreationAppAssetKind::Image,
asset_kind: "image",
media_type,
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
})
}
"ttf" | "otf" | "woff" | "woff2" => Some(AgentLocalProjectFileType {
category: "font",
asset_kind: GameCreationAppAssetKind::Font,
asset_kind: "font",
media_type: match extension.as_str() {
"ttf" => "font/ttf",
"otf" => "font/otf",
@@ -3887,68 +3874,68 @@ fn agent_local_project_file_type(
}),
"mp3" => Some(AgentLocalProjectFileType {
category: "audio",
asset_kind: GameCreationAppAssetKind::Audio,
asset_kind: "audio",
media_type: "audio/mpeg",
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
}),
"wav" => Some(AgentLocalProjectFileType {
category: "audio",
asset_kind: GameCreationAppAssetKind::Audio,
asset_kind: "audio",
media_type: "audio/wav",
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
}),
"ogg" => Some(AgentLocalProjectFileType {
category: "audio",
asset_kind: GameCreationAppAssetKind::Audio,
asset_kind: "audio",
media_type: "audio/ogg",
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
}),
"flac" => Some(AgentLocalProjectFileType {
category: "audio",
asset_kind: GameCreationAppAssetKind::Audio,
asset_kind: "audio",
media_type: "audio/flac",
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
}),
"m4a" => Some(AgentLocalProjectFileType {
category: "audio",
asset_kind: GameCreationAppAssetKind::Audio,
asset_kind: "audio",
media_type: "audio/mp4",
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
}),
"aac" => Some(AgentLocalProjectFileType {
category: "audio",
asset_kind: GameCreationAppAssetKind::Audio,
asset_kind: "audio",
media_type: "audio/aac",
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
}),
"opus" => Some(AgentLocalProjectFileType {
category: "audio",
asset_kind: GameCreationAppAssetKind::Audio,
asset_kind: "audio",
media_type: "audio/opus",
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
}),
"mp4" => Some(AgentLocalProjectFileType {
category: "video",
asset_kind: GameCreationAppAssetKind::Video,
asset_kind: "video",
media_type: "video/mp4",
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
}),
"webm" => Some(AgentLocalProjectFileType {
category: "video",
asset_kind: GameCreationAppAssetKind::Video,
asset_kind: "video",
media_type: "video/webm",
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
}),
"mov" => Some(AgentLocalProjectFileType {
category: "video",
asset_kind: GameCreationAppAssetKind::Video,
asset_kind: "video",
media_type: "video/quicktime",
max_file_size: UI_EDITOR_IMAGE_MAX_FILE_SIZE,
}),
"md" | "markdown" | "mdx" | "txt" | "json" | "yaml" | "yml" | "toml" | "csv" | "ini"
| "conf" | "xml" => Some(AgentLocalProjectFileType {
category: "document",
asset_kind: GameCreationAppAssetKind::Document,
asset_kind: "document",
media_type: if extension == "json" {
"application/json"
} else if matches!(extension.as_str(), "yaml" | "yml") {
@@ -3965,7 +3952,7 @@ fn agent_local_project_file_type(
| "swift" | "php" | "rb" | "lua" | "sh" | "bash" | "zsh" | "sql" | "graphql" | "gql"
| "vue" | "svelte" => Some(AgentLocalProjectFileType {
category: "code",
asset_kind: GameCreationAppAssetKind::Code,
asset_kind: "code",
media_type: if matches!(extension.as_str(), "html" | "htm") {
"text/html"
} else if matches!(extension.as_str(), "css" | "scss" | "less") {
@@ -4083,7 +4070,7 @@ pub(crate) fn import_local_project_assets_for_agent(
}
inputs.push((
local_path,
file_type.asset_kind,
file_type.asset_kind.to_string(),
file_type.media_type.to_string(),
bytes,
));
@@ -4102,7 +4089,7 @@ pub(crate) fn import_local_project_assets_for_agent(
imported.push(ImportedAsset {
id: existing.id.clone(),
local_path: existing.local_path.clone(),
asset_kind: Some(existing.kind.to_string()),
asset_kind: Some(existing.kind.clone()),
});
continue;
}
@@ -4141,7 +4128,7 @@ pub(crate) fn import_local_project_assets_for_agent(
let registered = register_local_asset_entry(
root,
&local_path,
asset_kind,
&asset_kind,
&media_type,
"local",
GameCreationAppAssetSource {
@@ -4160,7 +4147,7 @@ pub(crate) fn import_local_project_assets_for_agent(
imported.push(ImportedAsset {
id: registered.id,
local_path: registered.local_path,
asset_kind: Some(asset_kind.to_string()),
asset_kind: Some(asset_kind),
});
advance_agent_runtime_project_revision_locked(root).map_err(|error| {
format!("reconciliation-required: 本地资源已登记,但项目 revision 未能推进:{error}")
@@ -4177,14 +4164,15 @@ pub(crate) fn import_local_project_assets_for_agent(
/// 唯一一条"这东西是什么"的事实,并把角色立绘、怪物、道具、场景图一并钉进「UI 交互」栏
/// `ui → ui-design → ui-interaction`)。落盘 `category` 一旦是非 `unclassified` 值,
/// 读时自愈永远救不回来,所以宁可写 `image`(待归类)也不能写假 `ui`。
fn imported_platform_asset_kind(platform_kind: Option<&str>) -> GameCreationAppAssetKind {
fn imported_platform_asset_kind(platform_kind: Option<&str>) -> String {
match platform_kind.map(str::trim).filter(|kind| !kind.is_empty()) {
// 平台是外部系统,它的 `assetKind` 词汇(含大写 `UI` 这类变体)由共享契约统一收口到
// canonical 值;这里只把收口结果转成正式枚举,写入侧不再产出裸字符串
Some(kind) => GameCreationAppAssetKind::from_str_lossy(
shared_contracts::game_creation_app::canonical_game_creation_app_asset_kind(kind),
),
None => GameCreationAppAssetKind::Image,
// 平台侧词汇可能与 canonical 目录不完全一致(例如大写 `UI`),统一过 canonical 归一,
// 保证写入侧只产出 canonical `kind`
Some(kind) => {
shared_contracts::game_creation_app::canonical_game_creation_app_asset_kind(kind)
.to_string()
}
None => "image".to_string(),
}
}
@@ -4292,7 +4280,7 @@ pub(crate) async fn import_account_editor_assets_for_agent(
imported.push(ImportedAsset {
id: existing.id.clone(),
local_path: existing.local_path.clone(),
asset_kind: Some(existing.kind.to_string()),
asset_kind: Some(existing.kind.clone()),
});
continue;
}
@@ -4342,7 +4330,7 @@ pub(crate) async fn import_account_editor_assets_for_agent(
let registered = register_local_asset_entry(
root,
&local_path,
asset_kind,
&asset_kind,
&media_type,
"canvas",
GameCreationAppAssetSource {
@@ -4460,7 +4448,7 @@ pub(crate) async fn import_ui_editor_remote_assets(
let registered = register_local_asset_entry(
root,
&local_path,
asset_kind,
&asset_kind,
&media_type,
"canvas",
GameCreationAppAssetSource {
@@ -33,8 +33,8 @@ use shared_contracts::game_creation_app::{
GameCreationAgentCapabilityDescriptor, GameCreationAgentPassPlanTrace,
GameCreationAgentRepairRouteTrace, GameCreationAgentRunStep,
GameCreationAgentRunTaskGraphTrace, GameCreationAgentRunTrace, GameCreationAgentToolCallTrace,
GameCreationAppAgentGroup, GameCreationAppAssetKind, GameCreationAppAssetManifestEntry,
GameCreationAppAssetSource, GameCreationAppAssetSourceKind, GameCreationAppCommandRunState,
GameCreationAppAgentGroup, GameCreationAppAssetManifestEntry, GameCreationAppAssetSource,
GameCreationAppAssetSourceKind, GameCreationAppCommandRunState,
GameCreationAppCommandRunStatus, GameCreationAppLimitedRunCommandDescriptor,
GameCreationAppManifest, GameCreationAppPermission, GameCreationAppPreviewState,
GameCreationAppPreviewStatus, GameCreationAppTaskState, GameCreationAppTaskStatus,
@@ -1069,15 +1069,9 @@ pub(crate) fn validate_manifest_required_visual_asset(
task_id: &str,
) -> Result<(), String> {
let (expected_path, expected_kind) = match task_id {
"art-director" => ("assets/art-spec.png", GameCreationAppAssetKind::IconSpec),
"design-foundation" => (
"assets/ui-prototype.png",
GameCreationAppAssetKind::UiDesign,
),
"art-asset-plan" => (
"assets/art-spritesheet.png",
GameCreationAppAssetKind::IconSpritesheet,
),
"art-director" => ("assets/art-spec.png", "icon-spec"),
"design-foundation" => ("assets/ui-prototype.png", "ui-prototype"),
"art-asset-plan" => ("assets/art-spritesheet.png", "art-spritesheet"),
_ => return Ok(()),
};
let asset = manifest
@@ -1147,10 +1141,7 @@ pub(crate) fn validate_manifest_required_visual_asset(
let art_spec = manifest
.assets
.iter()
.find(|asset| {
asset.local_path == "assets/art-spec.png"
&& asset.kind == GameCreationAppAssetKind::IconSpec
})
.find(|asset| asset.local_path == "assets/art-spec.png" && asset.kind == "icon-spec")
.ok_or_else(|| "缺少当前统一视觉规范图".to_string())?;
let art_spec_project_id = art_spec
.source
@@ -19,7 +19,7 @@ fn project_with_two_assets(test_name: &str) -> (PathBuf, String) {
let first = register_local_asset_entry(
&root,
"assets/hero.png",
GameCreationAppAssetKind::Character,
"character",
"image/png",
"asset",
GameCreationAppAssetSource {
@@ -39,7 +39,7 @@ fn project_with_two_assets(test_name: &str) -> (PathBuf, String) {
register_local_asset_entry(
&root,
"assets/theme.mp3",
GameCreationAppAssetKind::BackgroundMusic,
"background-music",
"audio/mpeg",
"asset",
GameCreationAppAssetSource {
@@ -96,7 +96,7 @@ fn manifest_task_and_asset_mutations_are_serialized_across_the_full_transaction(
crate::assets::register_local_asset_entry(
&second_root,
"assets/concurrent.png",
GameCreationAppAssetKind::UiDesign,
"ui-prototype",
"image/png",
"test",
GameCreationAppAssetSource {
@@ -180,7 +180,7 @@ fn successful_first_playable_registration_creates_one_initial_version_with_asset
let mut manifest = new_game_creation_app_manifest("project-first-playable", "首板项目");
manifest.assets.push(GameCreationAppAssetManifestEntry {
id: "asset-player".to_string(),
kind: GameCreationAppAssetKind::Character,
kind: "character".to_string(),
media_type: "image/png".to_string(),
local_path: "assets/player.png".to_string(),
image_sequence_frames: None,

Some files were not shown because too many files have changed in this diff Show More