cc-viewer 1.6.307 → 1.6.308
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-PNzjx_9T.js → App-b-xfmA9m.js} +1 -1
- package/dist/assets/{MdxEditorPanel-CPWegamu.js → MdxEditorPanel-CotnkeiB.js} +1 -1
- package/dist/assets/{Mobile-Dep0eTnv.js → Mobile-B3EBr5U5.js} +1 -1
- package/dist/assets/{index-Bxq6pvmI.js → index-BD-SSlan.js} +2 -2
- package/dist/assets/seqResourceLoaders-D2-X1l9p.js +2 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/interceptor.js +15 -0
- package/server/lib/context-watcher.js +4 -4
- package/server/lib/delta-reconstructor.js +242 -120
- package/server/lib/log-management.js +19 -0
- package/server/lib/log-stream.js +6 -2
- package/server/pty-manager.js +51 -4
- package/server/scratch-pty-manager.js +11 -3
- package/server/server.js +17 -10
- package/dist/assets/seqResourceLoaders-DW4tbU6n.js +0 -2
package/server/pty-manager.js
CHANGED
|
@@ -26,6 +26,14 @@ let currentWorkspacePath = null;
|
|
|
26
26
|
let lastWorkspacePath = null; // 进程退出后保留,用于 respawn shell
|
|
27
27
|
let lastPtyCols = 120;
|
|
28
28
|
let lastPtyRows = 30;
|
|
29
|
+
// 主 PTY spawn 的在途闸:guard 在 await getPty 之前、ptyProcess 赋值在 await 之后,
|
|
30
|
+
// 两条同步到达的 input 消息会越过 guard 双开(首个 pty 失引用泄漏 + 输出串扰)。
|
|
31
|
+
// 同步占位一个 promise,并发调用复用它,绝不二次 spawn(仿 scratch-pty-manager._spawnInflight)。
|
|
32
|
+
let _spawnInflight = null;
|
|
33
|
+
// resize 入口的 cols/rows 钳制范围:上界足够宽(4K 显示器超宽终端),下界 ≥2 列/1 行
|
|
34
|
+
// 防 FitAddon 在 0 尺寸容器算出的 2×1 或畸形客户端的 NaN/负数毒化 lastPtyCols/Rows。
|
|
35
|
+
const PTY_COLS_MIN = 2, PTY_COLS_MAX = 1000;
|
|
36
|
+
const PTY_ROWS_MIN = 1, PTY_ROWS_MAX = 1000;
|
|
29
37
|
const MAX_BUFFER = 200000;
|
|
30
38
|
let batchBuffer = '';
|
|
31
39
|
let batchScheduled = false;
|
|
@@ -157,10 +165,19 @@ export function _markThinkingDisplayRejected(claudePath) {
|
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
export async function spawnClaude(proxyPort, cwd, extraArgs = [], claudePath = null, isNpmVersion = false, serverPort = null, serverProtocol = 'http', internalToken = null) {
|
|
168
|
+
// 等待任何在途 spawn 完成再 kill+spawn,避免与 spawnShell 双开/串台(自身串行化)。
|
|
169
|
+
// while 而非 if:≥3 个并发 spawn 时,A 完成后 B 会设新的 inflight=pB,单次 if 的 C
|
|
170
|
+
// 不会复查 pB 就 kill+spawn 致 implB/implC 并发双开——循环到真正无在途为止才放行。
|
|
171
|
+
while (_spawnInflight) { try { await _spawnInflight; } catch { } }
|
|
160
172
|
if (ptyProcess) {
|
|
161
173
|
killPty();
|
|
162
174
|
}
|
|
175
|
+
const p = _spawnClaudeImpl(proxyPort, cwd, extraArgs, claudePath, isNpmVersion, serverPort, serverProtocol, internalToken);
|
|
176
|
+
_spawnInflight = p;
|
|
177
|
+
try { return await p; } finally { if (_spawnInflight === p) _spawnInflight = null; }
|
|
178
|
+
}
|
|
163
179
|
|
|
180
|
+
async function _spawnClaudeImpl(proxyPort, cwd, extraArgs = [], claudePath = null, isNpmVersion = false, serverPort = null, serverProtocol = 'http', internalToken = null) {
|
|
164
181
|
const pty = await getPty();
|
|
165
182
|
|
|
166
183
|
fixSpawnHelperPermissions();
|
|
@@ -383,7 +400,21 @@ export function writeToPtySequential(chunks, onComplete, opts = {}) {
|
|
|
383
400
|
const chunk = chunks[idx];
|
|
384
401
|
idx++;
|
|
385
402
|
|
|
386
|
-
|
|
403
|
+
// 防御性纵深(server.js 入口已 every(string) 校验,这里是第二道):非字符串 chunk 的
|
|
404
|
+
// pty.write 抛 ERR_INVALID_ARG_TYPE、下方 chunk.endsWith 也抛——在 setTimeout 上下文中
|
|
405
|
+
// 脱离任何 try/catch 会变成 uncaughtException 打挂整个进程。统一拦成失败上报。
|
|
406
|
+
if (typeof chunk !== 'string') {
|
|
407
|
+
cleanup();
|
|
408
|
+
if (onComplete) onComplete(false);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
ptyProcess.write(chunk);
|
|
413
|
+
} catch (e) {
|
|
414
|
+
cleanup();
|
|
415
|
+
if (onComplete) onComplete(false);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
387
418
|
|
|
388
419
|
// Space, Enter, arrows need more time for inquirer to re-render
|
|
389
420
|
const isToggleOrSubmit = chunk === ' ' || chunk === '\r'
|
|
@@ -403,6 +434,13 @@ export function writeToPtySequential(chunks, onComplete, opts = {}) {
|
|
|
403
434
|
*/
|
|
404
435
|
export async function spawnShell() {
|
|
405
436
|
if (ptyProcess) return false; // 已有进程在运行
|
|
437
|
+
if (_spawnInflight) return _spawnInflight; // 复用在途 spawn,防双开
|
|
438
|
+
const p = _spawnShellImpl();
|
|
439
|
+
_spawnInflight = p;
|
|
440
|
+
try { return await p; } finally { if (_spawnInflight === p) _spawnInflight = null; }
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function _spawnShellImpl() {
|
|
406
444
|
const cwd = lastWorkspacePath || process.cwd();
|
|
407
445
|
|
|
408
446
|
const pty = await getPty();
|
|
@@ -465,11 +503,20 @@ export async function spawnShell() {
|
|
|
465
503
|
return true;
|
|
466
504
|
}
|
|
467
505
|
|
|
506
|
+
// cols/rows 钳制为有限正整数:FitAddon 在 0 尺寸容器会算出 2×1,畸形客户端可能发
|
|
507
|
+
// NaN/0/负数——未校验直接存进 lastPtyCols/Rows 会毒化后续 pty.spawn(cols:NaN 抛错,
|
|
508
|
+
// spawnShell 的异常被吞 → 终端永远拉不起且无日志)。非有限值回退到上一个有效值。
|
|
509
|
+
function _clampDim(v, min, max, fallback) {
|
|
510
|
+
const n = Math.floor(Number(v));
|
|
511
|
+
if (!Number.isFinite(n)) return fallback;
|
|
512
|
+
return Math.max(min, Math.min(max, n));
|
|
513
|
+
}
|
|
514
|
+
|
|
468
515
|
export function resizePty(cols, rows) {
|
|
469
|
-
lastPtyCols = cols;
|
|
470
|
-
lastPtyRows = rows;
|
|
516
|
+
lastPtyCols = _clampDim(cols, PTY_COLS_MIN, PTY_COLS_MAX, lastPtyCols);
|
|
517
|
+
lastPtyRows = _clampDim(rows, PTY_ROWS_MIN, PTY_ROWS_MAX, lastPtyRows);
|
|
471
518
|
if (ptyProcess) {
|
|
472
|
-
try { ptyProcess.resize(
|
|
519
|
+
try { ptyProcess.resize(lastPtyCols, lastPtyRows); } catch { }
|
|
473
520
|
}
|
|
474
521
|
}
|
|
475
522
|
|
|
@@ -203,13 +203,21 @@ export function writeScratch(id, data) {
|
|
|
203
203
|
return false;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
// cols/rows 钳制为有限正整数(同 pty-manager.resizePty 的理由:防 NaN/2×1 毒化 lastCols
|
|
207
|
+
// 后毒化 respawn)。非有限值回退到上一个有效值。
|
|
208
|
+
function _clampScratchDim(v, min, max, fallback) {
|
|
209
|
+
const n = Math.floor(Number(v));
|
|
210
|
+
if (!Number.isFinite(n)) return fallback;
|
|
211
|
+
return Math.max(min, Math.min(max, n));
|
|
212
|
+
}
|
|
213
|
+
|
|
206
214
|
export function resizeScratch(id, cols, rows) {
|
|
207
215
|
const s = ptys.get(id);
|
|
208
216
|
if (!s) return;
|
|
209
|
-
s.lastCols = cols;
|
|
210
|
-
s.lastRows = rows;
|
|
217
|
+
s.lastCols = _clampScratchDim(cols, 2, 1000, s.lastCols);
|
|
218
|
+
s.lastRows = _clampScratchDim(rows, 1, 1000, s.lastRows);
|
|
211
219
|
if (s.ptyProcess) {
|
|
212
|
-
try { s.ptyProcess.resize(
|
|
220
|
+
try { s.ptyProcess.resize(s.lastCols, s.lastRows); } catch { }
|
|
213
221
|
}
|
|
214
222
|
}
|
|
215
223
|
|
package/server/server.js
CHANGED
|
@@ -1470,16 +1470,23 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1470
1470
|
// 把 client 提供的 seq 透传回去 — 合并 ws 后多个发送方共享一条 ws,
|
|
1471
1471
|
// 只能靠 client 端按 seq 匹配自己发的请求(client 没传时也兼容,旧客户端不带 seq)。
|
|
1472
1472
|
const seq = msg.seq;
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
}
|
|
1473
|
+
// 元素必须全是字符串:非字符串 chunk 的 pty.write 在 setTimeout 上下文抛
|
|
1474
|
+
// ERR_INVALID_ARG_TYPE(脱离本 handler 的 try/catch)→ 进程崩溃。入口先拒绝。
|
|
1475
|
+
// 统一回 done(含拒绝路径 ok:false):带 seq 的客户端在等这条,静默丢弃会让它
|
|
1476
|
+
// 挂到自身超时。复用同一发送逻辑,无效输入立即回 ok:false。
|
|
1477
|
+
const replyDone = (ok) => {
|
|
1478
|
+
try {
|
|
1479
|
+
const reply = { type: 'input-sequential-done', ok };
|
|
1480
|
+
if (seq !== undefined) reply.seq = seq;
|
|
1481
|
+
ws.send(JSON.stringify(reply));
|
|
1482
|
+
} catch (e) {
|
|
1483
|
+
console.warn('[server] input-sequential-done send failed:', e?.message || e);
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
if (Array.isArray(chunks) && chunks.length > 0 && chunks.every(c => typeof c === 'string')) {
|
|
1487
|
+
writeToPtySequential(chunks, replyDone, { settleMs: msg.settleMs || 150 });
|
|
1488
|
+
} else {
|
|
1489
|
+
replyDone(false);
|
|
1483
1490
|
}
|
|
1484
1491
|
} else if (msg.type === 'ask-hook-answer') {
|
|
1485
1492
|
// Client answered AskUserQuestion via hook bridge.
|