cc-viewer 1.6.302 → 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-COezHOeh.js → App-DPAjZzou.js} +1 -1
- package/dist/assets/{App-eFrjLzF_.css → App-DfsHLalW.css} +1 -1
- package/dist/assets/{MdxEditorPanel-BkJKua3-.js → MdxEditorPanel-CMU1q8W_.js} +1 -1
- package/dist/assets/{Mobile-BtNceCJC.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 +76 -12
- package/server/lib/resync-nudge-gate.js +33 -0
- package/server/routes/files-content.js +16 -6
- package/server/server.js +39 -3
- package/dist/assets/index-DSz9bNGm.js +0 -2
- package/dist/assets/seqResourceLoaders-C8gBbhlC.js +0 -2
package/dist/index.html
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// 整体显示大小已弃用 CSS zoom:Electron 改用 webFrame.setZoomFactor(首屏抢占见
|
|
22
22
|
// electron/tab-content-preload.js),纯浏览器交由用户用浏览器自带快捷键缩放,故此处不再设 zoom。
|
|
23
23
|
</script>
|
|
24
|
-
<script type="module" crossorigin src="/assets/index-
|
|
24
|
+
<script type="module" crossorigin src="/assets/index-BmKQfyia.js"></script>
|
|
25
25
|
<link rel="modulepreload" crossorigin href="/assets/vendor-antd-Bur5ZxWE.js">
|
|
26
26
|
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-Si44UqBp.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/vendor-mdxeditor-Cco3AQJS.js">
|
package/findcc.js
CHANGED
|
@@ -201,6 +201,20 @@ export function resolveNpmClaudePath() {
|
|
|
201
201
|
return null;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/**
|
|
205
|
+
* 从 which/where 的原始输出中挑出能直接 CreateProcess/exec 的候选行。
|
|
206
|
+
* Windows 的 `where` 会列出 PATH 中全部同名匹配——npm 全局安装时第一行往往是给
|
|
207
|
+
* git-bash 用的**无扩展名 sh shim**(#!/bin/sh 文本文件),其后是 .cmd/.ps1,都不是
|
|
208
|
+
* PE:node-pty/ConPTY 直接 spawn 会抛 "Cannot create process, error code: 193"
|
|
209
|
+
* (ERROR_BAD_EXE_FORMAT)。win32 只接受 .exe 行;POSIX 取第一行。
|
|
210
|
+
* 导出供单测;生产代码经 resolveNativePath 调用。
|
|
211
|
+
*/
|
|
212
|
+
export function pickSpawnableLookupResult(rawOut, platform = process.platform) {
|
|
213
|
+
const lines = String(rawOut || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
214
|
+
if (platform === 'win32') return lines.find((l) => l.toLowerCase().endsWith('.exe')) || null;
|
|
215
|
+
return lines[0] || null;
|
|
216
|
+
}
|
|
217
|
+
|
|
204
218
|
export function resolveNativePath() {
|
|
205
219
|
const globalRoot = getGlobalNodeModulesDir();
|
|
206
220
|
|
|
@@ -220,7 +234,8 @@ export function resolveNativePath() {
|
|
|
220
234
|
for (const cmd of lookupCmds) {
|
|
221
235
|
try {
|
|
222
236
|
const rawOut = execSync(cmd, { encoding: 'utf-8', shell: true, env: process.env, windowsHide: true });
|
|
223
|
-
|
|
237
|
+
// win32 过滤掉 sh shim / .cmd / .ps1,只取 .exe(否则 ConPTY spawn 报 error 193)
|
|
238
|
+
const result = pickSpawnableLookupResult(rawOut);
|
|
224
239
|
if (result && existsSync(result)) {
|
|
225
240
|
// 只排除 .js 文件(老版本 npm 分发的 cli.js,需要 node 运行,
|
|
226
241
|
// 由 resolveNpmClaudePath 处理)。Claude Code 2.x+ 的 npm 包内
|
|
@@ -249,6 +264,11 @@ export function resolveNativePath() {
|
|
|
249
264
|
if (existsSync(p)) {
|
|
250
265
|
return p;
|
|
251
266
|
}
|
|
267
|
+
// Windows 原生安装器(install.ps1)落的是 claude.exe(如 ~/.local/bin/claude.exe),
|
|
268
|
+
// 无扩展名候选在 win32 上永远 miss,这里补查 .exe 变体。
|
|
269
|
+
if (process.platform === 'win32' && existsSync(p + '.exe')) {
|
|
270
|
+
return p + '.exe';
|
|
271
|
+
}
|
|
252
272
|
}
|
|
253
273
|
|
|
254
274
|
// 4. 兜底:wrapper 包的 bin/claude(.exe)(可能是 postinstall 后的真实二进制,
|
package/package.json
CHANGED
|
@@ -7,9 +7,14 @@
|
|
|
7
7
|
* (bufferedAmount > 1MB)介入,快 LAN 上洪泛字节全量到达前端,xterm 逐帧解析渲染
|
|
8
8
|
* 稠密 SGR+CJK 重绘把主线程打满。本器在发送侧限流:
|
|
9
9
|
*
|
|
10
|
-
* - 直通态:chunk 立即 send
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* - 直通态:leading-edge 微合并——空窗期首 chunk 立即 send(回显零延迟)并开
|
|
11
|
+
* ptCoalesceMs(默认 16ms)窗,同窗后续 chunk 并入 ptBuffer、到点合为一条 send
|
|
12
|
+
* (上限 2 条/窗 = 1000ms÷16ms×2 ≈125 msg/s)。低于洪泛阈值的持续小 chunk 流(/plugins 菜单导航
|
|
13
|
+
* 等 ConPTY 重绘)每 chunk 单发会打出数百条 ws 消息/秒,客户端逐条 MessageEvent
|
|
14
|
+
* 分发 + JSON.parse + xterm 主线程解析,**消息数风暴**即可锁死页面——字节率
|
|
15
|
+
* 限流(下方限流态)封不住这个维度。字节率仍按固定 flushMs 桶统计,
|
|
16
|
+
* 当前桶累计超 floodThresholdBytesPerWin → 进入限流态(ptBuffer 按序折入 pending)。
|
|
17
|
+
* 打字回显 / 正常 token 流是稀疏 chunk,每条都走 leading 立即发,不受影响。
|
|
13
18
|
* - 限流态:chunk 剥掉自带的 DEC 2026 标记后追加进 pending(pending 内部因此
|
|
14
19
|
* **绝无 2026 标记**,截断永不切坏配对);每 flushMs 把 pending 用单对
|
|
15
20
|
* SYNC_BEGIN/END 重新包裹成**一条** send 发出并清空(无论下游是否跳发,
|
|
@@ -37,10 +42,23 @@ const SYNC_END = '\x1b[?2026l';
|
|
|
37
42
|
const SYNC_MARKS_RE = /\x1b\[\?2026[hl]/g;
|
|
38
43
|
|
|
39
44
|
// 默认常量可经 CCV_FLOOD_* 环境变量覆盖(仿 CCV_FORCE_POLL 先例),便于 Windows
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
// 实机排障时调参而不改源码。严格十进制白名单:parseInt 遇非数字字符即截停——
|
|
46
|
+
// '1e9' 解析成 1、'0x10' 解析成 0,静默生效远比回落默认值危险,非纯数字一律回落。
|
|
47
|
+
// 位数上限 15(< Number.MAX_SAFE_INTEGER):超长数字串 parseInt 溢出为 Infinity 会
|
|
48
|
+
// 穿透 v>0 判断,setTimeout(fn, Infinity) 被 Node 钳到 1ms——33ms 桶宽静默变 1ms。
|
|
49
|
+
// 导出供 server.js 等复用(knob 解析逻辑收敛在此,不再各处内联)。
|
|
50
|
+
export function envInt(name, fallback) {
|
|
51
|
+
const s = (process.env[name] ?? '').trim();
|
|
52
|
+
if (!/^\d{1,15}$/.test(s)) return fallback;
|
|
53
|
+
const v = parseInt(s, 10);
|
|
54
|
+
return v > 0 ? v : fallback;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 同 envInt 但接受 0(0 = 关闭该功能的逃生口,envInt 的 v>0 会把 0 误回落默认值)。
|
|
58
|
+
export function envIntAllowZero(name, fallback) {
|
|
59
|
+
const s = (process.env[name] ?? '').trim();
|
|
60
|
+
if (!/^\d{1,15}$/.test(s)) return fallback;
|
|
61
|
+
return parseInt(s, 10);
|
|
44
62
|
}
|
|
45
63
|
|
|
46
64
|
const DEFAULT_FLUSH_MS = envInt('CCV_FLOOD_FLUSH_MS', 33); // 限流态合并窗口 = 字节率统计桶宽
|
|
@@ -50,6 +68,12 @@ const DEFAULT_PENDING_CAP = envInt('CCV_FLOOD_PENDING_CAP', 256 * 1024);
|
|
|
50
68
|
const DEFAULT_TRIM_TO = envInt('CCV_FLOOD_TRIM_TO', 128 * 1024); // pendingCap 截断后保留的尾部量
|
|
51
69
|
// 单次 flush 发送预算 = 真速率上限:64KB / 33ms ≈ 1.9MB/s,与前端 32KB/帧消化速率同量级
|
|
52
70
|
const DEFAULT_FLUSH_BUDGET = envInt('CCV_FLOOD_FLUSH_BUDGET', 64 * 1024);
|
|
71
|
+
// 直通态微合并窗口:低于洪泛阈值的持续小 chunk 流(如 /plugins 菜单导航的 ConPTY 重绘)
|
|
72
|
+
// 每 chunk 单发会打出每秒数百条 ws 消息——客户端每条都付 MessageEvent 分发 + JSON.parse +
|
|
73
|
+
// xterm 主线程同步解析,**消息数风暴**(非字节率)即可锁死页面(xterm.js#3368)。
|
|
74
|
+
// leading-edge 立即发(回显零延迟)+ 同窗后续合并 trailing 一条
|
|
75
|
+
// → 上限 2 条/窗 ≈125 msg/s(1000ms ÷ 16ms × 2 条)。0 = 禁用。
|
|
76
|
+
const DEFAULT_PT_COALESCE_MS = envIntAllowZero('CCV_FLOOD_PT_COALESCE_MS', 16);
|
|
53
77
|
|
|
54
78
|
/**
|
|
55
79
|
* @param {object} opts
|
|
@@ -63,6 +87,7 @@ const DEFAULT_FLUSH_BUDGET = envInt('CCV_FLOOD_FLUSH_BUDGET', 64 * 1024);
|
|
|
63
87
|
* @param {number} [opts.pendingCap]
|
|
64
88
|
* @param {number} [opts.trimTo]
|
|
65
89
|
* @param {number} [opts.flushBudgetBytes]
|
|
90
|
+
* @param {number} [opts.ptCoalesceMs] - 直通态微合并窗口(0 = 禁用,每 chunk 单发)
|
|
66
91
|
* @param {(fn: Function, ms: number) => any} [opts.setTimer] - 测试注入
|
|
67
92
|
* @param {(t: any) => void} [opts.clearTimer] - 测试注入
|
|
68
93
|
* @returns {{ offer: (chunk: string) => void, reset: () => void, dispose: () => void, isFlooding: () => boolean }}
|
|
@@ -78,6 +103,7 @@ export function createFloodCoalescer({
|
|
|
78
103
|
pendingCap = DEFAULT_PENDING_CAP,
|
|
79
104
|
trimTo = DEFAULT_TRIM_TO,
|
|
80
105
|
flushBudgetBytes = DEFAULT_FLUSH_BUDGET,
|
|
106
|
+
ptCoalesceMs = DEFAULT_PT_COALESCE_MS,
|
|
81
107
|
setTimer = setTimeout,
|
|
82
108
|
clearTimer = clearTimeout,
|
|
83
109
|
}) {
|
|
@@ -86,6 +112,8 @@ export function createFloodCoalescer({
|
|
|
86
112
|
let winBytes = 0; // 当前桶累计字节(直通态由 offer 累计,限流态由 flush 结算)
|
|
87
113
|
let calmWins = 0; // 连续低于阈值的桶数
|
|
88
114
|
let flushTimer = null; // 限流态周期 flush;直通态下亦作为桶边界 timer(见 offer)
|
|
115
|
+
let ptBuffer = ''; // 直通态微合并缓冲(窗口开启期间到达的后续 chunk)
|
|
116
|
+
let ptTimer = null; // 直通态微合并窗口 timer(16ms),与 flushTimer(33ms 字节桶)并存、职责正交
|
|
89
117
|
let disposed = false;
|
|
90
118
|
|
|
91
119
|
const stopTimer = () => {
|
|
@@ -95,6 +123,24 @@ export function createFloodCoalescer({
|
|
|
95
123
|
}
|
|
96
124
|
};
|
|
97
125
|
|
|
126
|
+
const stopPtTimer = () => {
|
|
127
|
+
if (ptTimer) {
|
|
128
|
+
clearTimer(ptTimer);
|
|
129
|
+
ptTimer = null;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// 微合并窗口到点:缓冲非空则一条发出,窗口关闭(不自动续约——下一 chunk 重新 leading 立即发)。
|
|
134
|
+
// ptBuffer 不剥 SYNC 标记:terminal 路径每 chunk 经 pty-manager flushBatch 已自配平(拼接守恒),
|
|
135
|
+
// scratch 路径 chunk 本就无标记(拼接平凡守恒);只有洪泛路径(有截断)才需要剥。
|
|
136
|
+
const onPtFlush = () => {
|
|
137
|
+
ptTimer = null;
|
|
138
|
+
if (disposed || flooding || !ptBuffer) return;
|
|
139
|
+
const out = ptBuffer;
|
|
140
|
+
ptBuffer = '';
|
|
141
|
+
try { send(out); } catch { }
|
|
142
|
+
};
|
|
143
|
+
|
|
98
144
|
// 直通态的桶边界:到点清零计数。无流量时 timer 不存在,零常驻开销。
|
|
99
145
|
const armPassthroughWindow = () => {
|
|
100
146
|
if (flushTimer) return;
|
|
@@ -143,21 +189,35 @@ export function createFloodCoalescer({
|
|
|
143
189
|
/** 每条 PTY chunk 调用。直通态立即 send;限流态进 pending 等周期 flush。 */
|
|
144
190
|
offer(chunk) {
|
|
145
191
|
if (disposed || !chunk) return;
|
|
146
|
-
winBytes += chunk.length;
|
|
192
|
+
winBytes += chunk.length; // 缓冲与直发都全量计账:微合并不致盲洪泛判定
|
|
147
193
|
if (!flooding) {
|
|
148
194
|
if (winBytes > floodThresholdBytesPerWin) {
|
|
149
|
-
// 进入限流态:当前 chunk
|
|
150
|
-
//
|
|
195
|
+
// 进入限流态:当前 chunk 是压垮桶的那条,连同微合并缓冲中未发的旧 chunk
|
|
196
|
+
// 按序一并纳入 pending(已 leading 发出的部分不回收——量级在阈值内)。
|
|
197
|
+
// 注意 stopTimer 只清 flushTimer,ptTimer 须显式清,否则残留窗口会在洪泛
|
|
198
|
+
// 期间触发 onPtFlush(虽有 flooding 守卫兜底,仍以显式清为准)。
|
|
151
199
|
flooding = true;
|
|
152
200
|
calmWins = 0;
|
|
153
201
|
stopTimer();
|
|
154
|
-
|
|
202
|
+
stopPtTimer();
|
|
203
|
+
pending = (ptBuffer + chunk).replace(SYNC_MARKS_RE, '');
|
|
204
|
+
ptBuffer = '';
|
|
155
205
|
flushTimer = setTimer(onFloodTick, flushMs);
|
|
156
206
|
flushTimer.unref?.();
|
|
157
207
|
try { onFloodStart?.(winBytes); } catch { }
|
|
158
208
|
return;
|
|
159
209
|
}
|
|
160
210
|
armPassthroughWindow();
|
|
211
|
+
if (ptCoalesceMs > 0) {
|
|
212
|
+
// 微合并:窗口开启(ptTimer 在跑)→ 追加缓冲不发;窗口关闭 → leading 立即发
|
|
213
|
+
// (单次回显零延迟)并开窗。上限 2 条/窗(leading + trailing flush)。
|
|
214
|
+
if (ptTimer) {
|
|
215
|
+
ptBuffer += chunk;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
ptTimer = setTimer(onPtFlush, ptCoalesceMs);
|
|
219
|
+
ptTimer.unref?.();
|
|
220
|
+
}
|
|
161
221
|
try { send(chunk); } catch { }
|
|
162
222
|
return;
|
|
163
223
|
}
|
|
@@ -170,10 +230,12 @@ export function createFloodCoalescer({
|
|
|
170
230
|
pending = pending.slice(safeStart);
|
|
171
231
|
}
|
|
172
232
|
},
|
|
173
|
-
/** bpGate onBehind/onResume 时调用:resync 快照是唯一真相源,清掉旧 pending 防回灌。 */
|
|
233
|
+
/** bpGate onBehind/onResume 时调用:resync 快照是唯一真相源,清掉旧 pending/ptBuffer 防回灌。 */
|
|
174
234
|
reset() {
|
|
175
235
|
stopTimer();
|
|
236
|
+
stopPtTimer();
|
|
176
237
|
pending = '';
|
|
238
|
+
ptBuffer = '';
|
|
177
239
|
winBytes = 0;
|
|
178
240
|
calmWins = 0;
|
|
179
241
|
flooding = false;
|
|
@@ -185,7 +247,9 @@ export function createFloodCoalescer({
|
|
|
185
247
|
dispose() {
|
|
186
248
|
disposed = true;
|
|
187
249
|
stopTimer();
|
|
250
|
+
stopPtTimer();
|
|
188
251
|
pending = '';
|
|
252
|
+
ptBuffer = '';
|
|
189
253
|
},
|
|
190
254
|
};
|
|
191
255
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resync 重绘 nudge 冷却门(防 behind→resume 死循环的退避器)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:ws-backpressure onResume 在发 data-resync 快照后会做一次重绘 nudge
|
|
5
|
+
* (POSIX SIGWINCH / Windows resize 抖动),让 claude TUI 全屏重绘以免画面停在
|
|
6
|
+
* 快照静止态。但 nudge 本身让 ConPTY 再吐 1~2 次全屏重绘 = 新洪泛燃料:客户端
|
|
7
|
+
* 仍慢 → bufferedAmount 再越线 → behind → resume → 再 nudge → 死循环,客户端
|
|
8
|
+
* 每轮 terminal.reset + 重放快照,表现为永久冻结。
|
|
9
|
+
*
|
|
10
|
+
* 语义:快照每次 resume 仍无条件发(修复 behind 期间被跳发的数据,不能省);
|
|
11
|
+
* 只有 nudge 走冷却——紧循环中 PTY 输出仍在流动,"画面停在快照"的风险不存在,
|
|
12
|
+
* 该风险只在 resume 稀疏时成立,而稀疏 resume 必然过冷却期、照常 nudge。
|
|
13
|
+
*
|
|
14
|
+
* 纯逻辑、时钟可注入(now),便于单测。仿 pty-flood-coalescer.js 惯例。
|
|
15
|
+
*
|
|
16
|
+
* @param {object} [opts]
|
|
17
|
+
* @param {number} [opts.cooldownMs=3000] - 两次 nudge 最小间隔;0 = 不冷却(恒放行,逃生口)
|
|
18
|
+
* @param {() => number} [opts.now=Date.now] - 测试注入
|
|
19
|
+
* @returns {{ shouldNudge: () => boolean }}
|
|
20
|
+
*/
|
|
21
|
+
export function createResyncNudgeGate({ cooldownMs = 3000, now = Date.now } = {}) {
|
|
22
|
+
let lastNudgeAt = -Infinity; // 首次必放行
|
|
23
|
+
return {
|
|
24
|
+
/** resume 时调用:放行则记账并返回 true,冷却期内返回 false(调用方跳过 nudge)。 */
|
|
25
|
+
shouldNudge() {
|
|
26
|
+
if (cooldownMs <= 0) return true;
|
|
27
|
+
const t = now();
|
|
28
|
+
if (t - lastNudgeAt < cooldownMs) return false;
|
|
29
|
+
lastNudgeAt = t;
|
|
30
|
+
return true;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -7,6 +7,21 @@ import { discoverClaudeMdCandidates, readCandidateById } from '../lib/claude-md-
|
|
|
7
7
|
import { getClaudeConfigDir } from '../../findcc.js';
|
|
8
8
|
import { _projectName } from '../interceptor.js';
|
|
9
9
|
|
|
10
|
+
// file-raw 扩展名 → MIME 映射。图片走 <img> 预览;html 走 iframe 预览(带下方 CSP sandbox);
|
|
11
|
+
// 其余为 HTML 预览的同目录子资源(c8/nyc/lcov-genhtml 等静态报告):CSP sandbox 使文档
|
|
12
|
+
// 处于 opaque origin,对一切子资源都是跨源——跨源样式表受浏览器严格 MIME 校验,
|
|
13
|
+
// octet-stream 的 CSS 会被直接拒用(报告渲染成裸 HTML)。js 虽为 classic script 不受
|
|
14
|
+
// MIME 门禁,配正确类型属卫生项 + 为未来 nosniff 铺路。
|
|
15
|
+
// 注意:值不带 charset 后缀——fileRaw 内 CSP 判等依赖 mime === 'text/html'。
|
|
16
|
+
const FILE_RAW_MIME = {
|
|
17
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
18
|
+
'.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon',
|
|
19
|
+
'.webp': 'image/webp', '.html': 'text/html', '.htm': 'text/html',
|
|
20
|
+
'.css': 'text/css', '.js': 'text/javascript', '.mjs': 'text/javascript',
|
|
21
|
+
'.json': 'application/json', '.map': 'application/json', '.txt': 'text/plain',
|
|
22
|
+
'.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf',
|
|
23
|
+
};
|
|
24
|
+
|
|
10
25
|
function planFile(req, res, parsedUrl) {
|
|
11
26
|
try {
|
|
12
27
|
const raw = parsedUrl.searchParams.get('path') || '';
|
|
@@ -290,13 +305,8 @@ function fileRaw(req, res, parsedUrl) {
|
|
|
290
305
|
res.end(JSON.stringify({ error: 'File too large' }));
|
|
291
306
|
return;
|
|
292
307
|
}
|
|
293
|
-
const extMime = {
|
|
294
|
-
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
295
|
-
'.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon',
|
|
296
|
-
'.webp': 'image/webp', '.html': 'text/html', '.htm': 'text/html',
|
|
297
|
-
};
|
|
298
308
|
const ext = (targetFile.match(/\.[^.]+$/) || [''])[0].toLowerCase();
|
|
299
|
-
const mime =
|
|
309
|
+
const mime = FILE_RAW_MIME[ext] || 'application/octet-stream';
|
|
300
310
|
const data = method === 'HEAD' ? null : readFileSync(targetFile);
|
|
301
311
|
const size = method === 'HEAD' ? stat.size : data.length;
|
|
302
312
|
const headers = { 'Content-Type': mime, 'Content-Length': size };
|
package/server/server.js
CHANGED
|
@@ -81,7 +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 } from './lib/pty-flood-coalescer.js';
|
|
84
|
+
import { createFloodCoalescer, envIntAllowZero } from './lib/pty-flood-coalescer.js';
|
|
85
|
+
import { createResyncNudgeGate } from './lib/resync-nudge-gate.js';
|
|
85
86
|
|
|
86
87
|
|
|
87
88
|
// 动态获取 getPrefsFile()(LOG_DIR 可能在运行时被 setLogDir 修改)
|
|
@@ -1121,20 +1122,30 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1121
1122
|
|
|
1122
1123
|
// 反压状态转换日志(observability:线上排"页面卡"时可直接判断是否触发过反压、几次、积压量)。
|
|
1123
1124
|
// behind/resume 在持续洪泛下以亚秒级周期振荡,5s 节流防刷屏;timeout 是终态必记。
|
|
1125
|
+
// resyncTotal/nudgeSkipped 判别 resync 循环:resync 涨 + skipped 同涨 = nudge 冷却在救;
|
|
1126
|
+
// resync 涨 + skipped≈0 = resume 间隔超过冷却期的慢振荡,需更激进策略(如洪泛期禁 nudge)。
|
|
1124
1127
|
const makeBpLogger = (label, ws) => {
|
|
1125
1128
|
let behindCount = 0;
|
|
1129
|
+
let resyncCount = 0;
|
|
1130
|
+
let nudgeSkipped = 0;
|
|
1126
1131
|
let lastLogAt = 0;
|
|
1127
1132
|
return (event, buffered) => {
|
|
1128
1133
|
if (event === 'behind') behindCount++;
|
|
1134
|
+
if (event === 'resume') resyncCount++;
|
|
1135
|
+
if (event === 'nudge-skip') { nudgeSkipped++; return; } // 只计数,随下一条节流日志带出
|
|
1129
1136
|
const now = Date.now();
|
|
1130
1137
|
if (event !== 'timeout' && now - lastLogAt < 5000) return;
|
|
1131
1138
|
lastLogAt = now;
|
|
1132
|
-
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}`);
|
|
1133
1140
|
};
|
|
1134
1141
|
};
|
|
1135
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
|
+
|
|
1136
1146
|
// 洪泛限流器状态日志(与 makeBpLogger 同款 5s 节流,独立实例不共享计数)。
|
|
1137
1147
|
// Windows 实机排"切主题/大流量卡死"时据此确认 ConPTY 洪泛是否触发、几次、量级。
|
|
1148
|
+
// 'rate' 事件 = 直通态 ws 消息率告警(msgsPerSec,计数在 send 闭包),判别消息数风暴。
|
|
1138
1149
|
const makeFloodLogger = (label, ws) => {
|
|
1139
1150
|
let floodCount = 0;
|
|
1140
1151
|
let lastLogAt = 0;
|
|
@@ -1143,7 +1154,23 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1143
1154
|
const now = Date.now();
|
|
1144
1155
|
if (now - lastLogAt < 5000) return;
|
|
1145
1156
|
lastLogAt = now;
|
|
1146
|
-
|
|
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++;
|
|
1147
1174
|
};
|
|
1148
1175
|
};
|
|
1149
1176
|
|
|
@@ -1196,10 +1223,12 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1196
1223
|
// 洪泛限流器:字节率超阈值时按窗口合并 + last-wins 截断(ConPTY 全屏重绘洪泛防卡死,
|
|
1197
1224
|
// 与 bpGate 互补——bpGate 管慢网络写缓冲,floodGate 管快 LAN 字节率,详见 lib/pty-flood-coalescer.js)
|
|
1198
1225
|
const _floodLog = makeFloodLogger('scratch-ws', ws);
|
|
1226
|
+
const _countMsg = makeMsgRateCounter(_floodLog);
|
|
1199
1227
|
floodGate = createFloodCoalescer({
|
|
1200
1228
|
send: (data) => {
|
|
1201
1229
|
if (ws.readyState === 1 && bpGate.offer()) {
|
|
1202
1230
|
try { ws.send(JSON.stringify({ type: 'data', data })); } catch {}
|
|
1231
|
+
_countMsg();
|
|
1203
1232
|
}
|
|
1204
1233
|
},
|
|
1205
1234
|
findSafeSliceStart,
|
|
@@ -1293,6 +1322,10 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1293
1322
|
// floodGate 前向引用(构造顺序同 scratch 路径):onBehind/onResume 必清 coalescer
|
|
1294
1323
|
// pending——resync 快照是唯一真相源,旧 pending 回灌会导致画面回退。
|
|
1295
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 });
|
|
1296
1329
|
const bpGate = createBackpressureGate({
|
|
1297
1330
|
getBufferedAmount: () => ws.bufferedAmount,
|
|
1298
1331
|
onBehind: (buffered) => {
|
|
@@ -1304,6 +1337,7 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1304
1337
|
floodGate?.reset();
|
|
1305
1338
|
if (ws.readyState !== 1) return;
|
|
1306
1339
|
try { ws.send(JSON.stringify({ type: 'data-resync', data: getOutputBuffer() })); } catch {}
|
|
1340
|
+
if (!nudgeGate.shouldNudge()) { _bpLog('nudge-skip', buffered); return; }
|
|
1307
1341
|
try {
|
|
1308
1342
|
if (process.platform !== 'win32') {
|
|
1309
1343
|
// POSIX:与下方 _needRedrawBootstrap 同款 SIGWINCH 兜底
|
|
@@ -1333,10 +1367,12 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1333
1367
|
// 洪泛限流器:字节率超阈值时按窗口合并 + last-wins 截断(ConPTY 全屏重绘洪泛防卡死,
|
|
1334
1368
|
// 与 bpGate 互补——bpGate 管慢网络写缓冲,floodGate 管快 LAN 字节率,详见 lib/pty-flood-coalescer.js)
|
|
1335
1369
|
const _floodLog = makeFloodLogger('terminal-ws', ws);
|
|
1370
|
+
const _countMsg = makeMsgRateCounter(_floodLog);
|
|
1336
1371
|
floodGate = createFloodCoalescer({
|
|
1337
1372
|
send: (data) => {
|
|
1338
1373
|
if (ws.readyState === 1 && bpGate.offer()) {
|
|
1339
1374
|
try { ws.send(JSON.stringify({ type: 'data', data })); } catch {}
|
|
1375
|
+
_countMsg();
|
|
1340
1376
|
}
|
|
1341
1377
|
},
|
|
1342
1378
|
findSafeSliceStart,
|