修复 ccweb MCP 启动与重载并重新打包

This commit is contained in:
shiyue
2026-09-12 21:13:30 +08:00
parent 965aefe9a4
commit d9db66ca8f
10 changed files with 217 additions and 28 deletions

View File

@@ -1444,8 +1444,28 @@ const CODEX_APP_UNROUTED_NOTIFICATION_LOG_THROTTLE_MS = 30 * 1000;
const CODEX_APP_UNROUTED_NOTIFICATION_LOG_MAX = 1000;
const CODEX_APP_MCP_STARTUP_STATUS_METHOD = 'mcpServer/startupStatus/updated';
const CODEX_APP_MCP_DEFAULT_SERVER = 'ccweb';
const CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS = 1200;
const CODEX_APP_MCP_RELOAD_TRACK_MS = 15000;
// Codex App 会并发启动多个 MCP;10 秒和 1.2 秒都不足以覆盖冷启动抖动。
// 保留环境变量覆盖,便于低配机器或 CI 按需调整,而不必重新发布代码。
const CODEX_APP_MCP_STARTUP_TIMEOUT_SEC = readPositiveIntEnv(
'CC_WEB_CODEX_APP_MCP_STARTUP_TIMEOUT_SEC',
30,
{ min: 10, max: 300 },
);
const CODEX_APP_MCP_TOOL_TIMEOUT_SEC = readPositiveIntEnv(
'CC_WEB_CODEX_MCP_TOOL_TIMEOUT_SEC',
60,
{ min: 10, max: 600 },
);
const CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS = readPositiveIntEnv(
'CC_WEB_CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS',
35000,
{ min: 1000, max: 180000 },
);
const CODEX_APP_MCP_RELOAD_TRACK_MS = readPositiveIntEnv(
'CC_WEB_CODEX_APP_MCP_RELOAD_TRACK_MS',
Math.max(60000, CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS + 10000),
{ min: 5000, max: 300000 },
);
const CODEX_APP_MCP_INVENTORY_TTL_MS = 3000;
const CODEX_APP_MCP_INVENTORY_MAX_PAGES = 10;
const codexAppMcpStartupStatusByServer = new Map();
@@ -3547,8 +3567,8 @@ function buildCcwebMcpRuntimeConfig(session, options = {}) {
type: 'streamable_http',
url: `http://127.0.0.1:${PORT}/api/internal/mcp/stream?${params.toString()}`,
bearer_token_env_var: CODEX_APP_CCWEB_MCP_BEARER_TOKEN_ENV,
startup_timeout_sec: 10,
tool_timeout_sec: 60,
startup_timeout_sec: CODEX_APP_MCP_STARTUP_TIMEOUT_SEC,
tool_timeout_sec: CODEX_APP_MCP_TOOL_TIMEOUT_SEC,
},
};
}
@@ -3565,8 +3585,8 @@ function buildCcwebMcpRuntimeConfig(session, options = {}) {
command: commandSpec.command,
args: commandSpec.args,
env,
startup_timeout_sec: 10,
tool_timeout_sec: 60,
startup_timeout_sec: CODEX_APP_MCP_STARTUP_TIMEOUT_SEC,
tool_timeout_sec: CODEX_APP_MCP_TOOL_TIMEOUT_SEC,
},
};
}
@@ -4600,6 +4620,9 @@ function markCodexAppMcpReloadPending(session, sessionId) {
requestedAt,
expiresAt: Date.now() + CODEX_APP_MCP_RELOAD_TRACK_MS,
reloadRequestId,
// config/mcpServer/reload 作用于整个 app-server,不保证通知携带当前线程。
// 因此 reload 窗口内允许用同一 app-server 的其他 threadId 状态完成关联。
allowThreadMismatch: true,
});
saveSession(session);
return {
@@ -4714,7 +4737,12 @@ function codexAppMcpStatusTargetSessionIds(statusRecord, routed) {
const targetSessionIds = new Set();
if (routed?.sessionId) targetSessionIds.add(routed.sessionId);
for (const [sessionId, pending] of pendingCodexAppMcpReloads.entries()) {
if (statusRecord.threadId && pending.threadId && statusRecord.threadId !== pending.threadId) continue;
if (
statusRecord.threadId
&& pending.threadId
&& statusRecord.threadId !== pending.threadId
&& pending.allowThreadMismatch !== true
) continue;
targetSessionIds.add(sessionId);
}
return targetSessionIds;
@@ -4785,12 +4813,26 @@ async function handleReloadMcpApi(req, res, rawSessionId) {
const result = typeof client.reloadMcpServers === 'function'
? await client.reloadMcpServers()
: await client.request('config/mcpServer/reload', {}, 30000);
const mcpStatus = await waitForCodexAppMcpStatusAfterReload(sessionId, reloadRequestedAt);
let mcpStatus = await waitForCodexAppMcpStatusAfterReload(sessionId, reloadRequestedAt);
let reloadTimedOut = false;
if (!isFinalCodexAppMcpStatus(mcpStatus?.status)) {
reloadTimedOut = true;
const timeoutMessage = `重载 MCP 在 ${Math.ceil(CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS / 1000)} 秒内未收到最终启动状态,可稍后再次重试。`;
mcpStatus = markCodexAppMcpReloadFailed(sessionId, timeoutMessage)
|| mcpStatus
|| pendingMcp.summary;
plog('WARN', 'codex_app_mcp_reload_timeout', {
sessionId: sessionId.slice(0, 8),
threadId: getRuntimeSessionId(session) || null,
waitMs: CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS,
});
}
plog('INFO', 'codex_app_mcp_reload_requested', {
sessionId: sessionId.slice(0, 8),
threadId: getRuntimeSessionId(session) || null,
status: mcpStatus?.status || pendingMcp.summary?.status || 'pending',
reloadTimedOut,
});
return jsonResponse(res, 200, {
@@ -4798,6 +4840,7 @@ async function handleReloadMcpApi(req, res, rawSessionId) {
sessionId,
threadId: getRuntimeSessionId(session) || null,
result: result || {},
reloadTimedOut,
mcpStatus: mcpStatus || pendingMcp.summary || buildCodexAppMcpStatusSummary(loadSession(sessionId), { reloadRequestedAt }),
});
} catch (err) {
@@ -7061,6 +7104,7 @@ function createMcpConversation(args = {}, sourceSessionId = '', sourceHopCount =
content: initialMessage,
}, sourceId, normalizedHopCount, { expectReply: requestReply });
if (!delivery?.ok) {
const persistedSession = loadSession(session.id) || session;
return mcpToolError(delivery?.code || 'initial_message_failed', delivery?.message || '创建对话后发送首条消息失败。', {
conversationId: session.id,
title: session.title,
@@ -7068,10 +7112,16 @@ function createMcpConversation(args = {}, sourceSessionId = '', sourceHopCount =
cwd: session.cwd || '',
mode: session.permissionMode || 'yolo',
status: isSessionRunning(session.id) ? 'running' : 'idle',
creationStatus: 'created_but_initial_message_failed',
retryable: true,
...(isCodexAppSession(persistedSession)
? { mcpStatus: buildCodexAppMcpStatusSummary(persistedSession) }
: {}),
});
}
}
const persistedSession = loadSession(session.id) || session;
broadcastSessionList();
return {
ok: true,
@@ -7082,6 +7132,9 @@ function createMcpConversation(args = {}, sourceSessionId = '', sourceHopCount =
mode: session.permissionMode || 'yolo',
status: isSessionRunning(session.id) ? 'running' : 'idle',
sourceConversationId: sourceId || null,
...(isCodexAppSession(persistedSession)
? { mcpStatus: buildCodexAppMcpStatusSummary(persistedSession) }
: {}),
...(delivery ? {
messageId: delivery.messageId || null,
deliveryStatus: delivery.deliveryStatus || 'delivered',