294 lines
9.7 KiB
JavaScript
294 lines
9.7 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { getCurrentSelection, queryAssetInfo, queryAssetMeta } = require('./assets');
|
|
const { runScriptDiagnostics } = require('./diagnostics');
|
|
const { getRecentProjectLogs } = require('./logs');
|
|
const { resolveProjectPath } = require('./path-safety');
|
|
|
|
function createResource(uri, name, description) {
|
|
return { uri, name, description, mimeType: 'text/plain' };
|
|
}
|
|
|
|
function createTemplate(uriTemplate, name, description) {
|
|
return { uriTemplate, name, description, mimeType: 'text/plain' };
|
|
}
|
|
|
|
function truncate(text, limit = 12000) {
|
|
if (typeof text !== 'string') {
|
|
return text;
|
|
}
|
|
if (text.length <= limit) {
|
|
return text;
|
|
}
|
|
return `${text.slice(0, limit)}\n... (truncated)`;
|
|
}
|
|
|
|
function toText(value) {
|
|
if (typeof value === 'string') {
|
|
return value;
|
|
}
|
|
return JSON.stringify(value, null, 2);
|
|
}
|
|
|
|
function summarizeSelection() {
|
|
if (!global.Editor || !Editor.Selection || typeof Editor.Selection.getSelected !== 'function') {
|
|
return 'Selection API is unavailable in this Cocos environment.';
|
|
}
|
|
|
|
try {
|
|
const selectedNode = Editor.Selection.getSelected('node');
|
|
const selectedAsset = Editor.Selection.getSelected('asset');
|
|
return [
|
|
`Selected node: ${selectedNode || '(none)'}`,
|
|
`Selected asset: ${selectedAsset || '(none)'}`,
|
|
].join('\n');
|
|
} catch (error) {
|
|
return `Failed to inspect current selection: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
class ResourceProvider {
|
|
constructor(getRuntimeContext, sceneBridge, interactionLog, runtimeLog) {
|
|
this.getRuntimeContext = getRuntimeContext;
|
|
this.sceneBridge = sceneBridge;
|
|
this.interactionLog = interactionLog;
|
|
this.runtimeLog = runtimeLog;
|
|
}
|
|
|
|
listResources() {
|
|
const { projectName } = this.getRuntimeContext();
|
|
return [
|
|
createResource('cocos://project/context', `${projectName} Project Context`, 'Live Cocos project context summary.'),
|
|
createResource('cocos://project/summary', `${projectName} Project Summary`, 'Project path, folder summary, and asset overview.'),
|
|
createResource('cocos://scene/active', `${projectName} Active Scene`, 'Summary of the active Cocos scene.'),
|
|
createResource('cocos://scene/current', `${projectName} Current Scene`, 'Alias of the active Cocos scene summary.'),
|
|
createResource('cocos://selection/current', `${projectName} Current Selection`, 'Summary of the current editor selection.'),
|
|
createResource('cocos://selection/asset', `${projectName} Selected Asset`, 'Details for the currently selected asset.'),
|
|
createResource('cocos://errors/scripts', `${projectName} Script Diagnostics`, 'Latest TypeScript diagnostic summary for the project.'),
|
|
createResource('cocos://logs/editor', `${projectName} Editor Logs`, 'Recent MCP runtime logs and tool interaction history.'),
|
|
createResource('cocos://logs/project', `${projectName} Project Logs`, 'Recent tails from common project log files.'),
|
|
createResource('cocos://mcp/interactions', `${projectName} MCP Interactions`, 'Recent MCP tool interaction summaries.'),
|
|
];
|
|
}
|
|
|
|
listResourceTemplates() {
|
|
return [
|
|
createTemplate('cocos://scene/node/{path}', 'Scene Node', 'Inspect a scene node by hierarchy path.'),
|
|
createTemplate('cocos://asset/path/{relative_path}', 'Asset By Path', 'Read a script or text asset by project-relative path.'),
|
|
createTemplate('cocos://asset/info/{uuid_or_path}', 'Asset Info', 'Inspect an asset by uuid, db url, or path.'),
|
|
];
|
|
}
|
|
|
|
async readResource(uri) {
|
|
const text = await this.resolveResourceText(uri);
|
|
return {
|
|
contents: [
|
|
{
|
|
uri,
|
|
mimeType: 'text/plain',
|
|
text,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
async resolveResourceText(uri) {
|
|
const { projectName, projectPath, cocosVersion, version, config } = this.getRuntimeContext();
|
|
|
|
switch (uri) {
|
|
case 'cocos://project/context':
|
|
return [
|
|
'Funplay Cocos MCP Project Context',
|
|
`Project: ${projectName}`,
|
|
`Project Path: ${projectPath}`,
|
|
`Cocos Creator: ${cocosVersion}`,
|
|
`Extension Version: ${version}`,
|
|
`Tool Profile: ${config.toolProfile}`,
|
|
`Server: http://${config.host}:${config.port}/`,
|
|
'',
|
|
'Selection',
|
|
summarizeSelection(),
|
|
'',
|
|
'Active Scene',
|
|
await this.safeSceneCall('getSceneInfo', { maxDepth: 2 }),
|
|
].join('\n');
|
|
case 'cocos://project/summary':
|
|
return this.buildProjectSummary(projectPath, cocosVersion, version);
|
|
case 'cocos://scene/active':
|
|
case 'cocos://scene/current':
|
|
return await this.safeSceneCall('getSceneInfo', { maxDepth: 3 });
|
|
case 'cocos://selection/current':
|
|
return summarizeSelection();
|
|
case 'cocos://selection/asset':
|
|
return await this.getSelectedAssetText();
|
|
case 'cocos://errors/scripts':
|
|
return await this.getScriptDiagnosticsText(projectPath);
|
|
case 'cocos://logs/editor':
|
|
return this.getEditorLogsText();
|
|
case 'cocos://logs/project':
|
|
return this.getProjectLogsText(projectPath);
|
|
case 'cocos://mcp/interactions':
|
|
return this.interactionLog.summary();
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (uri.startsWith('cocos://scene/node/')) {
|
|
const nodePath = decodeURIComponent(uri.slice('cocos://scene/node/'.length));
|
|
return await this.safeSceneCall('inspectNode', { path: nodePath });
|
|
}
|
|
|
|
if (uri.startsWith('cocos://asset/path/')) {
|
|
const relativePath = decodeURIComponent(uri.slice('cocos://asset/path/'.length));
|
|
return this.readAssetByPath(relativePath);
|
|
}
|
|
|
|
if (uri.startsWith('cocos://asset/info/')) {
|
|
const uuidOrPath = decodeURIComponent(uri.slice('cocos://asset/info/'.length));
|
|
return await this.getAssetInfoText(uuidOrPath);
|
|
}
|
|
|
|
return `Resource not found: ${uri}`;
|
|
}
|
|
|
|
buildProjectSummary(projectPath, cocosVersion, version) {
|
|
const topLevel = fs.existsSync(projectPath)
|
|
? fs
|
|
.readdirSync(projectPath, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.sort((left, right) => left.localeCompare(right))
|
|
: [];
|
|
|
|
const assetsDir = path.join(projectPath, 'assets');
|
|
const scriptCount = this.countFiles(assetsDir, ['.ts', '.js']);
|
|
const prefabCount = this.countFiles(assetsDir, ['.prefab']);
|
|
const sceneCount = this.countFiles(assetsDir, ['.scene']);
|
|
|
|
return [
|
|
'Project Summary',
|
|
`Project Root: ${projectPath}`,
|
|
`Assets Path: ${assetsDir}`,
|
|
`Cocos Creator: ${cocosVersion}`,
|
|
`Extension Version: ${version}`,
|
|
`Scripts: ${scriptCount}`,
|
|
`Prefabs: ${prefabCount}`,
|
|
`Scenes: ${sceneCount}`,
|
|
'',
|
|
`Top-Level Directories (${topLevel.length})`,
|
|
...topLevel.map((name) => `- ${name}`),
|
|
].join('\n');
|
|
}
|
|
|
|
countFiles(rootDir, extensions) {
|
|
if (!fs.existsSync(rootDir)) {
|
|
return 0;
|
|
}
|
|
|
|
let count = 0;
|
|
const stack = [rootDir];
|
|
while (stack.length) {
|
|
const current = stack.pop();
|
|
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(current, entry.name);
|
|
if (entry.isDirectory()) {
|
|
stack.push(fullPath);
|
|
continue;
|
|
}
|
|
if (extensions.includes(path.extname(entry.name).toLowerCase())) {
|
|
count += 1;
|
|
}
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
readAssetByPath(relativePath) {
|
|
const { projectPath } = this.getRuntimeContext();
|
|
const fullPath = resolveProjectPath(projectPath, relativePath);
|
|
if (!fs.existsSync(fullPath)) {
|
|
return `Asset not found: ${relativePath}`;
|
|
}
|
|
return truncate(`[${relativePath}]\n${fs.readFileSync(fullPath, 'utf8')}`);
|
|
}
|
|
|
|
async safeSceneCall(method, payload) {
|
|
try {
|
|
const result = await this.sceneBridge.call(method, payload);
|
|
return toText(result);
|
|
} catch (error) {
|
|
return `Scene bridge error (${method}): ${error.message}`;
|
|
}
|
|
}
|
|
|
|
async getSelectedAssetText() {
|
|
try {
|
|
const selection = getCurrentSelection();
|
|
if (!selection.asset) {
|
|
return 'No asset is currently selected.';
|
|
}
|
|
|
|
return await this.getAssetInfoText(selection.asset);
|
|
} catch (error) {
|
|
return `Selected asset lookup failed: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
async getAssetInfoText(uuidOrPath) {
|
|
try {
|
|
const info = await queryAssetInfo(uuidOrPath);
|
|
const meta = await queryAssetMeta(uuidOrPath).catch(() => null);
|
|
return JSON.stringify({ info, meta }, null, 2);
|
|
} catch (error) {
|
|
return `Asset lookup failed: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
async getScriptDiagnosticsText(projectPath) {
|
|
try {
|
|
const result = await runScriptDiagnostics(projectPath);
|
|
return JSON.stringify(result, null, 2);
|
|
} catch (error) {
|
|
return `Script diagnostics failed: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
getEditorLogsText() {
|
|
return [
|
|
'MCP Runtime Logs',
|
|
this.runtimeLog && typeof this.runtimeLog.summary === 'function'
|
|
? this.runtimeLog.summary(80)
|
|
: 'Runtime log is unavailable.',
|
|
'',
|
|
'MCP Tool Interactions',
|
|
this.interactionLog && typeof this.interactionLog.summary === 'function'
|
|
? this.interactionLog.summary(80)
|
|
: 'Interaction log is unavailable.',
|
|
].join('\n');
|
|
}
|
|
|
|
getProjectLogsText(projectPath) {
|
|
const logs = getRecentProjectLogs(projectPath, { limit: 10, lines: 100 });
|
|
if (!logs.length) {
|
|
return 'No project log files found in common project log directories.';
|
|
}
|
|
|
|
return logs
|
|
.map((log) => [
|
|
`# ${log.path}`,
|
|
`mtime: ${log.mtime}`,
|
|
`size: ${log.size}`,
|
|
'',
|
|
log.text,
|
|
].join('\n'))
|
|
.join('\n\n---\n\n');
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
ResourceProvider,
|
|
};
|