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/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) => _bpLog('behind', 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
- if (ws.readyState === 1 && bpGate.offer()) {
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) => _bpLog('behind', 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
- if (ws.readyState === 1 && bpGate.offer()) {
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);