diff --git a/packages/cocos-cli-types/__tests__/__snapshots__/dts-snapshot.test.ts.snap b/packages/cocos-cli-types/__tests__/__snapshots__/dts-snapshot.test.ts.snap index 5b2cdc6d7..34032cc74 100644 --- a/packages/cocos-cli-types/__tests__/__snapshots__/dts-snapshot.test.ts.snap +++ b/packages/cocos-cli-types/__tests__/__snapshots__/dts-snapshot.test.ts.snap @@ -658,7 +658,6 @@ export declare interface RtSpriteFrameAssetUserData { height?: number; } export declare function saveAsset(pathOrUrlOrUUID: string, data: string | Buffer): Promise; -export declare function saveAssetMeta(uuid: string, meta: IAssetMeta): Promise; export declare interface ScriptModuleUserData { isPlugin: false; } @@ -6817,7 +6816,6 @@ export declare namespace Assets { refresh, queryAssetInfo, queryAssetMeta, - saveAssetMeta, queryCreateMap, queryAssetInfos, queryAssetDBInfos, @@ -8733,7 +8731,6 @@ export declare interface RtSpriteFrameAssetUserData { } export declare function save(force?: boolean): Promise; export declare function saveAsset(pathOrUrlOrUUID: string, data: string | Buffer): Promise; -export declare function saveAssetMeta(uuid: string, meta: IAssetMeta): Promise; export declare namespace Scene { export { init_6 as init, diff --git a/src/api/assets/assets.ts b/src/api/assets/assets.ts index 2163792fb..eeee23c98 100644 --- a/src/api/assets/assets.ts +++ b/src/api/assets/assets.ts @@ -696,7 +696,7 @@ export class AssetsApi { */ @tool('assets-update-asset-user-data') @title('Update Asset User Data') // 更新资源用户数据 - @description('Update the user data configuration of the specified asset. Precisely update the asset\'s user data via path and value, supporting nested path access.') // 更新指定资源的用户数据配置。通过路径和值来精确更新资源的用户数据,支持嵌套路径访问。 + @description('Update the userData of the specified asset via path and value. urlOrUuidOrPath accepts an asset URL, UUID, file path, or sub asset UUID in parentUuid@subMetaId format.') // 更新指定资源的用户数据配置。通过路径和值来精确更新资源的用户数据,支持嵌套路径访问。 @result(SchemaUpdateAssetUserDataResult) async updateAssetUserData( @param(SchemaUrlOrUUIDOrPath) urlOrUuidOrPath: TUrlOrUUIDOrPath, diff --git a/src/api/base/schema-identifier.ts b/src/api/base/schema-identifier.ts index 7bc4554df..d5df8dc9c 100644 --- a/src/api/base/schema-identifier.ts +++ b/src/api/base/schema-identifier.ts @@ -101,6 +101,36 @@ export const SchemaUUID = z.string() return `${cleaned.slice(0, 8)}-${cleaned.slice(8, 12)}-${cleaned.slice(12, 16)}-${cleaned.slice(16, 20)}-${cleaned.slice(20, 32)}`; }); +const isSubAssetUUIDLike = (value: string): boolean => { + return /^[0-9a-fA-F-]{32,36}@/.test(removeWhitespace(value)); +}; + +export const SchemaSubAssetUUID = z.string() + .min(1, 'Sub asset UUID cannot be empty') + .describe('Sub asset UUID in parentUuid@subMetaId format') + .transform((value, ctx) => { + const cleaned = removeWhitespace(value).toLowerCase(); + const match = cleaned.match(/^([0-9a-f-]{32,36})((?:@[0-9a-f]{5,})+)$/); + + if (!match) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Invalid sub asset UUID format. Use parentUuid@subMetaId.', + }); + return z.NEVER; + } + + const parentUuid = SchemaUUID.safeParse(match[1]); + if (!parentUuid.success) { + parentUuid.error.errors.forEach((err) => { + ctx.addIssue(err); + }); + return z.NEVER; + } + + return `${parentUuid.data}${match[2]}`; + }); + // ==================== 3. Path Schema ==================== export const SchemaPath = z.string() .min(1, '路径不能为空') @@ -172,6 +202,9 @@ export const SchemaUrlOrUUIDOrPath = z.string() if (/^[0-9a-f]{32}$/.test(potentialUuid)) { return SchemaUUID.parse(cleaned); } + if (isSubAssetUUIDLike(cleaned)) { + return SchemaSubAssetUUID.parse(cleaned); + } // 3. Path return SchemaPath.parse(cleaned); @@ -184,7 +217,7 @@ export const SchemaUrlOrUUIDOrPath = z.string() } throw error; } - }).describe('Asset URL, UUID or file path'); // 资源的 URL、UUID 或文件路径 + }).describe('Asset URL, UUID, sub asset UUID, or file path'); // 资源的 URL、UUID 或文件路径 // PATH 或 UUID export const SchemaUUIDOrPath = z.string() @@ -198,6 +231,9 @@ export const SchemaUUIDOrPath = z.string() if (/^[0-9a-f]{32}$/.test(potentialUuid)) { return SchemaUUID.parse(cleaned); } + if (isSubAssetUUIDLike(cleaned)) { + return SchemaSubAssetUUID.parse(cleaned); + } // 2. Path return SchemaPath.parse(cleaned); @@ -210,7 +246,7 @@ export const SchemaUUIDOrPath = z.string() } throw error; } - }).describe('Use UUID or file path'); + }).describe('Use UUID, sub asset UUID, or file path'); export const SchemaUrlOrPath = z.string() @@ -256,6 +292,9 @@ export const SchemaUrlOrUUID = z.string() if (/^[0-9a-f]{32}$/.test(potentialUuid)) { return SchemaUUID.parse(cleaned); } + if (isSubAssetUUIDLike(cleaned)) { + return SchemaSubAssetUUID.parse(cleaned); + } ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -271,7 +310,7 @@ export const SchemaUrlOrUUID = z.string() } throw error; } - }).describe('Use db:// protocol format or UUID'); // 使用 db:// 协议格式或者 UUID + }).describe('Use db:// protocol format, UUID, or sub asset UUID'); // 使用 db:// 协议格式或者 UUID export const SchemaSceneIdentifier = z.object({ assetName: z.string().describe('Scene or Prefab asset name'), // 场景/预制体资源名称 diff --git a/src/core/assets/test/operation-filesystem-bridge.test.ts b/src/core/assets/test/operation-filesystem-bridge.test.ts index 3fe631911..fc44d5a0e 100644 --- a/src/core/assets/test/operation-filesystem-bridge.test.ts +++ b/src/core/assets/test/operation-filesystem-bridge.test.ts @@ -166,6 +166,38 @@ describe('asset operation filesystem bridge', () => { jest.restoreAllMocks(); }); + it('updateUserData updates sub asset userData through composite uuid without reimport', async () => { + const { assetOperation } = require('../manager/operation') as typeof import('../manager/operation'); + const reimport = jest.fn(); + const subAsset = { + uuid: '6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a', + meta: { + userData: { + minfilter: 'linear', + }, + }, + save: jest.fn().mockResolvedValue(true), + _assetDB: { + reimport, + }, + }; + mockQueryAsset.mockReturnValue(subAsset); + + const result = await assetOperation.updateUserData( + '6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a', + 'minfilter', + 'nearest', + ); + + expect(mockQueryAsset).toHaveBeenCalledWith('6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a'); + expect(subAsset.meta.userData).toEqual({ + minfilter: 'nearest', + }); + expect(subAsset.save).toHaveBeenCalledTimes(1); + expect(reimport).not.toHaveBeenCalled(); + expect(result).toBe(subAsset.meta.userData); + }); + it('renameAsset should delegate rename steps to filesystem bridge', async () => { const { assetOperation } = require('../manager/operation') as typeof import('../manager/operation'); const source = 'D:/project/assets/source.txt'; diff --git a/src/lib/assets/assets.ts b/src/lib/assets/assets.ts index 96051b42c..1fc0a3aea 100644 --- a/src/lib/assets/assets.ts +++ b/src/lib/assets/assets.ts @@ -108,13 +108,6 @@ export async function queryAssetMeta(urlOrUUIDOrPath: string): Promise { - return await assetManager.saveAssetMeta(uuid, meta); -} - /** * Query Creatable Asset Map // 查询可创建资源映射表 */ diff --git a/tests/assets-update-asset-user-data-api.test.ts b/tests/assets-update-asset-user-data-api.test.ts new file mode 100644 index 000000000..5b944523a --- /dev/null +++ b/tests/assets-update-asset-user-data-api.test.ts @@ -0,0 +1,53 @@ +const mockUpdateUserData = jest.fn(); + +jest.mock('../src/core/assets', () => ({ + assetDBManager: {}, + assetManager: { + updateUserData: (...args: unknown[]) => mockUpdateUserData(...args), + }, +})); + +jest.mock('../src/api/decorator/decorator.js', () => jest.requireActual('../src/api/decorator/decorator'), { virtual: true }); + +import { toolRegistry } from '../src/api/decorator/decorator.js'; +import { COMMON_STATUS } from '../src/api/base/schema-base'; +import { AssetsApi } from '../src/api/assets/assets'; + +describe('assets-update-asset-user-data api', () => { + beforeEach(() => { + mockUpdateUserData.mockReset(); + }); + + it('does not register a separate meta userData update tool', () => { + expect(toolRegistry.get('assets-update-asset-meta-user-data')).toBeUndefined(); + }); + + it('accepts sub asset UUID as the target asset identifier', () => { + const tool = toolRegistry.get('assets-update-asset-user-data'); + const schema = tool?.meta.paramSchemas.find((param) => param.name === 'urlOrUuidOrPath')?.schema; + + expect(schema).toBeDefined(); + expect(schema!.parse('6FA5FBAD0D324B6395D824507665775C@6C48A')).toBe('6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a'); + }); + + it('delegates sub asset UUID updates to assetManager.updateUserData', async () => { + const updatedUserData = { minfilter: 'nearest' }; + mockUpdateUserData.mockResolvedValue(updatedUserData); + + const result = await new AssetsApi().updateAssetUserData( + '6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a', + 'minfilter', + 'nearest', + ); + + expect(result).toEqual({ + code: COMMON_STATUS.SUCCESS, + data: updatedUserData, + }); + expect(mockUpdateUserData).toHaveBeenCalledWith( + '6fa5fbad-0d32-4b63-95d8-24507665775c@6c48a', + 'minfilter', + 'nearest', + ); + }); +}); diff --git a/tests/lib/assets-api.test.ts b/tests/lib/assets-api.test.ts index bb8c88a1c..8c8917161 100644 --- a/tests/lib/assets-api.test.ts +++ b/tests/lib/assets-api.test.ts @@ -1,4 +1,3 @@ -import type { IAssetMeta } from '../../src/core/assets/@types/public'; import { assetManager } from '../../src/core/assets'; import * as Assets from '../../src/lib/assets/assets'; @@ -7,26 +6,32 @@ describe('lib assets api', () => { jest.restoreAllMocks(); }); - it('exposes saveAssetMeta and delegates to assetManager', async () => { - const meta = { - ver: 'ver', - importer: 'database', - imported: true, - uuid: 'test-uuid', - files: [], - subMetas: {}, - userData: {}, - } as IAssetMeta; - const spy = jest.spyOn(assetManager, 'saveAssetMeta').mockResolvedValue(undefined); - const saveAssetMeta = (Assets as { saveAssetMeta?: typeof assetManager.saveAssetMeta }).saveAssetMeta; + it('does not expose saveAssetMeta from the public lib API', () => { + expect((Assets as { saveAssetMeta?: unknown }).saveAssetMeta).toBeUndefined(); + }); + + it('does not expose updateAssetMetaUserData from the public lib API', () => { + expect((Assets as { updateAssetMetaUserData?: unknown }).updateAssetMetaUserData).toBeUndefined(); + }); + + it('updateAssetUserData delegates sub asset uuid to assetManager', async () => { + const result = { minfilter: 'nearest' }; + const spy = jest.spyOn(assetManager, 'updateUserData').mockResolvedValue(result); + const updateAssetUserData = (Assets as { + updateAssetUserData?: ( + urlOrUuidOrPath: string, + path: string, + value: unknown + ) => Promise; + }).updateAssetUserData; - expect(saveAssetMeta).toEqual(expect.any(Function)); + expect(updateAssetUserData).toEqual(expect.any(Function)); - if (!saveAssetMeta) { - throw new Error('saveAssetMeta is not exposed from lib/assets/assets'); + if (!updateAssetUserData) { + throw new Error('updateAssetUserData is not exposed from lib/assets/assets'); } - await expect(saveAssetMeta('test-uuid', meta)).resolves.toBeUndefined(); - expect(spy).toHaveBeenCalledWith('test-uuid', meta); + await expect(updateAssetUserData('parent-uuid@6c48a', 'minfilter', 'nearest')).resolves.toBe(result); + expect(spy).toHaveBeenCalledWith('parent-uuid@6c48a', 'minfilter', 'nearest'); }); });