feat: enhance codex app and cross-conversation messaging
This commit is contained in:
220
lib/codex-app-server-client.js
Normal file
220
lib/codex-app-server-client.js
Normal file
@@ -0,0 +1,220 @@
|
||||
'use strict';
|
||||
|
||||
const readline = require('readline');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
function createCodexAppServerClient(options = {}) {
|
||||
const command = options.command || 'codex';
|
||||
const args = Array.isArray(options.args) && options.args.length > 0
|
||||
? options.args.slice()
|
||||
: ['app-server', '--stdio'];
|
||||
const env = options.env || process.env;
|
||||
const cwd = options.cwd || process.cwd();
|
||||
const clientInfo = options.clientInfo || {
|
||||
name: 'ccweb_codexapp',
|
||||
title: 'CC-Web Codex App',
|
||||
version: '0.1.0',
|
||||
};
|
||||
const onNotification = typeof options.onNotification === 'function' ? options.onNotification : () => {};
|
||||
const onServerRequest = typeof options.onServerRequest === 'function' ? options.onServerRequest : null;
|
||||
const onExit = typeof options.onExit === 'function' ? options.onExit : () => {};
|
||||
const onLog = typeof options.onLog === 'function' ? options.onLog : () => {};
|
||||
const postInitialize = typeof options.postInitialize === 'function' ? options.postInitialize : null;
|
||||
|
||||
let proc = null;
|
||||
let rl = null;
|
||||
let nextId = 1;
|
||||
let initPromise = null;
|
||||
let exited = false;
|
||||
const pending = new Map();
|
||||
|
||||
function rejectAllPending(err) {
|
||||
for (const [, pendingRequest] of pending) {
|
||||
clearTimeout(pendingRequest.timer);
|
||||
pendingRequest.reject(err);
|
||||
}
|
||||
pending.clear();
|
||||
}
|
||||
|
||||
function sendRaw(message) {
|
||||
if (!proc || !proc.stdin || proc.stdin.destroyed) {
|
||||
throw new Error('Codex app-server 未启动。');
|
||||
}
|
||||
proc.stdin.write(`${JSON.stringify(message)}\n`);
|
||||
}
|
||||
|
||||
function respondToServerRequest(id, result, error) {
|
||||
try {
|
||||
if (error) {
|
||||
sendRaw({ id, error });
|
||||
} else {
|
||||
sendRaw({ id, result: result || {} });
|
||||
}
|
||||
} catch (err) {
|
||||
onLog('WARN', 'codex_app_server_response_failed', { error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
function handleServerRequest(message) {
|
||||
const id = message.id;
|
||||
const method = message.method;
|
||||
const params = message.params || {};
|
||||
if (onServerRequest) {
|
||||
Promise.resolve()
|
||||
.then(() => onServerRequest({ method, params, id }))
|
||||
.then((result) => respondToServerRequest(id, result || {}))
|
||||
.catch((err) => respondToServerRequest(id, null, {
|
||||
code: -32603,
|
||||
message: err?.message || 'cc-web 无法处理 Codex app-server 请求。',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
respondToServerRequest(id, null, {
|
||||
code: -32601,
|
||||
message: `cc-web 暂不支持 Codex app-server 请求: ${method}`,
|
||||
});
|
||||
}
|
||||
|
||||
function handleMessage(line) {
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(line);
|
||||
} catch {
|
||||
onLog('WARN', 'codex_app_server_invalid_json', { line: String(line || '').slice(0, 200) });
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(message, 'id')) {
|
||||
const pendingRequest = pending.get(message.id);
|
||||
if (pendingRequest) {
|
||||
pending.delete(message.id);
|
||||
clearTimeout(pendingRequest.timer);
|
||||
if (message.error) {
|
||||
const err = new Error(message.error.message || 'Codex app-server 请求失败。');
|
||||
err.code = message.error.code;
|
||||
err.data = message.error.data;
|
||||
pendingRequest.reject(err);
|
||||
} else {
|
||||
pendingRequest.resolve(message.result || {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.method) {
|
||||
handleServerRequest(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.method) {
|
||||
onNotification(message);
|
||||
}
|
||||
}
|
||||
|
||||
function request(method, params = {}, timeoutMs = 300000) {
|
||||
const id = nextId++;
|
||||
const message = { id, method, params };
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
pending.delete(id);
|
||||
reject(new Error(`Codex app-server 请求超时: ${method}`));
|
||||
}, timeoutMs);
|
||||
pending.set(id, { resolve, reject, timer, method });
|
||||
try {
|
||||
sendRaw(message);
|
||||
} catch (err) {
|
||||
clearTimeout(timer);
|
||||
pending.delete(id);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function notification(method, params = {}) {
|
||||
sendRaw({ method, params });
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (initPromise) return initPromise;
|
||||
exited = false;
|
||||
proc = spawn(command, args, {
|
||||
env,
|
||||
cwd,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
proc.stderr.on('data', (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
if (stderr.length > 4000) stderr = stderr.slice(-4000);
|
||||
});
|
||||
|
||||
rl = readline.createInterface({ input: proc.stdout });
|
||||
rl.on('line', handleMessage);
|
||||
|
||||
proc.on('exit', (code, signal) => {
|
||||
exited = true;
|
||||
if (rl) rl.close();
|
||||
const err = new Error(`Codex app-server 已退出: code=${code ?? 'null'} signal=${signal || 'null'}`);
|
||||
err.exitCode = code;
|
||||
err.signal = signal;
|
||||
err.stderr = stderr;
|
||||
rejectAllPending(err);
|
||||
onExit({ code, signal, stderr });
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
rejectAllPending(err);
|
||||
onExit({ code: null, signal: null, stderr: err.message });
|
||||
});
|
||||
|
||||
initPromise = request('initialize', {
|
||||
clientInfo,
|
||||
capabilities: { experimentalApi: true },
|
||||
}, 30000)
|
||||
.then(async (result) => {
|
||||
notification('initialized', {});
|
||||
if (postInitialize) await postInitialize({ request, notification, onLog });
|
||||
return result;
|
||||
})
|
||||
.catch((err) => {
|
||||
stop();
|
||||
throw err;
|
||||
});
|
||||
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
function stop() {
|
||||
initPromise = null;
|
||||
if (rl) {
|
||||
try { rl.close(); } catch {}
|
||||
rl = null;
|
||||
}
|
||||
if (proc && !exited) {
|
||||
try { proc.kill('SIGTERM'); } catch {}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (proc && !proc.killed) proc.kill('SIGKILL');
|
||||
} catch {}
|
||||
}, 3000);
|
||||
}
|
||||
proc = null;
|
||||
rejectAllPending(new Error('Codex app-server 已停止。'));
|
||||
}
|
||||
|
||||
function isRunning() {
|
||||
return !!proc && !exited;
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
request,
|
||||
notification,
|
||||
isRunning,
|
||||
pid: () => proc?.pid || null,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { createCodexAppServerClient };
|
||||
Reference in New Issue
Block a user