Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8d5df4ccb | |||
| 965e80f374 | |||
| 9b6ed8a4cf | |||
| 16deb0ad61 | |||
| 13f56e644d | |||
| e08b031761 | |||
| 70ad794b28 | |||
| ca761e093c | |||
| a17367112f | |||
| d7a0c3cffd | |||
| fa64819b6e | |||
| f748d36d21 | |||
| cd75a9e565 | |||
| 0388fa0912 | |||
| 79b153e3fa | |||
| 362edcc49d | |||
| 1e434e0cb5 |
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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"] }
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -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(
|
||||
®istered_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(" / ");
|
||||
|
||||
+6
-12
@@ -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
Reference in New Issue
Block a user