修复 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
File diff suppressed because it is too large Load Diff
@@ -6203,15 +6203,17 @@ mod tests {
"projectId": "source-binding-project"
}]}}),
);
} else if request_line.starts_with("GET /api/external/v1/editor/assets/library ")
|| request_line.starts_with("GET /api/editor/assets/library ")
} else if request_line.starts_with(
"GET /api/external/v1/editor/assets/folders/source-binding-folder ",
) || request_line
.starts_with("GET /api/editor/assets/folders/source-binding-folder ")
{
write_json(
&mut stream,
"200 OK",
serde_json::json!({"data": {"library": {"folders": [{
serde_json::json!({"data": {"folder": {
"folderId": "source-binding-folder"
}]}}}),
}}}),
);
} else if request_line
.starts_with("POST /api/external/v1/assets/direct-upload-tickets ")
@@ -6900,7 +6902,7 @@ mod tests {
"generated/owner-b-source.{}",
if is_image { "png" } else { "mp4" }
);
let request_count = if is_image { 6 } else { 7 };
let request_count = 6;
let server_upload_url = upload_url.clone();
let server_object_key = owner_b_object_key.clone();
let (sender, receiver) = mpsc::channel();
@@ -6935,18 +6937,12 @@ mod tests {
serde_json::json!({"data": {"project": {"projectId": "owner-b-project"}}}),
);
} else if request_line
.starts_with("GET /api/external/v1/editor/assets/library ")
.starts_with("GET /api/external/v1/editor/assets/folders/owner-b-folder ")
{
write_json(
&mut stream,
"200 OK",
if is_image {
serde_json::json!({"data": {"library": {"folders": [{
"folderId": "owner-b-folder"
}]}}})
} else {
serde_json::json!({"data": {"library": {"folders": []}}})
},
serde_json::json!({"data": {"folder": {"folderId": "owner-b-folder"}}}),
);
} else if request_line
.starts_with("POST /api/external/v1/editor/assets/folders ")
@@ -7417,7 +7413,9 @@ mod tests {
assert!(requests.iter().all(|request| {
let line = request.lines().next().unwrap_or_default();
line.starts_with("GET /api/external/v1/editor/projects?view=summary ")
|| line.starts_with("GET /api/external/v1/editor/assets/library ")
|| line.starts_with(
"GET /api/external/v1/editor/assets/folders/source-binding-folder ",
)
}));
}
@@ -9582,7 +9580,7 @@ mod tests {
let server_generated_video = generated_video.clone();
let (sender, receiver) = mpsc::channel();
let server = std::thread::spawn(move || {
for request_index in 0..11 {
for request_index in 0..10 {
let mut stream = accept_resource_editor_fixture_connection(
&listener,
"External video fixture",
@@ -9608,12 +9606,6 @@ mod tests {
"projectId": "external-video-project"
}}}),
);
} else if request_line.starts_with("GET /api/external/v1/editor/assets/library ") {
write_json(
&mut stream,
"200 OK",
serde_json::json!({"data": {"library": {"folders": []}}}),
);
} else if request_line.starts_with("POST /api/external/v1/editor/assets/folders ") {
write_json(
&mut stream,
@@ -9731,7 +9723,7 @@ mod tests {
.expect("derive External video");
server.join().expect("join External video server");
let requests = std::iter::from_fn(|| receiver.try_recv().ok()).collect::<Vec<_>>();
assert_eq!(requests.len(), 11);
assert_eq!(requests.len(), 10);
assert!(requests.iter().all(|request| {
!request.starts_with("POST /api/assets/")
&& !request.starts_with("GET /api/runtime/external-generation/")
@@ -400,13 +400,15 @@ fn respond_canvas_context_request(stream: &mut TcpStream, request: &str) -> bool
serde_json::json!({"data": {"projects": [{"projectId": REMOTE_PROJECT_ID}]}}),
);
true
} else if line.starts_with("GET /api/external/v1/editor/assets/library ")
|| line.starts_with("GET /api/editor/assets/library ")
{
} else if line.starts_with(&format!(
"GET /api/external/v1/editor/assets/folders/{REMOTE_FOLDER_ID} "
)) || line.starts_with(&format!(
"GET /api/editor/assets/folders/{REMOTE_FOLDER_ID} "
)) {
write_json(
stream,
"200 OK",
serde_json::json!({"data": {"library": {"folders": [{"folderId": REMOTE_FOLDER_ID}]}}}),
serde_json::json!({"data": {"folder": {"folderId": REMOTE_FOLDER_ID}}}),
);
true
} else {
@@ -786,6 +786,46 @@
}
},
"/api/external/v1/editor/assets/folders/{folderId}": {
"get": {
"tags": ["Editor Assets"],
"operationId": "getEditorAssetFolder",
"summary": "按 ID 读取当前账号素材文件夹",
"description": "要求 editor:asset scope。仅按 folderId 读取目录并校验当前账号归属,不读取素材列表或创建默认目录;空目录也可读取。目录不存在返回 404,无权访问返回 403。",
"security": [
{
"ExternalApiKey": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/FolderId"
}
],
"responses": {
"200": {
"description": "素材文件夹",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExternalEditorAssetFolderResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"$ref": "#/components/responses/Forbidden"
},
"404": {
"$ref": "#/components/responses/NotFound"
}
}
},
"patch": {
"tags": ["Editor Assets"],
"operationId": "updateEditorAssetFolder",
@@ -53,6 +53,8 @@
## 验证路由
AGC 生成准备只通过单目录 GET 确认当前账号绑定,不能重新依赖完整素材库,也不能把目录查询失败当成首次无绑定而创建目录。目录删除时保留证据并停止;`prepared / accepted / legacy-completed` 生成账本继续用冻结快照恢复。该查询同时覆盖站内会话与 External v1,后端新增接口先发布,旧 API 保持兼容;详见 AGC 实施计划和后端数据契约。
Windows 下的移动壳 smoke 通过 Node 启动从当前 workspace 包解析出的 Expo/EAS CLI,不直接 `spawnSync('npm.cmd')`;保留原配置与导出断言。具体入口和警告清理边界见本地开发运维文档。
AGC 的纯桌面/生产接入按模块排除测试编译,共享实现与受测入口保持可测试;拆分后同时检查普通目标和测试目标,并保留原有行为断言。命令注册与退出接线的源码检查跟随真实模块位置更新,不能因移动文件而漏检;不通过全局允许死代码或虚假调用消除告警。具体边界见开发运维文档“编译告警的保留边界与待优化项”。
@@ -946,6 +946,10 @@ Agent Runtime 负责:
- 2026-07-10 补充:后台任务工具箱已加入 `preview.start`。Agent 可在 loop 中自行请求启动当前项目的本地 HTTP 预览;Runtime 会复用 `preview.start` 策略、项目写锁、共享 `PreviewRegistry`、manifest 预览状态、`.agent/logs/preview.log` 和 run trace 追加逻辑,并把 `agent.runtime.preview.start` 写入 `.agent/agent.db`。该 observation 只向 LLM 返回 localhost URL 与端口,不返回用户项目绝对路径。
- External Editor 的远端项目和资源属于服务 principal,不属于本地项目的跨账号身份。`.agent/manifest.json` 中 `source.canvasProjectId / resourceId / assetObjectId` 只记录生成来源,不得作为当前登录账号的 active editable binding。客户端在 `.agent/runtime/external-editor-bindings/` 私有保存账号级投影:项目键至少包含服务 origin、principal 和本地 `projectId`,资源键再包含远端项目、本地 asset ID、源文件 SHA-256、媒体类型与 canonical asset kind;值保存该 principal 下的远端 project/folder/resource/object 身份。Token 轮换继续复用同一 principal binding,账号、服务、源字节或 canonical kind 变化必须重新建立;同名本地项目和项目改名不得参与权威匹配。
- binding 的 lookup key 与稳定 payload 内容指纹分开校验;远端 project/folder/resource/object ID、object key 或尺寸被单独改写时必须失败关闭。同一本地项目和 principal 的首次项目 binding 建立必须串行化覆盖“复读 binding → 必要的远端创建 → 原子安装 binding”,不得因并发任务创建孤儿画布。External v1 项目、素材目录和项目资源创建请求携带由 binding key 派生的稳定 `Idempotency-Key`;服务端在 owner 与接口命名空间内生成稳定 ID,并允许同键同正文安全重试,使远端响应后、本地 sidecar 落盘前崩溃也不会重复创建。
- 素材生成准备的目录确认采用单目录读取:站内 `GET /api/editor/assets/folders/{folderId}` 与开发者 `GET /api/external/v1/editor/assets/folders/{folderId}` 返回现有 `{folder: ...}` 数据结构,后端按目录主键读取并校验 owner,不读取账户素材、不创建默认目录、不执行媒体修复。External 入口要求 `editor:asset` scope;缺失目录返回 404,越权返回 403,未认证返回 401。原素材库快照及目录创建、修改、删除接口的合同不变;不修改持久化表结构。先部署新增后端接口,再发布依赖该接口的客户端;新客户端不回退完整素材库。
- 已有当前账号项目 binding 或恢复 partial 的目录身份时,在新的生成准备中按该 ID 确认目录并核对响应 ID;合法空目录也可继续。首次没有可复用目录身份时直接沿用稳定幂等创建和冻结正文,不先列举素材库,不按同名认领目录。partial 继续只承载中间恢复进度,成功写入完整 binding 后删除;既有生成账本的 `prepared / accepted / legacy-completed` 恢复仍使用冻结快照,不重新查询或创建目录。
- 单目录查询超时、断网、服务异常、鉴权失效、越权或会话切换时停止准备并保留现有 binding / 恢复证据,不解释为目录缺失、不自动 POST 新目录。明确 404 时提示绑定目录不存在并停止,不用原键复活目录、不换键重建或重发生成;自动重新绑定与素材分页、关键词筛选、按素材 ID 导入不属于该行为。验证覆盖全库不可用而单目录正常、空目录、首次建绑、缺失与越权、查询失败及切号,并保留并发单次创建、创建响应丢失、冻结正文及生成账本恢复回归。
- 单目录合同的定向验证使用 AGC Rust `external_canvas_context`、`canvas_generation_tests`、`project::resource_editor::tests` 与 `background_removal_tests`,后端使用 `external_editor` 和 `protected_route_matrix_keeps_auth_methods_and_head_behavior`;`npm run check:editor-idempotency-procedures` 在独立 SpacetimeDB 中验证真实目录读取、归属、删除与只读行为,host 单测不能代替该运行时证据。
- 当前 binding wire schema 为 v2,payload fingerprint 必填。已落盘的 v1 project/resource JSON 只能经独立严格 wire 结构读取,在 lookup key、principal、本地 source、远端字段、对象键、时间和尺寸等不变量全部通过后重算指纹并原子回写 v2。不得用 `serde(default)` 把未知或篡改文档混入迁移。
- 已完成的本地正式资源以本地文件和 manifest asset 身份为权威。切换账号后,当前账号没有资源 binding 时,必须从本地字节上传、confirm 并登记到当前账号的远端项目后再编辑或派生;任何请求都不得把旧账号的 `canvasProjectId / resourceId / objectKey` 与新账号 Token 组合。新绑定不得覆盖历史来源,也不得迁移或删除旧账号远端对象。已经 `prepared / accepted / running` 的生成或编辑 operation 仍冻结原 principal、原正文和原 operation;切换账号只能停止恢复并保留对账证据,不能改绑、重放或重复扣费。
- 远端提交返回 `operationId` 后先持久化 accepted 身份,再在每次 poll、download 和本地 commit 前复验冻结会话;同步本地 commit 持有冻结会话租约,使切号只能发生在完整安装前或安装后。Tauri 手工生成同样使用稳定的 durable operation slot,不得使用进程内临时幂等键。历史资源编辑账本从 manifest 远端 resource ID 迁移到本地 asset ID 时,只允许在本地 asset、路径和内容摘要仍一致时兼容旧指纹,新请求不得恢复信任 manifest 远端 ID。
@@ -617,6 +617,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- Rust 结构体:`EditorAssetFolder`
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
- 说明:图片画布账号级素材文件夹表,归属于用户账号而不是 project;首次读取素材库时自动创建系统默认“项目素材”文件夹。文件夹支持重命名、折叠和删除,系统默认文件夹不能删除。
- 单目录读取:`GET /api/editor/assets/folders/{folderId}` 与 `GET /api/external/v1/editor/assets/folders/{folderId}` 经同一 `spacetime-client` facade / 只读 procedure 按目录主键和 owner 校验,返回现有目录 DTO;不读取素材、不创建默认目录或修复媒体。External 要求 `editor:asset` scope;未认证 401、越权 403、缺失 404。既有全库、目录创建/修改/删除 API 合同和持久表结构保持不变;先部署后端再发布依赖新接口的 AGC 客户端,不提供全库回退。
- 索引:`by_editor_asset_folder_owner_user_id`。
### `editor_asset`
+96 -1
View File
@@ -384,6 +384,28 @@ function assertDeletedConflict(result, recordLabel) {
}
async function runSmoke(configPath, serverUrl) {
const readFolder = (folderId, ownerUserId = 'receipt-owner') =>
callProcedure(configPath, serverUrl, 'get_editor_asset_folder_and_return', {
folder_id: folderId,
owner_user_id: ownerUserId,
});
const missingBeforeCreate = await readFolder('missing-folder', 'empty-owner');
assert(
missingBeforeCreate.ok === false &&
missingBeforeCreate.errorMessage === '素材文件夹不存在',
'Missing folder read did not return the missing-folder error.',
);
assert(
(
await sqlRows(
configPath,
serverUrl,
'SELECT folder_id FROM editor_asset_folder',
'folders after missing read',
)
).length === 0,
'Missing folder read created a default folder.',
);
const projectA = projectInput('project-a', 'First');
assertOk(
await callProcedure(
@@ -471,6 +493,73 @@ async function runSmoke(configPath, serverUrl) {
'asset folder drift replay',
);
const folderRowsBeforeRead = await sqlRows(
configPath,
serverUrl,
'SELECT * FROM editor_asset_folder',
'folders before read',
);
const receiptsBeforeRead = await sqlRows(
configPath,
serverUrl,
'SELECT * FROM editor_idempotent_create_receipt',
'receipts before read',
);
assert(
(
await sqlRows(
configPath,
serverUrl,
'SELECT asset_id FROM editor_asset',
'assets before folder read',
)
).length === 0,
'Folder read fixture must have no assets.',
);
const emptyFolder = await readFolder('folder-a');
assertOk(emptyFolder, 'empty asset folder read');
assert(
emptyFolder.record[0] === 'folder-a' &&
emptyFolder.record[1] === 'Folder A',
'Folder read returned a different folder.',
);
const foreignFolder = await readFolder('folder-a', 'other-owner');
assert(
foreignFolder.ok === false &&
foreignFolder.record === null &&
foreignFolder.errorMessage === '无权访问该素材文件夹',
'Foreign-owner folder read did not reject access.',
);
const missingFolder = await readFolder('missing-folder');
assert(
missingFolder.ok === false &&
missingFolder.record === null &&
missingFolder.errorMessage === '素材文件夹不存在',
'Missing folder read did not fail closed.',
);
assert(
JSON.stringify(
await sqlRows(
configPath,
serverUrl,
'SELECT * FROM editor_asset_folder',
'folders after read',
),
) === JSON.stringify(folderRowsBeforeRead),
'Folder reads changed folder rows or timestamps.',
);
assert(
JSON.stringify(
await sqlRows(
configPath,
serverUrl,
'SELECT * FROM editor_idempotent_create_receipt',
'receipts after read',
),
) === JSON.stringify(receiptsBeforeRead),
'Folder reads changed durable receipts.',
);
const sameRaceInput = projectInput('project-same-race', 'Same Race');
const sameRace = await Promise.all([
callProcedure(
@@ -641,6 +730,12 @@ async function runSmoke(configPath, serverUrl) {
},
);
assertOk(deleteFolderResult, 'asset folder delete');
const deletedFolder = await readFolder('folder-a');
assert(
deletedFolder.ok === false &&
deletedFolder.errorMessage === '素材文件夹不存在',
'Deleted folder read did not return the missing-folder error.',
);
assertDeletedConflict(
await callProcedure(
configPath,
@@ -757,7 +852,7 @@ export async function main() {
await runSmoke(configPath, serverUrl);
console.log(
'[editor-idempotency-smoke] Passed exact replay, body conflicts, deletion fail-close, concurrent calls, and orphan checks.',
'[editor-idempotency-smoke] Passed folder reads (empty, missing, foreign owner, no writes), exact replay, body conflicts, deletion fail-close, concurrent calls, and orphan checks.',
);
} catch (error) {
const standaloneOutput = sanitizeDiagnostic(
@@ -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"]),
(
@@ -52,7 +52,7 @@ pub use self::editor_agent::{
pub use self::editor_project::{
AdminEditorAssetListRecordInput, AdminEditorAssetRecord, EditorAssetCreateRecordInput,
EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderRecord,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderGetRecordInput, EditorAssetFolderRecord,
EditorAssetFolderUpdateRecordInput, EditorAssetGroupCohortCompleteRecordInput,
EditorAssetGroupSourceLookupRecordInput, EditorAssetLibraryRecord,
EditorAssetMediaRepairRecordInput, EditorAssetRecord, EditorAssetUpdateRecordInput,
@@ -362,6 +362,12 @@ pub struct EditorProjectResourcePublicShowcaseListRecordInput {
pub cursor: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorAssetFolderGetRecordInput {
pub folder_id: String,
pub owner_user_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorAssetFolderCreateRecordInput {
pub folder_id: String,
@@ -711,6 +717,15 @@ impl From<EditorProjectResourcePublicShowcaseListRecordInput>
}
}
impl From<EditorAssetFolderGetRecordInput> for crate::module_bindings::EditorAssetFolderGetInput {
fn from(input: EditorAssetFolderGetRecordInput) -> Self {
Self {
folder_id: input.folder_id,
owner_user_id: input.owner_user_id,
}
}
}
impl From<EditorAssetFolderCreateRecordInput>
for crate::module_bindings::EditorAssetFolderCreateInput
{
@@ -819,6 +819,28 @@ impl SpacetimeClient {
.await
}
pub async fn get_editor_asset_folder(
&self,
input: EditorAssetFolderGetRecordInput,
) -> Result<EditorAssetFolderRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"get_editor_asset_folder_and_return",
move |connection, sender| {
connection
.procedures()
.get_editor_asset_folder_and_return_then(procedure_input, move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_folder_procedure_result);
send_once(&sender, mapped);
});
},
)
.await
}
pub async fn create_editor_asset_folder(
&self,
input: EditorAssetFolderCreateRecordInput,
@@ -208,6 +208,7 @@ pub mod editor_asset_create_input_type;
pub mod editor_asset_delete_input_type;
pub mod editor_asset_folder_create_input_type;
pub mod editor_asset_folder_delete_input_type;
pub mod editor_asset_folder_get_input_type;
pub mod editor_asset_folder_procedure_result_type;
pub mod editor_asset_folder_snapshot_type;
pub mod editor_asset_folder_table;
@@ -460,6 +461,7 @@ pub mod get_asset_object_by_id_and_return_procedure;
pub mod get_asset_object_by_location_and_return_procedure;
pub mod get_asset_read_access_by_location_and_return_procedure;
pub mod get_editor_agent_conversation_and_return_procedure;
pub mod get_editor_asset_folder_and_return_procedure;
pub mod get_editor_asset_library_and_return_procedure;
pub mod get_editor_generation_pricing_config_and_return_procedure;
pub mod get_editor_project_and_return_procedure;
@@ -1009,6 +1011,7 @@ pub use editor_asset_create_input_type::EditorAssetCreateInput;
pub use editor_asset_delete_input_type::EditorAssetDeleteInput;
pub use editor_asset_folder_create_input_type::EditorAssetFolderCreateInput;
pub use editor_asset_folder_delete_input_type::EditorAssetFolderDeleteInput;
pub use editor_asset_folder_get_input_type::EditorAssetFolderGetInput;
pub use editor_asset_folder_procedure_result_type::EditorAssetFolderProcedureResult;
pub use editor_asset_folder_snapshot_type::EditorAssetFolderSnapshot;
pub use editor_asset_folder_table::*;
@@ -1261,6 +1264,7 @@ pub use get_asset_object_by_id_and_return_procedure::get_asset_object_by_id_and_
pub use get_asset_object_by_location_and_return_procedure::get_asset_object_by_location_and_return;
pub use get_asset_read_access_by_location_and_return_procedure::get_asset_read_access_by_location_and_return;
pub use get_editor_agent_conversation_and_return_procedure::get_editor_agent_conversation_and_return;
pub use get_editor_asset_folder_and_return_procedure::get_editor_asset_folder_and_return;
pub use get_editor_asset_library_and_return_procedure::get_editor_asset_library_and_return;
pub use get_editor_generation_pricing_config_and_return_procedure::get_editor_generation_pricing_config_and_return;
pub use get_editor_project_and_return_procedure::get_editor_project_and_return;
@@ -0,0 +1,16 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct EditorAssetFolderGetInput {
pub folder_id: String,
pub owner_user_id: String,
}
impl __sdk::InModule for EditorAssetFolderGetInput {
type Module = super::RemoteModule;
}
@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::editor_asset_folder_get_input_type::EditorAssetFolderGetInput;
use super::editor_asset_folder_procedure_result_type::EditorAssetFolderProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct GetEditorAssetFolderAndReturnArgs {
pub input: EditorAssetFolderGetInput,
}
impl __sdk::InModule for GetEditorAssetFolderAndReturnArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `get_editor_asset_folder_and_return`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait get_editor_asset_folder_and_return {
fn get_editor_asset_folder_and_return(&self, input: EditorAssetFolderGetInput) {
self.get_editor_asset_folder_and_return_then(input, |_, _| {});
}
fn get_editor_asset_folder_and_return_then(
&self,
input: EditorAssetFolderGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<EditorAssetFolderProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl get_editor_asset_folder_and_return for super::RemoteProcedures {
fn get_editor_asset_folder_and_return_then(
&self,
input: EditorAssetFolderGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<EditorAssetFolderProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, EditorAssetFolderProcedureResult>(
"get_editor_asset_folder_and_return",
GetEditorAssetFolderAndReturnArgs { input },
__callback,
);
}
}
@@ -972,6 +972,12 @@ pub struct EditorAssetLibraryGetInput {
pub now_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct EditorAssetFolderGetInput {
pub folder_id: String,
pub owner_user_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct EditorAssetFolderCreateInput {
pub folder_id: String,
@@ -1872,6 +1878,17 @@ pub fn find_editor_asset_group_source_and_return(
}
}
#[spacetimedb::procedure]
pub fn get_editor_asset_folder_and_return(
ctx: &mut ProcedureContext,
input: EditorAssetFolderGetInput,
) -> EditorAssetFolderProcedureResult {
match ctx.try_with_tx(|tx| get_editor_asset_folder(tx, input.clone())) {
Ok(folder) => editor_asset_folder_ok(Some(folder), None),
Err(message) => editor_asset_folder_error(message),
}
}
#[spacetimedb::procedure]
pub fn create_editor_asset_folder_and_return(
ctx: &mut ProcedureContext,
@@ -5777,6 +5794,17 @@ fn create_editor_asset_folder(
.ok_or_else(|| "素材文件夹创建失败".to_string())
}
fn get_editor_asset_folder(
ctx: &ReducerContext,
input: EditorAssetFolderGetInput,
) -> Result<EditorAssetFolderSnapshot, String> {
let folder_id = normalize_required(&input.folder_id, "editor_asset_folder.folder_id")?;
let owner_user_id =
normalize_required(&input.owner_user_id, "editor_asset_folder.owner_user_id")?;
require_owned_asset_folder(ctx, folder_id.as_str(), owner_user_id.as_str())
.map(asset_folder_snapshot_from_row)
}
fn update_editor_asset_folder(
ctx: &ReducerContext,
input: EditorAssetFolderUpdateInput,