/** * AGC Cocos Creator 插件运行时入口。 * * 宿主以插件目录为 cwd 用系统 `node` 启动本文件,stdio 上使用一行一个 JSON-RPC * 2.0 消息(`agc.plugin.v1`)。入口只做注册与转发:命令、能力、面板通过通用宿主 * 注册,编辑器操作经 `host.rpc` 路由到插件包自带 native 适配器。 * * 说明:通用 SDK `@genarrative/agc-plugin-sdk` 目前以 TypeScript 源码分发,而运行 * 入口必须能被系统 node 直接执行,因此这里内联同一份 stdio 协议实现;package.json * 仍声明 SDK 版本依赖,测试会校验协议常量、方法名与 native 适配器一致。 */ import { createInterface } from 'node:readline'; import { pathToFileURL } from 'node:url'; import { buildEditorRpcRequest, COCOS_EDITOR_OPERATIONS, validateExecuteCode, } from './cocos-editor-adapter.mjs'; import { buildCocosOperationCode, COCOS_EDITOR_OPERATIONS as COCOS_OPERATION_NAMES, } from './cocos-editor-operations.mjs'; export const COCOS_PLUGIN_PROTOCOL_VERSION = 'agc.plugin.v1'; export const COCOS_EXECUTE_COMMAND_ID = 'cocos.editor.execute'; export const COCOS_OPERATION_COMMAND_ID = 'cocos.editor.operation'; export const COCOS_CONNECTION_CAPABILITY_ID = 'cocos.editor.connection'; export const COCOS_EDITOR_PANEL = Object.freeze({ id: 'cocos-editor', title: 'Cocos Creator', entry: './panels/cocos-editor.html', placement: 'sidebar', }); export const PROJECT_CHANGED_EVENT = 'project.changed'; // 默认执行可先安装桥接;覆盖 15 秒引导、最长 60 秒命令和发现开销,早于宿主 90 秒截止。 const RPC_TIMEOUT_MS = 85_000; export function createCocosEditorPlugin({ send, log = () => undefined, timeoutMs = RPC_TIMEOUT_MS, }) { let nextId = 1; let activeProjectPath = null; let disposed = false; let executionUncertain = false; let executionPending = false; const pending = new Map(); const handlers = new Map([ [COCOS_EXECUTE_COMMAND_ID, handleExecute], [COCOS_OPERATION_COMMAND_ID, handleOperation], [COCOS_CONNECTION_CAPABILITY_ID, handleConnection], ]); function request(method, params) { const id = nextId++; return new Promise((resolve, reject) => { const timer = setTimeout(() => { pending.delete(id); reject(new Error(`插件宿主 RPC 超时:${method}`)); }, timeoutMs); pending.set(id, { resolve, reject, timer }); send({ jsonrpc: '2.0', id, method, params }); }); } function respond(id, result, error) { if (error) { send({ jsonrpc: '2.0', id, error: { code: -32603, message: error }, }); return; } send({ jsonrpc: '2.0', id, result: result ?? null }); } function resolveProjectPath(params) { const explicit = params?.projectPath; if (typeof explicit === 'string' && explicit) return explicit; if (activeProjectPath) return activeProjectPath; throw new Error('当前没有受控项目路径,宿主需先设置项目上下文'); } async function callEditor(operation, params) { const { method, params: editorParams } = buildEditorRpcRequest( operation, params, ); return request('host.rpc', { method, params: editorParams }); } async function handleExecute(params) { const code = params?.code; validateExecuteCode(code); const projectPath = resolveProjectPath(params); const reconcile = (response) => ({ status: 'needs-reconciliation', retryAllowed: false, response, }); if (executionUncertain) return reconcile({ ok: false, error: '先前执行结果待核对' }); // 不积压稍后执行的 mutation,避免调用方超时后请求仍从队列发出。 if (executionPending) return { status: 'failed', retryAllowed: false, response: { ok: false, error: '已有 Cocos execute 正在执行,请等待回执', }, }; executionPending = true; try { const response = await callEditor('execute', { projectPath, code, timeoutMs: params?.timeoutMs, }); if (response?.status === 'needs-reconciliation') { executionUncertain = true; return reconcile(response); } if (typeof response?.ok !== 'boolean') throw new Error('宿主缺少可信执行回执'); return { status: response.ok ? 'completed' : 'failed', response }; } catch (error) { // 已交给宿主的 execute 超时/断线不能推断为未执行。 executionUncertain = true; return reconcile({ ok: false, error: error.message }); } finally { executionPending = false; } } async function handleConnection(params) { const operation = params?.operation ?? 'detect'; if (!COCOS_EDITOR_OPERATIONS.includes(operation)) { throw new Error(`不支持的能力操作:${operation}`); } if (operation === 'execute') return handleExecute(params); if (operation === 'disconnect') { return callEditor('disconnect', {}); } return callEditor(operation, { projectPath: resolveProjectPath(params), processId: params?.processId, timeoutMs: params?.timeoutMs, payloadPath: params?.payloadPath, }); } async function handleOperation(params) { const operation = params?.operation; if (!COCOS_OPERATION_NAMES.includes(operation)) { throw new Error(`不支持的 Cocos 操作:${operation}`); } const projectPath = resolveProjectPath(params); const result = await handleExecute({ projectPath, code: buildCocosOperationCode(operation, params?.args ?? {}), timeoutMs: 60_000, }); const status = result?.response?.result?.status; if (status === 'failed' || status === 'needs-reconciliation') { result.status = status; if (status === 'needs-reconciliation') { executionUncertain = true; result.retryAllowed = false; } } return result; } async function handleMessage(message) { if (disposed) return; const envelope = typeof message === 'string' ? JSON.parse(message) : message; if (!envelope || envelope.jsonrpc !== '2.0') return; if (envelope.method === 'host.event') { const event = envelope.params ?? {}; if (event.type === PROJECT_CHANGED_EVENT) { const projectPath = event.payload?.projectPath; activeProjectPath = typeof projectPath === 'string' && projectPath ? projectPath : null; } return; } if (envelope.method !== undefined) { if (envelope.id === undefined) return; const handler = handlers.get(envelope.method); if (!handler) { respond(envelope.id, undefined, `插件未注册方法:${envelope.method}`); return; } try { respond(envelope.id, await handler(envelope.params ?? {})); } catch (error) { log(`cocos plugin 处理 ${envelope.method} 失败:${error.message}`); respond(envelope.id, undefined, error.message); } return; } const request = pending.get(envelope.id); if (!request) return; pending.delete(envelope.id); clearTimeout(request.timer); if (envelope.error) { request.reject(new Error(envelope.error.message ?? '插件宿主 RPC 失败')); return; } request.resolve(envelope.result); } async function start() { const command = await request('host.registerCommand', { id: COCOS_EXECUTE_COMMAND_ID, title: '在 Cocos Creator 中执行代码', description: '在已打开的 Cocos Creator 项目中执行受控 JavaScript 函数体', }); const capability = await request('host.registerCapability', { id: COCOS_CONNECTION_CAPABILITY_ID, description: 'Cocos Creator 连接探测、目标预检、ping、status 与注入', }); const operations = await request('host.registerCommand', { id: COCOS_OPERATION_COMMAND_ID, title: '调用 Cocos Creator 编辑器能力', description: `调用内置 Cocos 编辑器操作(${COCOS_OPERATION_NAMES.join(', ')})`, }); const panel = await request('host.registerPanel', { ...COCOS_EDITOR_PANEL, }); const subscription = await request('host.events.subscribe', { type: PROJECT_CHANGED_EVENT, }); activeProjectPath = subscription?.projectPath ?? null; return { command, capability, operations, panel, subscriptionId: subscription?.subscriptionId ?? null, }; } function dispose() { disposed = true; for (const request of pending.values()) { clearTimeout(request.timer); request.reject(new Error('插件已停止')); } pending.clear(); } return { start, handleMessage, dispose, get activeProjectPath() { return activeProjectPath; }, }; } export function startCocosEditorStdioPlugin({ stdin = process.stdin, stdout = process.stdout, log = (message) => process.stderr.write(`${message}\n`), } = {}) { const plugin = createCocosEditorPlugin({ send: (message) => stdout.write(`${JSON.stringify(message)}\n`), log, }); const lines = createInterface({ input: stdin, crlfDelay: Infinity }); lines.on('line', (line) => { const trimmed = line.trim(); if (!trimmed) return; void plugin.handleMessage(trimmed).catch((error) => { log(`cocos plugin 消息处理失败:${error.message}`); }); }); void plugin.start().catch((error) => { log(`cocos plugin 注册失败:${error.message}`); }); return plugin; } const entryPoint = process.argv[1]; if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) { startCocosEditorStdioPlugin(); }