Merge pull request '修复 AGC 栏目画布上传素材落「待归类」(#359)' (#596) from fix/agc-asset-upload-category into master
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Reviewed-on: #596
This commit was merged in pull request #596.
This commit is contained in:
@@ -567,6 +567,23 @@ pub(crate) fn upload_local_asset_at(
|
||||
file_name: &str,
|
||||
media_type: &str,
|
||||
bytes: &[u8],
|
||||
) -> Result<UploadLocalAssetResult, String> {
|
||||
upload_local_asset_at_with_category(root, file_name, media_type, bytes, None)
|
||||
}
|
||||
|
||||
/// 带**显式入口栏目**的上传:只给 GUI 工具栏上传用(前端 `targetCategory`)。
|
||||
///
|
||||
/// 上传的 manifest `kind` 只由内容证据推导(图片 / 视频 / 代码 → `unclassified`),
|
||||
/// 与栏目不是同一套词汇:在栏目画布里上传素材时,用户选定的栏目才是归属真相,
|
||||
/// 否则素材会落进「待归类」、在上传它的那一栏里看不见。
|
||||
/// 取值校验与失败关闭沿用 [`register_local_asset_entry_with_category`]:
|
||||
/// 非法值报错、绝不回退到 kind 派生;其它调用方继续走 [`upload_local_asset_at`]。
|
||||
pub(crate) fn upload_local_asset_at_with_category(
|
||||
root: &Path,
|
||||
file_name: &str,
|
||||
media_type: &str,
|
||||
bytes: &[u8],
|
||||
target_category: Option<&str>,
|
||||
) -> Result<UploadLocalAssetResult, String> {
|
||||
if bytes.is_empty() {
|
||||
return Err("上传文件不能为空".to_string());
|
||||
@@ -584,7 +601,7 @@ pub(crate) fn upload_local_asset_at(
|
||||
}
|
||||
crate::write_game_creator_private_file(&absolute_path, bytes, "上传文件")?;
|
||||
|
||||
register_local_asset_entry(
|
||||
register_local_asset_entry_with_category(
|
||||
root,
|
||||
&relative_path,
|
||||
uploaded_asset_kind(file_name, media_type),
|
||||
@@ -602,6 +619,7 @@ pub(crate) fn upload_local_asset_at(
|
||||
generation_kind: None,
|
||||
reference_resource_ids: Vec::new(),
|
||||
},
|
||||
target_category,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2375,6 +2393,70 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Issue 359:工具栏上传带上入口栏目,素材才不会落进「待归类」。
|
||||
///
|
||||
/// 上传的 manifest `kind` 只由内容证据推导,图片 / 视频 / 代码都派生成 `unclassified`;
|
||||
/// 在栏目画布(如「角色与对象」)里上传时栏目才是归属真相。不传时保持既有行为,
|
||||
/// 非法值失败关闭且不留下半成品登记。
|
||||
#[test]
|
||||
fn upload_registers_into_the_explicit_entry_category() {
|
||||
fn upload_category(root: &Path, asset_id: &str) -> GameCreationAppAssetCategory {
|
||||
read_existing_manifest_for_project(root)
|
||||
.expect("read manifest")
|
||||
.assets
|
||||
.into_iter()
|
||||
.find(|asset| asset.id == asset_id)
|
||||
.expect("uploaded asset is registered")
|
||||
.category
|
||||
}
|
||||
|
||||
let temporary = tempfile::tempdir().expect("tempdir");
|
||||
let root = temporary.path();
|
||||
crate::project::init_local_game_project_at(root, "upload-category-test", "上传入口栏目")
|
||||
.expect("init project");
|
||||
|
||||
let uploaded = upload_local_asset_at_with_category(
|
||||
root,
|
||||
"hero.png",
|
||||
"image/png",
|
||||
b"png-bytes",
|
||||
Some("character"),
|
||||
)
|
||||
.expect("upload with an entry category");
|
||||
assert_eq!(
|
||||
upload_category(root, &uploaded.id),
|
||||
GameCreationAppAssetCategory::Character
|
||||
);
|
||||
|
||||
let plain = upload_local_asset_at(root, "plain.png", "image/png", b"png-bytes")
|
||||
.expect("upload without an entry category");
|
||||
assert_eq!(
|
||||
upload_category(root, &plain.id),
|
||||
GameCreationAppAssetCategory::Unclassified
|
||||
);
|
||||
|
||||
// 非法入口栏目(栏目侧伪值 `version` 不在枚举里)失败关闭:不新增登记。
|
||||
let assets_before = read_existing_manifest_for_project(root)
|
||||
.expect("read manifest")
|
||||
.assets
|
||||
.len();
|
||||
assert!(upload_local_asset_at_with_category(
|
||||
root,
|
||||
"bad.png",
|
||||
"image/png",
|
||||
b"png-bytes",
|
||||
Some("version"),
|
||||
)
|
||||
.is_err());
|
||||
assert_eq!(
|
||||
read_existing_manifest_for_project(root)
|
||||
.expect("read manifest")
|
||||
.assets
|
||||
.len(),
|
||||
assets_before
|
||||
);
|
||||
}
|
||||
|
||||
/// 画板导出推断出的 kind 必须已经是 canonical 值。
|
||||
#[test]
|
||||
fn canvas_export_asset_kind_is_always_canonical() {
|
||||
|
||||
@@ -493,12 +493,19 @@ pub(crate) fn upload_local_asset(
|
||||
file_name: String,
|
||||
media_type: String,
|
||||
bytes: Vec<u8>,
|
||||
target_category: Option<String>,
|
||||
) -> Result<UploadLocalAssetResult, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "asset.upload")?;
|
||||
let _lock = acquire_project_write_lock(root, "asset.upload")?;
|
||||
advance_agent_runtime_project_revision_locked(root)?;
|
||||
upload_local_asset_at(root, file_name.trim(), media_type.trim(), &bytes)
|
||||
upload_local_asset_at_with_category(
|
||||
root,
|
||||
file_name.trim(),
|
||||
media_type.trim(),
|
||||
&bytes,
|
||||
target_category.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
+7
-1
@@ -99,7 +99,13 @@ export type ResourceCanvasAudioToolAction =
|
||||
audioKind: ResourceCanvasGenerationKind;
|
||||
};
|
||||
|
||||
/** 上传入口:复用既有 `upload_local_asset`。 */
|
||||
/**
|
||||
* 上传入口:复用既有 `upload_local_asset`。
|
||||
*
|
||||
* 调用方按当前栏目带 `targetCategory`(见 `uploadResourceCanvasToolbarFiles`)——上传的
|
||||
* manifest `kind` 只由内容证据推导,图片 / 视频 / 代码都会派生成 `unclassified`,
|
||||
* 不带入口栏目素材就落「待归类」、在上传它的那一栏里看不见。
|
||||
*/
|
||||
export type ResourceCanvasUploadToolAction =
|
||||
ResourceCanvasBottomToolActionBase & {
|
||||
route: 'upload';
|
||||
|
||||
@@ -3348,8 +3348,9 @@ export default function ProjectDevelopmentView({
|
||||
* 两条判据缺一不可:
|
||||
* 1. 这张卡已经进了布局(`resourcePositionById`),否则 reconcile 还没补位、写坐标是空操作;
|
||||
* 2. 这张卡已经有正式归类(投影里的 `category`)——`commitPosition` 的 `section` 必须与该资源
|
||||
* 在 sidecar 里的 section 一致,否则会被静默跳过。生成结果的实际归类与入口栏目本来就可能不同
|
||||
* (普通图片落「待归类」、规范落「文档」),所以这里按**正式归类**写,而不是入口栏目。
|
||||
* 在 sidecar 里的 section 一致,否则会被静默跳过。生成结果的实际归类与入口栏目**不一定相同**
|
||||
* (Agent / Direct 路径不传 `targetCategory`,普通图片仍落「待归类」),所以这里按**正式归类**写,
|
||||
* 而不是入口栏目。
|
||||
*
|
||||
* 写完撤掉占位:结果已经接管了它的位置。占位被用户先删掉时同样清掉这条意图(没有位置可接管)。
|
||||
*/
|
||||
@@ -9062,9 +9063,15 @@ export default function ProjectDevelopmentView({
|
||||
[selectResourceCanvasPage],
|
||||
);
|
||||
|
||||
/** 工具栏「上传」:与资源面板上传同一条「上传 + 配对读清单」链路。 */
|
||||
/**
|
||||
* 工具栏「上传」:与资源面板上传同一条「上传 + 配对读清单」链路。
|
||||
*
|
||||
* 入口栏目随上传一起交给原生(`targetCategory`):上传的 kind 只由内容证据推导
|
||||
* (图片 / 视频 / 代码 → `unclassified`),不带上它,素材会落进「待归类」、
|
||||
* 在上传它的那一栏里看不见。取值就是工具栏自己的栏目,与生成入口同一口径。
|
||||
*/
|
||||
const uploadResourceCanvasToolbarFiles = useCallback(
|
||||
async (files: readonly File[]) => {
|
||||
async (files: readonly File[], targetCategory: ResourceCategory | null) => {
|
||||
const invoke = window.__TAURI__?.core?.invoke;
|
||||
if (!invoke) {
|
||||
setResourceWorkbenchNotice('上传素材需要在客户端内打开');
|
||||
@@ -9088,6 +9095,7 @@ export default function ProjectDevelopmentView({
|
||||
bytes: Array.from(new Uint8Array(await file.arrayBuffer())),
|
||||
})),
|
||||
),
|
||||
targetCategory,
|
||||
invoke,
|
||||
});
|
||||
setResourceWorkbenchNotice(`已上传 ${uploadable.length} 个素材`);
|
||||
@@ -10548,7 +10556,10 @@ export default function ProjectDevelopmentView({
|
||||
}
|
||||
onSelectAction={handleResourceBottomToolAction}
|
||||
onUploadFiles={(files) => {
|
||||
void uploadResourceCanvasToolbarFiles(files);
|
||||
void uploadResourceCanvasToolbarFiles(
|
||||
files,
|
||||
resourceCanvasBottomToolbarCategory,
|
||||
);
|
||||
}}
|
||||
uploading={resourceBottomToolbarUploading}
|
||||
/>
|
||||
|
||||
+13
-1
@@ -1,4 +1,7 @@
|
||||
import type { GameCreationAppManifest } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import type {
|
||||
GameCreationAppManifest,
|
||||
ProjectResourceCanvasCategory,
|
||||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
|
||||
export type ProjectManifestSnapshotSource =
|
||||
| 'initial'
|
||||
@@ -285,6 +288,14 @@ export async function uploadProjectAssetFilesAndReadSnapshot(input: {
|
||||
mediaType: string;
|
||||
bytes: number[];
|
||||
}[];
|
||||
/**
|
||||
* 入口栏目:栏目画布里上传时由调用方显式给出当前栏目,原生按它登记归类。
|
||||
*
|
||||
* 上传的 manifest `kind` 只由内容证据推导(图片 / 视频 / 代码 → `unclassified`),
|
||||
* 不传这一项时素材会落进「待归类」、在上传它的那一栏里看不见。
|
||||
* 判定与失败关闭都在原生:非法值报错,前端不做伪分类。
|
||||
*/
|
||||
targetCategory?: ProjectResourceCanvasCategory | null;
|
||||
invoke<T>(command: string, args: Record<string, unknown>): Promise<T>;
|
||||
}): Promise<ProjectManifestSnapshot | null> {
|
||||
for (const file of input.files) {
|
||||
@@ -293,6 +304,7 @@ export async function uploadProjectAssetFilesAndReadSnapshot(input: {
|
||||
fileName: file.fileName,
|
||||
mediaType: file.mediaType,
|
||||
bytes: file.bytes,
|
||||
...(input.targetCategory ? { targetCategory: input.targetCategory } : {}),
|
||||
});
|
||||
}
|
||||
const fresh = await rereadAuthoritativeProjectManifestSnapshot({
|
||||
|
||||
@@ -8839,6 +8839,52 @@ export function registerProjectAgentStatusTests() {
|
||||
);
|
||||
}, 20_000);
|
||||
|
||||
it('uploads toolbar files into the entry column so they stay visible where they were uploaded', async () => {
|
||||
// Issue 359:图片只由内容证据派生成 `unclassified`,在「角色与对象」栏目里上传后
|
||||
// 会落进「待归类」、在上传它的那一栏里看不见。工具栏上传必须把当前栏目交给原生。
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-bottom-toolbar-upload-category',
|
||||
'底部工具栏上传归类项目',
|
||||
);
|
||||
const { calls } = installResourceBookBottomToolbarInvoke(manifest);
|
||||
|
||||
render(
|
||||
React.createElement(ProjectDevelopmentView, {
|
||||
projectName: manifest.name,
|
||||
projectPath: '/tmp/workbench-bottom-toolbar-upload-category',
|
||||
manifest,
|
||||
attachments: [],
|
||||
recentRunStatus: null,
|
||||
recentRunStopReason: null,
|
||||
chat: React.createElement('div', null, '项目总控'),
|
||||
onHomeOpen: vi.fn(),
|
||||
onProjectsOpen: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
await openResourceBookCategory('角色与对象');
|
||||
const uploadInput = screen.getByLabelText(
|
||||
'上传素材文件',
|
||||
) as HTMLInputElement;
|
||||
const file = new File(['png'], 'hero.png', { type: 'image/png' });
|
||||
if (typeof file.arrayBuffer !== 'function') {
|
||||
Object.defineProperty(file, 'arrayBuffer', {
|
||||
value: async () => new Uint8Array([112, 110, 103]).buffer,
|
||||
});
|
||||
}
|
||||
fireEvent.change(uploadInput, { target: { files: [file] } });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(uploadCall(calls, 'hero.png')).toMatchObject({
|
||||
projectPath: '/tmp/workbench-bottom-toolbar-upload-category',
|
||||
fileName: 'hero.png',
|
||||
mediaType: 'image/png',
|
||||
targetCategory: 'character',
|
||||
bytes: [112, 110, 103],
|
||||
}),
|
||||
);
|
||||
}, 20_000);
|
||||
|
||||
it('keeps the bottom toolbar clear of the zoom dock and above the book scene', () => {
|
||||
const styles = readFileSync(
|
||||
repoPath('apps/ai-game-creator-shell/src/styles.css'),
|
||||
|
||||
Reference in New Issue
Block a user