Files
Genarrative/plugins/agc-cocos-editor/src/entry.mjs
T
kdletters c1d26f11df 移除 Cocos 和 Unity 插件的工程类型限制
统一插件列表、启动、面板和 Agent 工具的可用性判断,保留开关与平台约束

调整前端自动启动并修复 Cocos 项目切换的旧连接与订阅快照竞态

补充插件宿主、工具目录和前端回归测试,同步插件技术规范与共享决策
2026-09-20 10:53:16 +08:00

306 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 projectEpoch = 0;
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;
projectEpoch += 1;
}
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 epoch = projectEpoch;
const subscription = await request('host.events.subscribe', {
type: PROJECT_CHANGED_EVENT,
});
if (epoch === projectEpoch) {
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();
}