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
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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
|