修复 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
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:
@@ -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"]),
|
||||
(
|
||||
|
||||
Reference in New Issue
Block a user