Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,6 @@ export declare interface RtSpriteFrameAssetUserData {
height?: number;
}
export declare function saveAsset(pathOrUrlOrUUID: string, data: string | Buffer): Promise<IAssetInfo>;
export declare function saveAssetMeta(uuid: string, meta: IAssetMeta): Promise<void>;
export declare interface ScriptModuleUserData {
isPlugin: false;
}
Expand Down Expand Up @@ -6817,7 +6816,6 @@ export declare namespace Assets {
refresh,
queryAssetInfo,
queryAssetMeta,
saveAssetMeta,
queryCreateMap,
queryAssetInfos,
queryAssetDBInfos,
Expand Down Expand Up @@ -8733,7 +8731,6 @@ export declare interface RtSpriteFrameAssetUserData {
}
export declare function save(force?: boolean): Promise<void>;
export declare function saveAsset(pathOrUrlOrUUID: string, data: string | Buffer): Promise<IAssetInfo>;
export declare function saveAssetMeta(uuid: string, meta: IAssetMeta): Promise<void>;
export declare namespace Scene {
export {
init_6 as init,
Expand Down
2 changes: 1 addition & 1 deletion src/api/assets/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
45 changes: 42 additions & 3 deletions src/api/base/schema-identifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, '路径不能为空')
Expand Down Expand Up @@ -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);
Expand All @@ -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()
Expand All @@ -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);
Expand All @@ -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()
Expand Down Expand Up @@ -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,
Expand All @@ -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'), // 场景/预制体资源名称
Expand Down
32 changes: 32 additions & 0 deletions src/core/assets/test/operation-filesystem-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
7 changes: 0 additions & 7 deletions src/lib/assets/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,6 @@ export async function queryAssetMeta(urlOrUUIDOrPath: string): Promise<IAssetMet
return await assetManager.queryAssetMeta(urlOrUUIDOrPath);
}

/**
* Save Asset Metadata // 保存资源元数据
*/
export async function saveAssetMeta(uuid: string, meta: IAssetMeta): Promise<void> {
return await assetManager.saveAssetMeta(uuid, meta);
}

/**
* Query Creatable Asset Map // 查询可创建资源映射表
*/
Expand Down
53 changes: 53 additions & 0 deletions tests/assets-update-asset-user-data-api.test.ts
Original file line number Diff line number Diff line change
@@ -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',
);
});
});
41 changes: 23 additions & 18 deletions tests/lib/assets-api.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<unknown>;
}).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');
});
});
Loading