feat: support MCP elicitation and rebuild release

This commit is contained in:
shiyue
2026-08-24 17:39:41 +08:00
parent dd233a40e8
commit bd20a79d4b
69 changed files with 8978 additions and 13 deletions

View File

@@ -2627,6 +2627,10 @@ function assertFrontendPrimaryCodexAppUiContract() {
serverSource.includes("const VALID_AGENTS = new Set(['claude', 'codex', 'codexapp']);"),
'Server explicit agent support should remain available for API/MCP paths'
);
assert(serverSource.includes("case 'mcpServer/elicitation/request':"), 'Server should route MCP elicitation requests through the interactive response path');
assert(serverSource.includes('pendingCodexAppElicitations') && serverSource.includes('10 * 60 * 1000') && serverSource.includes('resolvePendingCodexAppElicitationsForSession'), 'MCP elicitation should keep a bounded pending lifecycle with timeout and session cleanup');
assert(source.includes("case 'codex_app_elicitation_request':"), 'Frontend should render MCP elicitation requests');
assert(source.includes("type: 'codex_app_elicitation_response'"), 'Frontend should send structured MCP elicitation responses');
}
function assertSetTitleMcpContract() {
@@ -2668,6 +2672,7 @@ function assertSessionItemTooltipContract() {
function assertSessionProjectSnapshotContract() {
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const mergeSource = extractFunctionSource(frontendSource, 'mergeSessionListSnapshot');
const normalizeGiteaSourceSource = extractFunctionSource(frontendSource, 'normalizeGiteaSource');
const api = new Function(`
let sessions = [];
function normalizeAgent(agent) {
@@ -2680,6 +2685,7 @@ function assertSessionProjectSnapshotContract() {
function compareSessionUpdatedDesc(a, b) {
return new Date(b.updated || 0) - new Date(a.updated || 0);
}
${normalizeGiteaSourceSource}
${mergeSource}
return {
mergeSessionListSnapshot,
@@ -7991,6 +7997,43 @@ async function main() {
assert(/guided answer: A/.test(guidedDelta.text || ''), 'Codex App should continue after guided input response');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
ws.send(JSON.stringify({ type: 'message', text: 'codexapp elicitation prompt', sessionId: codexAppSession.sessionId, mode: 'default', agent: 'codexapp' }));
const elicitationRequest = await nextMessage(messages, ws, (msg) => msg.type === 'codex_app_elicitation_request' && msg.sessionId === codexAppSession.sessionId);
assert(elicitationRequest.mode === 'form', 'Codex App should forward MCP form elicitation requests');
assert(elicitationRequest.serverName === 'fixture-mcp', 'MCP elicitation should preserve server name');
assert(elicitationRequest.requestedSchema?.properties?.displayName, 'MCP elicitation should forward requested schema');
assert(!messages.some((msg) => msg.type === 'system_message' && /mcpServer\/elicitation\/request/.test(msg.message || '') && /保守策略拒绝/.test(msg.message || '')), 'MCP elicitation requests should not be rejected by the generic unsupported-request path');
ws.send(JSON.stringify({
type: 'codex_app_elicitation_response',
action: 'accept',
sessionId: codexAppSession.sessionId,
requestId: elicitationRequest.requestId,
content: { displayName: '回归用户', count: 3, enabled: true, tags: ['alpha'] },
}));
const elicitationSubmitted = await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.sessionId === codexAppSession.sessionId && /已提交 MCP elicitation/.test(msg.message || ''));
assert(/已提交 MCP elicitation/.test(elicitationSubmitted.message || ''), 'MCP elicitation acceptance should show confirmation hint');
const elicitationDelta = await nextMessage(messages, ws, (msg) => msg.type === 'text_delta' && msg.sessionId === codexAppSession.sessionId && /回归用户/.test(msg.text || ''));
assert(/回归用户/.test(elicitationDelta.text || ''), 'Codex App should continue after MCP elicitation response');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
ws.send(JSON.stringify({ type: 'message', text: 'codexapp elicitation openai', sessionId: codexAppSession.sessionId, mode: 'default', agent: 'codexapp' }));
const openAiElicitationRequest = await nextMessage(messages, ws, (msg) => msg.type === 'codex_app_elicitation_request' && msg.sessionId === codexAppSession.sessionId);
assert(openAiElicitationRequest.mode === 'openai/form', 'Codex App should forward openai/form elicitation requests');
ws.send(JSON.stringify({ type: 'codex_app_elicitation_response', action: 'decline', sessionId: codexAppSession.sessionId, requestId: openAiElicitationRequest.requestId }));
await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.sessionId === codexAppSession.sessionId && /已拒绝 MCP elicitation/.test(msg.message || ''));
const openAiElicitationDelta = await nextMessage(messages, ws, (msg) => msg.type === 'text_delta' && msg.sessionId === codexAppSession.sessionId && /"action":"decline"/.test(msg.text || ''));
assert(/"action":"decline"/.test(openAiElicitationDelta.text || ''), 'MCP openai/form elicitation should support decline');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
ws.send(JSON.stringify({ type: 'message', text: 'codexapp elicitation url', sessionId: codexAppSession.sessionId, mode: 'default', agent: 'codexapp' }));
const urlElicitationRequest = await nextMessage(messages, ws, (msg) => msg.type === 'codex_app_elicitation_request' && msg.sessionId === codexAppSession.sessionId);
assert(urlElicitationRequest.mode === 'url' && /example\.com/.test(urlElicitationRequest.url || ''), 'Codex App should forward URL elicitation requests');
ws.send(JSON.stringify({ type: 'codex_app_elicitation_response', action: 'cancel', sessionId: codexAppSession.sessionId, requestId: urlElicitationRequest.requestId }));
await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.sessionId === codexAppSession.sessionId && /已取消 MCP elicitation/.test(msg.message || ''));
const urlElicitationDelta = await nextMessage(messages, ws, (msg) => msg.type === 'text_delta' && msg.sessionId === codexAppSession.sessionId && /"action":"cancel"/.test(msg.text || ''));
assert(/"action":"cancel"/.test(urlElicitationDelta.text || ''), 'MCP URL elicitation should support cancel');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
ws.send(JSON.stringify({ type: 'message', text: 'codexapp approval prompt', sessionId: codexAppSession.sessionId, mode: 'default', agent: 'codexapp' }));
const approvalRequest = await nextMessage(messages, ws, (msg) => msg.type === 'codex_app_approval_request' && msg.sessionId === codexAppSession.sessionId);
assert(approvalRequest.method === 'item/commandExecution/requestApproval', 'Codex App should forward command approval requests');