cc-viewer 1.6.301 → 1.6.303
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/dist/assets/{App-CsXuZvCc.js → App-DPAjZzou.js} +1 -1
- package/dist/assets/{App-eFrjLzF_.css → App-DfsHLalW.css} +1 -1
- package/dist/assets/{MdxEditorPanel-B-y66Flk.js → MdxEditorPanel-CMU1q8W_.js} +1 -1
- package/dist/assets/{Mobile-O0bfec7C.js → Mobile-CiB3e3Wi.js} +1 -1
- package/dist/assets/index-BmKQfyia.js +2 -0
- package/dist/assets/seqResourceLoaders-CqcvNS4R.js +2 -0
- package/dist/index.html +1 -1
- package/findcc.js +21 -1
- package/package.json +1 -1
- package/server/lib/pty-flood-coalescer.js +255 -0
- package/server/lib/resync-nudge-gate.js +33 -0
- package/server/pty-manager.js +4 -1
- package/server/routes/files-content.js +16 -6
- package/server/routes/files-fs.js +6 -2
- package/server/routes/preferences.js +29 -9
- package/server/server.js +101 -10
- package/dist/assets/index-DpIkVZv8.js +0 -2
- package/dist/assets/seqResourceLoaders-DpUrNd29.js +0 -2
package/server/server.js
CHANGED
|
@@ -81,6 +81,8 @@ import { backupConfigs } from './lib/config-backup.js';
|
|
|
81
81
|
import { normalizeBasePath, validateBasePath, stripBasePath } from './lib/base-path.js';
|
|
82
82
|
import { createHardenedCleanup } from './lib/term-signals.js';
|
|
83
83
|
import { createBackpressureGate } from './lib/ws-backpressure.js';
|
|
84
|
+
import { createFloodCoalescer, envIntAllowZero } from './lib/pty-flood-coalescer.js';
|
|
85
|
+
import { createResyncNudgeGate } from './lib/resync-nudge-gate.js';
|
|
84
86
|
|
|
85
87
|
|
|
86
88
|
// 动态获取 getPrefsFile()(LOG_DIR 可能在运行时被 setLogDir 修改)
|
|
@@ -1027,7 +1029,7 @@ export async function startViewer() {
|
|
|
1027
1029
|
async function setupTerminalWebSocket(httpServer) {
|
|
1028
1030
|
try {
|
|
1029
1031
|
const { WebSocketServer } = await import('ws');
|
|
1030
|
-
const { writeToPty, writeToPtySequential, resizePty, onPtyData, onPtyExit, getPtyState, getOutputBuffer, getCurrentWorkspace, spawnShell } = await import('./pty-manager.js');
|
|
1032
|
+
const { writeToPty, writeToPtySequential, resizePty, onPtyData, onPtyExit, getPtyState, getOutputBuffer, getCurrentWorkspace, spawnShell, findSafeSliceStart } = await import('./pty-manager.js');
|
|
1031
1033
|
const {
|
|
1032
1034
|
spawnScratch,
|
|
1033
1035
|
writeScratch,
|
|
@@ -1120,15 +1122,55 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1120
1122
|
|
|
1121
1123
|
// 反压状态转换日志(observability:线上排"页面卡"时可直接判断是否触发过反压、几次、积压量)。
|
|
1122
1124
|
// behind/resume 在持续洪泛下以亚秒级周期振荡,5s 节流防刷屏;timeout 是终态必记。
|
|
1125
|
+
// resyncTotal/nudgeSkipped 判别 resync 循环:resync 涨 + skipped 同涨 = nudge 冷却在救;
|
|
1126
|
+
// resync 涨 + skipped≈0 = resume 间隔超过冷却期的慢振荡,需更激进策略(如洪泛期禁 nudge)。
|
|
1123
1127
|
const makeBpLogger = (label, ws) => {
|
|
1124
1128
|
let behindCount = 0;
|
|
1129
|
+
let resyncCount = 0;
|
|
1130
|
+
let nudgeSkipped = 0;
|
|
1125
1131
|
let lastLogAt = 0;
|
|
1126
1132
|
return (event, buffered) => {
|
|
1127
1133
|
if (event === 'behind') behindCount++;
|
|
1134
|
+
if (event === 'resume') resyncCount++;
|
|
1135
|
+
if (event === 'nudge-skip') { nudgeSkipped++; return; } // 只计数,随下一条节流日志带出
|
|
1128
1136
|
const now = Date.now();
|
|
1129
1137
|
if (event !== 'timeout' && now - lastLogAt < 5000) return;
|
|
1130
1138
|
lastLogAt = now;
|
|
1131
|
-
console.warn(`[${label}] ws backpressure ${event}: client=${ws._socket?.remoteAddress || '?'} bufferedAmount=${buffered} behindTotal=${behindCount}`);
|
|
1139
|
+
console.warn(`[${label}] ws backpressure ${event}: client=${ws._socket?.remoteAddress || '?'} bufferedAmount=${buffered} behindTotal=${behindCount} resyncTotal=${resyncCount} nudgeSkipped=${nudgeSkipped}`);
|
|
1140
|
+
};
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
// resync nudge 冷却(CCV_RESYNC_NUDGE_COOLDOWN_MS,0 = 不冷却;详见 lib/resync-nudge-gate.js)
|
|
1144
|
+
const RESYNC_NUDGE_COOLDOWN_MS = envIntAllowZero('CCV_RESYNC_NUDGE_COOLDOWN_MS', 3000);
|
|
1145
|
+
|
|
1146
|
+
// 洪泛限流器状态日志(与 makeBpLogger 同款 5s 节流,独立实例不共享计数)。
|
|
1147
|
+
// Windows 实机排"切主题/大流量卡死"时据此确认 ConPTY 洪泛是否触发、几次、量级。
|
|
1148
|
+
// 'rate' 事件 = 直通态 ws 消息率告警(msgsPerSec,计数在 send 闭包),判别消息数风暴。
|
|
1149
|
+
const makeFloodLogger = (label, ws) => {
|
|
1150
|
+
let floodCount = 0;
|
|
1151
|
+
let lastLogAt = 0;
|
|
1152
|
+
return (event, bytes) => {
|
|
1153
|
+
if (event === 'start') floodCount++;
|
|
1154
|
+
const now = Date.now();
|
|
1155
|
+
if (now - lastLogAt < 5000) return;
|
|
1156
|
+
lastLogAt = now;
|
|
1157
|
+
const metric = event === 'rate' ? 'msgsPerSec' : 'winBytes';
|
|
1158
|
+
console.warn(`[${label}] pty flood ${event}: client=${ws._socket?.remoteAddress || '?'} ${metric}=${bytes} floodTotal=${floodCount}`);
|
|
1159
|
+
};
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
// 直通态消息率计数器工厂:1s 整数桶,桶滚动时超过阈值经 floodLog('rate') 告警(5s 节流兜底)。
|
|
1163
|
+
const makeMsgRateCounter = (floodLog, warnAbove = 60) => {
|
|
1164
|
+
let count = 0;
|
|
1165
|
+
let winStart = 0;
|
|
1166
|
+
return () => {
|
|
1167
|
+
const now = Date.now();
|
|
1168
|
+
if (now - winStart >= 1000) {
|
|
1169
|
+
if (count > warnAbove) floodLog('rate', count);
|
|
1170
|
+
count = 0;
|
|
1171
|
+
winStart = now;
|
|
1172
|
+
}
|
|
1173
|
+
count++;
|
|
1132
1174
|
};
|
|
1133
1175
|
};
|
|
1134
1176
|
|
|
@@ -1157,11 +1199,18 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1157
1199
|
// 快照自身有界:scratch outputBuffer 50KB 滚动截断(scratch-pty-manager.js MAX_BUFFER),
|
|
1158
1200
|
// behind 期间继续灌也不会撑爆 resync 响应。
|
|
1159
1201
|
const _bpLog = makeBpLogger('scratch-ws', ws);
|
|
1202
|
+
// floodGate 在 bpGate 之后构造(send 闭包依赖 bpGate),onBehind/onResume 经 let 前向引用 reset:
|
|
1203
|
+
// resync 快照是唯一真相源,coalescer 残留 pending 不清会把早于快照的旧字节回灌导致画面回退。
|
|
1204
|
+
let floodGate = null;
|
|
1160
1205
|
const bpGate = createBackpressureGate({
|
|
1161
1206
|
getBufferedAmount: () => ws.bufferedAmount,
|
|
1162
|
-
onBehind: (buffered) =>
|
|
1207
|
+
onBehind: (buffered) => {
|
|
1208
|
+
_bpLog('behind', buffered);
|
|
1209
|
+
floodGate?.reset();
|
|
1210
|
+
},
|
|
1163
1211
|
onResume: (buffered) => {
|
|
1164
1212
|
_bpLog('resume', buffered);
|
|
1213
|
+
floodGate?.reset();
|
|
1165
1214
|
if (ws.readyState !== 1) return;
|
|
1166
1215
|
try { ws.send(JSON.stringify({ type: 'data-resync', data: getScratchOutputBuffer(id) })); } catch {}
|
|
1167
1216
|
},
|
|
@@ -1171,10 +1220,24 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1171
1220
|
},
|
|
1172
1221
|
});
|
|
1173
1222
|
|
|
1223
|
+
// 洪泛限流器:字节率超阈值时按窗口合并 + last-wins 截断(ConPTY 全屏重绘洪泛防卡死,
|
|
1224
|
+
// 与 bpGate 互补——bpGate 管慢网络写缓冲,floodGate 管快 LAN 字节率,详见 lib/pty-flood-coalescer.js)
|
|
1225
|
+
const _floodLog = makeFloodLogger('scratch-ws', ws);
|
|
1226
|
+
const _countMsg = makeMsgRateCounter(_floodLog);
|
|
1227
|
+
floodGate = createFloodCoalescer({
|
|
1228
|
+
send: (data) => {
|
|
1229
|
+
if (ws.readyState === 1 && bpGate.offer()) {
|
|
1230
|
+
try { ws.send(JSON.stringify({ type: 'data', data })); } catch {}
|
|
1231
|
+
_countMsg();
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
findSafeSliceStart,
|
|
1235
|
+
onFloodStart: (bytes) => _floodLog('start', bytes),
|
|
1236
|
+
onFloodEnd: () => _floodLog('end', 0),
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1174
1239
|
const removeDataListener = onScratchData(id, (data) => {
|
|
1175
|
-
|
|
1176
|
-
try { ws.send(JSON.stringify({ type: 'data', data })); } catch {}
|
|
1177
|
-
}
|
|
1240
|
+
floodGate.offer(data);
|
|
1178
1241
|
});
|
|
1179
1242
|
|
|
1180
1243
|
const removeExitListener = onScratchExit(id, (exitCode) => {
|
|
@@ -1203,6 +1266,7 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1203
1266
|
|
|
1204
1267
|
ws.on('close', () => {
|
|
1205
1268
|
bpGate.dispose();
|
|
1269
|
+
floodGate.dispose();
|
|
1206
1270
|
removeDataListener();
|
|
1207
1271
|
removeExitListener();
|
|
1208
1272
|
// pty 本身**不杀**(保留以支持刷新重连),由 kill 消息或 /api/workspaces/stop 触发;
|
|
@@ -1255,13 +1319,25 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1255
1319
|
// 快照自身有界:outputBuffer 200KB 滚动截断(pty-manager.js MAX_BUFFER + findSafeSliceStart
|
|
1256
1320
|
// ANSI 安全起点),behind 期间 PTY 继续灌也不会撑爆 resync 响应。
|
|
1257
1321
|
const _bpLog = makeBpLogger('terminal-ws', ws);
|
|
1322
|
+
// floodGate 前向引用(构造顺序同 scratch 路径):onBehind/onResume 必清 coalescer
|
|
1323
|
+
// pending——resync 快照是唯一真相源,旧 pending 回灌会导致画面回退。
|
|
1324
|
+
let floodGate = null;
|
|
1325
|
+
// nudge 冷却门:快照每次 resume 无条件发(修复 behind 期间跳发的数据),但重绘 nudge
|
|
1326
|
+
// 走冷却——nudge 让 ConPTY 再吐全屏重绘 = 新洪泛燃料,紧 behind→resume 循环里反复
|
|
1327
|
+
// nudge 会自我维持(客户端每轮 reset+重放快照 = 永久冻结表象)。详见 lib/resync-nudge-gate.js。
|
|
1328
|
+
const nudgeGate = createResyncNudgeGate({ cooldownMs: RESYNC_NUDGE_COOLDOWN_MS });
|
|
1258
1329
|
const bpGate = createBackpressureGate({
|
|
1259
1330
|
getBufferedAmount: () => ws.bufferedAmount,
|
|
1260
|
-
onBehind: (buffered) =>
|
|
1331
|
+
onBehind: (buffered) => {
|
|
1332
|
+
_bpLog('behind', buffered);
|
|
1333
|
+
floodGate?.reset();
|
|
1334
|
+
},
|
|
1261
1335
|
onResume: (buffered) => {
|
|
1262
1336
|
_bpLog('resume', buffered);
|
|
1337
|
+
floodGate?.reset();
|
|
1263
1338
|
if (ws.readyState !== 1) return;
|
|
1264
1339
|
try { ws.send(JSON.stringify({ type: 'data-resync', data: getOutputBuffer() })); } catch {}
|
|
1340
|
+
if (!nudgeGate.shouldNudge()) { _bpLog('nudge-skip', buffered); return; }
|
|
1265
1341
|
try {
|
|
1266
1342
|
if (process.platform !== 'win32') {
|
|
1267
1343
|
// POSIX:与下方 _needRedrawBootstrap 同款 SIGWINCH 兜底
|
|
@@ -1288,11 +1364,25 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1288
1364
|
},
|
|
1289
1365
|
});
|
|
1290
1366
|
|
|
1367
|
+
// 洪泛限流器:字节率超阈值时按窗口合并 + last-wins 截断(ConPTY 全屏重绘洪泛防卡死,
|
|
1368
|
+
// 与 bpGate 互补——bpGate 管慢网络写缓冲,floodGate 管快 LAN 字节率,详见 lib/pty-flood-coalescer.js)
|
|
1369
|
+
const _floodLog = makeFloodLogger('terminal-ws', ws);
|
|
1370
|
+
const _countMsg = makeMsgRateCounter(_floodLog);
|
|
1371
|
+
floodGate = createFloodCoalescer({
|
|
1372
|
+
send: (data) => {
|
|
1373
|
+
if (ws.readyState === 1 && bpGate.offer()) {
|
|
1374
|
+
try { ws.send(JSON.stringify({ type: 'data', data })); } catch {}
|
|
1375
|
+
_countMsg();
|
|
1376
|
+
}
|
|
1377
|
+
},
|
|
1378
|
+
findSafeSliceStart,
|
|
1379
|
+
onFloodStart: (bytes) => _floodLog('start', bytes),
|
|
1380
|
+
onFloodEnd: () => _floodLog('end', 0),
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1291
1383
|
// PTY 输出 → WebSocket(合并 ws 后客户端自行按 msg.type 分发,server 端不再 role 过滤)
|
|
1292
1384
|
const removeDataListener = onPtyData((data) => {
|
|
1293
|
-
|
|
1294
|
-
ws.send(JSON.stringify({ type: 'data', data }));
|
|
1295
|
-
}
|
|
1385
|
+
floodGate.offer(data);
|
|
1296
1386
|
});
|
|
1297
1387
|
|
|
1298
1388
|
// PTY 退出 → WebSocket
|
|
@@ -1670,6 +1760,7 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1670
1760
|
|
|
1671
1761
|
ws.on('close', () => {
|
|
1672
1762
|
bpGate.dispose();
|
|
1763
|
+
floodGate.dispose();
|
|
1673
1764
|
removeDataListener();
|
|
1674
1765
|
removeExitListener();
|
|
1675
1766
|
clientSizes.delete(ws);
|