cc-viewer 1.6.248 → 1.6.249
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-Dpya-PM3.js → App-DKy6_SMN.js} +1 -1
- package/dist/assets/{MdxEditorPanel-DMxSX9QP.js → MdxEditorPanel-6LZbpGlJ.js} +1 -1
- package/dist/assets/{Mobile-B60fQsoC.js → Mobile-DPKvAzpB.js} +1 -1
- package/dist/assets/ProxyModal-BdlASICk.js +2 -0
- package/dist/assets/index-etQ0_qi5.js +2 -0
- package/dist/index.html +1 -1
- package/interceptor.js +38 -8
- package/lib/interceptor-core.js +38 -0
- package/package.json +1 -1
- package/dist/assets/ProxyModal-CFWAqXkN.js +0 -2
- package/dist/assets/index-1OtQ-y7A.js +0 -2
package/dist/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>Claude Code Viewer</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico?v=1">
|
|
8
8
|
<link rel="shortcut icon" href="/favicon.ico?v=1">
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-etQ0_qi5.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-antd-BeN8xqGk.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-2nbmPewy.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/vendor-mdxeditor-C7DYEBoH.js">
|
package/interceptor.js
CHANGED
|
@@ -13,7 +13,7 @@ import { homedir } from 'node:os';
|
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
14
14
|
import { dirname, join, basename } from 'node:path';
|
|
15
15
|
import { LOG_DIR } from './findcc.js';
|
|
16
|
-
import { assembleStreamMessage, createStreamAssembler, cleanupTempFiles, findRecentLog, isAnthropicApiPath, isMainAgentRequest, rotateLogFile } from './lib/interceptor-core.js';
|
|
16
|
+
import { assembleStreamMessage, createStreamAssembler, cleanupTempFiles, findRecentLog, isAnthropicApiPath, isMainAgentRequest, rotateLogFile, fingerprintMsg } from './lib/interceptor-core.js';
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
|
|
@@ -239,14 +239,21 @@ function resolveResumeChoice(choice) {
|
|
|
239
239
|
// Delta storage: 增量存储开关和状态(默认开启,设置 CCV_DISABLE_DELTA=1 关闭)
|
|
240
240
|
// 注意:delta 计算依赖 mainAgent 请求串行(Claude CLI 保证),不做并发互斥
|
|
241
241
|
const _deltaStorageEnabled = process.env.CCV_DISABLE_DELTA !== '1';
|
|
242
|
+
// In-place last-msg replace 检测开关(默认开启,设置 CCV_DISABLE_TAIL_FP_CHECKPOINT=1 关闭)。
|
|
243
|
+
// 关闭后回退到旧行为(仅按长度算 delta,遇到末位原地替换会丢失"末位换内容"信息)。
|
|
244
|
+
const _tailFpCheckEnabled = process.env.CCV_DISABLE_TAIL_FP_CHECKPOINT !== '1';
|
|
242
245
|
let _lastMessagesCount = 0; // 上一次 mainAgent 写入的完整 messages 数量
|
|
246
|
+
let _lastTailFp = ''; // 上一次 mainAgent 末位 message 的指纹(用于 in-place replace 检测)
|
|
243
247
|
let _mainAgentDeltaCount = 0; // mainAgent 请求计数器(用于触发定期 checkpoint)
|
|
244
248
|
const CHECKPOINT_INTERVAL = 10; // 每 N 条 mainAgent 请求写一个 checkpoint
|
|
245
249
|
|
|
246
250
|
/** Delta storage: completed 写入成功后更新状态 */
|
|
247
|
-
function _commitDeltaState(originalLength) {
|
|
251
|
+
function _commitDeltaState(originalLength, originalTailFp) {
|
|
248
252
|
if (_deltaStorageEnabled && originalLength > 0) {
|
|
249
253
|
_lastMessagesCount = originalLength;
|
|
254
|
+
if (typeof originalTailFp === 'string') {
|
|
255
|
+
_lastTailFp = originalTailFp;
|
|
256
|
+
}
|
|
250
257
|
}
|
|
251
258
|
}
|
|
252
259
|
|
|
@@ -374,6 +381,7 @@ function checkAndRotateLogFile() {
|
|
|
374
381
|
// 重置 delta 状态,强制下一条 mainAgent 请求写完整 checkpoint
|
|
375
382
|
if (_deltaStorageEnabled) {
|
|
376
383
|
_lastMessagesCount = 0;
|
|
384
|
+
_lastTailFp = '';
|
|
377
385
|
_mainAgentDeltaCount = 0;
|
|
378
386
|
}
|
|
379
387
|
}
|
|
@@ -599,16 +607,33 @@ export function setupInterceptor() {
|
|
|
599
607
|
|
|
600
608
|
// Delta storage:仅 mainAgent 且开关启用时,将 body.messages 转为增量格式
|
|
601
609
|
let _deltaOriginalMessagesLength = 0; // 缓存本次请求的原始 messages 长度,用于 completed 后更新状态
|
|
610
|
+
let _deltaOriginalTailFp = ''; // 缓存本次请求末位 message 的指纹,用于 completed 后更新 _lastTailFp
|
|
602
611
|
if (_deltaStorageEnabled && requestEntry?.mainAgent && Array.isArray(requestEntry.body?.messages)) {
|
|
603
612
|
const messages = requestEntry.body.messages;
|
|
604
613
|
_deltaOriginalMessagesLength = messages.length;
|
|
614
|
+
// 立即把末位 fp 算成字符串保存(不存对象引用),避免后续 mutation 风险
|
|
615
|
+
_deltaOriginalTailFp = messages.length > 0 ? fingerprintMsg(messages[messages.length - 1]) : '';
|
|
605
616
|
_mainAgentDeltaCount++;
|
|
606
617
|
|
|
618
|
+
// In-place last-msg replace 检测:messages.length 不变但末位 fp 不同。
|
|
619
|
+
// 触发场景:CLI 在 mainAgent 末位"原地替换"user msg(SUGGESTION MODE → 用户真实输入;
|
|
620
|
+
// synthetic recap 通道注入;等),wire 上长度未变内容变了。旧逻辑 messages.slice(_lastMessagesCount)
|
|
621
|
+
// 算出 delta=[],丢失了"末位换内容"信息 → 客户端重建拿到错误的"前态末位"。
|
|
622
|
+
// 检测命中即强制写 checkpoint,让客户端拿到完整 wire 真实内容。
|
|
623
|
+
const _sameLenInPlaceReplace =
|
|
624
|
+
_tailFpCheckEnabled &&
|
|
625
|
+
messages.length === _lastMessagesCount &&
|
|
626
|
+
_lastMessagesCount > 0 &&
|
|
627
|
+
_lastTailFp !== '' &&
|
|
628
|
+
_deltaOriginalTailFp !== '' &&
|
|
629
|
+
_deltaOriginalTailFp !== _lastTailFp;
|
|
630
|
+
|
|
607
631
|
// 判断是否需要写 checkpoint
|
|
608
632
|
const needsCheckpoint =
|
|
609
633
|
_lastMessagesCount === 0 || // 进程重启 / 首次请求
|
|
610
634
|
messages.length < _lastMessagesCount || // messages 缩短(/clear、context 压缩)
|
|
611
|
-
(_mainAgentDeltaCount % CHECKPOINT_INTERVAL === 0)
|
|
635
|
+
(_mainAgentDeltaCount % CHECKPOINT_INTERVAL === 0) || // 定期 checkpoint
|
|
636
|
+
_sameLenInPlaceReplace; // in-place last-msg replace 检测
|
|
612
637
|
|
|
613
638
|
if (needsCheckpoint) {
|
|
614
639
|
// checkpoint:保持完整 messages,标记 _isCheckpoint
|
|
@@ -616,6 +641,11 @@ export function setupInterceptor() {
|
|
|
616
641
|
requestEntry._totalMessageCount = messages.length;
|
|
617
642
|
requestEntry._conversationId = 'mainAgent';
|
|
618
643
|
requestEntry._isCheckpoint = true;
|
|
644
|
+
if (_sameLenInPlaceReplace) {
|
|
645
|
+
// 诊断字段:标记此 checkpoint 是被 in-place replace 检测触发的(频率约 1-2%,
|
|
646
|
+
// 用于在生产 jsonl 里事后核对触发率,不影响重建逻辑)
|
|
647
|
+
requestEntry._inPlaceReplaceDetected = true;
|
|
648
|
+
}
|
|
619
649
|
} else {
|
|
620
650
|
// delta:只保留新增的 messages
|
|
621
651
|
const delta = messages.slice(_lastMessagesCount);
|
|
@@ -844,7 +874,7 @@ export function setupInterceptor() {
|
|
|
844
874
|
delete requestEntry.inProgress;
|
|
845
875
|
delete requestEntry.requestId;
|
|
846
876
|
appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
|
|
847
|
-
_commitDeltaState(_deltaOriginalMessagesLength);
|
|
877
|
+
_commitDeltaState(_deltaOriginalMessagesLength, _deltaOriginalTailFp);
|
|
848
878
|
// Release memory: clear large objects after disk write
|
|
849
879
|
streamedChunks = [];
|
|
850
880
|
streamedContentLen = 0;
|
|
@@ -855,7 +885,7 @@ export function setupInterceptor() {
|
|
|
855
885
|
delete requestEntry.inProgress;
|
|
856
886
|
delete requestEntry.requestId;
|
|
857
887
|
appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
|
|
858
|
-
_commitDeltaState(_deltaOriginalMessagesLength);
|
|
888
|
+
_commitDeltaState(_deltaOriginalMessagesLength, _deltaOriginalTailFp);
|
|
859
889
|
streamedChunks = [];
|
|
860
890
|
streamedContentLen = 0;
|
|
861
891
|
requestEntry.response = null;
|
|
@@ -925,7 +955,7 @@ export function setupInterceptor() {
|
|
|
925
955
|
delete requestEntry.inProgress;
|
|
926
956
|
delete requestEntry.requestId;
|
|
927
957
|
appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
|
|
928
|
-
_commitDeltaState(_deltaOriginalMessagesLength);
|
|
958
|
+
_commitDeltaState(_deltaOriginalMessagesLength, _deltaOriginalTailFp);
|
|
929
959
|
resetStreamingState();
|
|
930
960
|
}
|
|
931
961
|
} else {
|
|
@@ -953,12 +983,12 @@ export function setupInterceptor() {
|
|
|
953
983
|
delete requestEntry.requestId;
|
|
954
984
|
|
|
955
985
|
appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
|
|
956
|
-
_commitDeltaState(_deltaOriginalMessagesLength);
|
|
986
|
+
_commitDeltaState(_deltaOriginalMessagesLength, _deltaOriginalTailFp);
|
|
957
987
|
} catch (err) {
|
|
958
988
|
delete requestEntry.inProgress;
|
|
959
989
|
delete requestEntry.requestId;
|
|
960
990
|
appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
|
|
961
|
-
_commitDeltaState(_deltaOriginalMessagesLength);
|
|
991
|
+
_commitDeltaState(_deltaOriginalMessagesLength, _deltaOriginalTailFp);
|
|
962
992
|
}
|
|
963
993
|
}
|
|
964
994
|
}
|
package/lib/interceptor-core.js
CHANGED
|
@@ -336,6 +336,44 @@ export function migrateConversationContext(oldFile, newFile) {
|
|
|
336
336
|
} catch { }
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/**
|
|
340
|
+
* 计算单条 message 的轻量身份指纹,用于 delta storage 的 in-place last-msg replace 检测。
|
|
341
|
+
* 仅服务端 interceptor 使用 —— 触发 Plan C checkpoint 让客户端拿到 wire 真实内容。
|
|
342
|
+
* 历史上客户端 sessionManager.js 也复用过此算法做 isInPlaceLastMsgReplace 短路,
|
|
343
|
+
* 后被拆除(因 short-circuit 导致 same-ts 多记录被合并);现单层防御仅靠服务端。
|
|
344
|
+
*
|
|
345
|
+
* 80 字符前缀 + tool_use_id 后 8 字符 + tool_result body 下钻取真实文本(避开 String(array)
|
|
346
|
+
* 塌陷成 "[object Object]" 的 collision 坑)。
|
|
347
|
+
*/
|
|
348
|
+
export function fingerprintMsg(m) {
|
|
349
|
+
if (!m) return '';
|
|
350
|
+
const c = m.content;
|
|
351
|
+
let snip = '';
|
|
352
|
+
if (Array.isArray(c) && c.length > 0) {
|
|
353
|
+
const f = c[0];
|
|
354
|
+
if (f && typeof f === 'object') {
|
|
355
|
+
if (f.type === 'text') {
|
|
356
|
+
snip = String(f.text || '').slice(0, 80);
|
|
357
|
+
} else if (f.type === 'tool_use') {
|
|
358
|
+
snip = '<tool_use:' + (f.name || '?') + ':' + (f.id || '').slice(-8) + '>';
|
|
359
|
+
} else if (f.type === 'tool_result') {
|
|
360
|
+
let body = '';
|
|
361
|
+
if (typeof f.content === 'string') body = f.content;
|
|
362
|
+
else if (Array.isArray(f.content) && f.content[0]) {
|
|
363
|
+
const cf = f.content[0];
|
|
364
|
+
body = (typeof cf === 'string') ? cf : (cf.text || cf.type || '');
|
|
365
|
+
}
|
|
366
|
+
snip = '<tool_result:' + (f.tool_use_id || '').slice(-8) + ':' + String(body).slice(0, 40) + '>';
|
|
367
|
+
} else {
|
|
368
|
+
snip = '<' + (f.type || '?') + '>';
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
} else if (typeof c === 'string') {
|
|
372
|
+
snip = c.slice(0, 80);
|
|
373
|
+
}
|
|
374
|
+
return (m.role || '?') + ':' + snip.replace(/\s+/g, ' ').slice(0, 80);
|
|
375
|
+
}
|
|
376
|
+
|
|
339
377
|
/**
|
|
340
378
|
* Rotate log file when it exceeds maxSize.
|
|
341
379
|
* Creates an empty new file (no content migration) and appends '\n' to old file
|