Release v0.4.0

This commit is contained in:
winlifes
2026-06-10 20:26:09 -07:00
parent 03e5ab8dfe
commit 6405dced7d
27 changed files with 1983 additions and 52 deletions
+221
View File
@@ -0,0 +1,221 @@
'use strict';
const fs = require('fs');
const path = require('path');
const { listAssets, queryAssetInfo } = require('../assets');
const { resolveProjectPath } = require('../path-safety');
const UUID_KEY_PATTERN = /uuid|assetUuid|prefabUuid|sceneUuid|__uuid__/i;
const UUID_LITERAL_PATTERN = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[A-Za-z0-9+/=-]{20,32}/g;
function assetUrlToPath(projectPath, url) {
if (!url || !String(url).startsWith('db://assets/')) {
return '';
}
return path.join(projectPath, String(url).slice('db://'.length));
}
function assetFilePath(projectPath, info) {
const candidates = [
info && info.file,
info && info.path,
info && info.source,
info && info.url ? assetUrlToPath(projectPath, info.url) : '',
].filter(Boolean);
for (const candidate of candidates) {
const fullPath = resolveProjectPath(projectPath, candidate);
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
return fullPath;
}
}
return '';
}
function collectStructuredUuidReferences(value, refs = [], pointer = '') {
if (value == null) {
return refs;
}
if (Array.isArray(value)) {
value.forEach((item, index) => collectStructuredUuidReferences(item, refs, `${pointer}/${index}`));
return refs;
}
if (typeof value !== 'object') {
return refs;
}
for (const [key, child] of Object.entries(value)) {
const childPointer = `${pointer}/${key}`;
if (typeof child === 'string' && UUID_KEY_PATTERN.test(key)) {
refs.push({ uuid: child, path: childPointer, key, source: 'structured' });
continue;
}
collectStructuredUuidReferences(child, refs, childPointer);
}
return refs;
}
function collectTextUuidReferences(text) {
const refs = [];
const seen = new Set();
let match;
while ((match = UUID_LITERAL_PATTERN.exec(String(text || '')))) {
const uuid = match[0];
if (seen.has(uuid)) {
continue;
}
seen.add(uuid);
refs.push({ uuid, path: `@${match.index}`, key: '', source: 'text' });
}
return refs;
}
function dedupeReferences(refs) {
const seen = new Set();
const result = [];
for (const ref of refs) {
const key = `${ref.uuid}:${ref.path}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
result.push(ref);
}
return result;
}
function collectUuidReferences(content) {
const refs = [];
try {
refs.push(...collectStructuredUuidReferences(JSON.parse(content)));
} catch (error) {
// Non-JSON assets still get a literal reference scan below.
}
refs.push(...collectTextUuidReferences(content));
return dedupeReferences(refs);
}
async function inspectAssetDependencies(projectPath, target, options = {}) {
const info = await queryAssetInfo(target);
const filePath = assetFilePath(projectPath, info);
if (!filePath) {
throw new Error(`Asset file was not found: ${target}`);
}
const content = fs.readFileSync(filePath, 'utf8');
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.min(500, options.limit)) : 200;
const references = collectUuidReferences(content).slice(0, limit);
const dependencies = [];
const missing = [];
for (const ref of references) {
try {
const asset = await queryAssetInfo(ref.uuid);
dependencies.push({
...ref,
exists: true,
asset: {
uuid: asset.uuid,
url: asset.url,
type: asset.type,
importer: asset.importer,
},
});
} catch (error) {
missing.push({ ...ref, exists: false, error: error.message });
}
}
return {
ok: missing.length === 0,
target,
asset: {
uuid: info.uuid,
url: info.url,
type: info.type,
},
filePath: path.relative(projectPath, filePath).replace(/\\/g, '/'),
referenceCount: references.length,
dependencyCount: dependencies.length,
missingCount: missing.length,
dependencies,
missing,
};
}
async function validateAssetDependencies(projectPath, options = {}) {
const targets = options.target
? [options.target]
: (await listAssets({ pattern: options.pattern || 'db://assets/**', ccType: options.ccType }))
.slice(0, Number.isFinite(options.limit) ? Math.max(1, Math.min(200, options.limit)) : 50)
.map((asset) => asset.uuid || asset.url)
.filter(Boolean);
const assets = [];
for (const target of targets) {
try {
assets.push(await inspectAssetDependencies(projectPath, target, options));
} catch (error) {
assets.push({
ok: false,
target,
error: error.message,
missingCount: 1,
});
}
}
const missingCount = assets.reduce((sum, asset) => sum + (Number(asset.missingCount) || 0), 0);
return {
ok: missingCount === 0,
assetCount: assets.length,
missingCount,
assets,
};
}
function createAssetsAdvancedTools({ createSchema, getRuntimeContext }) {
return [
{
name: 'inspect_asset_dependencies',
profile: 'core',
description: '[specialist] Inspect UUID-style dependencies referenced by a serialized Cocos asset.',
inputSchema: createSchema(
{
target: { type: 'string', description: 'Asset uuid, db url, or project path.' },
limit: { type: 'number', description: 'Maximum dependency references to inspect.' },
},
['target']
),
handler: async (args) => {
const { projectPath } = getRuntimeContext();
return await inspectAssetDependencies(projectPath, args.target, args);
},
},
{
name: 'validate_asset_dependencies',
profile: 'core',
description: '[specialist] Validate UUID-style dependencies for one asset or a project asset query.',
inputSchema: createSchema(
{
target: { type: 'string', description: 'Optional asset uuid, db url, or project path.' },
pattern: { type: 'string', description: 'Asset-db pattern used when target is omitted.' },
ccType: { type: 'string', description: 'Optional Cocos asset type filter.' },
limit: { type: 'number', description: 'Maximum assets to scan when target is omitted.' },
},
[]
),
handler: async (args) => {
const { projectPath } = getRuntimeContext();
return await validateAssetDependencies(projectPath, args);
},
},
];
}
module.exports = {
collectUuidReferences,
createAssetsAdvancedTools,
inspectAssetDependencies,
validateAssetDependencies,
};