修复完整会话历史与长气泡交互并更新 CentOS 7 发布包

提交当前工作区全部源码、回归脚本、任务记录及重新构建的 Bun baseline 发布包。发布单文件 MCP 初始化及前端资源一致性验证通过。
This commit is contained in:
shiyue
2026-10-10 23:40:16 +08:00
parent 747771a048
commit 69b6db0f5b
71 changed files with 9536 additions and 503 deletions

View File

@@ -13,14 +13,9 @@ function readPositiveIntEnv(name, fallback, options = {}) {
return Math.max(min, Math.min(max, raw));
}
const RUNTIME_FULL_TEXT_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_FULL_TEXT_MAX_CHARS', 256 * 1024, { min: 4096 });
const RUNTIME_AGENT_ITEM_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_AGENT_ITEM_MAX_CHARS', 128 * 1024, { min: 4096 });
const RUNTIME_TOOL_DELTA_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_TOOL_DELTA_MAX_CHARS', 64 * 1024, { min: 1024 });
const RUNTIME_TOOL_RESULT_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_TOOL_RESULT_MAX_CHARS', 32 * 1024, { min: 1024 });
const RUNTIME_TOOL_INPUT_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_TOOL_INPUT_MAX_CHARS', 16 * 1024, { min: 1024 });
const RUNTIME_STREAM_DELTA_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_STREAM_DELTA_MAX_CHARS', 16 * 1024, { min: 1024 });
const RUNTIME_MAX_TOOL_CALLS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_MAX_TOOL_CALLS', 120, { min: 1, max: 1000 });
const RUNTIME_TRUNCATED_HEAD = '[cc-web: 前文过长,已保留尾部以保护服务稳定性]\n';
const RUNTIME_TRUNCATED_TAIL = '\n[cc-web: 内容过长,已截断以保护服务稳定性]';
const CODEX_APP_PLAN_ITEM_TYPES = new Set(['plan', 'plan_list', 'planlist', 'todo', 'todo_list', 'todolist', 'task_list']);
const CODEX_APP_PLAN_TOOL_NAMES = new Set(['update_plan', 'plan', 'plan_list', 'todo_list', 'updateplan', 'todolist']);
@@ -36,7 +31,6 @@ function createCodexAppRuntime(deps = {}) {
wsSend,
loadSession,
saveSession,
truncateObj,
} = deps;
function limitPreviewValue(value, options = {}, depth = 0, seen = new WeakSet()) {
@@ -102,19 +96,13 @@ function createCodexAppRuntime(deps = {}) {
return `${text.slice(0, keep)}${RUNTIME_TRUNCATED_TAIL}`;
}
function keepTail(value, maxLen) {
const text = String(value || '');
if (!Number.isFinite(maxLen) || maxLen <= 0 || text.length <= maxLen) return text;
const keep = Math.max(0, maxLen - RUNTIME_TRUNCATED_HEAD.length);
return `${RUNTIME_TRUNCATED_HEAD}${text.slice(-keep)}`;
function appendFullText(current, addition) {
return `${String(current || '')}${String(addition || '')}`;
}
function appendCappedText(current, addition, maxLen) {
return keepTail(`${String(current || '')}${String(addition || '')}`, maxLen);
}
function capStreamDelta(text) {
return truncateEnd(text, RUNTIME_STREAM_DELTA_MAX_CHARS);
function stringifyFullValue(value) {
if (typeof value === 'string') return value;
return JSON.stringify(value, null, 2);
}
function truncate(value, maxLen) {
@@ -243,7 +231,7 @@ function createCodexAppRuntime(deps = {}) {
if (!entries) return null;
const items = entries
.map((entry) => {
const text = truncateEnd(planEntryText(entry), RUNTIME_TOOL_INPUT_MAX_CHARS);
const text = planEntryText(entry);
if (!text) return null;
return {
text,
@@ -265,7 +253,7 @@ function createCodexAppRuntime(deps = {}) {
const status = normalizeIdentifier(entry.status || entry.state);
return ['inprogress', 'in_progress', 'active', 'running', 'working'].includes(status);
});
return truncateEnd(planEntryText(activeEntry), RUNTIME_TOOL_INPUT_MAX_CHARS);
return planEntryText(activeEntry);
}
function isTodoListPlanDone(item, todoList) {
@@ -397,13 +385,13 @@ function createCodexAppRuntime(deps = {}) {
status,
hasReadableSourceTitle: !!title,
};
if (prompt) state.taskDescription = truncateEnd(prompt, RUNTIME_TOOL_INPUT_MAX_CHARS);
if (prompt) state.taskDescription = prompt;
return {
type: 'subAgentActivity',
kind: activityKind,
agentThreadId,
agentPath,
prompt: prompt ? truncateEnd(prompt, RUNTIME_TOOL_INPUT_MAX_CHARS) : null,
prompt: prompt || null,
tool: 'subAgentActivity',
status,
receiverThreadIds: agentThreadId ? [agentThreadId] : [],
@@ -438,7 +426,25 @@ function createCodexAppRuntime(deps = {}) {
}
function sendRuntime(entry, sessionId, payload) {
wsSend(entry.ws, { ...payload, sessionId });
const output = { ...payload, sessionId };
if (output.type === 'text_delta') {
// 分块发送长增量而不是截断,客户端累积正文应与服务端完整正文一致。
const text = String(output.text || '');
const chunkSize = Math.min(RUNTIME_STREAM_DELTA_MAX_CHARS, 16 * 1024);
for (let offset = 0; offset < text.length; offset += chunkSize) {
wsSend(entry.ws, { ...output, text: text.slice(offset, offset + chunkSize) });
}
return;
}
for (const key of ['text', 'message', 'result', 'input', 'blocks', 'meta']) {
if (Object.prototype.hasOwnProperty.call(output, key)) {
output[key] = limitPreviewValue(output[key], {
maxString: key === 'input' ? RUNTIME_TOOL_INPUT_MAX_CHARS : key === 'text' || key === 'message'
? RUNTIME_STREAM_DELTA_MAX_CHARS : RUNTIME_TOOL_RESULT_MAX_CHARS,
});
}
}
return wsSend(entry.ws, output);
}
function formatAgentMessageDividerTime(date = new Date()) {
@@ -521,53 +527,43 @@ function createCodexAppRuntime(deps = {}) {
if (todoList) return todoList;
switch (item.type) {
case 'commandExecution':
return { command: truncateEnd(item.command || '', RUNTIME_TOOL_INPUT_MAX_CHARS) };
return { command: item.command || '' };
case 'mcpToolCall':
return {
server: truncateEnd(item.server || '', 256),
tool: truncateEnd(item.tool || '', 256),
arguments: limitPreviewValue(item.arguments ?? null, {
maxString: RUNTIME_TOOL_INPUT_MAX_CHARS,
maxDepth: 5,
maxArray: 80,
maxKeys: 80,
}),
server: item.server || '',
tool: item.tool || '',
arguments: item.arguments ?? null,
};
case 'fileChange':
return { changes: limitPreviewValue(item.changes || [], { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 5 }) };
return { changes: item.changes || [] };
case 'reasoning':
return {
content: limitPreviewValue(item.content || [], { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 4 }),
summary: limitPreviewValue(item.summary || [], { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 4 }),
content: item.content || [],
summary: item.summary || [],
};
case 'dynamicToolCall':
return {
tool: truncateEnd(item.tool || '', 256),
namespace: truncateEnd(item.namespace || '', 256) || null,
arguments: limitPreviewValue(item.arguments ?? null, { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 5 }),
tool: item.tool || '',
namespace: item.namespace || null,
arguments: item.arguments ?? null,
};
case 'collabAgentToolCall':
return {
tool: truncateEnd(item.tool || '', 256),
prompt: truncateEnd(item.prompt || '', RUNTIME_TOOL_INPUT_MAX_CHARS) || null,
receiverThreadIds: limitPreviewValue(item.receiverThreadIds || [], { maxString: 512, maxDepth: 3 }),
agentsStates: limitPreviewValue(item.agentsStates || {}, { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 5 }),
tool: item.tool || '',
prompt: item.prompt || null,
receiverThreadIds: item.receiverThreadIds || [],
agentsStates: item.agentsStates || {},
};
case 'subAgentActivity':
return normalizeSubAgentActivityItem(item);
case 'imageGeneration':
return {
prompt: truncateEnd(item.prompt || item.query || '', RUNTIME_TOOL_INPUT_MAX_CHARS),
prompt: item.prompt || item.query || '',
size: item.size || null,
quality: item.quality || null,
};
default:
return limitPreviewValue(item, {
maxString: Math.min(500, RUNTIME_TOOL_INPUT_MAX_CHARS),
maxDepth: 4,
maxArray: 30,
maxKeys: 40,
});
return item;
}
}
@@ -647,14 +643,14 @@ function createCodexAppRuntime(deps = {}) {
function stringifyMcpResult(result) {
if (!result) return '';
if (Array.isArray(result.content)) {
if (Array.isArray(result.content) && Object.keys(result).every(key => key === 'content')) {
const text = result.content.map((part) => {
if (typeof part?.text === 'string') return truncateEnd(part.text, RUNTIME_TOOL_RESULT_MAX_CHARS);
return safeStringifyPreview(part, RUNTIME_TOOL_RESULT_MAX_CHARS);
if (typeof part?.text === 'string') return part.text;
return stringifyFullValue(part);
}).filter(Boolean).join('\n');
if (text) return truncateEnd(text, RUNTIME_TOOL_RESULT_MAX_CHARS);
if (text) return text;
}
return safeStringifyPreview(result, RUNTIME_TOOL_RESULT_MAX_CHARS);
return stringifyFullValue(result);
}
function itemResult(item) {
@@ -663,28 +659,28 @@ function createCodexAppRuntime(deps = {}) {
if (todoList) return JSON.stringify(todoList, null, 2);
switch (item.type) {
case 'commandExecution':
return truncateEnd(item.aggregatedOutput || '', RUNTIME_TOOL_RESULT_MAX_CHARS);
return item.aggregatedOutput || '';
case 'mcpToolCall':
return item.error?.message || stringifyMcpResult(item.result);
case 'fileChange':
return safeStringifyPreview(item.changes || [], RUNTIME_TOOL_RESULT_MAX_CHARS);
return stringifyFullValue(item.changes || []);
case 'reasoning':
return truncateEnd(reasoningTextFromItem(item), RUNTIME_TOOL_RESULT_MAX_CHARS);
return reasoningTextFromItem(item);
case 'dynamicToolCall':
return safeStringifyPreview({
return stringifyFullValue({
success: item.success ?? null,
contentItems: item.contentItems || null,
}, RUNTIME_TOOL_RESULT_MAX_CHARS);
});
case 'collabAgentToolCall':
return safeStringifyPreview({
return stringifyFullValue({
status: item.status || null,
receiverThreadIds: item.receiverThreadIds || [],
agentsStates: item.agentsStates || {},
}, RUNTIME_TOOL_RESULT_MAX_CHARS);
});
case 'subAgentActivity':
return safeStringifyPreview(normalizeSubAgentActivityItem(item, true), RUNTIME_TOOL_RESULT_MAX_CHARS);
return stringifyFullValue(normalizeSubAgentActivityItem(item, true));
case 'imageGeneration':
return safeStringifyPreview({
return stringifyFullValue({
status: item.status || null,
images: Array.isArray(item.images) ? item.images.map((image) => ({
path: image.path || image.filePath || null,
@@ -692,17 +688,22 @@ function createCodexAppRuntime(deps = {}) {
size: image.size || null,
})) : null,
outputPath: item.outputPath || item.path || null,
}, RUNTIME_TOOL_RESULT_MAX_CHARS);
});
default:
if (typeof item.text === 'string') return truncateEnd(item.text, RUNTIME_TOOL_RESULT_MAX_CHARS);
return safeStringifyPreview(item, Math.min(1200, RUNTIME_TOOL_RESULT_MAX_CHARS));
if (typeof item.text === 'string') return item.text;
return stringifyFullValue(item);
}
}
function itemIdForEntry(entry, itemId) {
if (!itemId) return itemId;
return entry?.runtimeItemScope ? `codexapp-item:${entry.runtimeItemScope}:${itemId}` : itemId;
}
function ensureToolCall(entry, item, sessionId) {
if (!item?.id) return null;
const kind = itemKind(item);
const itemId = isCcwebPlanItem(item) ? 'ccweb-update-plan' : item.id;
const itemId = itemIdForEntry(entry, isCcwebPlanItem(item) ? 'ccweb-update-plan' : item.id);
let toolCall = entry.toolCalls.find((tool) => tool.id === itemId);
if (toolCall) {
toolCall.name = itemName(item);
@@ -716,31 +717,6 @@ function createCodexAppRuntime(deps = {}) {
return toolCall;
}
if (entry.toolCalls.length >= RUNTIME_MAX_TOOL_CALLS) {
let overflowTool = entry.toolCalls.find((tool) => tool.id === 'ccweb-toolcalls-overflow');
if (!overflowTool) {
overflowTool = {
name: 'cc-web',
id: 'ccweb-toolcalls-overflow',
kind: 'system',
meta: { kind: 'system', title: 'Tool Calls', subtitle: 'too many tool calls', status: 'inProgress' },
input: null,
done: false,
result: '工具调用数量过多,后续工具调用已折叠显示。',
};
entry.toolCalls.push(overflowTool);
sendRuntime(entry, sessionId, {
type: 'tool_start',
name: overflowTool.name,
toolUseId: overflowTool.id,
input: overflowTool.input,
kind: overflowTool.kind,
meta: overflowTool.meta,
});
}
return overflowTool;
}
toolCall = {
name: itemName(item),
id: itemId,
@@ -762,6 +738,7 @@ function createCodexAppRuntime(deps = {}) {
}
function appendAgentText(entry, itemId, text) {
itemId = itemIdForEntry(entry, itemId);
const nextText = String(text || '');
if (!nextText) return '';
if (!entry.agentMessageItems) entry.agentMessageItems = new Map();
@@ -776,35 +753,36 @@ function createCodexAppRuntime(deps = {}) {
const separator = agentMessageSeparator(entry, itemId, nextText);
const firstText = currentItemText ? nextText : `${pendingPrefix}${nextText}`;
const appended = separator + firstText;
entry.agentMessageItems.set(itemId, appendCappedText(currentItemText, firstText, RUNTIME_AGENT_ITEM_MAX_CHARS));
entry.fullText = appendCappedText(entry.fullText || '', appended, RUNTIME_FULL_TEXT_MAX_CHARS);
return capStreamDelta(appended);
entry.agentMessageItems.set(itemId, appendFullText(currentItemText, firstText));
entry.fullText = appendFullText(entry.fullText || '', appended);
return appended;
}
function appendAgentCompletedText(entry, item) {
const itemId = itemIdForEntry(entry, item?.id);
const text = String(item?.text || '');
if (!text) return '';
if (!entry.agentMessageItems) entry.agentMessageItems = new Map();
const currentItemText = entry.agentMessageItems.get(item.id) || '';
const currentItemText = entry.agentMessageItems.get(itemId) || '';
if (!entry.agentMessagePendingPrefixes) entry.agentMessagePendingPrefixes = new Map();
const pendingPrefix = entry.agentMessagePendingPrefixes.get(item.id) || '';
entry.agentMessagePendingPrefixes.delete(item.id);
const pendingPrefix = entry.agentMessagePendingPrefixes.get(itemId) || '';
entry.agentMessagePendingPrefixes.delete(itemId);
if (!currentItemText && !text.trim()) return '';
if (currentItemText && text.startsWith(currentItemText)) {
const remainder = text.slice(currentItemText.length);
entry.agentMessageItems.set(item.id, keepTail(text, RUNTIME_AGENT_ITEM_MAX_CHARS));
entry.fullText = appendCappedText(entry.fullText || '', remainder, RUNTIME_FULL_TEXT_MAX_CHARS);
return capStreamDelta(remainder);
entry.agentMessageItems.set(itemId, text);
entry.fullText = appendFullText(entry.fullText || '', remainder);
return remainder;
}
if (currentItemText === text) return '';
const completedText = !currentItemText && pendingPrefix && !text.startsWith(pendingPrefix)
? `${pendingPrefix}${text}`
: text;
const separator = agentMessageSeparator(entry, item.id, completedText);
const separator = agentMessageSeparator(entry, itemId, completedText);
const appended = separator + completedText;
entry.agentMessageItems.set(item.id, keepTail(completedText, RUNTIME_AGENT_ITEM_MAX_CHARS));
entry.fullText = appendCappedText(entry.fullText || '', appended, RUNTIME_FULL_TEXT_MAX_CHARS);
return capStreamDelta(appended);
entry.agentMessageItems.set(itemId, completedText);
entry.fullText = appendFullText(entry.fullText || '', appended);
return appended;
}
function agentMessageSeparator(entry, itemId, nextText) {
@@ -817,12 +795,8 @@ function createCodexAppRuntime(deps = {}) {
function updateToolResult(entry, sessionId, itemId, result, done = false, patch = {}) {
if (!itemId) return;
itemId = itemIdForEntry(entry, itemId);
let toolCall = entry.toolCalls.find((tool) => tool.id === itemId);
if (!toolCall) {
const targetItemId = entry.toolCalls.length >= RUNTIME_MAX_TOOL_CALLS ? 'ccweb-toolcalls-overflow' : itemId;
toolCall = entry.toolCalls.find((tool) => tool.id === targetItemId);
itemId = targetItemId;
}
if (!toolCall) {
toolCall = {
name: patch.name || 'CodexAppItem',
@@ -846,22 +820,16 @@ function createCodexAppRuntime(deps = {}) {
if (patch.kind) toolCall.kind = patch.kind;
if (patch.meta) toolCall.meta = patch.meta;
if (patch.input !== undefined) {
toolCall.input = limitPreviewValue(patch.input, {
maxString: RUNTIME_TOOL_INPUT_MAX_CHARS,
maxDepth: 5,
maxArray: 80,
maxKeys: 80,
});
toolCall.input = patch.input;
}
const safeResult = truncateEnd(result, RUNTIME_TOOL_RESULT_MAX_CHARS);
toolCall.done = done;
toolCall.result = safeResult;
toolCall.result = result;
sendRuntime(entry, sessionId, {
type: done ? 'tool_end' : 'tool_update',
toolUseId: itemId,
name: toolCall.name,
input: toolCall.input,
result: safeResult,
result,
kind: toolCall.kind,
meta: toolCall.meta,
});
@@ -911,10 +879,11 @@ function createCodexAppRuntime(deps = {}) {
case 'item/commandExecution/outputDelta': {
const itemId = params.itemId;
const current = entry.toolOutputDeltas?.get(itemId) || '';
const next = appendCappedText(current, params.delta || '', RUNTIME_TOOL_DELTA_MAX_CHARS);
const scopedItemId = itemIdForEntry(entry, itemId);
const current = entry.toolOutputDeltas?.get(scopedItemId) || '';
const next = appendFullText(current, params.delta || '');
if (!entry.toolOutputDeltas) entry.toolOutputDeltas = new Map();
entry.toolOutputDeltas.set(itemId, next);
entry.toolOutputDeltas.set(scopedItemId, next);
updateToolResult(entry, sessionId, itemId, next, false, {
name: 'CommandExecution',
kind: 'command_execution',
@@ -966,10 +935,11 @@ function createCodexAppRuntime(deps = {}) {
const itemId = params.itemId;
const delta = String(params.delta || '');
if (!itemId || !delta) return { done: false };
const current = entry.toolOutputDeltas?.get(itemId) || '';
const next = appendCappedText(current, delta, RUNTIME_TOOL_DELTA_MAX_CHARS);
const scopedItemId = itemIdForEntry(entry, itemId);
const current = entry.toolOutputDeltas?.get(scopedItemId) || '';
const next = appendFullText(current, delta);
if (!entry.toolOutputDeltas) entry.toolOutputDeltas = new Map();
entry.toolOutputDeltas.set(itemId, next);
entry.toolOutputDeltas.set(scopedItemId, next);
updateToolResult(entry, sessionId, itemId, next, false, {
name: 'Reasoning',
kind: 'reasoning',
@@ -987,7 +957,7 @@ function createCodexAppRuntime(deps = {}) {
}
if (item.type === 'userMessage') return { done: false };
if (item.type === 'reasoning') {
const result = truncateEnd(itemResult(item) || entry.toolOutputDeltas?.get(item.id) || '', RUNTIME_TOOL_RESULT_MAX_CHARS);
const result = itemResult(item) || entry.toolOutputDeltas?.get(itemIdForEntry(entry, item.id)) || '';
if (!result.trim()) return { done: false };
const toolCall = ensureToolCall(entry, item, sessionId);
if (!toolCall) return { done: false };
@@ -1006,8 +976,8 @@ function createCodexAppRuntime(deps = {}) {
const toolCall = ensureToolCall(entry, item, sessionId);
if (!toolCall) return { done: false };
const result = item.type === 'subAgentActivity'
? safeStringifyPreview(toolCall.input, RUNTIME_TOOL_RESULT_MAX_CHARS)
: truncateEnd(itemResult(item), RUNTIME_TOOL_RESULT_MAX_CHARS);
? stringifyFullValue(toolCall.input)
: itemResult(item);
toolCall.done = true;
toolCall.result = result;
toolCall.meta = itemMeta(item) || toolCall.meta;
@@ -1060,6 +1030,7 @@ function createCodexAppRuntime(deps = {}) {
}
return {
itemIdForEntry,
planUpdateFromNotification,
processCodexAppNotification,
updateUsage,