修复 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

@@ -1133,24 +1133,29 @@ function handleRequest(message) {
}
if (method === 'config/mcpServer/reload') {
mcpReloadCount += 1;
send({
method: 'mcpServer/startupStatus/updated',
params: {
server: 'ccweb',
state: 'starting',
message: 'ccweb MCP starting',
threadId: null,
},
});
send({
method: 'mcpServer/startupStatus/updated',
params: {
name: 'ccweb',
status: 'ready',
message: 'ccweb MCP ready CC_WEB_MCP_TOKEN=mock-secret-token',
threadId: null,
},
});
const suppressReloadStatus = process.env.MOCK_CODEX_APP_MCP_RELOAD_TIMEOUT_ON_SECOND === '1'
&& mcpReloadCount >= 2;
if (!suppressReloadStatus) {
send({
method: 'mcpServer/startupStatus/updated',
params: {
server: 'ccweb',
state: 'starting',
message: 'ccweb MCP starting',
// 模拟 app-server 的全局 reload 通知可能携带其他线程 ID。
threadId: 'mock-reload-unrelated-thread',
},
});
send({
method: 'mcpServer/startupStatus/updated',
params: {
name: 'ccweb',
status: 'ready',
message: 'ccweb MCP ready CC_WEB_MCP_TOKEN=mock-secret-token',
threadId: 'mock-reload-unrelated-thread',
},
});
}
send({ id, result: { reloaded: true, reloadCount: mcpReloadCount } });
return;
}

View File

@@ -1613,6 +1613,32 @@ function assertFrontendMcpReloadContract() {
assert(source.includes('MCP 启动失败'), 'Frontend should expose a failed startup toast');
}
function assertCcwebMcpRecoveryContract() {
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
const runtimeSource = fs.readFileSync(path.join(REPO_DIR, 'lib', 'agent-runtime.js'), 'utf8');
const mockSource = fs.readFileSync(MOCK_CODEX_APP_SERVER, 'utf8');
assert(
serverSource.includes('CC_WEB_CODEX_APP_MCP_STARTUP_TIMEOUT_SEC')
&& serverSource.includes('CODEX_APP_MCP_STARTUP_TIMEOUT_SEC,')
&& serverSource.includes('CC_WEB_CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS')
&& serverSource.includes('CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS,')
&& serverSource.includes('allowThreadMismatch: true'),
'ccweb MCP recovery should expose configurable startup/reload windows and tolerate global reload thread IDs'
);
assert(
serverSource.includes('codex_app_mcp_reload_timeout')
&& serverSource.includes("creationStatus: 'created_but_initial_message_failed'")
&& serverSource.includes('mcpStatus: buildCodexAppMcpStatusSummary'),
'ccweb MCP recovery should persist timeout failure and expose creation/MCP status'
);
assert(
runtimeSource.includes('CC_WEB_CODEX_APP_MCP_STARTUP_TIMEOUT_SEC')
&& runtimeSource.includes('mcp_servers.ccweb.startup_timeout_sec=${CCWEB_MCP_STARTUP_TIMEOUT_SEC}'),
'Legacy Codex runtime should use the same configurable ccweb MCP startup timeout'
);
assert(mockSource.includes('mock-reload-unrelated-thread'), 'MCP reload regression fixture should cover unrelated notification thread ids');
}
function assertFrontendSubagentCardMetadataContract() {
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const styleSource = fs.readFileSync(PUBLIC_STYLE_PATH, 'utf8');
@@ -6637,6 +6663,7 @@ async function main() {
assertFrontendMarkdownLinkContract();
assertMockCodexAppPromptUserNotTextTriggered();
assertFrontendMcpReloadContract();
assertCcwebMcpRecoveryContract();
assertPlanListProgressContract();
assertFrontendSubagentCardMetadataContract();
assertCodexAppRuntimeSubAgentActivityContract();
@@ -6793,6 +6820,8 @@ async function main() {
CLAUDE_PATH: MOCK_CLAUDE,
CODEX_PATH: MOCK_CODEX_APP_SERVER,
CC_WEB_CODEX_TRANSIENT_RETRY_BASE_DELAY_MS: '100',
CC_WEB_CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS: '1000',
MOCK_CODEX_APP_MCP_RELOAD_TIMEOUT_ON_SECOND: '1',
}, async () => {
await assertWsUpgradeRejected(port, '/not-ws');
@@ -8212,6 +8241,11 @@ async function main() {
Number(reloadAfterTracking.result?.reloadCount || 0) === baselineMcpReloadCount + 1,
'Task tracking changes must not trigger an implicit MCP reload; only the explicit reload request should count'
);
assert(reloadAfterTracking.reloadTimedOut === true, 'MCP reload without a final startup notification should report a timeout');
assert(reloadAfterTracking.mcpStatus?.status === 'failed', 'MCP reload timeout should converge to failed instead of pending');
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
assert(storedCodexApp.codexAppMcpStartupStatus?.servers?.ccweb?.status === 'failed', 'MCP reload timeout should persist failed status for retry');
assert(/可稍后再次重试/.test(storedCodexApp.codexAppMcpStartupStatus?.servers?.ccweb?.message || ''), 'MCP reload timeout should explain that retry is available');
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-codexapp-prompt-user-mcp', trigger: '/', query: 'prompt_user', sessionId: codexAppSession.sessionId, agent: 'codexapp' }));
const codexAppPromptUserMcpComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-codexapp-prompt-user-mcp');