cc-viewer 1.6.302 → 1.6.304
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-CE9-RsMp.js} +1 -1
- package/dist/assets/{App-eFrjLzF_.css → App-DfsHLalW.css} +1 -1
- package/dist/assets/{MdxEditorPanel-BkJKua3-.js → MdxEditorPanel-CDLhUUIj.js} +1 -1
- package/dist/assets/{Mobile-BtNceCJC.js → Mobile-BkZFYw5X.js} +1 -1
- package/dist/assets/index-BDK4eIE5.js +2 -0
- package/dist/assets/{index-Be9T-kDq.css → index-Cy7Xfu81.css} +1 -1
- package/dist/assets/{seqResourceLoaders-De_-fYhE.css → seqResourceLoaders-D-2EA12R.css} +2 -2
- package/dist/assets/seqResourceLoaders-aRa4CORe.js +2 -0
- package/dist/index.html +2 -2
- package/findcc.js +21 -1
- package/package.json +1 -1
- package/server/lib/enrich-plan-input.js +9 -3
- package/server/lib/enrich-workflow.js +95 -0
- package/server/lib/log-watcher.js +2 -0
- package/server/lib/pty-flood-coalescer.js +76 -12
- package/server/lib/resync-nudge-gate.js +33 -0
- package/server/lib/session-transcript-reader.js +66 -7
- package/server/lib/workflow-journal.js +155 -0
- package/server/lib/workflow-live.js +255 -0
- package/server/lib/workflow-watcher.js +280 -0
- package/server/routes/files-content.js +16 -6
- package/server/routes/workflow-journal.js +78 -0
- package/server/routes/workspaces.js +2 -0
- package/server/server.js +43 -3
- package/dist/assets/index-DSz9bNGm.js +0 -2
- package/dist/assets/seqResourceLoaders-C8gBbhlC.js +0 -2
|
@@ -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 };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Workflow run journal route: serve normalized panel data + arm live watch.
|
|
2
|
+
// 完成后读权威快照 <runId>.json;运行中(快照未落盘)回退到 subagents/workflows/<runId>
|
|
3
|
+
// 的逐帧推导,并武装运行中目录监视,使面板逐帧实时刷新。
|
|
4
|
+
import { resolveJournalPath, resolveWorkflowsDir, readNormalizedJournal } from '../lib/workflow-journal.js';
|
|
5
|
+
import { resolveRunDir, deriveLiveJournal } from '../lib/workflow-live.js';
|
|
6
|
+
import { armWorkflowWatch, armWorkflowLiveWatch } from '../lib/workflow-watcher.js';
|
|
7
|
+
|
|
8
|
+
// session 会进入 findTranscriptPath 的路径拼接(<root>/<dir>/<session>.jsonl),
|
|
9
|
+
// 限制为无路径分隔符/`..` 的安全字符集,纵深防御穿越(底层 realpath 容器校验之外的早拒)。
|
|
10
|
+
const SESSION_RE = /^[A-Za-z0-9_-]+$/;
|
|
11
|
+
|
|
12
|
+
function workflowJournal(req, res, parsedUrl, isLocal, deps) {
|
|
13
|
+
try {
|
|
14
|
+
const session = parsedUrl.searchParams.get('session') || '';
|
|
15
|
+
const runId = parsedUrl.searchParams.get('runId') || '';
|
|
16
|
+
const taskId = parsedUrl.searchParams.get('taskId') || '';
|
|
17
|
+
const project = parsedUrl.searchParams.get('project') || undefined;
|
|
18
|
+
|
|
19
|
+
if (!session) {
|
|
20
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
21
|
+
res.end(JSON.stringify({ ok: false, error: 'missing session' }));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (!SESSION_RE.test(session)) {
|
|
25
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
26
|
+
res.end(JSON.stringify({ ok: false, error: 'invalid session' }));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!runId && !taskId) {
|
|
30
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
31
|
+
res.end(JSON.stringify({ ok: false, error: 'missing runId or taskId' }));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 惰性 arm:只监视真有人在看的 session workflows 目录,后续 journal 覆写经 SSE 实时推送。
|
|
36
|
+
const wfDir = resolveWorkflowsDir(session, project);
|
|
37
|
+
if (wfDir && deps && Array.isArray(deps.clients)) {
|
|
38
|
+
try {
|
|
39
|
+
armWorkflowWatch({ workflowsDir: wfDir, sessionId: session, project, clients: deps.clients });
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 1) 完成快照优先(权威,含 phase 分组)
|
|
44
|
+
const journalPath = resolveJournalPath({ sessionId: session, projectHint: project, runId, taskId });
|
|
45
|
+
if (journalPath) {
|
|
46
|
+
const data = readNormalizedJournal(journalPath);
|
|
47
|
+
if (data) {
|
|
48
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
49
|
+
res.end(JSON.stringify({ ok: true, data }));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2) 运行中逐帧推导(快照尚未落盘)+ 武装运行中目录监视
|
|
55
|
+
if (runId) {
|
|
56
|
+
const runDir = resolveRunDir(session, project, runId);
|
|
57
|
+
const live = runDir ? deriveLiveJournal(runDir, runId) : null;
|
|
58
|
+
if (live) {
|
|
59
|
+
if (deps && Array.isArray(deps.clients)) {
|
|
60
|
+
try { armWorkflowLiveWatch({ runDir, runId, sessionId: session, project, clients: deps.clients }); } catch {}
|
|
61
|
+
}
|
|
62
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
63
|
+
res.end(JSON.stringify({ ok: true, data: live }));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
69
|
+
res.end(JSON.stringify({ ok: false, error: 'not found' }));
|
|
70
|
+
} catch (e) {
|
|
71
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
72
|
+
res.end(JSON.stringify({ ok: false, error: String(e?.message || e) }));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const workflowJournalRoutes = [
|
|
77
|
+
{ method: 'GET', match: 'exact', path: '/api/workflow-journal', handler: workflowJournal },
|
|
78
|
+
];
|
|
@@ -3,6 +3,7 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
3
3
|
import { basename } from 'node:path';
|
|
4
4
|
import { LOG_FILE, initForWorkspace, resetWorkspace } from '../interceptor.js';
|
|
5
5
|
import { watchLogFile, unwatchAll } from '../lib/log-watcher.js';
|
|
6
|
+
import { unwatchAllWorkflows } from '../lib/workflow-watcher.js';
|
|
6
7
|
import { readClaudeProjectModel } from '../lib/context-watcher.js';
|
|
7
8
|
import { countLogEntries, streamRawEntriesAsync } from '../lib/log-stream.js';
|
|
8
9
|
|
|
@@ -139,6 +140,7 @@ function workspacesStop(req, res, parsedUrl, isLocal, deps) {
|
|
|
139
140
|
|
|
140
141
|
// 停止日志监听
|
|
141
142
|
unwatchAll();
|
|
143
|
+
unwatchAllWorkflows();
|
|
142
144
|
|
|
143
145
|
// 重置 interceptor 状态
|
|
144
146
|
resetWorkspace();
|
package/server/server.js
CHANGED
|
@@ -24,6 +24,7 @@ import { voicePackRoutes } from './routes/voice-pack.js';
|
|
|
24
24
|
import { skillsRoutes } from './routes/skills.js';
|
|
25
25
|
import { ultraAgentsRoutes } from './routes/ultra-agents.js';
|
|
26
26
|
import { filesContentRoutes } from './routes/files-content.js';
|
|
27
|
+
import { workflowJournalRoutes } from './routes/workflow-journal.js';
|
|
27
28
|
import { filesFsRoutes } from './routes/files-fs.js';
|
|
28
29
|
import { workspacesRoutes } from './routes/workspaces.js';
|
|
29
30
|
import { eventsRoutes } from './routes/events.js';
|
|
@@ -76,12 +77,14 @@ import { checkAndUpdate } from './lib/updater.js';
|
|
|
76
77
|
import { loadPlugins, runWaterfallHook, runParallelHook } from './lib/plugin-loader.js';
|
|
77
78
|
import { CONTEXT_WINDOW_FILE, readModelContextSize } from './lib/context-watcher.js';
|
|
78
79
|
import { watchLogFile, startWatching, unwatchAll, sendEventToClients, sendToClients } from './lib/log-watcher.js';
|
|
80
|
+
import { unwatchAllWorkflows } from './lib/workflow-watcher.js';
|
|
79
81
|
import { cleanupExtractCache } from './lib/jsonl-archive.js';
|
|
80
82
|
import { backupConfigs } from './lib/config-backup.js';
|
|
81
83
|
import { normalizeBasePath, validateBasePath, stripBasePath } from './lib/base-path.js';
|
|
82
84
|
import { createHardenedCleanup } from './lib/term-signals.js';
|
|
83
85
|
import { createBackpressureGate } from './lib/ws-backpressure.js';
|
|
84
|
-
import { createFloodCoalescer } from './lib/pty-flood-coalescer.js';
|
|
86
|
+
import { createFloodCoalescer, envIntAllowZero } from './lib/pty-flood-coalescer.js';
|
|
87
|
+
import { createResyncNudgeGate } from './lib/resync-nudge-gate.js';
|
|
85
88
|
|
|
86
89
|
|
|
87
90
|
// 动态获取 getPrefsFile()(LOG_DIR 可能在运行时被 setLogDir 修改)
|
|
@@ -555,6 +558,7 @@ const _routes = [
|
|
|
555
558
|
...skillsRoutes,
|
|
556
559
|
...ultraAgentsRoutes,
|
|
557
560
|
...filesContentRoutes,
|
|
561
|
+
...workflowJournalRoutes,
|
|
558
562
|
...filesFsRoutes,
|
|
559
563
|
...workspacesRoutes,
|
|
560
564
|
...eventsRoutes,
|
|
@@ -1121,20 +1125,30 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1121
1125
|
|
|
1122
1126
|
// 反压状态转换日志(observability:线上排"页面卡"时可直接判断是否触发过反压、几次、积压量)。
|
|
1123
1127
|
// behind/resume 在持续洪泛下以亚秒级周期振荡,5s 节流防刷屏;timeout 是终态必记。
|
|
1128
|
+
// resyncTotal/nudgeSkipped 判别 resync 循环:resync 涨 + skipped 同涨 = nudge 冷却在救;
|
|
1129
|
+
// resync 涨 + skipped≈0 = resume 间隔超过冷却期的慢振荡,需更激进策略(如洪泛期禁 nudge)。
|
|
1124
1130
|
const makeBpLogger = (label, ws) => {
|
|
1125
1131
|
let behindCount = 0;
|
|
1132
|
+
let resyncCount = 0;
|
|
1133
|
+
let nudgeSkipped = 0;
|
|
1126
1134
|
let lastLogAt = 0;
|
|
1127
1135
|
return (event, buffered) => {
|
|
1128
1136
|
if (event === 'behind') behindCount++;
|
|
1137
|
+
if (event === 'resume') resyncCount++;
|
|
1138
|
+
if (event === 'nudge-skip') { nudgeSkipped++; return; } // 只计数,随下一条节流日志带出
|
|
1129
1139
|
const now = Date.now();
|
|
1130
1140
|
if (event !== 'timeout' && now - lastLogAt < 5000) return;
|
|
1131
1141
|
lastLogAt = now;
|
|
1132
|
-
console.warn(`[${label}] ws backpressure ${event}: client=${ws._socket?.remoteAddress || '?'} bufferedAmount=${buffered} behindTotal=${behindCount}`);
|
|
1142
|
+
console.warn(`[${label}] ws backpressure ${event}: client=${ws._socket?.remoteAddress || '?'} bufferedAmount=${buffered} behindTotal=${behindCount} resyncTotal=${resyncCount} nudgeSkipped=${nudgeSkipped}`);
|
|
1133
1143
|
};
|
|
1134
1144
|
};
|
|
1135
1145
|
|
|
1146
|
+
// resync nudge 冷却(CCV_RESYNC_NUDGE_COOLDOWN_MS,0 = 不冷却;详见 lib/resync-nudge-gate.js)
|
|
1147
|
+
const RESYNC_NUDGE_COOLDOWN_MS = envIntAllowZero('CCV_RESYNC_NUDGE_COOLDOWN_MS', 3000);
|
|
1148
|
+
|
|
1136
1149
|
// 洪泛限流器状态日志(与 makeBpLogger 同款 5s 节流,独立实例不共享计数)。
|
|
1137
1150
|
// Windows 实机排"切主题/大流量卡死"时据此确认 ConPTY 洪泛是否触发、几次、量级。
|
|
1151
|
+
// 'rate' 事件 = 直通态 ws 消息率告警(msgsPerSec,计数在 send 闭包),判别消息数风暴。
|
|
1138
1152
|
const makeFloodLogger = (label, ws) => {
|
|
1139
1153
|
let floodCount = 0;
|
|
1140
1154
|
let lastLogAt = 0;
|
|
@@ -1143,7 +1157,23 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1143
1157
|
const now = Date.now();
|
|
1144
1158
|
if (now - lastLogAt < 5000) return;
|
|
1145
1159
|
lastLogAt = now;
|
|
1146
|
-
|
|
1160
|
+
const metric = event === 'rate' ? 'msgsPerSec' : 'winBytes';
|
|
1161
|
+
console.warn(`[${label}] pty flood ${event}: client=${ws._socket?.remoteAddress || '?'} ${metric}=${bytes} floodTotal=${floodCount}`);
|
|
1162
|
+
};
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
// 直通态消息率计数器工厂:1s 整数桶,桶滚动时超过阈值经 floodLog('rate') 告警(5s 节流兜底)。
|
|
1166
|
+
const makeMsgRateCounter = (floodLog, warnAbove = 60) => {
|
|
1167
|
+
let count = 0;
|
|
1168
|
+
let winStart = 0;
|
|
1169
|
+
return () => {
|
|
1170
|
+
const now = Date.now();
|
|
1171
|
+
if (now - winStart >= 1000) {
|
|
1172
|
+
if (count > warnAbove) floodLog('rate', count);
|
|
1173
|
+
count = 0;
|
|
1174
|
+
winStart = now;
|
|
1175
|
+
}
|
|
1176
|
+
count++;
|
|
1147
1177
|
};
|
|
1148
1178
|
};
|
|
1149
1179
|
|
|
@@ -1196,10 +1226,12 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1196
1226
|
// 洪泛限流器:字节率超阈值时按窗口合并 + last-wins 截断(ConPTY 全屏重绘洪泛防卡死,
|
|
1197
1227
|
// 与 bpGate 互补——bpGate 管慢网络写缓冲,floodGate 管快 LAN 字节率,详见 lib/pty-flood-coalescer.js)
|
|
1198
1228
|
const _floodLog = makeFloodLogger('scratch-ws', ws);
|
|
1229
|
+
const _countMsg = makeMsgRateCounter(_floodLog);
|
|
1199
1230
|
floodGate = createFloodCoalescer({
|
|
1200
1231
|
send: (data) => {
|
|
1201
1232
|
if (ws.readyState === 1 && bpGate.offer()) {
|
|
1202
1233
|
try { ws.send(JSON.stringify({ type: 'data', data })); } catch {}
|
|
1234
|
+
_countMsg();
|
|
1203
1235
|
}
|
|
1204
1236
|
},
|
|
1205
1237
|
findSafeSliceStart,
|
|
@@ -1293,6 +1325,10 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1293
1325
|
// floodGate 前向引用(构造顺序同 scratch 路径):onBehind/onResume 必清 coalescer
|
|
1294
1326
|
// pending——resync 快照是唯一真相源,旧 pending 回灌会导致画面回退。
|
|
1295
1327
|
let floodGate = null;
|
|
1328
|
+
// nudge 冷却门:快照每次 resume 无条件发(修复 behind 期间跳发的数据),但重绘 nudge
|
|
1329
|
+
// 走冷却——nudge 让 ConPTY 再吐全屏重绘 = 新洪泛燃料,紧 behind→resume 循环里反复
|
|
1330
|
+
// nudge 会自我维持(客户端每轮 reset+重放快照 = 永久冻结表象)。详见 lib/resync-nudge-gate.js。
|
|
1331
|
+
const nudgeGate = createResyncNudgeGate({ cooldownMs: RESYNC_NUDGE_COOLDOWN_MS });
|
|
1296
1332
|
const bpGate = createBackpressureGate({
|
|
1297
1333
|
getBufferedAmount: () => ws.bufferedAmount,
|
|
1298
1334
|
onBehind: (buffered) => {
|
|
@@ -1304,6 +1340,7 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1304
1340
|
floodGate?.reset();
|
|
1305
1341
|
if (ws.readyState !== 1) return;
|
|
1306
1342
|
try { ws.send(JSON.stringify({ type: 'data-resync', data: getOutputBuffer() })); } catch {}
|
|
1343
|
+
if (!nudgeGate.shouldNudge()) { _bpLog('nudge-skip', buffered); return; }
|
|
1307
1344
|
try {
|
|
1308
1345
|
if (process.platform !== 'win32') {
|
|
1309
1346
|
// POSIX:与下方 _needRedrawBootstrap 同款 SIGWINCH 兜底
|
|
@@ -1333,10 +1370,12 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
1333
1370
|
// 洪泛限流器:字节率超阈值时按窗口合并 + last-wins 截断(ConPTY 全屏重绘洪泛防卡死,
|
|
1334
1371
|
// 与 bpGate 互补——bpGate 管慢网络写缓冲,floodGate 管快 LAN 字节率,详见 lib/pty-flood-coalescer.js)
|
|
1335
1372
|
const _floodLog = makeFloodLogger('terminal-ws', ws);
|
|
1373
|
+
const _countMsg = makeMsgRateCounter(_floodLog);
|
|
1336
1374
|
floodGate = createFloodCoalescer({
|
|
1337
1375
|
send: (data) => {
|
|
1338
1376
|
if (ws.readyState === 1 && bpGate.offer()) {
|
|
1339
1377
|
try { ws.send(JSON.stringify({ type: 'data', data })); } catch {}
|
|
1378
|
+
_countMsg();
|
|
1340
1379
|
}
|
|
1341
1380
|
},
|
|
1342
1381
|
findSafeSliceStart,
|
|
@@ -2017,6 +2056,7 @@ async function _doStop() {
|
|
|
2017
2056
|
} catch { }
|
|
2018
2057
|
}
|
|
2019
2058
|
unwatchAll();
|
|
2059
|
+
unwatchAllWorkflows();
|
|
2020
2060
|
unwatchFile(CONTEXT_WINDOW_FILE);
|
|
2021
2061
|
clients.forEach(client => client.end());
|
|
2022
2062
|
// Truncate in place (not `clients = []`) so the array reference stays stable across
|