cc-viewer 1.6.306 → 1.6.307
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 +2 -0
- package/dist/assets/{MdxEditorPanel-oSs95ieb.js → MdxEditorPanel-CPWegamu.js} +1 -1
- package/dist/assets/{Mobile-CVLG_J2s.js → Mobile-Dep0eTnv.js} +1 -1
- package/dist/assets/{index-1bh2o4MD.js → index-Bxq6pvmI.js} +2 -2
- package/dist/assets/seqResourceLoaders-DW4tbU6n.js +2 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/lib/im-log-watcher.js +94 -0
- package/server/routes/im.js +2 -0
- package/server/server.js +23 -0
- package/dist/assets/App-Ob0BsD2o.js +0 -2
- package/dist/assets/seqResourceLoaders-CC7nzyEk.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-Bxq6pvmI.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/package.json
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// im-log-watcher.js — 监听 IM worker 日志目录,写入即推 SSE,让「对话记录」弹窗零滞后刷新。
|
|
2
|
+
//
|
|
3
|
+
// 背景:IM worker 是独立进程、独立端口(见 im-process-manager.buildChildEnv,CCV_START_PORT=7050+)。
|
|
4
|
+
// 它的 Claude 子进程 turn 结束时把 turn_end POST 到 worker 自己的端口,主 web 服务收不到。
|
|
5
|
+
// 但 IM worker 的日志写在共享文件系统 ~/.claude/cc-viewer/IM_<id>/*.jsonl,主服务能直接 watch。
|
|
6
|
+
// 于是:主服务 fs.watch 这些目录,助手回复落盘即广播 `im_log_update` SSE,前端据此自动重拉。
|
|
7
|
+
//
|
|
8
|
+
// 设计:
|
|
9
|
+
// - ensure(platformId) 幂等、惰性:「对话记录」弹窗首次请求 /api/im/:platform/logs 时才开始 watch,
|
|
10
|
+
// 避免为从未打开的平台占 watcher(平台数 ≤ 4,成本可忽略)。dir 不存在时先 mkdir 再 watch。
|
|
11
|
+
// - 每个目录的 change 事件按 debounceMs 合并(fs.watch 一次写会抖多次),只在 .jsonl(排除 *_temp.jsonl)
|
|
12
|
+
// 变化时触发——与 findRecentLog 的「最新真实日志」语义对齐,过滤流式临时文件噪声。
|
|
13
|
+
// - watchImpl / mkdirImpl 可注入,便于确定性单测(无需真实 FS 时序)。
|
|
14
|
+
|
|
15
|
+
import { watch as fsWatch, mkdirSync, existsSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
// fs.watch 一次文件写会抖多次(macOS/Linux 尤甚),通常 50~300ms 内合并;选 250ms 平衡实时性与稳定性。
|
|
19
|
+
const DEFAULT_DEBOUNCE_MS = 250;
|
|
20
|
+
// platformId 纵深防御:上游 platformOf() 已用白名单挡住非法平台,这里再独立校验一次,
|
|
21
|
+
// 杜绝被污染的 id 经 `IM_${id}` 拼进路径造成穿越(与路由正则 /^[a-z0-9_-]+$/ 同集)。
|
|
22
|
+
const PLATFORM_ID_RE = /^[a-z0-9_-]+$/;
|
|
23
|
+
|
|
24
|
+
export function createImLogWatcher({ getLogDir, onChange, debounceMs = DEFAULT_DEBOUNCE_MS, watchImpl, mkdirImpl, existsImpl } = {}) {
|
|
25
|
+
const _watch = watchImpl || fsWatch;
|
|
26
|
+
const _mkdir = mkdirImpl || ((d) => { try { mkdirSync(d, { recursive: true }); } catch { /* ignore */ } });
|
|
27
|
+
const _exists = existsImpl || existsSync;
|
|
28
|
+
const _onChange = typeof onChange === 'function' ? onChange : () => {};
|
|
29
|
+
const _getLogDir = typeof getLogDir === 'function' ? getLogDir : () => '';
|
|
30
|
+
|
|
31
|
+
const watchers = new Map(); // platformId -> { w: FSWatcher, dir: string } —— 连同所属目录登记,便于 LOG_DIR 切换检测
|
|
32
|
+
const timers = new Map(); // platformId -> timeout
|
|
33
|
+
let _disposed = false;
|
|
34
|
+
|
|
35
|
+
function _schedule(platformId) {
|
|
36
|
+
const existing = timers.get(platformId);
|
|
37
|
+
if (existing) clearTimeout(existing);
|
|
38
|
+
timers.set(platformId, setTimeout(() => {
|
|
39
|
+
timers.delete(platformId);
|
|
40
|
+
if (_disposed) return;
|
|
41
|
+
try { _onChange(platformId); } catch { /* never propagate to fs.watch */ }
|
|
42
|
+
}, debounceMs));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 只认真实日志的 .jsonl 写入;filename 为 null(部分平台)时保守放行(让前端重拉,pure refresh 无副作用)。
|
|
46
|
+
function _relevant(filename) {
|
|
47
|
+
if (!filename) return true;
|
|
48
|
+
const name = String(filename);
|
|
49
|
+
if (!name.endsWith('.jsonl')) return false;
|
|
50
|
+
if (name.endsWith('_temp.jsonl')) return false;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ensure(platformId) {
|
|
55
|
+
if (_disposed || !platformId) return;
|
|
56
|
+
if (!PLATFORM_ID_RE.test(platformId)) return; // 纵深防御:非白名单字符不放行(防 `IM_${id}` 路径穿越)
|
|
57
|
+
const logDir = _getLogDir();
|
|
58
|
+
if (!logDir) return;
|
|
59
|
+
const dir = join(logDir, `IM_${platformId}`);
|
|
60
|
+
const reg = watchers.get(platformId);
|
|
61
|
+
if (reg) {
|
|
62
|
+
// 幂等仅当「目录路径未变且仍存在」时成立;否则关旧重建:
|
|
63
|
+
// - reg.dir !== dir:LOG_DIR 运行时切换(切项目),旧 watcher 还盯着旧目录,新目录无人监听;
|
|
64
|
+
// - !exists(dir):目录被删(某些平台 fs.watch 删目录不报 error,留下永不恢复的幽灵监听)。
|
|
65
|
+
if (reg.dir === dir && _exists(dir)) return;
|
|
66
|
+
try { reg.w.close(); } catch { /* ignore */ }
|
|
67
|
+
watchers.delete(platformId);
|
|
68
|
+
}
|
|
69
|
+
_mkdir(dir);
|
|
70
|
+
let w;
|
|
71
|
+
try {
|
|
72
|
+
w = _watch(dir, (_eventType, filename) => {
|
|
73
|
+
if (_relevant(filename)) _schedule(platformId);
|
|
74
|
+
});
|
|
75
|
+
} catch { return; }
|
|
76
|
+
// watcher 出错(目录被删等)→ 关闭并撤销登记,下次 ensure 可重建。
|
|
77
|
+
if (w && typeof w.on === 'function') {
|
|
78
|
+
w.on('error', () => { try { w.close(); } catch { /* ignore */ } watchers.delete(platformId); });
|
|
79
|
+
}
|
|
80
|
+
watchers.set(platformId, { w, dir });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function dispose() {
|
|
84
|
+
_disposed = true;
|
|
85
|
+
for (const t of timers.values()) clearTimeout(t);
|
|
86
|
+
timers.clear();
|
|
87
|
+
for (const reg of watchers.values()) { try { reg.w.close(); } catch { /* ignore */ } }
|
|
88
|
+
watchers.clear();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { ensure, dispose, _watchers: watchers };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default createImLogWatcher;
|
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 {
|
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;
|