v1.2.2: 对齐原生 compact 并补齐超限自动续跑

将 /compact 改为真实下发 Claude CLI 执行,并在 Request too large (max 20MB) 场景下自动压缩后重放上一条请求,同时加入失败保护避免循环重试。
This commit is contained in:
Daniel
2026-03-09 14:21:15 +00:00
parent b24a3c74b2
commit 50a953673e
2 changed files with 100 additions and 8 deletions

View File

@@ -22,6 +22,10 @@ https://github.com/ZgDaniel/cc-web 给我装!
## 更新记录
- **v1.2.2**
- 对齐 Claude Code 原生上下文压缩策略:`/compact` 改为真实下发到 CLI 执行,不再使用本地会话伪重置。
- 补齐超限自动恢复链路:当出现 `Request too large (max 20MB)` 时,自动执行 `/compact` 并在压缩后自动重放上一条失败请求继续运行。
- 增加自动续跑保护:若压缩后仍超限,停止自动重试并提示用户手动缩小输入范围,避免死循环。
- **v1.2.1**
- 修复 `AskUserQuestion` 交互选项在 Web 端不显示的问题:后端保留完整结构化参数并前端按问题/选项渲染。
- 新增交互选项快捷填充:点击选项即可把对应答案插入输入框,便于快速确认并发送。

104
server.js
View File

@@ -227,6 +227,12 @@ let PASSWORD = authConfig.password;
const activeTokens = new Set();
// Pending slash command metadata: sessionId -> { kind: string }
const pendingSlashCommands = new Map();
// Pending compact retry metadata: sessionId -> { text: string, mode: string }
const pendingCompactRetries = new Map();
// Active processes: sessionId -> { pid, ws, fullText, toolCalls, lastCost, tailer }
const activeProcesses = new Map();
@@ -396,6 +402,9 @@ function handleProcessComplete(sessionId, exitCode, signal) {
? ((new Date(completeTime) - new Date(entry.wsDisconnectTime)) / 1000).toFixed(1) + 's'
: null;
const pendingRetry = pendingCompactRetries.get(sessionId) || null;
let requestTooLarge = false;
// Read stderr for error clues
let stderrSnippet = '';
try {
@@ -406,6 +415,8 @@ function handleProcessComplete(sessionId, exitCode, signal) {
}
} catch {}
requestTooLarge = /Request too large \(max 20MB\)/i.test(entry.fullText || '') || /Request too large \(max 20MB\)/i.test(stderrSnippet || '');
plog(exitCode === 0 || exitCode === null ? 'INFO' : 'WARN', 'process_complete', {
sessionId: sessionId.slice(0, 8),
pid: entry.pid,
@@ -418,6 +429,7 @@ function handleProcessComplete(sessionId, exitCode, signal) {
toolCallCount: (entry.toolCalls || []).length,
cost: entry.lastCost,
stderr: stderrSnippet || null,
requestTooLarge,
});
// Final read
@@ -426,6 +438,9 @@ function handleProcessComplete(sessionId, exitCode, signal) {
entry.tailer.stop();
}
const pendingSlash = pendingSlashCommands.get(sessionId) || null;
if (pendingSlash) pendingSlashCommands.delete(sessionId);
// Save result to session
const session = loadSession(sessionId);
if (session && entry.fullText) {
@@ -440,8 +455,38 @@ function handleProcessComplete(sessionId, exitCode, signal) {
saveSession(session);
}
if (pendingSlash?.kind === 'compact' && session) {
if (entry.lastCost) {
session.totalCost = Math.max(0, (session.totalCost || 0) - entry.lastCost);
}
session.updated = new Date().toISOString();
saveSession(session);
}
let shouldReturnForFollowup = false;
// Notify client
if (entry.ws) {
if (pendingSlash?.kind === 'compact') {
wsSend(entry.ws, { type: 'system_message', message: '上下文压缩完成。已按 Claude Code 原生策略执行 /compact下次继续在同一会话发送即可。' });
const retry = pendingCompactRetries.get(sessionId);
if (retry?.text) {
if (requestTooLarge) {
pendingCompactRetries.delete(sessionId);
wsSend(entry.ws, { type: 'system_message', message: '已尝试执行 /compact但仍未成功解除上下文超限。请手动缩小输入范围后重试。' });
} else {
wsSend(entry.ws, { type: 'system_message', message: '检测到上一条请求因上下文过大失败,现已自动按压缩计划继续执行。' });
shouldReturnForFollowup = true;
}
}
}
if (requestTooLarge && !pendingSlash && session && session.claudeSessionId) {
pendingCompactRetries.set(sessionId, { text: pendingRetry?.text || '', mode: pendingRetry?.mode || session.permissionMode || 'yolo' });
wsSend(entry.ws, { type: 'system_message', message: '检测到上下文达到上限,正在按 Claude Code 原版策略自动执行 /compact然后继续当前任务…' });
shouldReturnForFollowup = true;
}
wsSend(entry.ws, { type: 'done', sessionId, costUsd: entry.lastCost || null });
sendSessionList(entry.ws);
} else {
@@ -470,6 +515,28 @@ function handleProcessComplete(sessionId, exitCode, signal) {
activeProcesses.delete(sessionId);
cleanRunDir(sessionId);
pendingSlashCommands.delete(sessionId);
if (!shouldReturnForFollowup && !requestTooLarge && pendingRetry && pendingRetry.text === (entry.fullText || '').trim()) {
pendingCompactRetries.delete(sessionId);
}
if (shouldReturnForFollowup && entry.ws && entry.ws.readyState === 1 && session) {
if (pendingSlash?.kind === 'compact') {
const retry = pendingCompactRetries.get(sessionId);
if (retry?.text) {
pendingCompactRetries.delete(sessionId);
handleMessage(entry.ws, { text: retry.text, sessionId, mode: retry.mode || session.permissionMode || 'yolo' });
}
return;
}
if (requestTooLarge && !pendingSlash && session.claudeSessionId) {
pendingSlashCommands.set(sessionId, { kind: 'compact' });
handleMessage(entry.ws, { text: '/compact', sessionId, mode: session.permissionMode || 'yolo' }, { hideInHistory: true });
return;
}
}
}
// Global PID monitor: detect process completion (especially after server restart)
@@ -791,12 +858,22 @@ function handleSlashCommand(ws, text, sessionId) {
}
case '/compact': {
if (session) {
session.claudeSessionId = null;
session.updated = new Date().toISOString();
saveSession(session);
if (!sessionId || !session) {
wsSend(ws, { type: 'system_message', message: '当前没有可压缩的会话。请先进入一个已进行过对话的会话后再执行 /compact。' });
break;
}
wsSend(ws, { type: 'system_message', message: '上下文已压缩Claude 会话 ID 已重置,下次发送将开始新的 Claude 会话,但聊天记录保留)。' });
if (activeProcesses.has(sessionId)) {
wsSend(ws, { type: 'system_message', message: '当前会话正在处理中,请先等待完成或点击停止,再执行 /compact。' });
break;
}
if (!session.claudeSessionId) {
wsSend(ws, { type: 'system_message', message: '当前会话尚未建立 Claude 上下文,暂时无需压缩。' });
break;
}
wsSend(ws, { type: 'system_message', message: '正在执行 Claude 原生 /compact 压缩上下文,请稍候…' });
pendingSlashCommands.set(session.id, { kind: 'compact' });
handleMessage(ws, { text: '/compact', sessionId: session.id, mode: session.permissionMode || 'yolo' }, { hideInHistory: true });
break;
}
@@ -831,7 +908,7 @@ function handleSlashCommand(ws, text, sessionId) {
'/model [名称] — 查看/切换模型opus, sonnet, haiku\n' +
'/mode [模式] — 查看/切换权限模式default, plan, yolo\n' +
'/cost — 查看当前会话累计费用\n' +
'/compact — 压缩上下文(重置 Claude 会话但保留聊天记录\n' +
'/compact — 执行 Claude 原生上下文压缩(保留压缩计划并可自动续跑\n' +
'/help — 显示本帮助',
});
break;
@@ -912,6 +989,8 @@ function handleLoadSession(ws, sessionId) {
}
function handleDeleteSession(ws, sessionId) {
pendingSlashCommands.delete(sessionId);
pendingCompactRetries.delete(sessionId);
if (activeProcesses.has(sessionId)) {
const entry = activeProcesses.get(sessionId);
try { killProcess(entry.pid); } catch {}
@@ -984,10 +1063,13 @@ function handleAbort(ws) {
}
// === Claude Message Handler ===
function handleMessage(ws, msg) {
function handleMessage(ws, msg, options = {}) {
const { text, sessionId, mode } = msg;
const { hideInHistory = false } = options;
if (!text || !text.trim()) return;
const normalizedText = text.trim();
if (sessionId && activeProcesses.has(sessionId)) {
return wsSend(ws, { type: 'error', message: '正在处理中,请先点击停止按钮。' });
}
@@ -1013,11 +1095,17 @@ function handleMessage(ws, msg) {
session.permissionMode = mode;
}
if (!hideInHistory && normalizedText !== '/compact' && session.claudeSessionId) {
pendingCompactRetries.set(session.id, { text: normalizedText, mode: session.permissionMode || 'yolo' });
}
if (session.title === 'New Chat' || session.title === 'Untitled') {
session.title = text.slice(0, 60).replace(/\n/g, ' ');
}
session.messages.push({ role: 'user', content: text, timestamp: new Date().toISOString() });
if (!hideInHistory) {
session.messages.push({ role: 'user', content: text, timestamp: new Date().toISOString() });
}
session.updated = new Date().toISOString();
saveSession(session);