You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

615 lines
78 KiB

2 weeks ago
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.methods = void 0;
exports.load = load;
exports.unload = unload;
// @ts-ignore
const package_json_1 = __importDefault(require("../package.json"));
const fs_extra_1 = require("fs-extra");
const path_1 = require("path");
const os_1 = require("os");
const ws_server_1 = require("./ws-server");
const mcp_server_1 = require("./mcp-server");
const DEFAULT_PORT = 33000;
/** 固定静态连接令牌:不做动态生成,保证脚本/节点令牌与插件始终一致。 */
const STATIC_TOKEN = 'www.yyka.cn';
let server = null;
let logBuffer = [];
/** 面板已消费到的缓冲位置:queryLog 改为增量取走而非清空,避免饿死 MCP 的非破坏性 getLogs。 */
let logCursor = 0;
const serverToken = STATIC_TOKEN;
let mcpHost = null;
const pending = new Map();
let requestSeq = 0;
function ensureServer() {
if (!server) {
server = new ws_server_1.WsServer();
}
return server;
}
function getServerState() {
var _a, _b, _c, _d, _e, _f;
const info = (_a = server === null || server === void 0 ? void 0 : server.clientInfo) !== null && _a !== void 0 ? _a : null;
return {
running: (_b = server === null || server === void 0 ? void 0 : server.isRunning) !== null && _b !== void 0 ? _b : false,
port: (_c = server === null || server === void 0 ? void 0 : server.boundPort) !== null && _c !== void 0 ? _c : 0,
clients: (_d = server === null || server === void 0 ? void 0 : server.clients) !== null && _d !== void 0 ? _d : 0,
clientType: (_e = info === null || info === void 0 ? void 0 : info.type) !== null && _e !== void 0 ? _e : '',
clientUrl: (_f = info === null || info === void 0 ? void 0 : info.url) !== null && _f !== void 0 ? _f : '',
};
}
function pushLog(item) {
logBuffer.push(item);
while (logBuffer.length > 200) {
logBuffer.shift();
if (logCursor > 0) {
logCursor--;
}
}
}
/** 客户端类型 → 中文标签(日志用)。 */
function typeLabel(type) {
switch (type) {
case 'h5':
return '浏览器预览';
case 'editor':
return '编辑器预览';
case 'simulator':
return '模拟器';
case 'device':
return '真机';
case 'unknown':
default:
return '未知来源';
}
}
/** 从项目 settings 中递归查找数值端口。 */
function findNumericPort(cfg) {
if (!cfg || typeof cfg !== 'object') {
return 0;
}
if (typeof cfg.port === 'number' && cfg.port > 0) {
return cfg.port;
}
if (typeof cfg.port === 'string' && !Number.isNaN(Number(cfg.port))) {
return Number(cfg.port);
}
for (const k of Object.keys(cfg)) {
const v = cfg[k];
if (v && typeof v === 'object') {
const p = findNumericPort(v);
if (p > 0) {
return p;
}
}
}
return 0;
}
/**
* 读取当前项目的预览配置,得出浏览器预港口/URL。
* 用于面板内嵌 iframe 预览的默认地址。
*/
function getPreviewInfo() {
var _a;
const projectPath = (_a = Editor.Project) === null || _a === void 0 ? void 0 : _a.path;
const candidates = ['settings/preview.json', 'settings/v2/packages/preview.json'];
if (projectPath) {
for (const rel of candidates) {
try {
const p = (0, path_1.join)(projectPath, rel);
if (!(0, fs_extra_1.existsSync)(p)) {
continue;
}
const cfg = (0, fs_extra_1.readJsonSync)(p);
const port = findNumericPort(cfg);
if (port > 0) {
return { host: 'localhost', port, url: `http://localhost:${port}/`, found: p };
}
}
catch (_b) {
// 忽略解析失败,尝试下一个候选文件
}
}
}
return { host: 'localhost', port: 7456, url: 'http://localhost:7456/', found: '' };
}
/** 处理来自游戏侧的消息:优先匹配 pending 请求,否则进入日志缓冲。 */
function handleIncoming(data) {
var _a;
const m = data && typeof data === 'object' ? data : null;
if (m && typeof m.requestId === 'number' && pending.has(m.requestId)) {
const p = pending.get(m.requestId);
pending.delete(m.requestId);
if (p.timer)
clearTimeout(p.timer);
p.resolve((_a = m.data) !== null && _a !== void 0 ? _a : m);
return;
}
// 命令回包/握手等内部协议消息不进入日志,避免 requestId/response/hello 刷屏
if (m && (m.evt === 'response' || m.evt === 'request' || m.evt === 'hello')) {
return;
}
pushLog(data);
}
/** 按限制条数与级别读取最近日志(不清空缓冲,供面板/MCP 使用)。 */
function getLogsInternal(limit, level) {
const items = [];
for (let i = logBuffer.length - 1; i >= 0 && items.length < limit; i--) {
const it = logBuffer[i];
if (!it || typeof it !== 'object') {
continue;
}
// 过滤掉心跳/握手/预览帧等噪声,只保留控制台与系统消息
if (it.evt !== 'console' && it.evt !== 'sys') {
continue;
}
if (level && it.evt === 'console' && it.level !== level) {
continue;
}
items.unshift(it);
}
return items;
}
/** 返回 MCP 服务状态与连接配置(URL + 令牌)。 */
function getMcpState() {
var _a, _b;
return {
running: (_a = mcpHost === null || mcpHost === void 0 ? void 0 : mcpHost.isRunning) !== null && _a !== void 0 ? _a : false,
port: (_b = mcpHost === null || mcpHost === void 0 ? void 0 : mcpHost.boundPort) !== null && _b !== void 0 ? _b : 0,
token: serverToken,
url: (mcpHost === null || mcpHost === void 0 ? void 0 : mcpHost.isRunning) ? `http://127.0.0.1:${mcpHost.boundPort}/mcp` : '',
};
}
/** 向游戏侧桥接脚本发送一条请求,并等待其回包。 */
function callGame(cmd, payload, timeout = 5000) {
return new Promise((resolve, reject) => {
if (!server || !server.isRunning) {
reject(new Error('WS 服务未运行,请先启动服务'));
return;
}
const requestId = ++requestSeq;
const timer = setTimeout(() => {
if (pending.has(requestId)) {
pending.delete(requestId);
reject(new Error(`命令超时(>${timeout}ms): ${cmd}`));
}
}, timeout);
pending.set(requestId, { resolve, reject, timer });
server.broadcast({ requestId, cmd, payload });
});
}
/** 获取宿主机第一个非内网 IPv4 地址(用于注入桥接脚本,供模拟器/真机通过局域网连上插件服务)。 */
function getHostLanIP() {
var _a;
// 优先返回内网私有段地址(Wi-Fi/有线网卡),避免选到虚拟机或 VPN 虚拟网卡导致真机连不通
let fallback = '';
try {
for (const name of Object.keys((0, os_1.networkInterfaces)())) {
for (const net of (_a = (0, os_1.networkInterfaces)()[name]) !== null && _a !== void 0 ? _a : []) {
if (net.family === 'IPv4' && !net.internal) {
const a = net.address.split('.').map(Number);
const isPrivate = a[0] === 10 ||
(a[0] === 172 && a[1] >= 16 && a[1] <= 31) ||
(a[0] === 192 && a[1] === 168);
if (isPrivate) {
return net.address;
}
if (!fallback) {
fallback = net.address;
}
}
}
}
}
catch (_b) {
/* ignore */
}
return fallback;
}
/**
* @zh 为扩展的主进程的注册方法
*/
/** 收集目录下所有 .scene 文件。 */
function collectSceneFiles(dir, out) {
let entries;
try {
entries = (0, fs_extra_1.readdirSync)(dir, { withFileTypes: true });
}
catch (_a) {
return;
}
for (const e of entries) {
const p = (0, path_1.join)(dir, e.name);
if (e.isDirectory()) {
collectSceneFiles(p, out);
}
else if (e.isFile() && e.name.endsWith('.scene')) {
out.push(p);
}
}
}
/** 遍历场景 JSON 中的节点树,对匹配指定脚本 uuid 的组件执行回调。 */
function walkSceneComponents(sceneJson, compUuid, cb) {
const walkNode = (node) => {
if (!node || typeof node !== 'object') {
return;
}
if (node.__type__ === 'cc.Node') {
for (const c of node._components || []) {
if (c && c.__type__ === compUuid) {
cb(c);
}
}
for (const ch of node._children || []) {
walkNode(ch);
}
}
};
walkNode(sceneJson && sceneJson.scene);
}
exports.methods = {
openPanel() {
Editor.Panel.open(package_json_1.default.name);
},
openPreviewPanel() {
Editor.Panel.open(`${package_json_1.default.name}.preview`);
},
/**
* 启动 WS 服务。返回当前服务状态。
*/
async startServer(port = DEFAULT_PORT) {
const ws = ensureServer();
ws.token = serverToken;
try {
if (!ws.isRunning) {
ws.on('message', (_client, data) => handleIncoming(data));
ws.on('connection', (_client, info) => pushLog({ evt: 'sys', msg: `客户端已连接 (${typeLabel(info === null || info === void 0 ? void 0 : info.type)})` }));
ws.on('disconnect', (_client, info) => pushLog({ evt: 'sys', msg: `客户端已断开 (${typeLabel(info === null || info === void 0 ? void 0 : info.type)})` }));
ws.on('auth-fail', (got, url) => pushLog({ evt: 'sys', msg: `连接被拒绝:客户端令牌(${got}) ≠ 服务端令牌(${serverToken})` }));
await ws.start(port);
}
}
catch (err) {
pushLog({ evt: 'sys', msg: `启动服务失败: ${String(err)}` });
}
return getServerState();
},
/**
* 停止 WS 服务。返回当前服务状态。
*/
async stopServer() {
if (server) {
await server.stop();
}
return getServerState();
},
getServerState() {
return getServerState();
},
/**
* 返回固定连接令牌(静态 token)。
*/
getToken() {
return serverToken;
},
/**
* 读取当前项目预览配置得出的预港口/URL(供面板内嵌 iframe 用)。
*/
getPreviewInfo() {
return getPreviewInfo();
},
/**
* 读取当前项目设计分辨率(Project Settings → general.designResolution)。
* 首选编辑器内置 `project/query-config`(实时读配置缓存),多模块/按键都试一遍;
* 失败再退回到读 settings/v2/packages/project.json 文件,兜底 960×640。
*/
async getProjectsSettings() {
var _a;
try {
const modules = ['general', 'engine', 'project'];
for (const mod of modules) {
try {
const d = await Editor.Message.request('project', 'query-config', mod, 'designResolution');
if (d && Number(d.width) > 0 && Number(d.height) > 0) {
return { designResolution: { width: Number(d.width), height: Number(d.height) } };
}
}
catch (_b) {
// 该模块无此配置,尝试下一个
}
}
}
catch (_c) {
// 继续走文件兜底
}
const projectPath = (_a = Editor.Project) === null || _a === void 0 ? void 0 : _a.path;
const candidates = [
'settings/v2/packages/project.json',
'settings/project.json',
];
if (projectPath) {
for (const rel of candidates) {
try {
const p = (0, path_1.join)(projectPath, rel);
if (!(0, fs_extra_1.existsSync)(p))
continue;
const cfg = (0, fs_extra_1.readJsonSync)(p);
const d = cfg && cfg.general && cfg.general.designResolution;
if (d && Number(d.width) > 0 && Number(d.height) > 0) {
return { designResolution: { width: Number(d.width), height: Number(d.height) } };
}
}
catch (_d) {
// 忽略解析失败,尝试下一个候选文件
}
}
}
return { designResolution: { width: 960, height: 640 } };
},
/**
* 面板向游戏侧桥接脚本广播一条 fire-and-forget 命令。
* args: { cmd: string, payload?: any }
*/
sendCommand(args) {
if (server) {
server.broadcast({ cmd: args === null || args === void 0 ? void 0 : args.cmd, payload: args === null || args === void 0 ? void 0 : args.payload });
}
return getServerState();
},
/**
* 面板请求游戏侧数据:向游戏发带 requestId 的命令并等待回包。
* args: { cmd: string, payload?: any, timeout?: number }
*/
async queryCommand(args) {
var _a;
const r = await callGame(args === null || args === void 0 ? void 0 : args.cmd, args === null || args === void 0 ? void 0 : args.payload, (_a = args === null || args === void 0 ? void 0 : args.timeout) !== null && _a !== void 0 ? _a : 5000);
return r;
},
/**
* 取回自上次查询以来收到的消息(游戏 → 面板)。按游标增量读取,不破坏缓冲。
*/
queryLog() {
const batch = logBuffer.slice(logCursor);
logCursor = logBuffer.length;
return batch;
},
/**
* 读取最近日志(不清空缓冲),供 MCP/AI Agent 使用。
* args: { limit?: number, level?: 'all'|'log'|'warn'|'error' }
*/
getLogs(args) {
const limit = Math.max(1, Math.min(200, Number(args === null || args === void 0 ? void 0 : args.limit) || 50));
const level = (args === null || args === void 0 ? void 0 : args.level) === 'log' || (args === null || args === void 0 ? void 0 : args.level) === 'warn' || (args === null || args === void 0 ? void 0 : args.level) === 'error' ? args.level : '';
return getLogsInternal(limit, level);
},
/**
* 启动 MCP 服务(HTTP Streamable,供 Codex 等 AI Agent 连接)。
*/
async startMcp(port = mcp_server_1.MCP_DEFAULT_PORT) {
if (!mcpHost) {
mcpHost = new mcp_server_1.McpHttpServer();
}
try {
if (!mcpHost.isRunning) {
const token = serverToken;
await mcpHost.start(port, token, {
callGame,
getServerState,
getToken: () => serverToken,
getLogs: (l, lv) => getLogsInternal(l, lv),
});
pushLog({ evt: 'sys', msg: `MCP 服务已启动: http://127.0.0.1:${mcpHost.boundPort}/mcp` });
}
}
catch (err) {
pushLog({ evt: 'sys', msg: `MCP 服务启动失败: ${String(err)}` });
}
return getMcpState();
},
/**
* 停止 MCP 服务。
*/
async stopMcp() {
if (mcpHost) {
await mcpHost.stop();
pushLog({ evt: 'sys', msg: 'MCP 服务已停止' });
}
return getMcpState();
},
/**
* 返回 MCP 服务状态与连接配置(URL + 令牌)。
*/
getMcpState() {
return getMcpState();
},
/**
* 把游戏侧桥接脚本 RuntimeBridge.ts 自动安装到当前项目的 assets 目录。
*/
async installBridge() {
var _a;
const projectPath = (_a = Editor.Project) === null || _a === void 0 ? void 0 : _a.path;
if (!projectPath) {
return { ok: false, msg: '无法获取项目路径(Editor.Project.path 为空)' };
}
const src = (0, path_1.join)(__dirname, '../runtime-bridge-sample/RuntimeBridge.ts');
const assetsDir = (0, path_1.join)(projectPath, 'assets');
if (!(0, fs_extra_1.existsSync)(src)) {
return { ok: false, msg: `未找到桥接脚本模板: ${src}` };
}
if (!(0, fs_extra_1.existsSync)(assetsDir)) {
return { ok: false, msg: `找不到项目的 assets 目录: ${assetsDir}` };
}
const dest = (0, path_1.join)(assetsDir, 'RuntimeBridge.ts');
const overwrote = (0, fs_extra_1.existsSync)(dest);
const currentToken = serverToken;
// 保留用户自定义 host/port(如真机调试时改为电脑 IP),避免每次覆盖被重置。
// 优先取场景中已存在常驻节点组件的现值(用户最近可能直接在节点上配置),
// 其次取已安装脚本文件里的默认值。
let keepHost = '';
let keepPort = 0;
if (overwrote) {
try {
const oldContent = (0, fs_extra_1.readFileSync)(dest, 'utf-8');
const hm = /host\s*=\s*'([^']*)'/.exec(oldContent);
const pm = /port\s*=\s*(\d+)/.exec(oldContent);
if (hm) {
keepHost = hm[1];
}
if (pm) {
keepPort = Number(pm[1]);
}
}
catch (_b) {
/* 读取失败则用模板默认值 */
}
}
// 解析桥接脚本组件 uuid,用于在场景文件中定位已存在的 RuntimeBridge 组件
let compUuid = '';
const metaPath = dest + '.meta';
try {
if ((0, fs_extra_1.existsSync)(metaPath)) {
const meta = (0, fs_extra_1.readJsonSync)(metaPath);
compUuid = (meta && meta.uuid) || '';
}
}
catch (_c) {
/* 忽略 meta 读取失败 */
}
let sceneFiles = [];
if (compUuid) {
try {
collectSceneFiles(assetsDir, sceneFiles);
// 第一遍:以已存在常驻节点组件上的 host/port 为准(用户配置),覆盖脚本旧值
for (const sp of sceneFiles) {
try {
const sj = (0, fs_extra_1.readJsonSync)(sp);
walkSceneComponents(sj, compUuid, (comp) => {
if (typeof comp.host === 'string' && comp.host) {
keepHost = comp.host;
}
if (typeof comp.port === 'number' && comp.port > 0) {
keepPort = comp.port;
}
});
}
catch (_d) {
/* 单个场景解析失败跳过 */
}
}
}
catch (_e) {
/* 扫描失败则跳过场景同步 */
}
}
// 桥接脚本内置固定令牌 www.yyka.cn,无需注入;
// 仅把 BRIDGE_VERSION 同步为插件版本号,避免面板显示过时版本
const srcContent = (0, fs_extra_1.readFileSync)(src, 'utf-8');
let injected = srcContent.replace(/BRIDGE_VERSION = '[^']*'/, `BRIDGE_VERSION = '${package_json_1.default.version}'`);
// 默认连接地址:用户配置过 host 则保留(如真机调试时自定义的电脑 IP);
// 否则自动注入宿主机局域网 IP —— 固定 127.0.0.1 时真机/模拟器连的是设备自身回环,无法接通插件服务
const lanIP = getHostLanIP();
if (keepHost) {
injected = injected.replace('host = DEFAULT_HOST;', `host = '${keepHost}';`);
}
else if (lanIP) {
injected = injected.replace('host = DEFAULT_HOST;', `host = '${lanIP}';`);
}
if (keepPort) {
injected = injected.replace('port = DEFAULT_PORT;', `port = ${keepPort};`);
}
(0, fs_extra_1.writeFileSync)(dest, injected, 'utf-8');
try {
await Editor.Message.request('asset-db', 'refresh-asset', dest);
}
catch (err) {
console.warn('[yyka-runtime-inspector] refresh-asset failed:', err);
}
// 第二遍:把已存在常驻节点的 host/port 同步为最新配置;
// token 已改为脚本内置常量,顺带清掉旧版本场景里残留的 token 字段,避免误导
if (compUuid) {
for (const sp of sceneFiles) {
try {
const sj = (0, fs_extra_1.readJsonSync)(sp);
let changed = false;
walkSceneComponents(sj, compUuid, (comp) => {
if (keepHost) {
comp.host = keepHost;
}
if (keepPort) {
comp.port = keepPort;
}
if (typeof comp.token !== 'undefined') {
delete comp.token;
changed = true;
}
if (keepHost || keepPort) {
changed = true;
}
});
if (changed) {
(0, fs_extra_1.writeJsonSync)(sp, sj, { spaces: 2 });
console.log(`[yyka-runtime-inspector] 已同步常驻节点属性: ${sp}`);
try {
await Editor.Message.request('asset-db', 'refresh-asset', sp);
}
catch (_f) {
/* ignore */
}
}
}
catch (_g) {
/* 单个场景同步失败跳过 */
}
}
}
return { ok: true, path: dest, overwrote, token: currentToken, scenes: sceneFiles.length };
},
/**
* 通知游戏侧高亮/遮罩指定节点。id 为空串则清除。
*/
async highlightNode(id, mode = 'highlight') {
try {
const res = await callGame('highlight', { id: id || '', mode: mode === 'mask' ? 'mask' : 'highlight' }, 3000);
return res;
}
catch (err) {
return { error: String(err) };
}
},
};
/**
* @zh 扩展启动时自动拉起 WS 服务,便于游戏侧随时连接。
*/
function load() {
console.log('[yyka-runtime-inspector] load, starting ws server...');
// 令牌为固定静态值,服务启动时自动带上鉴权
exports.methods.startServer();
// 每次打开插件自动安装最新桥接脚本(保留用户自定义 host/port),保证游戏侧脚本与插件版本同步
exports.methods.installBridge().then((res) => {
if (res && res.ok) {
pushLog({ evt: 'sys', msg: `桥接脚本${res.overwrote ? '已更新' : '已安装'}: ${res.path}` });
}
else if (res && res.msg) {
console.warn('[yyka-runtime-inspector] auto installBridge skipped:', res.msg);
}
}).catch((err) => {
console.warn('[yyka-runtime-inspector] auto installBridge failed:', err);
});
}
/**
* @zh 卸载扩展时关闭 WS 服务。
*/
function unload() {
if (server) {
server.stop();
server = null;
}
if (mcpHost) {
mcpHost.stop();
mcpHost = null;
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NvdXJjZS9tYWluLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7OztBQWdsQkEsb0JBY0M7QUFLRCx3QkFTQztBQTVtQkQsYUFBYTtBQUNiLG1FQUEwQztBQUMxQyx1Q0FBNkc7QUFDN0csK0JBQTRCO0FBQzVCLDJCQUF1QztBQUN2QywyQ0FBdUM7QUFDdkMsNkNBQStEO0FBRS9ELE1BQU0sWUFBWSxHQUFHLEtBQUssQ0FBQztBQUUzQix3Q0FBd0M7QUFDeEMsTUFBTSxZQUFZLEdBQUcsYUFBYSxDQUFDO0FBRW5DLElBQUksTUFBTSxHQUFvQixJQUFJLENBQUM7QUFDbkMsSUFBSSxTQUFTLEdBQWMsRUFBRSxDQUFDO0FBQzlCLDhEQUE4RDtBQUM5RCxJQUFJLFNBQVMsR0FBRyxDQUFDLENBQUM7QUFDbEIsTUFBTSxXQUFXLEdBQUcsWUFBWSxDQUFDO0FBQ2pDLElBQUksT0FBTyxHQUF5QixJQUFJLENBQUM7QUFRekMsTUFBTSxPQUFPLEdBQUcsSUFBSSxHQUFHLEVBQXNCLENBQUM7QUFDOUMsSUFBSSxVQUFVLEdBQUcsQ0FBQyxDQUFDO0FBRW5CLFNBQVMsWUFBWTtJQUNqQixJQUFJLENBQUMsTUFBTSxFQUFFLENBQUM7UUFDVixNQUFNLEdBQUcsSUFBSSxvQkFBUSxFQUFFLENBQUM7SUFDNUIsQ0FBQztJQUNELE9BQU8sTUFBTSxDQUFDO0FBQ2xCLENBQUM7QUFFRCxTQUFTLGNBQWM7O0lBQ25CLE1BQU0sSUFBSSxHQUFHLE1BQUEsTUFBTSxhQUFOLE1BQU0sdUJBQU4sTUFBTSxDQUFFLFVBQVUsbUNBQUksSUFBSSxDQUFDO0lBQ3hDLE9BQU87UUFDSCxPQUFPLEVBQUUsTUFBQSxNQUFNLGFBQU4sTUFBTSx1QkFBTixNQUFNLENBQUUsU0FBUyxtQ0FBSSxLQUFLO1FBQ25DLElBQUksRUFBRSxNQUFBLE1BQU0sYUFBTixNQUFNLHVCQUFOLE1BQU0sQ0FBRSxTQUFTLG1DQUFJLENB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