修复 ccweb MCP 启动与重载并重新打包
This commit is contained in:
69
server.js
69
server.js
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user