cc-viewer 1.6.306 → 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-b-xfmA9m.js +2 -0
- package/dist/assets/{MdxEditorPanel-oSs95ieb.js → MdxEditorPanel-CotnkeiB.js} +1 -1
- package/dist/assets/{Mobile-CVLG_J2s.js → Mobile-B3EBr5U5.js} +1 -1
- package/dist/assets/{index-1bh2o4MD.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/im-log-watcher.js +94 -0
- 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/routes/im.js +2 -0
- package/server/scratch-pty-manager.js +11 -3
- package/server/server.js +40 -10
- package/dist/assets/App-Ob0BsD2o.js +0 -2
- package/dist/assets/seqResourceLoaders-CC7nzyEk.js +0 -2
package/server/lib/log-stream.js
CHANGED
|
@@ -162,11 +162,13 @@ export function streamReconstructedEntries(filePath, onSegment, opts = {}) {
|
|
|
162
162
|
let currentSegment = [];
|
|
163
163
|
let dedup = new Map();
|
|
164
164
|
let sentCount = 0;
|
|
165
|
+
// 跨段共享 seq 守卫状态:stale checkpoint 自成段边界,必须文件级跟踪才能识破乱序
|
|
166
|
+
const seqState = { lastSeq: 0, lastEpoch: null };
|
|
165
167
|
|
|
166
168
|
function flushSegment(nextCp) {
|
|
167
169
|
if (currentSegment.length === 0) return;
|
|
168
170
|
const dedupedSegment = Array.from(dedup.values());
|
|
169
|
-
reconstructSegment(dedupedSegment, nextCp);
|
|
171
|
+
reconstructSegment(dedupedSegment, nextCp, seqState);
|
|
170
172
|
|
|
171
173
|
let toSend = dedupedSegment;
|
|
172
174
|
if (sinceMs) {
|
|
@@ -217,11 +219,13 @@ export async function streamReconstructedEntriesAsync(filePath, onSegment, opts
|
|
|
217
219
|
let currentSegment = [];
|
|
218
220
|
let dedup = new Map();
|
|
219
221
|
let sentCount = 0;
|
|
222
|
+
// 跨段共享 seq 守卫状态:stale checkpoint 自成段边界,必须文件级跟踪才能识破乱序
|
|
223
|
+
const seqState = { lastSeq: 0, lastEpoch: null };
|
|
220
224
|
|
|
221
225
|
async function flushSegment(nextCp) {
|
|
222
226
|
if (currentSegment.length === 0) return;
|
|
223
227
|
const dedupedSegment = Array.from(dedup.values());
|
|
224
|
-
reconstructSegment(dedupedSegment, nextCp);
|
|
228
|
+
reconstructSegment(dedupedSegment, nextCp, seqState);
|
|
225
229
|
|
|
226
230
|
let toSend = dedupedSegment;
|
|
227
231
|
if (sinceMs) {
|
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
|
|
package/server/routes/im.js
CHANGED
|
@@ -212,6 +212,8 @@ function imProcessPost(req, res, parsedUrl, isLocal, deps) {
|
|
|
212
212
|
function imLogs(req, res, parsedUrl, isLocal, deps) {
|
|
213
213
|
const id = platformOf(parsedUrl.pathname);
|
|
214
214
|
if (!id) { notFound(res); return; }
|
|
215
|
+
// 弹窗打开即请求本接口 → 惰性登记该平台日志目录监听,后续写入经 im_log_update SSE 零滞后推送。
|
|
216
|
+
deps.ensureImWatch?.(id);
|
|
215
217
|
const project = `IM_${id}`;
|
|
216
218
|
let latest = null;
|
|
217
219
|
try {
|
|
@@ -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
|
@@ -77,6 +77,7 @@ import { checkAndUpdate } from './lib/updater.js';
|
|
|
77
77
|
import { loadPlugins, runWaterfallHook, runParallelHook } from './lib/plugin-loader.js';
|
|
78
78
|
import { CONTEXT_WINDOW_FILE, readModelContextSize } from './lib/context-watcher.js';
|
|
79
79
|
import { watchLogFile, startWatching, unwatchAll, sendEventToClients, sendToClients } from './lib/log-watcher.js';
|
|
80
|
+
import { createImLogWatcher } from './lib/im-log-watcher.js';
|
|
80
81
|
import { unwatchAllWorkflows } from './lib/workflow-watcher.js';
|
|
81
82
|
import { cleanupExtractCache } from './lib/jsonl-archive.js';
|
|
82
83
|
import { backupConfigs } from './lib/config-backup.js';
|
|
@@ -440,6 +441,25 @@ function getAllLocalIps() {
|
|
|
440
441
|
// exposed via GETTERS (read fresh at request time — never captured at import), while
|
|
441
442
|
// never-reassigned Maps/arrays are shared by reference. Helpers/constants that live in
|
|
442
443
|
// server.js (not importable elsewhere) are funneled through here too.
|
|
444
|
+
|
|
445
|
+
// IM 日志目录监听器:仅主 web 服务启用(IM worker 无浏览器 SSE 客户端,广播无意义)。
|
|
446
|
+
// 惰性 ensure:「对话记录」弹窗请求 /api/im/:platform/logs 时才开始 watch 该平台目录;
|
|
447
|
+
// 写入即广播 im_log_update SSE → 前端零滞后重拉(详见 im-log-watcher.js / AppBase im_log_update 监听)。
|
|
448
|
+
const _imLogWatcher = process.env.CCV_IM_PLATFORM ? null : createImLogWatcher({
|
|
449
|
+
getLogDir: () => LOG_DIR,
|
|
450
|
+
onChange: (platform) => {
|
|
451
|
+
if (clients.length > 0 && sendEventToClients) {
|
|
452
|
+
sendEventToClients(clients, 'im_log_update', { platform, ts: Date.now() });
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// 惰性登记 IM 日志目录监听(主服务);worker 上 _imLogWatcher 为 null → no-op。
|
|
458
|
+
function _ensureImWatch(id) {
|
|
459
|
+
try { _imLogWatcher?.ensure(id); }
|
|
460
|
+
catch (e) { console.warn(`[im-log-watcher] ensure(${id}) failed:`, e?.message || e); }
|
|
461
|
+
}
|
|
462
|
+
|
|
443
463
|
const deps = {
|
|
444
464
|
// Reassignable runtime state — must stay getters.
|
|
445
465
|
get protocol() { return serverProtocol; },
|
|
@@ -479,6 +499,7 @@ const deps = {
|
|
|
479
499
|
notifyParentPending: _notifyParentPending,
|
|
480
500
|
logWatcherOpts: _logWatcherOpts,
|
|
481
501
|
scheduleTurnEndBroadcast: _scheduleTurnEndBroadcast,
|
|
502
|
+
ensureImWatch: _ensureImWatch,
|
|
482
503
|
maskProfiles: _maskProfiles,
|
|
483
504
|
maskApiKey: _maskApiKey,
|
|
484
505
|
isMasked: _isMasked,
|
|
@@ -1154,6 +1175,8 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1154
1175
|
let lastLogAt = 0;
|
|
1155
1176
|
return (event, bytes) => {
|
|
1156
1177
|
if (event === 'start') floodCount++;
|
|
1178
|
+
// 该日志为 Windows 实机排洪泛卡死的观测点,macOS 下纯噪声,静默(限流逻辑本身不受影响)。
|
|
1179
|
+
if (process.platform === 'darwin') return;
|
|
1157
1180
|
const now = Date.now();
|
|
1158
1181
|
if (now - lastLogAt < 5000) return;
|
|
1159
1182
|
lastLogAt = now;
|
|
@@ -1447,16 +1470,23 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1447
1470
|
// 把 client 提供的 seq 透传回去 — 合并 ws 后多个发送方共享一条 ws,
|
|
1448
1471
|
// 只能靠 client 端按 seq 匹配自己发的请求(client 没传时也兼容,旧客户端不带 seq)。
|
|
1449
1472
|
const seq = msg.seq;
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
}
|
|
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);
|
|
1460
1490
|
}
|
|
1461
1491
|
} else if (msg.type === 'ask-hook-answer') {
|
|
1462
1492
|
// Client answered AskUserQuestion via hook bridge.
|