Stabilize ccweb codex app runtime
This commit is contained in:
165
lib/codex-app-worker.js
Normal file
165
lib/codex-app-worker.js
Normal file
@@ -0,0 +1,165 @@
|
||||
'use strict';
|
||||
|
||||
const { createCodexAppServerClient } = require('./codex-app-server-client');
|
||||
|
||||
let client = null;
|
||||
let currentSpec = null;
|
||||
let nextParentRequestId = 1;
|
||||
const pendingParentRequests = new Map();
|
||||
|
||||
function send(message) {
|
||||
if (typeof process.send === 'function') {
|
||||
process.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
function serializeError(err) {
|
||||
return {
|
||||
code: err?.code || -32603,
|
||||
message: err?.message || String(err || 'Codex App worker error'),
|
||||
data: err?.data || null,
|
||||
};
|
||||
}
|
||||
|
||||
function reply(id, result, error) {
|
||||
if (!id) return;
|
||||
if (error) {
|
||||
send({ id, error: serializeError(error) });
|
||||
} else {
|
||||
send({ id, result: result || {} });
|
||||
}
|
||||
}
|
||||
|
||||
function requestParent(request, timeoutMs = 300000) {
|
||||
const requestId = String(nextParentRequestId++);
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
pendingParentRequests.delete(requestId);
|
||||
reject(new Error(`Codex App worker 等待主进程处理请求超时: ${request.method || ''}`));
|
||||
}, timeoutMs);
|
||||
pendingParentRequests.set(requestId, { resolve, reject, timer });
|
||||
send({ type: 'serverRequest', requestId, request });
|
||||
});
|
||||
}
|
||||
|
||||
async function postInitialize({ request, onLog } = {}) {
|
||||
if (typeof request !== 'function') return;
|
||||
try {
|
||||
await request('experimentalFeature/enablement/set', { enablement: { goals: true } }, 30000);
|
||||
if (typeof onLog === 'function') onLog('INFO', 'codex_app_worker_goals_feature_enabled', {});
|
||||
} catch (err) {
|
||||
if (typeof onLog === 'function') {
|
||||
onLog('INFO', 'codex_app_worker_goals_feature_enable_failed', { error: err?.message || String(err || '') });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await request('collaborationMode/list', {}, 30000);
|
||||
if (typeof onLog === 'function') onLog('INFO', 'codex_app_worker_collaboration_modes', { result });
|
||||
} catch (err) {
|
||||
if (typeof onLog === 'function') {
|
||||
onLog('INFO', 'codex_app_worker_collaboration_mode_list_failed', { error: err?.message || String(err || '') });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function configure(spec = {}) {
|
||||
const nextSpec = {
|
||||
command: spec.command || 'codex',
|
||||
args: Array.isArray(spec.args) && spec.args.length > 0 ? spec.args : ['app-server', '--stdio'],
|
||||
env: spec.env || process.env,
|
||||
cwd: spec.cwd || process.cwd(),
|
||||
clientInfo: spec.clientInfo || undefined,
|
||||
};
|
||||
|
||||
const nextSignature = JSON.stringify({
|
||||
command: nextSpec.command,
|
||||
args: nextSpec.args,
|
||||
cwd: nextSpec.cwd,
|
||||
envCodeHome: nextSpec.env.CODEX_HOME || '',
|
||||
});
|
||||
const currentSignature = currentSpec ? JSON.stringify({
|
||||
command: currentSpec.command,
|
||||
args: currentSpec.args,
|
||||
cwd: currentSpec.cwd,
|
||||
envCodeHome: currentSpec.env.CODEX_HOME || '',
|
||||
}) : '';
|
||||
|
||||
if (client && nextSignature === currentSignature) {
|
||||
currentSpec = nextSpec;
|
||||
return;
|
||||
}
|
||||
|
||||
if (client) {
|
||||
try { client.stop(); } catch {}
|
||||
client = null;
|
||||
}
|
||||
currentSpec = nextSpec;
|
||||
client = createCodexAppServerClient({
|
||||
command: nextSpec.command,
|
||||
args: nextSpec.args,
|
||||
env: nextSpec.env,
|
||||
cwd: nextSpec.cwd,
|
||||
clientInfo: nextSpec.clientInfo,
|
||||
onNotification: (notification) => send({ type: 'notification', notification }),
|
||||
onServerRequest: (request) => requestParent(request),
|
||||
onExit: (info) => send({ type: 'exit', info }),
|
||||
onLog: (level, event, data) => send({ type: 'log', level, event, data }),
|
||||
postInitialize,
|
||||
});
|
||||
}
|
||||
|
||||
process.on('message', (message = {}) => {
|
||||
if (message.type === 'serverRequestResult') {
|
||||
const item = pendingParentRequests.get(String(message.requestId || ''));
|
||||
if (!item) return;
|
||||
pendingParentRequests.delete(String(message.requestId || ''));
|
||||
clearTimeout(item.timer);
|
||||
if (message.error) {
|
||||
const err = new Error(message.error.message || '主进程处理 Codex App 请求失败。');
|
||||
err.code = message.error.code;
|
||||
err.data = message.error.data;
|
||||
item.reject(err);
|
||||
} else {
|
||||
item.resolve(message.result || {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
switch (message.type) {
|
||||
case 'configure':
|
||||
configure(message.spec || {});
|
||||
return {};
|
||||
case 'start':
|
||||
if (!client) configure(currentSpec || {});
|
||||
return client.start();
|
||||
case 'request':
|
||||
if (!client) configure(currentSpec || {});
|
||||
return client.request(message.method, message.params || {}, message.timeoutMs || 300000);
|
||||
case 'notification':
|
||||
if (!client) configure(currentSpec || {});
|
||||
client.notification(message.method, message.params || {});
|
||||
return {};
|
||||
case 'reloadMcpServers':
|
||||
if (!client) configure(currentSpec || {});
|
||||
return client.reloadMcpServers();
|
||||
case 'stop':
|
||||
if (client) client.stop();
|
||||
process.exit(0);
|
||||
return {};
|
||||
default:
|
||||
throw new Error(`未知 Codex App worker 消息: ${message.type}`);
|
||||
}
|
||||
})
|
||||
.then((result) => reply(message.id, result))
|
||||
.catch((err) => reply(message.id, null, err));
|
||||
});
|
||||
|
||||
process.on('disconnect', () => {
|
||||
if (client) {
|
||||
try { client.stop(); } catch {}
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
Reference in New Issue
Block a user