Release v0.4.0
This commit is contained in:
@@ -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,
|
||||
};
|
||||
@@ -0,0 +1,245 @@
|
||||
'use strict';
|
||||
|
||||
function hasEditorMessage() {
|
||||
return Boolean(global.Editor && Editor.Message);
|
||||
}
|
||||
|
||||
function ensureEditorMessage() {
|
||||
if (!hasEditorMessage()) {
|
||||
throw new Error('Editor.Message is unavailable in this Cocos extension host.');
|
||||
}
|
||||
}
|
||||
|
||||
async function requestEditorMessage(channel, method, ...args) {
|
||||
ensureEditorMessage();
|
||||
if (typeof Editor.Message.request !== 'function') {
|
||||
throw new Error('Editor.Message.request is unavailable in this Cocos extension host.');
|
||||
}
|
||||
return await Editor.Message.request(channel, method, ...args);
|
||||
}
|
||||
|
||||
async function tryEditorRequests(candidates) {
|
||||
const attempts = [];
|
||||
for (const candidate of candidates) {
|
||||
const channel = candidate.channel;
|
||||
const method = candidate.method;
|
||||
const args = Array.isArray(candidate.args) ? candidate.args : [];
|
||||
try {
|
||||
const result = await requestEditorMessage(channel, method, ...args);
|
||||
return {
|
||||
ok: true,
|
||||
channel,
|
||||
method,
|
||||
result,
|
||||
attempts,
|
||||
};
|
||||
} catch (error) {
|
||||
attempts.push({ channel, method, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
const message = attempts.length
|
||||
? attempts.map((attempt) => `${attempt.channel}.${attempt.method}: ${attempt.error}`).join('; ')
|
||||
: 'no editor message candidates were provided';
|
||||
const error = new Error(`No compatible Cocos editor message succeeded: ${message}`);
|
||||
error.attempts = attempts;
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function tryEditorRequestsStatus(candidates) {
|
||||
try {
|
||||
return await tryEditorRequests(candidates);
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
available: false,
|
||||
attempts: error.attempts || [],
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function openPanel(panelName) {
|
||||
const id = String(panelName || 'builder').trim();
|
||||
if (!id) {
|
||||
throw new Error('panelName is required.');
|
||||
}
|
||||
if (!global.Editor || !Editor.Panel || typeof Editor.Panel.open !== 'function') {
|
||||
throw new Error('Editor.Panel.open is unavailable in this Cocos extension host.');
|
||||
}
|
||||
const result = await Editor.Panel.open(id);
|
||||
return { opened: true, panelName: id, result };
|
||||
}
|
||||
|
||||
function getEditorPreference(scope, key) {
|
||||
if (!global.Editor || !Editor.Profile) {
|
||||
throw new Error('Editor.Profile is unavailable in this Cocos extension host.');
|
||||
}
|
||||
const normalizedScope = String(scope || 'project').toLowerCase();
|
||||
const target = normalizedScope === 'global' ? Editor.Profile : Editor.Profile;
|
||||
const getters = normalizedScope === 'global'
|
||||
? ['getConfig', 'getGlobal']
|
||||
: ['getProject', 'getConfig'];
|
||||
for (const getter of getters) {
|
||||
if (typeof target[getter] === 'function') {
|
||||
return target[getter](key);
|
||||
}
|
||||
}
|
||||
throw new Error('No compatible Editor.Profile getter is available.');
|
||||
}
|
||||
|
||||
function setEditorPreference(scope, key, value) {
|
||||
if (!global.Editor || !Editor.Profile) {
|
||||
throw new Error('Editor.Profile is unavailable in this Cocos extension host.');
|
||||
}
|
||||
const normalizedScope = String(scope || 'project').toLowerCase();
|
||||
const target = Editor.Profile;
|
||||
const setters = normalizedScope === 'global'
|
||||
? ['setConfig', 'setGlobal']
|
||||
: ['setProject', 'setConfig'];
|
||||
for (const setter of setters) {
|
||||
if (typeof target[setter] === 'function') {
|
||||
const result = target[setter](key, value);
|
||||
return { set: true, scope: normalizedScope, key, value, method: setter, result };
|
||||
}
|
||||
}
|
||||
throw new Error('No compatible Editor.Profile setter is available.');
|
||||
}
|
||||
|
||||
function broadcastEditorMessage(options = {}) {
|
||||
ensureEditorMessage();
|
||||
const channel = String(options.channel || '').trim();
|
||||
const message = String(options.message || '').trim();
|
||||
if (!message) {
|
||||
throw new Error('message is required.');
|
||||
}
|
||||
const payload = options.payload === undefined ? {} : options.payload;
|
||||
if (channel && typeof Editor.Message.send === 'function') {
|
||||
const result = Editor.Message.send(channel, message, payload);
|
||||
return { sent: true, mode: 'send', channel, message, payload, result };
|
||||
}
|
||||
if (typeof Editor.Message.broadcast === 'function') {
|
||||
const result = Editor.Message.broadcast(message, payload);
|
||||
return { sent: true, mode: 'broadcast', message, payload, result };
|
||||
}
|
||||
throw new Error('Neither Editor.Message.send nor Editor.Message.broadcast is available.');
|
||||
}
|
||||
|
||||
function createCocosProjectTools({ createSchema }) {
|
||||
return [
|
||||
{
|
||||
name: 'save_current_scene',
|
||||
profile: 'full',
|
||||
description: '[core] Save the currently open Cocos scene using available editor scene messages.',
|
||||
inputSchema: createSchema({}, []),
|
||||
handler: async () => {
|
||||
const result = await tryEditorRequests([
|
||||
{ channel: 'scene', method: 'save-scene' },
|
||||
{ channel: 'scene', method: 'save' },
|
||||
]);
|
||||
return { saved: true, ...result };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'open_build_panel',
|
||||
profile: 'full',
|
||||
description: '[core] Open the Cocos build panel, defaulting to the builder panel id.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
panelName: { type: 'string', description: 'Panel id to open. Defaults to builder.' },
|
||||
},
|
||||
[]
|
||||
),
|
||||
handler: async (args) => openPanel(args.panelName || 'builder'),
|
||||
},
|
||||
{
|
||||
name: 'get_build_status',
|
||||
profile: 'core',
|
||||
description: '[specialist] Query Cocos build/preview status using known builder message variants.',
|
||||
inputSchema: createSchema({}, []),
|
||||
handler: async () => await tryEditorRequestsStatus([
|
||||
{ channel: 'builder', method: 'query-build-status' },
|
||||
{ channel: 'builder', method: 'get-build-status' },
|
||||
{ channel: 'builder', method: 'query-build-tasks' },
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: 'run_project_preview',
|
||||
profile: 'full',
|
||||
description: '[core] Start Cocos preview/run using known preview and builder message variants.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
platform: { type: 'string', description: 'Optional preview platform or build target.' },
|
||||
},
|
||||
[]
|
||||
),
|
||||
handler: async (args) => await tryEditorRequests([
|
||||
{ channel: 'preview', method: 'start', args: [args || {}] },
|
||||
{ channel: 'preview', method: 'open-preview', args: [args || {}] },
|
||||
{ channel: 'builder', method: 'preview', args: [args || {}] },
|
||||
]),
|
||||
},
|
||||
{
|
||||
name: 'get_editor_preference',
|
||||
profile: 'full',
|
||||
description: '[core] Read a Cocos editor preference through Editor.Profile when available.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
scope: { type: 'string', description: 'Preference scope: project or global. Defaults to project.' },
|
||||
key: { type: 'string', description: 'Preference key.' },
|
||||
},
|
||||
['key']
|
||||
),
|
||||
handler: async (args) => ({
|
||||
scope: args.scope || 'project',
|
||||
key: args.key,
|
||||
value: getEditorPreference(args.scope, args.key),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'set_editor_preference',
|
||||
profile: 'full',
|
||||
description: '[core] Write a Cocos editor preference through Editor.Profile when available.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
scope: { type: 'string', description: 'Preference scope: project or global. Defaults to project.' },
|
||||
key: { type: 'string', description: 'Preference key.' },
|
||||
valueJson: { type: 'string', description: 'JSON encoded preference value.' },
|
||||
},
|
||||
['key', 'valueJson']
|
||||
),
|
||||
handler: async (args) => {
|
||||
let value;
|
||||
try {
|
||||
value = JSON.parse(args.valueJson);
|
||||
} catch (error) {
|
||||
throw new Error(`valueJson must be valid JSON: ${error.message}`);
|
||||
}
|
||||
return setEditorPreference(args.scope, args.key, value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'broadcast_editor_message',
|
||||
profile: 'full',
|
||||
description: '[core] Send or broadcast a Cocos editor message for advanced editor automation.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
channel: { type: 'string', description: 'Optional Editor.Message channel for send().' },
|
||||
message: { type: 'string', description: 'Message name to send or broadcast.' },
|
||||
payload: { type: 'object', description: 'Optional JSON payload.' },
|
||||
},
|
||||
['message']
|
||||
),
|
||||
handler: async (args) => broadcastEditorMessage(args),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
broadcastEditorMessage,
|
||||
createCocosProjectTools,
|
||||
getEditorPreference,
|
||||
setEditorPreference,
|
||||
tryEditorRequests,
|
||||
tryEditorRequestsStatus,
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
|
||||
function createSceneEventTools({ createSchema, sceneBridge }) {
|
||||
return [
|
||||
{
|
||||
name: 'list_button_click_events',
|
||||
profile: 'full',
|
||||
description: '[core] List click event bindings on a Cocos Button component.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Button node hierarchy path.' },
|
||||
uuid: { type: 'string', description: 'Button node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact button node name.' },
|
||||
},
|
||||
[]
|
||||
),
|
||||
handler: async (args) => sceneBridge.call('listButtonClickEvents', args),
|
||||
},
|
||||
{
|
||||
name: 'bind_button_click_event',
|
||||
profile: 'full',
|
||||
description: '[core] Bind a Cocos Button click event to a target node component method.',
|
||||
inputSchema: createSchema(
|
||||
{
|
||||
path: { type: 'string', description: 'Button node hierarchy path.' },
|
||||
uuid: { type: 'string', description: 'Button node uuid.' },
|
||||
name: { type: 'string', description: 'Fallback exact button node name.' },
|
||||
targetPath: { type: 'string', description: 'Target node path containing the handler component.' },
|
||||
targetUuid: { type: 'string', description: 'Target node uuid containing the handler component.' },
|
||||
targetName: { type: 'string', description: 'Fallback exact target node name.' },
|
||||
componentName: { type: 'string', description: 'Target component class name.' },
|
||||
handler: { type: 'string', description: 'Method name to invoke on the target component.' },
|
||||
customEventData: { type: 'string', description: 'Optional custom event data string.' },
|
||||
replace: { type: 'boolean', description: 'Replace an identical existing binding.' },
|
||||
},
|
||||
['componentName', 'handler']
|
||||
),
|
||||
handler: async (args) => sceneBridge.call('bindButtonClickEvent', args),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createSceneEventTools,
|
||||
};
|
||||
Reference in New Issue
Block a user