chore: rebuild CentOS7 release package
This commit is contained in:
229
server.js
229
server.js
@@ -105,6 +105,10 @@ const SESSION_MESSAGE_CONTENT_MAX_CHARS = readPositiveIntEnv('CC_WEB_SESSION_MES
|
||||
const SESSION_TOOL_INPUT_MAX_CHARS = readPositiveIntEnv('CC_WEB_SESSION_TOOL_INPUT_MAX_CHARS', 16 * 1024, { min: 1024 });
|
||||
const SESSION_TOOL_RESULT_MAX_CHARS = readPositiveIntEnv('CC_WEB_SESSION_TOOL_RESULT_MAX_CHARS', 32 * 1024, { min: 1024 });
|
||||
const SESSION_MAX_TOOL_CALLS_PER_MESSAGE = readPositiveIntEnv('CC_WEB_SESSION_MAX_TOOL_CALLS_PER_MESSAGE', 80, { min: 1, max: 1000 });
|
||||
const SESSION_TRANSPORT_MESSAGE_CONTENT_MAX_CHARS = readPositiveIntEnv('CC_WEB_SESSION_TRANSPORT_MESSAGE_CONTENT_MAX_CHARS', 64 * 1024, { min: 4096 });
|
||||
const SESSION_TRANSPORT_TOOL_INPUT_MAX_CHARS = readPositiveIntEnv('CC_WEB_SESSION_TRANSPORT_TOOL_INPUT_MAX_CHARS', 8 * 1024, { min: 1024 });
|
||||
const SESSION_TRANSPORT_TOOL_RESULT_MAX_CHARS = readPositiveIntEnv('CC_WEB_SESSION_TRANSPORT_TOOL_RESULT_MAX_CHARS', 16 * 1024, { min: 1024 });
|
||||
const SESSION_TRANSPORT_MAX_TOOL_CALLS_PER_MESSAGE = readPositiveIntEnv('CC_WEB_SESSION_TRANSPORT_MAX_TOOL_CALLS_PER_MESSAGE', 40, { min: 1, max: 1000 });
|
||||
const HISTORY_PREFETCH_CHUNKS = readPositiveIntEnv('CC_WEB_HISTORY_PREFETCH_CHUNKS', 3, { min: 0, max: 20 });
|
||||
const HISTORY_MAX_CHUNKS_PER_LOAD = readPositiveIntEnv('CC_WEB_HISTORY_MAX_CHUNKS_PER_LOAD', 8, { min: 1, max: 100 });
|
||||
const CODEX_APP_STATE_MAX_BYTES = readPositiveIntEnv('CC_WEB_CODEX_APP_STATE_MAX_BYTES', 2 * 1024 * 1024, { min: 128 * 1024 });
|
||||
@@ -1276,7 +1280,25 @@ const MIME_TYPES = {
|
||||
// === Utility Functions ===
|
||||
|
||||
function wsSend(ws, data) {
|
||||
if (ws && ws.readyState === 1) ws.send(JSON.stringify(data));
|
||||
if (!ws || ws.readyState !== 1) return;
|
||||
try {
|
||||
ws.send(JSON.stringify(data));
|
||||
markWsActivity(ws);
|
||||
} catch (err) {
|
||||
plog('WARN', 'ws_send_failed', {
|
||||
wsId: ws._ccWebId || null,
|
||||
type: data?.type || null,
|
||||
sessionId: data?.sessionId ? String(data.sessionId).slice(0, 8) : null,
|
||||
error: err?.message || String(err || ''),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function markWsActivity(ws) {
|
||||
if (!ws) return;
|
||||
ws.isAlive = true;
|
||||
ws._ccWebLastActivityAt = Date.now();
|
||||
ws._ccWebMissedPongs = 0;
|
||||
}
|
||||
|
||||
function sanitizeId(id) {
|
||||
@@ -3508,6 +3530,21 @@ function sanitizeMessagesForPersist(messages, limits = {}) {
|
||||
return output;
|
||||
}
|
||||
|
||||
function sanitizeMessageForTransport(message) {
|
||||
return sanitizeMessageForPersist(message, {
|
||||
contentMaxChars: SESSION_TRANSPORT_MESSAGE_CONTENT_MAX_CHARS,
|
||||
toolInputMaxChars: SESSION_TRANSPORT_TOOL_INPUT_MAX_CHARS,
|
||||
toolResultMaxChars: SESSION_TRANSPORT_TOOL_RESULT_MAX_CHARS,
|
||||
maxToolCalls: SESSION_TRANSPORT_MAX_TOOL_CALLS_PER_MESSAGE,
|
||||
metaMaxChars: SESSION_TRANSPORT_MESSAGE_CONTENT_MAX_CHARS,
|
||||
});
|
||||
}
|
||||
|
||||
function sanitizeMessagesForTransport(messages) {
|
||||
const list = Array.isArray(messages) ? messages : [];
|
||||
return list.map((message) => sanitizeMessageForTransport(message));
|
||||
}
|
||||
|
||||
function sanitizeSessionForPersist(session, limits = {}) {
|
||||
const output = {};
|
||||
const skipKeys = new Set([
|
||||
@@ -3755,7 +3792,7 @@ function sessionModelLabel(session) {
|
||||
}
|
||||
|
||||
function splitHistoryMessages(messages, options = {}) {
|
||||
const list = Array.isArray(messages) ? messages : [];
|
||||
const list = sanitizeMessagesForTransport(messages);
|
||||
if (list.length <= INITIAL_HISTORY_COUNT) {
|
||||
return { recentMessages: list, olderChunks: [], historyRemaining: 0, historyBuffered: list.length };
|
||||
}
|
||||
@@ -6147,6 +6184,7 @@ const server = http.createServer((req, res) => {
|
||||
// === WebSocket Server ===
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
const WS_HEARTBEAT_INTERVAL_MS = 30000;
|
||||
const WS_HEARTBEAT_MAX_MISSES = readPositiveIntEnv('CC_WEB_WS_HEARTBEAT_MAX_MISSES', 4, { min: 2, max: 20 });
|
||||
|
||||
server.on('upgrade', (req, socket, head) => {
|
||||
let pathname = '';
|
||||
@@ -6184,14 +6222,17 @@ wss.on('connection', (ws, req) => {
|
||||
const wsId = crypto.randomBytes(4).toString('hex'); // short id for log correlation
|
||||
const wsConnectTime = new Date().toISOString();
|
||||
ws.isAlive = true;
|
||||
ws._ccWebMissedPongs = 0;
|
||||
ws._ccWebId = wsId;
|
||||
markWsActivity(ws);
|
||||
plog('INFO', 'ws_connect', { wsId });
|
||||
|
||||
ws.on('pong', () => {
|
||||
ws.isAlive = true;
|
||||
markWsActivity(ws);
|
||||
});
|
||||
|
||||
ws.on('message', (raw) => {
|
||||
markWsActivity(ws);
|
||||
let msg;
|
||||
try {
|
||||
msg = JSON.parse(raw);
|
||||
@@ -6244,6 +6285,9 @@ wss.on('connection', (ws, req) => {
|
||||
case 'load_session':
|
||||
handleLoadSession(ws, msg);
|
||||
break;
|
||||
case 'resume_session':
|
||||
handleResumeSession(ws, msg);
|
||||
break;
|
||||
case 'load_history_page':
|
||||
handleLoadHistoryPage(ws, msg);
|
||||
break;
|
||||
@@ -6339,19 +6383,36 @@ wss.on('connection', (ws, req) => {
|
||||
|
||||
// WebSocket 心跳:避免反向代理因空闲连接关闭 /ws。
|
||||
const wsHeartbeatTimer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const client of wss.clients) {
|
||||
if (client.readyState !== 1) continue;
|
||||
if (client.isAlive === false) {
|
||||
client.terminate();
|
||||
continue;
|
||||
const lastActivityAt = Number(client._ccWebLastActivityAt || 0);
|
||||
const hasRecentActivity = lastActivityAt > 0 && now - lastActivityAt < WS_HEARTBEAT_INTERVAL_MS * 2;
|
||||
if (hasRecentActivity) {
|
||||
client._ccWebMissedPongs = 0;
|
||||
} else {
|
||||
client._ccWebMissedPongs = Number(client._ccWebMissedPongs || 0) + 1;
|
||||
}
|
||||
if (client._ccWebMissedPongs >= WS_HEARTBEAT_MAX_MISSES) {
|
||||
plog('WARN', 'ws_heartbeat_terminate', {
|
||||
wsId: client._ccWebId || null,
|
||||
missedPongs: client._ccWebMissedPongs,
|
||||
lastActivityAgeMs: lastActivityAt ? now - lastActivityAt : null,
|
||||
});
|
||||
client.terminate();
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
client._ccWebMissedPongs = 0;
|
||||
}
|
||||
client.isAlive = false;
|
||||
if (client.readyState === 1) {
|
||||
try {
|
||||
client.ping();
|
||||
} catch (err) {
|
||||
plog('WARN', 'ws_ping_failed', { wsId: client._ccWebId || null, error: err.message });
|
||||
client.terminate();
|
||||
}
|
||||
try {
|
||||
client.ping();
|
||||
} catch (err) {
|
||||
plog('WARN', 'ws_ping_failed', { wsId: client._ccWebId || null, error: err.message });
|
||||
client.terminate();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}, WS_HEARTBEAT_INTERVAL_MS);
|
||||
@@ -7191,7 +7252,7 @@ function createPersistentConversationSession(args = {}, options = {}) {
|
||||
|
||||
function buildSessionInfoPayload(session) {
|
||||
const waitState = crossConversationWaitState(session.id);
|
||||
const messages = session.messages || [];
|
||||
const messages = sanitizeMessagesForTransport(session.messages || []);
|
||||
return {
|
||||
type: 'session_info',
|
||||
sessionId: session.id,
|
||||
@@ -7268,9 +7329,14 @@ function handleLoadHistoryPage(ws, msg = {}) {
|
||||
const sessionId = sanitizeId(msg.sessionId || '');
|
||||
const session = loadSession(sessionId);
|
||||
if (!session) {
|
||||
return wsSend(ws, { type: 'error', message: 'Session not found' });
|
||||
return wsSend(ws, attachClientRequestId({
|
||||
type: 'error',
|
||||
code: 'session_not_found',
|
||||
sessionId,
|
||||
message: 'Session not found',
|
||||
}, msg));
|
||||
}
|
||||
const list = Array.isArray(session.messages) ? session.messages : [];
|
||||
const list = sanitizeMessagesForTransport(session.messages);
|
||||
const requestedBefore = Number.parseInt(String(msg.before || ''), 10);
|
||||
const before = Number.isFinite(requestedBefore)
|
||||
? Math.max(0, Math.min(list.length, requestedBefore))
|
||||
@@ -7288,12 +7354,93 @@ function handleLoadHistoryPage(ws, msg = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function attachActiveRuntimeToWs(ws, sessionId, source = {}) {
|
||||
if (activeProcesses.has(sessionId)) {
|
||||
const entry = activeProcesses.get(sessionId);
|
||||
entry.ws = ws;
|
||||
entry.wsDisconnectTime = null; // clear disconnect marker
|
||||
plog('INFO', 'ws_resume_attach', {
|
||||
sessionId: sessionId.slice(0, 8),
|
||||
pid: entry.pid,
|
||||
responseLen: (entry.fullText || '').length,
|
||||
});
|
||||
wsSend(ws, attachClientRequestId({
|
||||
type: 'resume_generating',
|
||||
sessionId,
|
||||
text: truncateTextValue(entry.fullText || '', SESSION_MESSAGE_CONTENT_MAX_CHARS),
|
||||
toolCalls: sanitizeToolCallsForPersist(entry.toolCalls || []),
|
||||
}, source));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (activeCodexAppTurns.has(sessionId)) {
|
||||
const entry = activeCodexAppTurns.get(sessionId);
|
||||
entry.ws = ws;
|
||||
entry.wsDisconnectTime = null;
|
||||
plog('INFO', 'codex_app_ws_resume_attach', {
|
||||
sessionId: sessionId.slice(0, 8),
|
||||
threadId: entry.threadId || null,
|
||||
turnId: entry.turnId || null,
|
||||
responseLen: (entry.fullText || '').length,
|
||||
});
|
||||
wsSend(ws, attachClientRequestId({
|
||||
type: 'resume_generating',
|
||||
sessionId,
|
||||
text: truncateTextValue(entry.fullText || '', SESSION_MESSAGE_CONTENT_MAX_CHARS),
|
||||
toolCalls: sanitizeToolCallsForPersist(entry.toolCalls || []),
|
||||
}, source));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (activeCodexAppGoalCommands.has(sessionId)) {
|
||||
const entry = activeCodexAppGoalCommands.get(sessionId);
|
||||
entry.ws = ws;
|
||||
entry.wsDisconnectTime = null;
|
||||
wsSend(ws, attachClientRequestId({
|
||||
type: 'system_message',
|
||||
sessionId,
|
||||
message: '正在同步 Goal...',
|
||||
}, source));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleResumeSession(ws, msg = {}) {
|
||||
const sessionId = sanitizeId(msg.sessionId || '');
|
||||
const session = loadSession(sessionId);
|
||||
if (!session) {
|
||||
return wsSend(ws, attachClientRequestId({
|
||||
type: 'error',
|
||||
code: 'session_not_found',
|
||||
sessionId,
|
||||
message: 'Session not found',
|
||||
}, msg));
|
||||
}
|
||||
|
||||
detachWsFromActiveRuntimes(ws);
|
||||
wsSessionMap.set(ws, sessionId);
|
||||
const attached = attachActiveRuntimeToWs(ws, sessionId, msg);
|
||||
wsSend(ws, attachClientRequestId({
|
||||
type: 'resume_session_result',
|
||||
sessionId,
|
||||
isRunning: isSessionRunning(sessionId),
|
||||
attached,
|
||||
}, msg));
|
||||
}
|
||||
|
||||
function handleLoadSession(ws, msg) {
|
||||
const sessionId = sanitizeId(typeof msg === 'string' ? msg : msg?.sessionId);
|
||||
reconcilePendingCrossConversationReplies();
|
||||
const session = loadSession(sessionId);
|
||||
if (!session) {
|
||||
return wsSend(ws, { type: 'error', message: 'Session not found' });
|
||||
return wsSend(ws, attachClientRequestId({
|
||||
type: 'error',
|
||||
code: 'session_not_found',
|
||||
sessionId,
|
||||
message: 'Session not found',
|
||||
}, msg));
|
||||
}
|
||||
flushPendingCrossConversationReplies(sessionId);
|
||||
const refreshedSession = loadSession(sessionId) || session;
|
||||
@@ -7367,44 +7514,8 @@ function handleLoadSession(ws, msg) {
|
||||
});
|
||||
}
|
||||
|
||||
// Resume streaming if process is still active
|
||||
if (activeProcesses.has(sessionId)) {
|
||||
const entry = activeProcesses.get(sessionId);
|
||||
entry.ws = ws;
|
||||
entry.wsDisconnectTime = null; // clear disconnect marker
|
||||
plog('INFO', 'ws_resume_attach', {
|
||||
sessionId: sessionId.slice(0, 8),
|
||||
pid: entry.pid,
|
||||
responseLen: (entry.fullText || '').length,
|
||||
});
|
||||
wsSend(ws, {
|
||||
type: 'resume_generating',
|
||||
sessionId,
|
||||
text: truncateTextValue(entry.fullText || '', SESSION_MESSAGE_CONTENT_MAX_CHARS),
|
||||
toolCalls: sanitizeToolCallsForPersist(entry.toolCalls || []),
|
||||
});
|
||||
} else if (activeCodexAppTurns.has(sessionId)) {
|
||||
const entry = activeCodexAppTurns.get(sessionId);
|
||||
entry.ws = ws;
|
||||
entry.wsDisconnectTime = null;
|
||||
plog('INFO', 'codex_app_ws_resume_attach', {
|
||||
sessionId: sessionId.slice(0, 8),
|
||||
threadId: entry.threadId || null,
|
||||
turnId: entry.turnId || null,
|
||||
responseLen: (entry.fullText || '').length,
|
||||
});
|
||||
wsSend(ws, {
|
||||
type: 'resume_generating',
|
||||
sessionId,
|
||||
text: truncateTextValue(entry.fullText || '', SESSION_MESSAGE_CONTENT_MAX_CHARS),
|
||||
toolCalls: sanitizeToolCallsForPersist(entry.toolCalls || []),
|
||||
});
|
||||
} else if (activeCodexAppGoalCommands.has(sessionId)) {
|
||||
const entry = activeCodexAppGoalCommands.get(sessionId);
|
||||
entry.ws = ws;
|
||||
entry.wsDisconnectTime = null;
|
||||
wsSend(ws, { type: 'system_message', sessionId, message: '正在同步 Goal...' });
|
||||
}
|
||||
// Resume streaming if process is still active.
|
||||
attachActiveRuntimeToWs(ws, sessionId);
|
||||
}
|
||||
|
||||
function sqlQuote(value) {
|
||||
@@ -9940,10 +10051,11 @@ function handleImportNativeSession(ws, msg) {
|
||||
};
|
||||
saveSession(session);
|
||||
wsSessionMap.set(ws, id);
|
||||
const transportMessages = sanitizeMessagesForTransport(session.messages);
|
||||
wsSend(ws, attachClientRequestId({
|
||||
type: 'session_info',
|
||||
sessionId: id,
|
||||
messages: session.messages,
|
||||
messages: transportMessages,
|
||||
title: session.title,
|
||||
pinnedAt: session.pinnedAt || null,
|
||||
mode: session.permissionMode,
|
||||
@@ -9953,6 +10065,8 @@ function handleImportNativeSession(ws, msg) {
|
||||
totalCost: session.totalCost || 0,
|
||||
totalUsage: session.totalUsage || null,
|
||||
updated: session.updated,
|
||||
historyTotal: transportMessages.length,
|
||||
historyBaseIndex: 0,
|
||||
hasUnread: false,
|
||||
historyPending: false,
|
||||
isRunning: false,
|
||||
@@ -10109,10 +10223,11 @@ function handleImportCodexSession(ws, msg) {
|
||||
|
||||
saveSession(session);
|
||||
wsSessionMap.set(ws, id);
|
||||
const transportMessages = sanitizeMessagesForTransport(session.messages);
|
||||
wsSend(ws, attachClientRequestId({
|
||||
type: 'session_info',
|
||||
sessionId: id,
|
||||
messages: session.messages,
|
||||
messages: transportMessages,
|
||||
title: session.title,
|
||||
pinnedAt: session.pinnedAt || null,
|
||||
mode: session.permissionMode,
|
||||
@@ -10122,6 +10237,8 @@ function handleImportCodexSession(ws, msg) {
|
||||
totalCost: session.totalCost || 0,
|
||||
totalUsage: session.totalUsage || null,
|
||||
updated: session.updated,
|
||||
historyTotal: transportMessages.length,
|
||||
historyBaseIndex: 0,
|
||||
hasUnread: false,
|
||||
historyPending: false,
|
||||
isRunning: false,
|
||||
|
||||
Reference in New Issue
Block a user