修复消息渲染并更新发布包

This commit is contained in:
shiyue
2026-09-28 08:13:09 +08:00
parent 6fb46385e2
commit d70bbf08d9
15 changed files with 739 additions and 20 deletions

View File

@@ -2,6 +2,39 @@
const readline = require('readline');
const { spawn } = require('child_process');
const {
acquireCodexHomeLock,
} = require('./codex-home-lock');
const DEFAULT_START_RETRY_ATTEMPTS = 4;
const DEFAULT_START_RETRY_DELAY_MS = 500;
const DEFAULT_START_RETRY_MAX_DELAY_MS = 5000;
function positiveInt(value, fallback, { min = 1, max = Number.MAX_SAFE_INTEGER } = {}) {
const parsed = Number.parseInt(String(value ?? ''), 10);
if (!Number.isFinite(parsed) || parsed < min) return fallback;
return Math.min(parsed, max);
}
function isCodexHomeBusyError(value) {
const parts = [];
const visit = (item, depth = 0) => {
if (!item || depth > 3) return;
if (typeof item === 'string') parts.push(item);
else if (typeof item === 'object') {
for (const key of ['message', 'stderr', 'cause', 'code', 'details']) visit(item[key], depth + 1);
}
};
visit(value);
const text = parts.join(' ');
return /database\s+(?:is\s+)?locked|database\s+table\s+is\s+locked|SQLITE_BUSY(?:_TIMEOUT)?|failed\s+to\s+(?:open|initialize)\s+(?:state|log|database)|failed\s+to\s+initialize\s+state\s+runtime/i.test(text);
}
function retryDelayMs(attempt, base, max) {
const exponential = Math.min(max, base * (2 ** Math.max(0, attempt - 1)));
const jitter = Math.floor(Math.random() * Math.max(1, Math.min(100, exponential * 0.2)));
return Math.min(max, exponential + jitter);
}
function createCodexAppServerClient(options = {}) {
const command = options.command || 'codex';
@@ -26,6 +59,11 @@ function createCodexAppServerClient(options = {}) {
let nextId = 1;
let initPromise = null;
let exited = false;
let processExitPromise = null;
let resolveProcessExit = null;
let lockHandle = null;
let lastStartError = null;
let stopRequested = false;
const pending = new Map();
function rejectAllPending(err) {
@@ -137,8 +175,14 @@ function createCodexAppServerClient(options = {}) {
return request('config/mcpServer/reload', {}, 30000);
}
function start() {
if (initPromise) return initPromise;
function releaseLock() {
const current = lockHandle;
lockHandle = null;
if (current) Promise.resolve(current.release()).catch(() => {});
}
function spawnAndInitialize(lock) {
lockHandle = lock;
exited = false;
proc = spawn(command, args, {
env,
@@ -147,6 +191,8 @@ function createCodexAppServerClient(options = {}) {
windowsHide: true,
});
processExitPromise = new Promise((resolve) => { resolveProcessExit = resolve; });
let stderr = '';
proc.stderr.on('data', (chunk) => {
stderr += chunk.toString();
@@ -164,15 +210,21 @@ function createCodexAppServerClient(options = {}) {
err.signal = signal;
err.stderr = stderr;
rejectAllPending(err);
releaseLock();
if (resolveProcessExit) resolveProcessExit({ code, signal, stderr });
resolveProcessExit = null;
onExit({ code, signal, stderr });
});
proc.on('error', (err) => {
rejectAllPending(err);
releaseLock();
if (resolveProcessExit) resolveProcessExit({ code: null, signal: null, stderr: err.message });
resolveProcessExit = null;
onExit({ code: null, signal: null, stderr: err.message });
});
initPromise = request('initialize', {
return request('initialize', {
clientInfo,
capabilities: { experimentalApi: true },
}, 30000)
@@ -182,28 +234,108 @@ function createCodexAppServerClient(options = {}) {
return result;
})
.catch((err) => {
stop();
throw err;
});
}
async function terminateProcessAndWait() {
const child = proc;
if (!child || exited) {
releaseLock();
return;
}
try { child.kill('SIGTERM'); } catch {}
const timer = setTimeout(() => {
try { if (!child.killed) child.kill('SIGKILL'); } catch {}
}, 3000);
try {
await processExitPromise;
} finally {
clearTimeout(timer);
}
}
async function startWithRetry() {
const env = options.env || process.env;
const attempts = positiveInt(
options.startRetryAttempts ?? env.CC_WEB_CODEX_START_RETRY_ATTEMPTS,
DEFAULT_START_RETRY_ATTEMPTS,
{ min: 1, max: 8 },
);
const baseDelay = positiveInt(
options.startRetryDelayMs ?? env.CC_WEB_CODEX_START_RETRY_DELAY_MS,
DEFAULT_START_RETRY_DELAY_MS,
{ min: 0, max: 30_000 },
);
const maxDelay = positiveInt(
options.startRetryMaxDelayMs ?? env.CC_WEB_CODEX_START_RETRY_MAX_DELAY_MS,
DEFAULT_START_RETRY_MAX_DELAY_MS,
{ min: baseDelay || 1, max: 60_000 },
);
for (let attempt = 1; attempt <= attempts; attempt += 1) {
if (stopRequested) {
const aborted = new Error('Codex app-server 启动已取消。');
aborted.code = 'CODEX_APP_START_CANCELLED';
throw aborted;
}
const lock = await acquireCodexHomeLock({
env,
waitMs: options.lockWaitMs,
lockPath: options.lockPath,
});
if (stopRequested) {
await lock.release();
const aborted = new Error('Codex app-server 启动已取消。');
aborted.code = 'CODEX_APP_START_CANCELLED';
throw aborted;
}
try {
const result = await spawnAndInitialize(lock);
lastStartError = null;
return result;
} catch (error) {
lastStartError = error;
await terminateProcessAndWait();
if (!isCodexHomeBusyError(error) || attempt >= attempts) {
const detail = error?.stderr || error?.message || String(error || '');
const finalError = new Error(`Codex app-server 启动失败(尝试 ${attempt}/${attempts}):${detail}`);
finalError.code = error?.code || 'CODEX_APP_START_FAILED';
finalError.cause = error;
finalError.stderr = error?.stderr || '';
finalError.lockPath = lock.path;
throw finalError;
}
const delay = retryDelayMs(attempt, baseDelay, maxDelay);
onLog('WARN', 'codex_app_server_start_retry', {
attempt,
nextAttempt: attempt + 1,
delayMs: delay,
reason: 'sqlite_busy',
lockPath: lock.path,
});
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastStartError || new Error('Codex app-server 启动失败。');
}
function start() {
if (initPromise) return initPromise;
stopRequested = false;
initPromise = startWithRetry();
return initPromise;
}
function stop() {
stopRequested = true;
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;
terminateProcessAndWait().catch(() => {});
rejectAllPending(new Error('Codex app-server 已停止。'));
}