chore: rebuild CentOS7 release package

This commit is contained in:
shiyue
2026-06-29 14:05:55 +08:00
parent ac03e9a6e4
commit ff313807e6
8 changed files with 297 additions and 8 deletions

View File

@@ -82,7 +82,7 @@ function createAgentRuntime(deps) {
const config = mcpServer?.config && typeof mcpServer.config === 'object' ? mcpServer.config : null;
if (!server || !config) return;
const prefix = `mcp_servers.${tomlKeySegment(server)}`;
for (const key of ['type', 'command', 'args', 'env', 'env_vars', 'url', 'startup_timeout_sec', 'tool_timeout_sec']) {
for (const key of ['type', 'command', 'args', 'env', 'env_vars', 'url', 'bearer_token_env_var', 'startup_timeout_sec', 'tool_timeout_sec']) {
if (!Object.prototype.hasOwnProperty.call(config, key)) continue;
args.push('-c', `${prefix}.${key}=${tomlValue(config[key])}`);
}

View File

@@ -2,7 +2,7 @@
(function () {
'use strict';
const ASSET_VERSION = '20260626-ccweb-prompt-compact-ui';
const ASSET_VERSION = '20260629-ccweb-prompt-dark-theme';
const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
const RENDER_DEBOUNCE = 100;
const COMPOSER_SUGGESTION_DEBOUNCE = 120;

View File

@@ -20,7 +20,7 @@
document.documentElement.dataset.dividerTime = dividerTime;
})();
</script>
<link rel="stylesheet" href="style.css?v=20260625-branch-bubble">
<link rel="stylesheet" href="style.css?v=20260629-ccweb-prompt-dark-theme">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
</head>
<body>
@@ -173,6 +173,6 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.1/mermaid.min.js"></script>
<script src="app.js?v=20260625-branch-bubble"></script>
<script src="app.js?v=20260629-ccweb-prompt-dark-theme"></script>
</body>
</html>

View File

@@ -6254,3 +6254,128 @@ html[data-theme='coolvibe'] .settings-back:hover {
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .theme-card-swatch {
border-color: rgba(255, 255, 255, 0.16);
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-card {
background: linear-gradient(180deg, var(--dark-panel-bg), var(--bg-bubble-assistant));
border-color: var(--border-color);
border-left-color: var(--accent);
color: var(--text-primary);
box-shadow: var(--shadow-strong);
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-card.ccweb-prompt-focus {
animation: ccwebPromptFocusDark 1.4s ease;
}
@keyframes ccwebPromptFocusDark {
0% {
box-shadow: 0 0 0 0 var(--accent-light), var(--shadow-strong);
}
45% {
box-shadow: 0 0 0 5px var(--accent-light), var(--shadow-strong);
}
100% {
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0), var(--shadow-strong);
}
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-header,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-footer {
background: rgba(0, 0, 0, 0.12);
border-color: var(--border-color);
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-status,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-required,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-option-badge {
background: var(--accent-light);
border-color: var(--theme-card-hover-border);
color: var(--accent);
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-card[data-status='submitted'] .ccweb-prompt-status {
background: rgba(103, 201, 135, 0.14);
border-color: rgba(103, 201, 135, 0.32);
color: var(--success);
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-view-switcher,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-tab,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-tab-nav-btn,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-question,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-option,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-answer,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-selected-readonly,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-answer-readonly,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-secondary,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .pending-ccweb-prompt-action {
background: var(--dark-panel-soft);
border-color: var(--border-color);
color: var(--text-primary);
box-shadow: none;
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-question {
background: rgba(255, 255, 255, 0.035);
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-option::before {
background: rgba(0, 0, 0, 0.18);
border-color: var(--text-muted);
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-option:hover,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-tab-nav-btn:hover,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-secondary:hover,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .pending-ccweb-prompt-action:hover {
background: var(--accent-light);
border-color: var(--accent);
color: var(--text-primary);
box-shadow: none;
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-tab.is-active,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-option.is-selected {
background: var(--accent-light);
border-color: var(--accent);
color: var(--text-primary);
box-shadow: inset 0 0 0 1px var(--theme-card-hover-border);
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-option.is-selected::before {
background: var(--accent);
border-color: var(--accent);
color: var(--accent-ink);
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-answer {
background: rgba(0, 0, 0, 0.18);
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-answer::placeholder {
color: var(--text-muted);
opacity: 0.86;
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-answer:focus {
background: var(--bg-bubble-assistant);
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-light);
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-submit:hover:not(:disabled) {
box-shadow: 0 8px 18px var(--accent-light);
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-submit:disabled,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .ccweb-prompt-secondary:disabled {
opacity: 0.56;
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .pending-ccweb-prompt-badge {
box-shadow: 0 0 0 3px var(--accent-light);
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .pending-ccweb-prompt-dismiss:hover {
background: var(--accent-light);
}

View File

@@ -425,12 +425,24 @@ function completeMcpToolTurn(thread, turnId) {
const ccwebConfig = thread.config?.['mcp_servers.ccweb'] || null;
const projectConfig = thread.config?.['mcp_servers.reg-app-project'] || null;
const env = ccwebConfig?.env || {};
let urlSourceSessionId = null;
let urlSourceHopCount = null;
try {
if (ccwebConfig?.url) {
const parsedUrl = new URL(ccwebConfig.url);
urlSourceSessionId = parsedUrl.searchParams.get('sourceSessionId') || null;
urlSourceHopCount = parsedUrl.searchParams.get('sourceHopCount') || null;
}
} catch {}
const payload = {
ok: true,
currentConversationId: env.CC_WEB_SOURCE_SESSION_ID || null,
sourceHopCount: env.CC_WEB_CROSS_HOP_COUNT || null,
currentConversationId: env.CC_WEB_SOURCE_SESSION_ID || urlSourceSessionId,
sourceHopCount: env.CC_WEB_CROSS_HOP_COUNT || urlSourceHopCount,
hasCcwebMcpConfig: Boolean(ccwebConfig),
hasProjectMcpConfig: Boolean(projectConfig),
ccwebType: ccwebConfig?.type || (ccwebConfig?.url ? 'streamable_http' : (ccwebConfig?.command ? 'stdio' : null)),
ccwebUrl: ccwebConfig?.url || null,
ccwebBearerTokenEnvVar: ccwebConfig?.bearer_token_env_var || null,
ccwebCommand: ccwebConfig?.command || null,
ccwebArgs: ccwebConfig?.args || null,
};

View File

@@ -1516,7 +1516,10 @@ 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');
assert(/"ccwebType": "streamable_http"/.test(codexAppDynamicTool.result || ''), 'Codex App ccweb MCP should default to shared streamable HTTP');
assert(/"ccwebUrl": "http:\/\/127\.0\.0\.1:\d+\/api\/internal\/mcp\/stream\?/.test(codexAppDynamicTool.result || ''), 'Codex App ccweb MCP should point to the shared cc-web HTTP endpoint');
assert(/"ccwebBearerTokenEnvVar": "CC_WEB_CODEX_APP_MCP_TOKEN"/.test(codexAppDynamicTool.result || ''), 'Codex App ccweb MCP should use bearer_token_env_var for the shared endpoint');
assert(!/--ccweb-mcp-server/.test(codexAppDynamicTool.result || ''), 'Codex App ccweb MCP should not launch a per-thread stdio bridge by default');
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' }));

151
server.js
View File

@@ -11,6 +11,7 @@ const { createCodexAppWorkerClient } = require('./lib/codex-app-worker-client');
const { createCodexAppRuntime } = require('./lib/codex-app-runtime');
const { createCodexRolloutStore } = require('./lib/codex-rollouts');
const { TOOLS: CCWEB_MCP_TOOLS } = require('./lib/ccweb-mcp-server');
const CCWEB_MCP_SERVER_INFO = { name: 'ccweb', version: '1.0.0' };
if (process.argv.includes('--ccweb-mcp-server')) {
require('./lib/ccweb-mcp-server').runStdioServer();
@@ -68,6 +69,12 @@ function readPositiveIntEnv(name, fallback, options = {}) {
return Math.max(min, Math.min(max, raw));
}
function normalizeCodexAppCcwebMcpTransport(value) {
const raw = String(value || '').trim().toLowerCase();
if (raw === 'stdio') return 'stdio';
return 'streamable_http';
}
const PORT = parseInt(process.env.PORT) || 8002;
const CLAUDE_PATH = process.env.CLAUDE_PATH || 'claude';
const CODEX_PATH = process.env.CODEX_PATH || 'codex';
@@ -112,11 +119,14 @@ const MCP_CREATE_CONVERSATION_MAX_HOP_COUNT = readPositiveIntEnv('CC_WEB_MCP_CRE
const CODEX_TRANSIENT_RETRY_MAX_ATTEMPTS = readPositiveIntEnv('CC_WEB_CODEX_TRANSIENT_RETRY_MAX_ATTEMPTS', 3, { min: 1, max: 10 });
const CODEX_TRANSIENT_RETRY_BASE_DELAY_MS = readPositiveIntEnv('CC_WEB_CODEX_TRANSIENT_RETRY_BASE_DELAY_MS', 2000, { min: 100, max: 60000 });
const MAX_CODEX_GOAL_OBJECTIVE_CHARS = 4000;
const CODEX_APP_CCWEB_MCP_BEARER_TOKEN_ENV = 'CC_WEB_CODEX_APP_MCP_TOKEN';
const CODEX_APP_CCWEB_MCP_TRANSPORT = normalizeCodexAppCcwebMcpTransport(process.env.CC_WEB_CODEX_APP_CCWEB_MCP_TRANSPORT);
const CODEX_APP_WORKER_DISABLED = /^(0|false|no|off)$/i.test(String(process.env.CC_WEB_CODEX_APP_WORKER || ''));
const CODEX_APP_WORKER_ENABLED = !CODEX_APP_WORKER_DISABLED;
const CODEX_APP_PROCESS_ENV_STRIP_KEYS = [
'CC_WEB_MCP_URL',
'CC_WEB_MCP_TOKEN',
CODEX_APP_CCWEB_MCP_BEARER_TOKEN_ENV,
'CC_WEB_SOURCE_SESSION_ID',
'CC_WEB_CROSS_HOP_COUNT',
'CODEX_THREAD_ID',
@@ -974,6 +984,8 @@ function normalizeCodexMcpServerConfig(name, rawConfig, source) {
const url = normalizeSkillMcpUrl(rawConfig.url || rawConfig.server_url || '');
if (!url) return null;
config.url = url;
const bearerTokenEnvVar = String(rawConfig.bearer_token_env_var || rawConfig.bearerTokenEnvVar || '').trim();
if (bearerTokenEnvVar) config.bearer_token_env_var = bearerTokenEnvVar;
}
for (const key of ['startup_timeout_sec', 'tool_timeout_sec']) {
if (Number.isFinite(rawConfig[key])) config[key] = rawConfig[key];
@@ -2297,7 +2309,31 @@ function summarizeSkillForMention(skill, configuredMcpNames = new Set()) {
}
function buildCcwebMcpRuntimeConfig(session, options = {}) {
const env = codexAppCcwebMcpEnv(session, options);
if (!session?.id || !INTERNAL_MCP_TOKEN) return null;
const rawHopCount = Number.parseInt(String(options.mcpContext?.hopCount || 0), 10);
const hopCount = Number.isFinite(rawHopCount) ? Math.max(0, rawHopCount) : 0;
if (normalizeAgent(options.agent || session?.agent || 'codex') === 'codexapp'
&& CODEX_APP_CCWEB_MCP_TRANSPORT !== 'stdio') {
const params = new URLSearchParams({
sourceSessionId: session.id,
sourceHopCount: String(hopCount),
});
return {
server: 'ccweb',
name: 'ccweb',
source: 'builtin',
type: 'streamable_http',
description: 'ccweb 内置共享 MCP server可用于跨会话协作。',
config: {
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,
},
};
}
const env = codexAppCcwebMcpEnv(session, { ...options, mcpContext: { hopCount } });
if (!env) return null;
const commandSpec = ccwebMcpServerCommandSpec();
return {
@@ -5076,6 +5112,110 @@ function callInternalMcpTool(tool, args, sourceSessionId, sourceHopCount) {
}
}
function mcpJsonRpcResult(id, result) {
return { jsonrpc: '2.0', id, result };
}
function mcpJsonRpcError(id, code, message, data) {
const error = { code, message };
if (data !== undefined) error.data = data;
return { jsonrpc: '2.0', id, error };
}
function mcpToolResponse(payload) {
const text = JSON.stringify(payload, null, 2);
return {
content: [{ type: 'text', text }],
structuredContent: payload,
isError: !payload?.ok,
};
}
function handleMcpJsonRpcMessage(message, context = {}) {
const hasId = Object.prototype.hasOwnProperty.call(message || {}, 'id');
if (!hasId) return null;
const id = message.id;
const method = String(message?.method || '');
try {
switch (method) {
case 'initialize':
return mcpJsonRpcResult(id, {
protocolVersion: message.params?.protocolVersion || '2024-11-05',
capabilities: { tools: {} },
serverInfo: CCWEB_MCP_SERVER_INFO,
});
case 'ping':
return mcpJsonRpcResult(id, {});
case 'tools/list':
return mcpJsonRpcResult(id, { tools: CCWEB_MCP_TOOLS });
case 'tools/call': {
const name = String(message.params?.name || '');
const args = message.params?.arguments || {};
if (!CCWEB_MCP_TOOLS.some((tool) => tool.name === name)) {
return mcpJsonRpcResult(id, mcpToolResponse(mcpToolError('unknown_tool', `未知工具: ${name}`)));
}
const payload = callInternalMcpTool(
name,
args,
context.sourceSessionId || '',
context.sourceHopCount || 0,
);
return mcpJsonRpcResult(id, mcpToolResponse(payload));
}
case 'resources/list':
return mcpJsonRpcResult(id, { resources: [] });
case 'prompts/list':
return mcpJsonRpcResult(id, { prompts: [] });
default:
return mcpJsonRpcError(id, -32601, `Method not found: ${method}`);
}
} catch (err) {
return mcpJsonRpcError(id, -32603, err?.message || 'Internal error');
}
}
async function handleSharedMcpHttpApi(req, res, url) {
if (req.method !== 'POST') {
res.writeHead(405, {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'no-cache',
Allow: 'POST',
});
res.end(JSON.stringify(mcpJsonRpcError(null, -32600, 'Only POST is supported.')));
return;
}
const token = getInternalMcpRequestToken(req);
if (!token || token !== INTERNAL_MCP_TOKEN) {
return jsonResponse(res, 401, mcpJsonRpcError(null, -32001, 'MCP 内部接口未授权。'));
}
let payload;
try {
payload = await readJsonBody(req);
} catch (err) {
return jsonResponse(res, 400, mcpJsonRpcError(null, -32700, err?.message || '请求体无效。'));
}
const context = {
sourceSessionId: sanitizeId(url.searchParams.get('sourceSessionId') || ''),
sourceHopCount: Number.parseInt(String(url.searchParams.get('sourceHopCount') || 0), 10) || 0,
};
const messages = Array.isArray(payload) ? payload : [payload];
const responses = messages
.map((message) => handleMcpJsonRpcMessage(message, context))
.filter(Boolean);
if (responses.length === 0) {
res.writeHead(202, { 'Cache-Control': 'no-cache' });
res.end();
return;
}
return jsonResponse(res, 200, Array.isArray(payload) ? responses : responses[0]);
}
async function handleInternalMcpApi(req, res) {
const token = getInternalMcpRequestToken(req);
if (!token || token !== INTERNAL_MCP_TOKEN) {
@@ -5845,6 +5985,10 @@ function recoverProcesses() {
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname === '/api/internal/mcp/stream') {
return handleSharedMcpHttpApi(req, res, url);
}
if (req.method === 'POST' && url.pathname === '/api/internal/mcp') {
return handleInternalMcpApi(req, res);
}
@@ -8930,6 +9074,9 @@ function buildCodexAppClientSpec() {
delete env.CC_WEB_PASSWORD;
delete env.CLAUDECODE;
delete env.CLAUDE_CODE;
if (CODEX_APP_CCWEB_MCP_TRANSPORT !== 'stdio') {
env[CODEX_APP_CCWEB_MCP_BEARER_TOKEN_ENV] = INTERNAL_MCP_TOKEN;
}
if (runtimeConfig?.mode === 'custom') {
env.CODEX_HOME = runtimeConfig.homeDir;
env.OPENAI_API_KEY = runtimeConfig.apiKey;
@@ -8945,6 +9092,8 @@ function buildCodexAppClientSpec() {
codeHome: env.CODEX_HOME || '',
apiKeyHash: runtimeConfig?.apiKey ? crypto.createHash('sha256').update(runtimeConfig.apiKey).digest('hex') : '',
worker: CODEX_APP_WORKER_ENABLED,
ccwebMcpTransport: CODEX_APP_CCWEB_MCP_TRANSPORT,
internalMcpTokenHash: crypto.createHash('sha256').update(INTERNAL_MCP_TOKEN).digest('hex'),
});
return {