chore: rebuild CentOS7 release package

This commit is contained in:
shiyue
2026-06-27 19:47:52 +08:00
parent 911dd84c35
commit cd37ecf10b
14 changed files with 3128 additions and 653 deletions

View File

@@ -10,6 +10,7 @@ const WebSocket = require('ws');
const REPO_DIR = path.resolve(__dirname, '..');
const SERVER_PATH = path.join(REPO_DIR, 'server.js');
const PUBLIC_APP_PATH = path.join(REPO_DIR, 'public', 'app.js');
const PUBLIC_INDEX_PATH = path.join(REPO_DIR, 'public', 'index.html');
const MOCK_CLAUDE = path.join(REPO_DIR, 'scripts', 'mock-claude.js');
const MOCK_CODEX = path.join(REPO_DIR, 'scripts', 'mock-codex.js');
const MOCK_CODEX_APP_SERVER = path.join(REPO_DIR, 'scripts', 'mock-codex-app-server.js');
@@ -503,6 +504,7 @@ function assertFrontendGenerationControlsContract() {
function assertFrontendComposerMcpContract() {
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
const requestStart = source.indexOf('function requestComposerSuggestions()');
const requestEnd = source.indexOf('\n function handleComposerSuggestions', requestStart);
assert(requestStart >= 0 && requestEnd > requestStart, 'Frontend should define requestComposerSuggestions before handleComposerSuggestions');
@@ -520,13 +522,68 @@ function assertFrontendComposerMcpContract() {
assert(menuStart >= 0 && menuEnd > menuStart, 'Frontend should define showCmdMenu before requestComposerSuggestions');
const menuBlock = source.slice(menuStart, menuEnd);
assert(/item\.kind\s*===\s*'mcp'/.test(menuBlock) && menuBlock.includes("'MCP'"), 'Composer menu should render MCP item labels');
const selectStart = source.indexOf('function selectComposerItemByIndex(index)');
const selectEnd = source.indexOf('\n function selectCmdMenuItem()', selectStart);
assert(selectStart >= 0 && selectEnd > selectStart, 'Frontend should define selectComposerItemByIndex before selectCmdMenuItem');
const selectBlock = source.slice(selectStart, selectEnd);
assert(selectBlock.includes('const insertion = String(item.insertion || item.label || item.name || \'\');'), 'Composer should insert selected item text through the generic insertion path');
assert(selectBlock.includes('const appendSpace = item.appendSpace !== false;'), 'Composer should honor appendSpace for generic MCP insertion');
assert(!source.includes('function showCcwebPromptUserComposerModal'), 'Composer should not open a parameter builder for ccweb_prompt_user');
assert(!source.includes('composer_mcp_tool_submit'), 'Frontend should not submit ccweb_prompt_user from slash composer as structured MCP args');
assert(!serverSource.includes('composer_mcp_tool_submit'), 'Server should not accept slash-composer structured MCP tool submissions');
assert(!source.includes('data-composer-mcp-questions'), 'Frontend should not render a slash-composer MCP argument builder');
assert(!source.includes('data-option-field="recommended"'), 'Frontend should not render slash-composer MCP option editors');
assert(source.includes('function renderComposerMentionsStrip(meta)'), 'Frontend should define composer mention strip renderer');
assert(source.includes("className = 'msg-mentions'"), 'Frontend should render a dedicated mention strip container');
}
function assertMockCodexAppPromptUserNotTextTriggered() {
const source = fs.readFileSync(MOCK_CODEX_APP_SERVER, 'utf8');
assert(!source.includes('codexapp runtime prompt mcp'), 'Mock Codex App should not expose a text-triggered ccweb_prompt_user path');
assert(!source.includes('mcp-ccweb-prompt-user'), 'Regression should not depend on a mock ccweb_prompt_user tool call id');
}
function assertFrontendCcwebPromptContract() {
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const indexSource = fs.readFileSync(PUBLIC_INDEX_PATH, 'utf8');
assert(source.includes('function createCcwebPromptElement(prompt, meta = {})'), 'Frontend should define ccweb prompt renderer');
assert(source.includes("type: 'ccweb_prompt_user_response'"), 'Frontend should send ccweb prompt answers over WebSocket');
assert(source.includes("type: 'ccweb_prompt_user_dismiss'"), 'Frontend should send ccweb prompt dismiss requests over WebSocket');
assert(source.includes("case 'ccweb_prompt_user_update':"), 'Frontend should handle ccweb prompt status updates');
assert(source.includes("case 'ccweb_prompt_user_remove':"), 'Frontend should remove submitted ccweb prompt bubbles');
assert(source.includes('applyCcwebPromptUserUpdate(msg);'), 'Frontend should apply ccweb prompt updates to cached messages and DOM');
assert(source.includes('removeCcwebPromptMessageFromSnapshot'), 'Frontend should remove submitted prompt messages from cached snapshots');
assert(source.includes('renderPendingCcwebPrompts'), 'Frontend should render pending ccweb prompt reminders');
assert(indexSource.includes('id="ccweb-prompt-outline-btn"') && indexSource.includes('class="ccweb-prompt-outline-anchor" hidden'), 'Frontend should expose a hidden ccweb prompt outline button');
assert(source.includes('toggleCcwebPromptOutlinePanel') && source.includes('ccwebPromptOutlineBtn.dataset.count'), 'Frontend should render pending forms behind a compact outline button');
assert(source.includes('dismissCcwebPrompt'), 'Frontend should allow users to ignore pending ccweb prompts');
assert(source.includes('CCWEB_PROMPT_VIEW_MODE_STORAGE_KEY'), 'Frontend should persist ccweb prompt view mode');
assert(source.includes("className = 'ccweb-prompt-tabs'"), 'Frontend should render ccweb prompt tabs for multi-question forms');
assert(source.includes("className = 'pending-ccweb-prompt-dismiss'"), 'Frontend should render a compact dismiss action for pending prompts');
assert(source.includes('card.dataset.viewMode = normalized'), 'Frontend should switch ccweb prompt card view mode');
assert(source.includes('m.ccwebPrompt'), 'Message rebuild should render persisted ccweb prompt messages');
assert(source.includes("className = 'ccweb-prompt-answer'"), 'Each ccweb prompt question should expose an editable answer textarea');
}
function assertFrontendMcpReloadContract() {
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
assert(source.includes('function mcpStartupStatusToastText(status)'), 'Frontend should format MCP startup status toast text');
assert(source.includes("payload.status && typeof payload.status === 'object'"), 'Frontend should preserve plain MCP status summary objects');
assert(source.includes('data.mcpStatus'), 'Frontend reload button should consume reload-mcp mcpStatus payload');
assert(source.includes("case 'mcp_startup_status':"), 'Frontend should handle pushed MCP startup status updates');
assert(source.includes('showMcpStartupStatusToast'), 'Frontend should show explicit MCP startup status toasts');
assert(source.includes('notifyReady: true'), 'Frontend should only show ready MCP startup toasts for explicit reload actions');
assert(source.includes("state === 'ready' && !options.notifyReady"), 'Frontend should suppress background ready MCP startup toasts');
assert(source.includes('MCP 已启动'), 'Frontend should expose a ready toast for ccweb MCP');
assert(source.includes('MCP 启动失败'), 'Frontend should expose a failed startup toast');
}
async function main() {
assertFrontendGenerationControlsContract();
assertFrontendComposerMcpContract();
assertFrontendCcwebPromptContract();
assertMockCodexAppPromptUserNotTextTriggered();
assertFrontendMcpReloadContract();
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-web-regression-'));
const configDir = path.join(tempRoot, 'config');
@@ -1125,6 +1182,7 @@ async function main() {
.split('\n')
.find((line) => line.includes(`"event":"process_spawn"`) && line.includes(crossTargetSession.sessionId.slice(0, 8)));
assert(mcpSpawnLine && mcpSpawnLine.includes('mcp_servers.ccweb.command') && mcpSpawnLine.includes('mcp_servers.ccweb.env_vars'), 'Codex spawn should inject ccweb MCP config');
assert(mcpSpawnLine.includes('server.js') && mcpSpawnLine.includes('--ccweb-mcp-server'), 'Codex spawn should launch ccweb MCP through server.js in Node mode');
assert(!mcpSpawnLine.includes(internalMcpToken), 'Codex spawn log should not expose internal MCP token');
const projectMcpSpawnLine = processLogAfterMcp
.trim()
@@ -1442,6 +1500,15 @@ async function main() {
const reloadMcpResult = await postAuthedJson(port, token, `/api/sessions/${codexAppSession.sessionId}/reload-mcp`);
assert(reloadMcpResult.sessionId === codexAppSession.sessionId, 'Codex App MCP reload should return the target session id');
assert(reloadMcpResult.result?.reloaded === true, 'Codex App MCP reload should call app-server config/mcpServer/reload');
assert(reloadMcpResult.mcpStatus?.server === 'ccweb', 'Codex App MCP reload should return ccweb server startup status');
assert(reloadMcpResult.mcpStatus?.status === 'ready', 'Codex App MCP reload should surface ready startup status from app-server notification');
assert(reloadMcpResult.mcpStatus?.hasStartupStatus === true, 'Codex App MCP reload should distinguish real startupStatus from pending fallback');
const reloadMcpStatusText = JSON.stringify(reloadMcpResult.mcpStatus);
assert(/CC_WEB_MCP_TOKEN=\[redacted\]/.test(reloadMcpStatusText), 'Codex App MCP reload status should redact token-looking values');
assert(!reloadMcpStatusText.includes('mock-secret-token'), 'Codex App MCP reload status should not leak raw token-looking values');
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
assert(storedCodexApp.codexAppMcpStartupStatus?.servers?.ccweb?.status === 'ready', 'Codex App MCP startup status should be persisted on the session');
assert(!JSON.stringify(storedCodexApp.codexAppMcpStartupStatus).includes('mock-secret-token'), 'Persisted MCP startup status should not leak raw token-looking values');
ws.send(JSON.stringify({ type: 'message', text: 'codexapp dynamic prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
const codexAppDynamicTool = await nextMessage(messages, ws, (msg) => msg.type === 'tool_end' && msg.sessionId === codexAppSession.sessionId && msg.toolUseId === 'mcp-ccweb-list');
@@ -1449,8 +1516,149 @@ async function main() {
assert(/currentConversationId/.test(codexAppDynamicTool.result || ''), 'Codex App MCP tool should return ccweb conversation data');
assert(/"hasCcwebMcpConfig": true/.test(codexAppDynamicTool.result || ''), 'Codex App thread/start should pass ccweb MCP config');
assert(/"hasProjectMcpConfig": true/.test(codexAppDynamicTool.result || ''), 'Codex App thread/start should pass project MCP config from session cwd');
assert(/server\.js/.test(codexAppDynamicTool.result || '') && /--ccweb-mcp-server/.test(codexAppDynamicTool.result || ''), 'Codex App ccweb MCP config should launch through server.js in Node mode');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-codexapp-empty-slash-prompt-user-mcp', trigger: '/', query: '', sessionId: codexAppSession.sessionId, agent: 'codexapp' }));
const codexAppEmptySlashMcpComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-codexapp-empty-slash-prompt-user-mcp');
const emptySlashPromptUserIndex = codexAppEmptySlashMcpComposer.items.findIndex((item) => item.kind === 'mcp' && item.server === 'ccweb' && item.name === 'ccweb_prompt_user');
const firstOtherCcwebMcpIndex = codexAppEmptySlashMcpComposer.items.findIndex((item) => item.kind === 'mcp' && item.server === 'ccweb' && item.name !== 'ccweb_prompt_user');
assert(emptySlashPromptUserIndex >= 0, 'Codex App empty slash composer should include ccweb_prompt_user');
assert(firstOtherCcwebMcpIndex < 0 || emptySlashPromptUserIndex < firstOtherCcwebMcpIndex, 'ccweb_prompt_user should be pinned before other ccweb MCP tools');
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');
const promptUserComposerItem = codexAppPromptUserMcpComposer.items.find((item) => item.kind === 'mcp' && item.server === 'ccweb' && item.name === 'ccweb_prompt_user');
assert(promptUserComposerItem, 'Codex App composer should show ccweb_prompt_user when ccweb MCP is runtime-configured');
assert(promptUserComposerItem.itemType === 'tool', 'ccweb_prompt_user composer item should be a normal MCP tool suggestion');
assert(!promptUserComposerItem.action, 'ccweb_prompt_user composer item should not declare a form action');
assert(promptUserComposerItem.insertion === 'mcp:ccweb/ccweb_prompt_user', 'ccweb_prompt_user composer item should insert the MCP mention text');
assert(promptUserComposerItem.appendSpace === true, 'ccweb_prompt_user composer item should append a space like other MCP suggestions');
const promptUserResult = await callInternalMcp(port, internalMcpToken, {
tool: 'ccweb_prompt_user',
sourceSessionId: codexAppSession.sessionId,
sourceHopCount: 0,
args: {
title: '确认实现方案',
description: '回归测试多问题表单',
questions: [
{
id: 'ui_choice',
title: '交互方式',
question: '用哪种交互方式?',
required: true,
options: [
{ id: 'fineui', label: 'FineUI 弹窗', recommended: true, answerText: '使用 FineUI 弹窗。' },
{ id: 'prompt', label: '浏览器 prompt', answerText: '使用浏览器 prompt。' },
],
answerPlaceholder: '填写方案',
},
{
id: 'button_id',
title: '按钮 ID',
question: '确认按钮 ID 是什么?',
required: true,
options: [
{ id: 'confirm', label: 'btnShortageReleaseConfirm', recommended: true, answerText: '按钮 ID 固定为 btnShortageReleaseConfirm。' },
],
},
],
},
});
assert(promptUserResult.status === 200 && promptUserResult.body?.ok, `MCP prompt user should render: ${JSON.stringify(promptUserResult.body)}`);
assert(promptUserResult.body.status === 'rendered', 'MCP prompt user should return rendered status without waiting for user input');
assert(promptUserResult.body.questionCount === 2, 'MCP prompt user should preserve multiple questions');
const promptRendered = await nextMessage(messages, ws, (msg) => (
msg.type === 'session_message' &&
msg.sessionId === codexAppSession.sessionId &&
msg.message?.ccwebPrompt?.id === promptUserResult.body.promptId
));
assert(promptRendered.message.ccwebPrompt.status === 'pending', 'Prompt message should start pending');
assert(promptRendered.message.ccwebPrompt.questions?.length === 2, 'Prompt message should carry all questions to the UI');
assert(promptRendered.message.ccwebPrompt.questions[0]?.options?.some((option) => option.id === 'fineui' && option.recommended === true), 'Prompt message should preserve recommended options');
const promptDismissResult = await callInternalMcp(port, internalMcpToken, {
tool: 'ccweb_prompt_user',
sourceSessionId: codexAppSession.sessionId,
sourceHopCount: 0,
args: {
title: '可忽略表单',
questions: [
{
id: 'dismiss_choice',
title: '是否忽略',
question: '这个表单会被忽略删除。',
required: false,
options: [
{ id: 'skip', label: '忽略', answerText: '忽略这个表单。' },
],
},
],
},
});
assert(promptDismissResult.status === 200 && promptDismissResult.body?.ok, 'Dismissable MCP prompt should render');
await nextMessage(messages, ws, (msg) => (
msg.type === 'session_message' &&
msg.sessionId === codexAppSession.sessionId &&
msg.message?.ccwebPrompt?.id === promptDismissResult.body.promptId
));
ws.send(JSON.stringify({
type: 'ccweb_prompt_user_dismiss',
sessionId: codexAppSession.sessionId,
promptId: promptDismissResult.body.promptId,
}));
const promptDismissed = await nextMessage(messages, ws, (msg) => (
msg.type === 'ccweb_prompt_user_remove' &&
msg.sessionId === codexAppSession.sessionId &&
msg.promptId === promptDismissResult.body.promptId
));
assert(promptDismissed.reason === 'dismissed', 'Dismissed prompt remove event should carry dismissed reason');
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
assert(!storedCodexApp.messages.some((message) => message.ccwebPrompt?.id === promptDismissResult.body.promptId), 'Dismissed prompt message should be removed from session history');
ws.send(JSON.stringify({
type: 'ccweb_prompt_user_response',
sessionId: codexAppSession.sessionId,
promptId: promptUserResult.body.promptId,
answers: {
ui_choice: {
selectedOptionIds: ['fineui'],
answerText: '使用 FineUI 弹窗,方便固定按钮 ID。',
},
button_id: {
selectedOptionIds: ['confirm'],
answerText: '按钮 ID 固定为 btnShortageReleaseConfirm。',
},
},
}));
const promptSubmitted = await nextMessage(messages, ws, (msg) => (
msg.type === 'ccweb_prompt_user_remove' &&
msg.sessionId === codexAppSession.sessionId &&
msg.promptId === promptUserResult.body.promptId
));
assert(promptSubmitted.prompt?.status === 'submitted', 'Prompt remove event should carry submitted status');
assert(promptSubmitted.prompt?.answers?.ui_choice?.selectedOptionLabels?.[0] === 'FineUI 弹窗', 'Prompt remove event should include selected option labels');
const promptAnswerMessage = await nextMessage(messages, ws, (msg) => (
msg.type === 'session_message' &&
msg.sessionId === codexAppSession.sessionId &&
msg.message?.role === 'user' &&
/我已回答 ccweb 提示的问题/.test(msg.message.content || '') &&
/btnShortageReleaseConfirm/.test(msg.message.content || '')
));
assert(/使用 FineUI 弹窗,方便固定按钮 ID。/.test(promptAnswerMessage.message.content || ''), 'Prompt submission should become a normal user message with the free-form answer');
const promptAnswerDelta = await nextMessage(messages, ws, (msg) => (
msg.type === 'text_delta' &&
msg.sessionId === codexAppSession.sessionId &&
/btnShortageReleaseConfirm/.test(msg.text || '')
));
assert(/我已回答 ccweb 提示的问题/.test(promptAnswerDelta.text || ''), 'Prompt submission should trigger a Codex App turn with the answer text');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
const storedPromptMessage = storedCodexApp.messages.find((message) => message.ccwebPrompt?.id === promptUserResult.body.promptId);
assert(!storedPromptMessage, 'Submitted prompt message should be removed from session history');
assert(storedCodexApp.messages.some((message) => message.role === 'user' && /btnShortageReleaseConfirm/.test(String(message.content || ''))), 'Prompt response user message should persist in session history');
ws.send(JSON.stringify({ type: 'message', text: 'codexapp subagent prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
const ccwebMcpChildRunning = await nextMessage(messages, ws, (msg) =>
msg.type === 'ccweb_mcp_child_agent_update' &&
@@ -1542,7 +1750,7 @@ async function main() {
ws.send(JSON.stringify({ type: 'message', text: 'slow codexapp prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
await nextMessage(messages, ws, (msg) => msg.type === 'session_list' && msg.sessions.some((s) => s.id === codexAppSession.sessionId && s.isRunning));
await sleep(150);
await sleep(500);
ws.send(JSON.stringify({
type: 'message',
text: 'runtime steer insert',