修复 AGC 生成准备依赖全量素材库的问题
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m33s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 4m41s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 3m41s
Project CI / Backend tests (pull_request) Failing after 3m38s
Project CI / Frontend tests (pull_request) Successful in 2m27s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m56s
Project CI / Repository checks (pull_request) Successful in 3m1s
Project CI / Native shell tests (pull_request) Successful in 6m12s

新增站内与 External v1 单目录读取接口并校验账号归属,保持既有 API 兼容
将已有绑定与 partial 恢复改为按目录 ID 确认,首次建绑不再读取全库
目录缺失或查询失败时停止并保留恢复证据,保持生成幂等与账号隔离
同步 OpenAPI、生成绑定、技术文档及目录读取和生成恢复回归测试
This commit is contained in:
2026-10-02 22:25:17 +08:00
parent db0332ea09
commit 0afd0b0b10
19 changed files with 842 additions and 184 deletions
@@ -48,7 +48,7 @@ use spacetime_client::editor_project::{
};
use spacetime_client::{
EditorAssetCreateRecordInput, EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderRecord,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderGetRecordInput, EditorAssetFolderRecord,
EditorAssetFolderUpdateRecordInput, EditorAssetLibraryRecord,
EditorAssetMediaRepairRecordInput, EditorAssetRecord, EditorAssetUpdateRecordInput,
EditorCanvasRecord, EditorCanvasViewportRecord, EditorProjectCreateRecordInput,
@@ -2386,6 +2386,29 @@ pub async fn get_editor_asset_library(
))
}
pub async fn get_editor_asset_folder(
State(state): State<AppState>,
Path(folder_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, AppError> {
let folder = state
.spacetime_client()
.get_editor_asset_folder(EditorAssetFolderGetRecordInput {
folder_id,
owner_user_id: current_owner_user_id(&authenticated),
})
.await
.map_err(map_editor_project_error)?;
Ok(json_success_body(
Some(&request_context),
EditorAssetFolderResponse {
folder: editor_asset_folder_payload_from_record(folder),
},
))
}
pub async fn create_editor_asset_folder(
State(state): State<AppState>,
headers: HeaderMap,
@@ -17,11 +17,11 @@ use shared_contracts::external_generation::{
use shared_kernel::build_prefixed_uuid_id;
use spacetime_client::{
EditorAssetCreateRecordInput, EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderUpdateRecordInput,
EditorAssetUpdateRecordInput, EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput,
EditorProjectGetRecordInput, EditorProjectRenameRecordInput,
EditorProjectResourceCreateRecordInput, ExternalGenerationJobGetRecordInput,
ExternalGenerationJobRecord, SpacetimeClientError,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderGetRecordInput,
EditorAssetFolderUpdateRecordInput, EditorAssetUpdateRecordInput,
EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput, EditorProjectGetRecordInput,
EditorProjectRenameRecordInput, EditorProjectResourceCreateRecordInput,
ExternalGenerationJobGetRecordInput, ExternalGenerationJobRecord, SpacetimeClientError,
};
use crate::editor_project_icon::{
@@ -468,6 +468,30 @@ pub async fn get_external_editor_asset_library(
))
}
pub async fn get_external_editor_asset_folder(
State(state): State<AppState>,
Path(folder_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(principal): Extension<ExternalApiPrincipal>,
) -> Result<Json<Value>, AppError> {
require_scope(&principal, SCOPE_EDITOR_ASSET)?;
let folder = state
.spacetime_client()
.get_editor_asset_folder(EditorAssetFolderGetRecordInput {
folder_id,
owner_user_id: principal.owner_user_id().to_string(),
})
.await
.map_err(map_editor_project_error)?;
Ok(json_success_body(
Some(&request_context),
ExternalEditorAssetFolderResponse {
folder: editor_asset_folder_payload_from_record(folder),
},
))
}
pub async fn create_external_editor_asset_folder(
State(state): State<AppState>,
headers: HeaderMap,
@@ -1194,7 +1218,11 @@ fn serialize_external_editor_image_sequence_frames(
mod tests {
use super::*;
use crate::editor_project::PROJECT_COVER_SNAPSHOT_ASSET_KIND;
use axum::{Router, body::Body, routing::post};
use axum::{
Router,
body::Body,
routing::{get, post},
};
use spacetime_client::{
EditorCanvasRecord, EditorCanvasViewportRecord, EditorProjectRecord,
EditorProjectResourceRecord,
@@ -2317,6 +2345,72 @@ mod tests {
);
}
#[tokio::test]
async fn external_editor_asset_folder_read_requires_asset_scope_before_store_access() {
let state = AppState::new(crate::config::AppConfig::default()).expect("state should build");
let app = Router::new()
.route(
"/api/external/v1/editor/assets/folders/{folder_id}",
get(get_external_editor_asset_folder),
)
.layer(Extension(request_context(false)))
.layer(Extension(ExternalApiPrincipal::for_test("user-1", &[])))
.with_state(state);
let response = app
.oneshot(
axum::http::Request::builder()
.uri("/api/external/v1/editor/assets/folders/folder-1")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should complete");
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn external_editor_asset_folder_read_maps_missing_and_foreign_owner_errors() {
for (message, status) in [
("素材文件夹不存在", StatusCode::NOT_FOUND),
("无权访问该素材文件夹", StatusCode::FORBIDDEN),
] {
assert_eq!(
map_editor_project_error(SpacetimeClientError::Procedure(message.to_string()))
.status_code(),
status
);
}
}
#[test]
fn external_editor_asset_folder_read_openapi_matches_route_contract() {
let parsed: Value = serde_json::from_str(OPENAPI_JSON).expect("openapi json should parse");
let operation =
&parsed["paths"]["/api/external/v1/editor/assets/folders/{folderId}"]["get"];
assert_eq!(operation["operationId"], "getEditorAssetFolder");
assert_eq!(
operation["parameters"][0]["$ref"],
"#/components/parameters/FolderId"
);
assert_eq!(operation["security"], json!([{ "ExternalApiKey": [] }]));
assert!(
operation["description"]
.as_str()
.unwrap()
.contains("editor:asset")
);
assert_eq!(
operation["responses"]["200"]["content"]["application/json"]["schema"]["$ref"],
"#/components/schemas/ExternalEditorAssetFolderResponse"
);
for status in ["200", "400", "401", "403", "404"] {
assert!(
operation["responses"].get(status).is_some(),
"missing {status}"
);
}
}
#[test]
fn exported_openapi_json_contains_external_editor_routes_and_security() {
let parsed: Value = serde_json::from_str(OPENAPI_JSON).expect("openapi json should parse");
@@ -22,10 +22,10 @@ use crate::{
create_editor_project, create_editor_project_resource, delete_editor_asset,
delete_editor_asset_folder, delete_editor_project, edit_editor_image,
extract_editor_ui_design_assets, generate_editor_image, generate_editor_scene,
get_editor_asset_library, get_editor_generation_pricing, get_editor_project,
list_editor_projects, list_public_editor_project_resources, load_recent_editor_project,
remove_editor_image_background, rename_editor_project, save_editor_project_layout,
snap_editor_image_to_pixel_art, submit_editor_asset_showcase,
get_editor_asset_folder, get_editor_asset_library, get_editor_generation_pricing,
get_editor_project, list_editor_projects, list_public_editor_project_resources,
load_recent_editor_project, remove_editor_image_background, rename_editor_project,
save_editor_project_layout, snap_editor_image_to_pixel_art, submit_editor_asset_showcase,
toggle_editor_showcase_asset_like, update_editor_asset, update_editor_asset_folder,
update_editor_project_resource_showcase,
},
@@ -110,7 +110,9 @@ pub fn router(state: AppState) -> Router<AppState> {
),
(
"/api/editor/assets/folders/{folder_id}",
patch(update_editor_asset_folder).delete(delete_editor_asset_folder),
get(get_editor_asset_folder)
.patch(update_editor_asset_folder)
.delete(delete_editor_asset_folder),
),
("/api/editor/assets", post(create_editor_asset)),
(
@@ -291,7 +293,7 @@ mod route_contract_tests {
("/api/editor/assets/folders", &["POST"]),
(
"/api/editor/assets/folders/{folder_id}",
&["PATCH", "DELETE"],
&["GET", "PATCH", "DELETE"],
),
("/api/editor/assets", &["POST"]),
("/api/editor/assets/{asset_id}", &["PATCH", "DELETE"]),
@@ -21,10 +21,11 @@ use crate::{
generate_external_editor_character_animation, generate_external_editor_icon_spritesheet,
generate_external_editor_image, generate_external_editor_scene,
generate_external_editor_sound_effect, generate_external_editor_video,
get_external_editor_asset_library, get_external_editor_generation_job,
get_external_editor_project, list_external_editor_projects,
load_recent_external_editor_project, openapi_json, remove_external_editor_image_background,
rename_external_editor_project, save_external_editor_canvas, update_external_editor_asset,
get_external_editor_asset_folder, get_external_editor_asset_library,
get_external_editor_generation_job, get_external_editor_project,
list_external_editor_projects, load_recent_external_editor_project, openapi_json,
remove_external_editor_image_background, rename_external_editor_project,
save_external_editor_canvas, update_external_editor_asset,
update_external_editor_asset_folder,
},
external_mcp,
@@ -93,7 +94,9 @@ pub fn router(state: AppState) -> Router<AppState> {
),
(
"/api/external/v1/editor/assets/folders/{folder_id}",
patch(update_external_editor_asset_folder).delete(delete_external_editor_asset_folder),
get(get_external_editor_asset_folder)
.patch(update_external_editor_asset_folder)
.delete(delete_external_editor_asset_folder),
),
(
"/api/external/v1/editor/assets",
@@ -252,7 +255,7 @@ mod route_contract_tests {
("/api/external/v1/editor/assets/folders", &["POST"]),
(
"/api/external/v1/editor/assets/folders/{folder_id}",
&["PATCH", "DELETE"],
&["GET", "PATCH", "DELETE"],
),
("/api/external/v1/editor/assets", &["POST"]),
(