cc-viewer 1.6.296 → 1.6.298
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +9 -6
- package/concepts/en/GlobalSettings.md +1 -0
- package/concepts/zh/GlobalSettings.md +1 -0
- package/dist/assets/{App-BeCGow-I.js → App-oeDximbJ.js} +1 -1
- package/dist/assets/{MdxEditorPanel-D52b5qxi.js → MdxEditorPanel-CQon0eDT.js} +1 -1
- package/dist/assets/{Mobile-8fflztx7.js → Mobile-DiJ7Htae.js} +1 -1
- package/dist/assets/index-wQd3rxy2.js +2 -0
- package/dist/assets/seqResourceLoaders-CFn50loc.js +2 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/lib/ask-bridge.js +11 -0
- package/server/lib/interceptor-core.js +3 -3
- package/server/lib/kv-cache-analyzer.js +3 -3
- package/server/lib/log-management.js +4 -4
- package/server/lib/log-stream.js +123 -24
- package/server/lib/log-watcher.js +4 -1
- package/server/lib/ws-backpressure.js +106 -0
- package/server/pty-manager.js +7 -1
- package/server/routes/events.js +20 -1
- package/server/routes/logs.js +21 -8
- package/server/server.js +114 -6
- package/dist/assets/index-DtpelJc4.js +0 -2
- package/dist/assets/seqResourceLoaders-DM-48tr-.js +0 -2
package/server/server.js
CHANGED
|
@@ -77,6 +77,7 @@ import { loadPlugins, runWaterfallHook, runParallelHook } from './lib/plugin-loa
|
|
|
77
77
|
import { CONTEXT_WINDOW_FILE, readModelContextSize } from './lib/context-watcher.js';
|
|
78
78
|
import { watchLogFile, startWatching, unwatchAll, sendEventToClients, sendToClients } from './lib/log-watcher.js';
|
|
79
79
|
import { cleanupExtractCache } from './lib/jsonl-archive.js';
|
|
80
|
+
import { createBackpressureGate } from './lib/ws-backpressure.js';
|
|
80
81
|
|
|
81
82
|
|
|
82
83
|
// 动态获取 getPrefsFile()(LOG_DIR 可能在运行时被 setLogDir 修改)
|
|
@@ -292,7 +293,10 @@ const MAX_POST_BODY = 10 * 1024 * 1024;
|
|
|
292
293
|
// 用户显式 ?limit=0 可恢复全量加载(power-user 逃生口)。
|
|
293
294
|
const DEFAULT_EVENTS_LIMIT = 1000;
|
|
294
295
|
// SSE 单客户端 backpressure 容忍上限:连续未排空 > 此时长则视为 dead 客户端剔除。
|
|
295
|
-
|
|
296
|
+
// 调高至 30s:大会话首屏/重连重放时,渲染器(尤其 Windows 浏览器,大 DOM layout 更重)
|
|
297
|
+
// 可能短暂忙到来不及排空 socket。过早剔除会触发「断开→EventSource 自动重连→再次重放」
|
|
298
|
+
// 风暴,把瞬时卡顿放大成持续卡死。30s 仍能剔除真正死掉的连接。
|
|
299
|
+
const SSE_BACKPRESSURE_TIMEOUT_MS = 30000;
|
|
296
300
|
|
|
297
301
|
|
|
298
302
|
|
|
@@ -559,7 +563,15 @@ const dispatch = createDispatcher(_routes);
|
|
|
559
563
|
|
|
560
564
|
async function handleRequest(req, res) {
|
|
561
565
|
const parsedUrl = new URL(req.url, `${serverProtocol}://${req.headers.host}`);
|
|
562
|
-
|
|
566
|
+
let url = parsedUrl.pathname;
|
|
567
|
+
|
|
568
|
+
// CCV_BASE_PATH reverse proxy: strip prefix at TOP so API/WS/static/SPA
|
|
569
|
+
// all work with original unprefixed paths. basePath normalized.
|
|
570
|
+
const bpRaw = process.env.CCV_BASE_PATH || '';
|
|
571
|
+
const bp = bpRaw && bpRaw !== '/' ? bpRaw.replace(/\/?$/, '/') : '';
|
|
572
|
+
if (bp && url.startsWith(bp)) {
|
|
573
|
+
url = url.slice(bp.length) || '/';
|
|
574
|
+
}
|
|
563
575
|
const method = req.method;
|
|
564
576
|
|
|
565
577
|
// WebSocket 路径不处理,交给 upgrade 事件
|
|
@@ -664,7 +676,15 @@ async function handleRequest(req, res) {
|
|
|
664
676
|
|
|
665
677
|
// 静态文件服务
|
|
666
678
|
if (method === 'GET') {
|
|
667
|
-
|
|
679
|
+
const rawBase = process.env.CCV_BASE_PATH || '';
|
|
680
|
+
// Normalize to ensure trailing slash; prevents /proxy/ws from
|
|
681
|
+
// incorrectly matching /proxy/ws-other due to startsWith ambiguity.
|
|
682
|
+
const basePath = rawBase && rawBase !== '/' ? rawBase.replace(/\/?$/, '/') : '';
|
|
683
|
+
let filePath = url;
|
|
684
|
+
if (basePath && url.startsWith(basePath)) {
|
|
685
|
+
filePath = url.slice(basePath.length) || '/';
|
|
686
|
+
}
|
|
687
|
+
if (filePath === '/') filePath = '/index.html';
|
|
668
688
|
// 去掉 query string
|
|
669
689
|
filePath = filePath.split('?')[0];
|
|
670
690
|
|
|
@@ -705,6 +725,15 @@ async function handleRequest(req, res) {
|
|
|
705
725
|
console.warn('[serveIndexHtml] dist/index.html 没有 <html data-theme="..."> 属性,SSR theme 注入将不生效。检查 index.html 模板。');
|
|
706
726
|
}
|
|
707
727
|
html = html.replace(/<html([^>]*?)data-theme="[^"]*"/, `<html$1data-theme="${themeColor}"`);
|
|
728
|
+
// 运行时注入 <base> 标签:当 CCV_BASE_PATH 设置为非空非根路径时,
|
|
729
|
+
// 使浏览器将所有相对 URL 解析到代理子路径下。配合 Vite base='' 输出相对路径。
|
|
730
|
+
const basePath = process.env.CCV_BASE_PATH || '';
|
|
731
|
+
if (basePath && basePath !== '/') {
|
|
732
|
+
const safeBase = basePath.replace(/\/?$/, '/');
|
|
733
|
+
const escapedBase = safeBase.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
734
|
+
const jsSafeBase = safeBase.replace(/"/g, '"').replace(/<\//g, '<\/');
|
|
735
|
+
html = html.replace(/<head[^>]*>/i, m => m + `<base href="${escapedBase}"><script>window.__CCV_BASE_PATH__="${jsSafeBase}"</script>`);
|
|
736
|
+
}
|
|
708
737
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' });
|
|
709
738
|
res.end(html);
|
|
710
739
|
return true;
|
|
@@ -1030,7 +1059,12 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1030
1059
|
|
|
1031
1060
|
httpServer.on('upgrade', (req, socket, head) => {
|
|
1032
1061
|
const wsUrl = new URL(req.url, `${serverProtocol}://${req.headers.host}`);
|
|
1033
|
-
|
|
1062
|
+
let pathname = wsUrl.pathname;
|
|
1063
|
+
const bpRaw = process.env.CCV_BASE_PATH || '';
|
|
1064
|
+
const wsBp = bpRaw && bpRaw !== '/' ? bpRaw.replace(/\/?$/, '/') : '';
|
|
1065
|
+
if (wsBp && pathname.startsWith(wsBp)) {
|
|
1066
|
+
pathname = '/' + pathname.slice(wsBp.length);
|
|
1067
|
+
}
|
|
1034
1068
|
// 与 HTTP 一致的鉴权(此前 WS upgrade 完全不校验 token,远程终端实为无门禁——本次堵洞)。
|
|
1035
1069
|
// 在此显式计算 isLocal(与 handleRequest 同款三态判断),WS 视作非 HTML 请求。
|
|
1036
1070
|
const wsRemoteIp = req.socket.remoteAddress;
|
|
@@ -1077,6 +1111,20 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1077
1111
|
}
|
|
1078
1112
|
});
|
|
1079
1113
|
|
|
1114
|
+
// 反压状态转换日志(observability:线上排"页面卡"时可直接判断是否触发过反压、几次、积压量)。
|
|
1115
|
+
// behind/resume 在持续洪泛下以亚秒级周期振荡,5s 节流防刷屏;timeout 是终态必记。
|
|
1116
|
+
const makeBpLogger = (label, ws) => {
|
|
1117
|
+
let behindCount = 0;
|
|
1118
|
+
let lastLogAt = 0;
|
|
1119
|
+
return (event, buffered) => {
|
|
1120
|
+
if (event === 'behind') behindCount++;
|
|
1121
|
+
const now = Date.now();
|
|
1122
|
+
if (event !== 'timeout' && now - lastLogAt < 5000) return;
|
|
1123
|
+
lastLogAt = now;
|
|
1124
|
+
console.warn(`[${label}] ws backpressure ${event}: client=${ws._socket?.remoteAddress || '?'} bufferedAmount=${buffered} behindTotal=${behindCount}`);
|
|
1125
|
+
};
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1080
1128
|
// scratch 终端 WS:极简版,仅承载 input/resize/data/exit + 显式 kill;不掺杂 hook/SDK/preset
|
|
1081
1129
|
wssScratch.on('connection', async (ws, req) => {
|
|
1082
1130
|
const id = req.ccvScratchId;
|
|
@@ -1097,8 +1145,27 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1097
1145
|
try { ws.send(JSON.stringify({ type: 'data', data: buffer })); } catch {}
|
|
1098
1146
|
}
|
|
1099
1147
|
|
|
1148
|
+
// 反压闸门:写缓冲堆积时停发 data,恢复后用 outputBuffer 快照 resync 追赶
|
|
1149
|
+
// (防 Windows ConPTY 洪泛把慢客户端 / server 内存拖垮,详见 lib/ws-backpressure.js)。
|
|
1150
|
+
// 快照自身有界:scratch outputBuffer 50KB 滚动截断(scratch-pty-manager.js MAX_BUFFER),
|
|
1151
|
+
// behind 期间继续灌也不会撑爆 resync 响应。
|
|
1152
|
+
const _bpLog = makeBpLogger('scratch-ws', ws);
|
|
1153
|
+
const bpGate = createBackpressureGate({
|
|
1154
|
+
getBufferedAmount: () => ws.bufferedAmount,
|
|
1155
|
+
onBehind: (buffered) => _bpLog('behind', buffered),
|
|
1156
|
+
onResume: (buffered) => {
|
|
1157
|
+
_bpLog('resume', buffered);
|
|
1158
|
+
if (ws.readyState !== 1) return;
|
|
1159
|
+
try { ws.send(JSON.stringify({ type: 'data-resync', data: getScratchOutputBuffer(id) })); } catch {}
|
|
1160
|
+
},
|
|
1161
|
+
onTimeout: (buffered) => {
|
|
1162
|
+
_bpLog('timeout', buffered);
|
|
1163
|
+
try { ws.terminate(); } catch {}
|
|
1164
|
+
},
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1100
1167
|
const removeDataListener = onScratchData(id, (data) => {
|
|
1101
|
-
if (ws.readyState === 1) {
|
|
1168
|
+
if (ws.readyState === 1 && bpGate.offer()) {
|
|
1102
1169
|
try { ws.send(JSON.stringify({ type: 'data', data })); } catch {}
|
|
1103
1170
|
}
|
|
1104
1171
|
});
|
|
@@ -1128,6 +1195,7 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1128
1195
|
});
|
|
1129
1196
|
|
|
1130
1197
|
ws.on('close', () => {
|
|
1198
|
+
bpGate.dispose();
|
|
1131
1199
|
removeDataListener();
|
|
1132
1200
|
removeExitListener();
|
|
1133
1201
|
// pty 本身**不杀**(保留以支持刷新重连),由 kill 消息或 /api/workspaces/stop 触发;
|
|
@@ -1174,9 +1242,48 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1174
1242
|
// 注:仅 PTY 已运行时才需要兜底;shell 不在 alternate-screen 不需要。
|
|
1175
1243
|
let _needRedrawBootstrap = state.running === true;
|
|
1176
1244
|
|
|
1245
|
+
// 反压闸门:写缓冲堆积时停发 data,恢复后用 outputBuffer 快照 resync 追赶;
|
|
1246
|
+
// resync 后强制 claude TUI 全屏重绘,避免洪泛结束于 TUI 静止态时画面停在快照
|
|
1247
|
+
// (防 Windows ConPTY 洪泛拖垮慢客户端 / server 内存,详见 lib/ws-backpressure.js)。
|
|
1248
|
+
// 快照自身有界:outputBuffer 200KB 滚动截断(pty-manager.js MAX_BUFFER + findSafeSliceStart
|
|
1249
|
+
// ANSI 安全起点),behind 期间 PTY 继续灌也不会撑爆 resync 响应。
|
|
1250
|
+
const _bpLog = makeBpLogger('terminal-ws', ws);
|
|
1251
|
+
const bpGate = createBackpressureGate({
|
|
1252
|
+
getBufferedAmount: () => ws.bufferedAmount,
|
|
1253
|
+
onBehind: (buffered) => _bpLog('behind', buffered),
|
|
1254
|
+
onResume: (buffered) => {
|
|
1255
|
+
_bpLog('resume', buffered);
|
|
1256
|
+
if (ws.readyState !== 1) return;
|
|
1257
|
+
try { ws.send(JSON.stringify({ type: 'data-resync', data: getOutputBuffer() })); } catch {}
|
|
1258
|
+
try {
|
|
1259
|
+
if (process.platform !== 'win32') {
|
|
1260
|
+
// POSIX:与下方 _needRedrawBootstrap 同款 SIGWINCH 兜底
|
|
1261
|
+
const pid = getClaudePid();
|
|
1262
|
+
if (pid && pid !== process.pid) process.kill(pid, 'SIGWINCH');
|
|
1263
|
+
} else {
|
|
1264
|
+
// Windows 无 SIGWINCH:resize 抖动经 ConPTY 通知重绘(恢复路径偶发,闪烁可接受)。
|
|
1265
|
+
// 尺寸仲裁与 resize 消息处理一致:移动端优先,否则活跃客户端(activeWs 为 null 时
|
|
1266
|
+
// 本 ws 视为所有者)——恢复的 ws 可能是非权威的慢后台 tab,用它自己的尺寸抖动会把
|
|
1267
|
+
// 共享 PTY 永久改成它的尺寸、挤掉活跃/移动端画面;无权威尺寸则跳过抖动。
|
|
1268
|
+
const mSize = getMobileSize();
|
|
1269
|
+
const size = mSize
|
|
1270
|
+
|| ((activeWs === ws || activeWs === null) ? clientSizes.get(ws) : clientSizes.get(activeWs));
|
|
1271
|
+
if (size) {
|
|
1272
|
+
resizePty(size.cols, size.rows + 1);
|
|
1273
|
+
resizePty(size.cols, size.rows);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
} catch {}
|
|
1277
|
+
},
|
|
1278
|
+
onTimeout: (buffered) => {
|
|
1279
|
+
_bpLog('timeout', buffered);
|
|
1280
|
+
try { ws.terminate(); } catch {}
|
|
1281
|
+
},
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1177
1284
|
// PTY 输出 → WebSocket(合并 ws 后客户端自行按 msg.type 分发,server 端不再 role 过滤)
|
|
1178
1285
|
const removeDataListener = onPtyData((data) => {
|
|
1179
|
-
if (ws.readyState === 1) {
|
|
1286
|
+
if (ws.readyState === 1 && bpGate.offer()) {
|
|
1180
1287
|
ws.send(JSON.stringify({ type: 'data', data }));
|
|
1181
1288
|
}
|
|
1182
1289
|
});
|
|
@@ -1555,6 +1662,7 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1555
1662
|
});
|
|
1556
1663
|
|
|
1557
1664
|
ws.on('close', () => {
|
|
1665
|
+
bpGate.dispose();
|
|
1558
1666
|
removeDataListener();
|
|
1559
1667
|
removeExitListener();
|
|
1560
1668
|
clientSizes.delete(ws);
|