diff --git a/src/core/launcher.ts b/src/core/launcher.ts index 90e42994e..3088bfe8f 100644 --- a/src/core/launcher.ts +++ b/src/core/launcher.ts @@ -100,6 +100,9 @@ export default class Launcher { middlewareService.register('Scene', SceneMiddleware); await sceneConfigInstance.init(); + const { Rpc } = await import('./scene/main-process/rpc'); + await Rpc.startup(); + const browserPath = process.platform === 'win32' ? 'start' : process.platform === 'darwin' diff --git a/src/core/scene/main-process/rpc.ts b/src/core/scene/main-process/rpc.ts index cdfd5a07a..1f5d56b81 100644 --- a/src/core/scene/main-process/rpc.ts +++ b/src/core/scene/main-process/rpc.ts @@ -22,17 +22,19 @@ export class RpcProxy { return this.rpcInstance?.isConnect(); } - async startup(prc: ChildProcess | NodeJS.Process) { + async startup(prc?: ChildProcess | NodeJS.Process) { // 在创建新实例前,先清理旧实例,防止内存泄漏 this.dispose(); this.rpcInstance = new ProcessRPC(); - this.rpcInstance.attach(prc); + if (prc) { + this.rpcInstance.attach(prc); + } this.rpcInstance.register({ assetManager: assetManager, programming: scriptManager, sceneConfigInstance: sceneConfigInstance, }); - console.log('[Node] Scene Process RPC ready'); + console.log(`[Node] Scene Process RPC ready ${prc ? '(Attached)' : '(Detached - Web Mode)'}`); } /** diff --git a/src/core/scene/process-rpc/process-rpc.ts b/src/core/scene/process-rpc/process-rpc.ts index 8acb0d510..753c274ed 100644 --- a/src/core/scene/process-rpc/process-rpc.ts +++ b/src/core/scene/process-rpc/process-rpc.ts @@ -84,6 +84,8 @@ export class ProcessRPC> { private msgId = 0; private process: NodeJS.Process | ChildProcess | undefined; private onMessageBind = this.onMessage.bind(this); + private serverUrl: string | undefined; + private isWebMode = false; /** * @param proc - NodeJS.Process 或 ChildProcess 实例 @@ -91,9 +93,20 @@ export class ProcessRPC> { attach(proc: NodeJS.Process | ChildProcess) { this.dispose(); this.process = proc; + this.isWebMode = false; this.listen(); } + /** + * 设置 Web 传输模式(浏览器环境使用) + * @param baseUrl - 服务器基础连接 + */ + setWebTransport(baseUrl: string) { + this.dispose(); + this.serverUrl = baseUrl; + this.isWebMode = true; + } + /** * 注册模块,只支持对象或者类实例 * @param handler - 注册模块列表 @@ -110,6 +123,8 @@ export class ProcessRPC> { this.callbacks.clear(); this.process?.off('message', this.onMessageBind); this.process = undefined; + this.serverUrl = undefined; + this.isWebMode = false; } /** @@ -194,6 +209,27 @@ export class ProcessRPC> { : [args: Parameters, options?: RequestOptions] ): Promise>> { const [args, options] = rest; + + if (this.isWebMode && this.serverUrl) { + const url = `${this.serverUrl}/rpc/${String(module)}/${String(method)}`; + return fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(args || []) + }).then(async (res) => { + if (res.ok) { + const rpcRes = (await res.json()) as RpcResponse; + if (rpcRes.error) { + throw new Error(rpcRes.error); + } + return rpcRes.result; + } + throw new Error(`RPC request failed with status: ${res.status}`); + }); + } + return new Promise((resolve, reject) => { const id = ++this.msgId; @@ -219,12 +255,27 @@ export class ProcessRPC> { }); if (!this.process) { - throw new Error('未挂载进程'); + throw new Error('RPC 尚未挂载进程且未开启 Web 模式'); } this.process.send?.(req); }); } + /** + * 直接执行本地注册的模块方法(用于服务器接收到基于 HTTP 的 RPC 请求时直接处理) + */ + async executeLocal( + module: K, + method: M, + args: any[] + ): Promise>> { + const target = this.handlers[module as string]; + if (!target || typeof target[method as string] !== 'function') { + throw new Error(`Method not found: ${String(module)}.${String(method)}`); + } + return await target[method as string](...(args || [])); + } + /** * 发送单向消息(无返回值) */ diff --git a/src/core/scene/scene-process/engine-bootstrap.ts b/src/core/scene/scene-process/engine-bootstrap.ts index dafdd67bc..25e15849b 100644 --- a/src/core/scene/scene-process/engine-bootstrap.ts +++ b/src/core/scene/scene-process/engine-bootstrap.ts @@ -1,4 +1,5 @@ import * as EditorExtends from '../../engine/editor-extends'; +import { Rpc } from './rpc'; import { serviceManager } from './service/service-manager'; import { Service as DecoratorService } from './service/core/decorator'; import './service'; @@ -79,6 +80,7 @@ export async function startup(options: { if (EditorExtends.init) { await EditorExtends.init(); } + await Rpc.startup({ serverURL }); cc.physics.selector.runInEditor = true; await cc.game.init(config); diff --git a/src/core/scene/scene-process/rpc.ts b/src/core/scene/scene-process/rpc.ts index 3a356887a..d3b8e7286 100644 --- a/src/core/scene/scene-process/rpc.ts +++ b/src/core/scene/scene-process/rpc.ts @@ -11,14 +11,19 @@ export class RpcProxy { return this.rpcInstance; } - async startup() { + async startup(options?: { serverURL: string }) { // 在创建新实例前,先清理旧实例,防止内存泄漏 this.dispose(); this.rpcInstance = new ProcessRPC(); - this.rpcInstance.attach(process); - const { Service } = await import('./service/core/decorator'); - this.rpcInstance.register(Service); - console.log('[Scene] Scene Process RPC ready'); + if (options?.serverURL) { + this.rpcInstance.setWebTransport(options.serverURL); + console.log('[Scene] Scene Process Web RPC ready'); + } else { + this.rpcInstance.attach(process); + const { Service } = await import('./service/core/decorator'); + this.rpcInstance.register(Service); + console.log('[Scene] Scene Process RPC ready'); + } } /** diff --git a/src/core/scene/scene-process/service/editor.ts b/src/core/scene/scene-process/service/editor.ts index c61387132..3cc3eff65 100644 --- a/src/core/scene/scene-process/service/editor.ts +++ b/src/core/scene/scene-process/service/editor.ts @@ -15,8 +15,7 @@ import { } from '../../common'; import { PrefabEditor, SceneEditor } from './editors'; import { IAssetInfo } from '../../../assets/@types/public'; -import { parseCommandLineArgs } from '../utils'; -import { serviceManager } from './service-manager'; +import { Rpc } from '../rpc'; /** * EditorAsset - 统一的编辑器管理入口 @@ -56,19 +55,6 @@ export class EditorService extends BaseService implements IEditor } } - private async queryAssetInfo(urlOrUUID: string): Promise { - const serverURL = serviceManager.getServerUrl(); - try { - const res = await fetch(`${serverURL}/query-asset-info/${urlOrUUID}`); - if (res.ok) { - return await res.json() as IAssetInfo; - } - } catch (error) { - console.warn(`[Scene] queryAssetInfo failed:`, error); - } - return null; - } - async waitLocks() { if (this.lockPromise) { await this.lockPromise; @@ -125,7 +111,7 @@ export class EditorService extends BaseService implements IEditor async open(params: IOpenOptions): Promise { const { urlOrUUID, simpleNode = true } = params; - const assetInfo = await this.queryAssetInfo(urlOrUUID); + const assetInfo = await Rpc.getInstance().request('assetManager', 'queryAssetInfo', [urlOrUUID]); if (!assetInfo) { throw new Error(`通过 ${urlOrUUID} 无法打开,查询不到该资源信息`); } @@ -135,7 +121,7 @@ export class EditorService extends BaseService implements IEditor if (currentEditor) { try { // 关闭当前场景 - const assetInfo = await this.queryAssetInfo(this.currentEditorUuid); + const assetInfo = await Rpc.getInstance().request('assetManager', 'queryAssetInfo', [this.currentEditorUuid]); if (assetInfo) { await currentEditor.close(); } @@ -190,7 +176,7 @@ export class EditorService extends BaseService implements IEditor try { if (!urlOrUUID) return true; - const assetInfo = await this.queryAssetInfo(urlOrUUID); + const assetInfo = await Rpc.getInstance().request('assetManager', 'queryAssetInfo', [urlOrUUID]); if (!assetInfo) { throw new Error(`通过 ${urlOrUUID} 请求资源失败`); } @@ -226,7 +212,7 @@ export class EditorService extends BaseService implements IEditor throw new Error('当前没有打开任何编辑器'); } - const assetInfo = await this.queryAssetInfo(urlOrUUID); + const assetInfo = await Rpc.getInstance().request('assetManager', 'queryAssetInfo', [urlOrUUID]); if (!assetInfo) { throw new Error(`通过 ${urlOrUUID} 请求资源失败`); } @@ -264,7 +250,7 @@ export class EditorService extends BaseService implements IEditor return ReloadResult.NO_EDITOR; } - const assetInfo = await this.queryAssetInfo(urlOrUUID); + const assetInfo = await Rpc.getInstance().request('assetManager', 'queryAssetInfo', [urlOrUUID]); if (!assetInfo) { console.warn(`通过 ${urlOrUUID} 请求资源失败`); this._isReloading = false; diff --git a/src/core/scene/scene-process/service/script.ts b/src/core/scene/scene-process/service/script.ts index 2f2683a6a..f09728acd 100644 --- a/src/core/scene/scene-process/service/script.ts +++ b/src/core/scene/scene-process/service/script.ts @@ -131,17 +131,14 @@ export class ScriptService extends BaseService implements IScript classConstructor, 'i18n:menu.custom_script/' + className, -1); } }); - const serverUrl = serviceManager.getServerUrl(); - const serialPromise = await fetch(`${serverUrl}/programming/getPackerDriverLoaderContext/editor`); - const serializedPackLoaderContext = await serialPromise.json(); + const serializedPackLoaderContext = await Rpc.getInstance().request('programming', 'getPackerDriverLoaderContext', ['editor']); if (!serializedPackLoaderContext) { throw new Error('packer-driver/get-loader-context is not defined'); } const quickPackLoaderContext = QuickPackLoaderContext.deserialize(serializedPackLoaderContext); const { loadDynamic } = await import('cc/preload'); - const moduleMapPromise = await fetch(`${serverUrl}/programming/queryCCEModuleMap`); - const cceModuleMap = await moduleMapPromise.json(); + const cceModuleMap = await Rpc.getInstance().request('programming', 'queryCCEModuleMap'); this._executor = await Executor.create({ // @ts-ignore importEngineMod: async (id) => { @@ -278,29 +275,11 @@ export class ScriptService extends BaseService implements IScript * @private */ private async _syncPluginScriptList() { - const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; - if (isBrowser) { - try { - const serverUrl = serviceManager.getServerUrl(); - const res = await fetch(`${serverUrl}/assetManager/querySortedPlugins`); - if (res.ok) { - const pluginScripts = await res.json(); - this._executor.setPluginScripts(pluginScripts || []); - } else { - this._executor.setPluginScripts([]); - } - } catch (err) { - console.error('Failed to fetch plugin scripts', err); - this._executor.setPluginScripts([]); - } - return; - } - return Promise.resolve(Rpc.getInstance().request('assetManager', 'querySortedPlugins', [{ loadPluginInEditor: true, }])) .then((pluginScripts) => { - this._executor.setPluginScripts(pluginScripts); + this._executor.setPluginScripts(pluginScripts || []); }) .catch((reason) => { console.error(reason); diff --git a/src/core/scene/scene.middleware.ts b/src/core/scene/scene.middleware.ts index 1109eae1f..f31095fa5 100644 --- a/src/core/scene/scene.middleware.ts +++ b/src/core/scene/scene.middleware.ts @@ -104,97 +104,107 @@ export default { // then fall back to library directories on disk url: '/:dir/:uuid/:nativeName.:ext', async handler(req: Request, res: Response, next: NextFunction) { - if (req.params.dir === 'build' || req.params.dir === 'mcp' || req.params.dir === 'static' || req.params.dir === 'scripting') { + if (req.params.dir === 'build' || req.params.dir === 'mcp') { return next(); } - try { - const { uuid, ext, nativeName } = req.params; - const { assetManager } = await import('../assets'); - const assetInfo = assetManager.queryAssetInfo(uuid); - const filePath = assetInfo && assetInfo.library[`${nativeName}.${ext}`]; - if (!filePath || !(await fse.pathExists(filePath))) { - return next(); - } + const { uuid, ext, nativeName } = req.params; + const { assetManager } = await import('../assets'); + const assetInfo = assetManager.queryAssetInfo(uuid); + const filePath = assetInfo && assetInfo.library[`${nativeName}.${ext}`]; + if (!filePath) { + console.warn(`Asset not found: ${req.url}`); + return res.status(404).json({ + error: 'Asset not found', + requested: req.url, + uuid, + file: `${nativeName}.${ext}` + }); + } + + const isBrowser = !!(req.headers['accept']?.includes('text/html') || + req.headers['sec-ch-ua'] || + req.query.isBrowser === 'true'); + + if (isBrowser) { const content = await fse.readFile(filePath); - const mimeMap: Record = { '.json': 'application/json', '.bin': 'application/octet-stream', '.cconb': 'application/octet-stream' }; - res.setHeader('Content-Type', mimeMap[`.${ext}`] || 'application/octet-stream'); - res.status(200).send(content); - } catch (err) { - console.error(`[Scene] Error serving asset ${req.url}:`, err); - next(err); + const extname = path.extname(filePath); + const mimeMap: Record = { + '.json': 'application/json', + '.bin': 'application/octet-stream', + '.cconb': 'application/octet-stream', + '.wasm': 'application/wasm', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg' + }; + res.setHeader('Content-Type', mimeMap[extname] || 'application/octet-stream'); + return res.status(200).send(content); } + + res.status(200).send(filePath || req.url); }, }, { - // Serve library assets by UUID url: '/:dir/:uuid.:ext', - async handler(req: Request, res: Response, next: NextFunction) { + async handler(req: Request, res: Response) { const { uuid, ext } = req.params; - if (req.params.dir === 'build' || req.params.dir === 'mcp' || req.params.dir === 'static' || req.params.dir === 'scripting') { - return next(); + const { assetManager } = await import('../assets'); + const assetInfo = assetManager.queryAssetInfo(uuid); + const filePath = assetInfo && assetInfo.library[`.${ext}`]; + if (!filePath) { + console.warn(`Asset not found: ${req.url}`); + return res.status(404).json({ + error: 'Asset not found', + requested: req.url, + uuid, + }); } - try { - const { assetManager } = await import('../assets'); - const assetInfo = assetManager.queryAssetInfo(uuid); - const filePath = assetInfo && assetInfo.library[`.${ext}`]; - if (!filePath || !(await fse.pathExists(filePath))) { - return next(); - } + + const isBrowser = !!(req.headers['accept']?.includes('text/html') || + req.headers['sec-ch-ua'] || + req.query.isBrowser === 'true'); + + if (isBrowser) { const content = await fse.readFile(filePath); - const mimeMap: Record = { '.json': 'application/json', '.bin': 'application/octet-stream', '.cconb': 'application/octet-stream' }; - res.setHeader('Content-Type', mimeMap[`.${ext}`] || 'application/octet-stream'); - res.status(200).send(content); - } catch (err) { - console.error(`[Scene] Error serving asset ${req.url}:`, err); - next(err); + const extname = path.extname(filePath); + const mimeMap: Record = { + '.json': 'application/json', + '.bin': 'application/octet-stream', + '.cconb': 'application/octet-stream', + '.wasm': 'application/wasm', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg' + }; + res.setHeader('Content-Type', mimeMap[extname] || 'application/octet-stream'); + return res.status(200).send(content); } + + res.status(200).send(filePath || req.url); }, - }, + } + ], + post: [ { - // Fallback: serve library files directly from disk. - // Checks project library (library/cli/) and engine internal - // library (engine/editor/library/) for UUID-based asset paths. - url: /^\/(?:remote\/\w+\/)?([0-9a-f]{2})\/([0-9a-f-]+(?:@[0-9a-f]+)?)\.(json|bin|cconb)$/, - async handler(req: Request, res: Response, next: NextFunction) { + url: '/rpc/:module/:method', + async handler(req: Request, res: Response) { + const { module, method } = req.params; + const args = req.body; try { - const relPath = req.path.replace(/^\/remote\/\w+\//, '/'); - const { default: scripting } = await import('../../core/scripting'); - const projectPath = scripting.projectPath; - - // Try project library first - const projectLibPath = path.join(projectPath, 'library', 'cli', relPath); - if (await fse.pathExists(projectLibPath)) { - const content = await fse.readFile(projectLibPath); - const ext = path.extname(relPath); - const mimeMap: Record = { '.json': 'application/json', '.bin': 'application/octet-stream', '.cconb': 'application/octet-stream' }; - res.setHeader('Content-Type', mimeMap[ext] || 'application/octet-stream'); - return res.status(200).send(content); - } - - // Try engine internal library - const { Engine } = await import('../engine'); - const enginePath = Engine.getInfo().typescript.path; - const engineLibPath = path.join(enginePath, 'editor', 'library', relPath); - if (await fse.pathExists(engineLibPath)) { - const content = await fse.readFile(engineLibPath); - const ext = path.extname(relPath); - const mimeMap: Record = { '.json': 'application/json', '.bin': 'application/octet-stream', '.cconb': 'application/octet-stream' }; - res.setHeader('Content-Type', mimeMap[ext] || 'application/octet-stream'); - return res.status(200).send(content); - } - - next(); - } catch (err) { - console.error(`[Scene] Error serving library asset ${req.url}:`, err); - next(err); + const { Rpc } = await import('./main-process/rpc'); + const result = await Rpc.getInstance().executeLocal(module as any, method as any, args); + console.log(`[Scene Web RPC] ${module}.${method} ->`, typeof result === 'undefined' ? 'undefined' : (result === null ? 'null' : typeof result)); + res.status(200).json({ type: 'response', result }); + } catch (e: any) { + console.error(`[Scene] RPC Error (${module}.${method}):`, e); + res.status(200).json({ type: 'response', error: e?.message || String(e) }); } - }, - }, + } + } ], - post: [], staticFiles: [], socket: { connection: (socket: any) => { }, disconnect: (socket: any) => { } }, -} as IMiddlewareContribution; +} as IMiddlewareContribution; \ No newline at end of file diff --git a/src/core/scene/scripting.middleware.ts b/src/core/scene/scripting.middleware.ts index 5bf212e71..b686c1c07 100644 --- a/src/core/scene/scripting.middleware.ts +++ b/src/core/scene/scripting.middleware.ts @@ -45,27 +45,6 @@ export default { res.json(config); }, }, - { - url: '/programming/getPackerDriverLoaderContext/editor', - async handler(req: Request, res: Response, next: NextFunction) { - const { default: scripting } = await import('../../core/scripting'); - res.json(scripting.getPackerDriverLoaderContext('editor')); - }, - }, - { - url: '/programming/queryCCEModuleMap', - async handler(req: Request, res: Response, next: NextFunction) { - const { default: scripting } = await import('../../core/scripting'); - res.json(scripting.queryCCEModuleMap()); - }, - }, - { - url: '/assetManager/querySortedPlugins', - async handler(req: Request, res: Response) { - const { assetManager } = await import('../assets'); - res.json(assetManager.querySortedPlugins({ loadPluginInEditor: true })); - }, - }, { url: '/scripting/engine/modules', async handler(req: Request, res: Response) { diff --git a/src/server/server.ts b/src/server/server.ts index 6163b90f2..ef05c8a42 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -138,6 +138,7 @@ export class ServerService { init() { this.app.use(compression()); + this.app.use(express.json()); this.app.use(cors); this.app.use(consoleLogService.injectMiddleware); this.app.use(middlewareService.router);