cc-viewer 1.6.294 → 1.6.295
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/cli.js +7 -2
- package/dist/assets/App-Br-u2TKk.js +2 -0
- package/dist/assets/{MdxEditorPanel-B8xrlDZJ.js → MdxEditorPanel-Cy4egsQx.js} +1 -1
- package/dist/assets/{Mobile-fsi8-Lpb.js → Mobile-ZHF74GQs.js} +1 -1
- package/dist/assets/index-DMuCrfTo.js +2 -0
- package/dist/assets/seqResourceLoaders-C7X23dCJ.js +2 -0
- package/dist/index.html +1 -1
- package/findcc.js +3 -3
- package/package.json +1 -1
- package/server/i18n.js +224 -8
- package/server/interceptor.js +21 -18
- package/server/lib/adapters/dingtalk-adapter.js +62 -0
- package/server/lib/adapters/discord-adapter.js +35 -0
- package/server/lib/adapters/feishu-adapter.js +37 -0
- package/server/lib/ask-store.js +19 -90
- package/server/lib/async-file-lock.js +123 -0
- package/server/lib/async-write-queue.js +131 -0
- package/server/lib/git-diff.js +4 -1
- package/server/lib/im-bridge-core.js +119 -14
- package/server/lib/im-config.js +11 -6
- package/server/lib/im-process-manager.js +1 -1
- package/server/lib/jsonl-archive.js +0 -1
- package/server/lib/log-watcher.js +224 -177
- package/server/lib/plugin-manager.js +1 -1
- package/server/lib/updater.js +4 -2
- package/server/pty-manager.js +1 -1
- package/server/routes/ask-perm.js +2 -2
- package/server/routes/dingtalk.js +2 -0
- package/server/routes/files-fs.js +4 -4
- package/server/routes/project-meta.js +18 -1
- package/server/routes/workspaces.js +7 -10
- package/server/server.js +23 -20
- package/server/workspace-registry.js +9 -53
- package/dist/assets/App-C66LoBEz.js +0 -2
- package/dist/assets/index-BTZqk5O5.js +0 -2
- package/dist/assets/seqResourceLoaders-6k4uXcNn.js +0 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { readFileSync, existsSync, watchFile, unwatchFile, openSync, readSync, closeSync, statSync } from 'node:fs';
|
|
1
|
+
import { readFileSync, existsSync, watch, watchFile, unwatchFile, openSync, readSync, closeSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, basename } from 'node:path';
|
|
2
3
|
import { isMainAgentEntry, extractCachedContent } from './kv-cache-analyzer.js';
|
|
3
4
|
import { buildContextWindowEvent, getContextSizeForModel } from './context-watcher.js';
|
|
4
5
|
import { reconstructEntries, createIncrementalReconstructor } from './delta-reconstructor.js';
|
|
@@ -6,17 +7,26 @@ import { countLogEntries, streamReconstructedEntries } from './log-stream.js';
|
|
|
6
7
|
import { enrichEntry } from './enrich-plan-input.js';
|
|
7
8
|
import { resolveJsonlPath } from './jsonl-archive.js';
|
|
8
9
|
|
|
9
|
-
// 跟踪所有被 watch
|
|
10
|
+
// 跟踪所有被 watch 的日志文件。value: fileState 对象(外部只用 .has()/.keys())
|
|
10
11
|
const watchedFiles = new Map();
|
|
11
12
|
|
|
13
|
+
// 目录级 fs.watch 实例注册表(事件驱动,替代 per-file 轮询)
|
|
14
|
+
const _dirWatchers = new Map();
|
|
15
|
+
|
|
16
|
+
// Windows 单次 appendFileSync 触发 2+ 事件,防抖合并
|
|
17
|
+
const FSWATCH_DEBOUNCE_MS = 80;
|
|
18
|
+
|
|
19
|
+
// 安全网慢轮询:fs.watch 可能漏事件(buffer overflow 等),冷 fallback 兜底
|
|
20
|
+
const SAFETY_POLL_MS = 5000;
|
|
21
|
+
|
|
22
|
+
const FORCE_POLL = process.env.CCV_FORCE_POLL === '1';
|
|
23
|
+
|
|
12
24
|
/**
|
|
13
25
|
* Read and parse a JSONL log file.
|
|
14
26
|
* @param {string} logFile - absolute path to the log file
|
|
15
27
|
* @returns {Array} parsed and deduplicated entries
|
|
16
28
|
*/
|
|
17
29
|
export function readLogFile(logFile) {
|
|
18
|
-
// 透明支持归档后的 .jsonl.zip。active log(被 watchLogFile 追加写入的文件)不会被归档
|
|
19
|
-
// (archive 拒绝最新文件),watchLogFile 内部的 openSync/readSync 因此无需走 resolveJsonlPath。
|
|
20
30
|
logFile = resolveJsonlPath(logFile);
|
|
21
31
|
if (!existsSync(logFile)) {
|
|
22
32
|
return [];
|
|
@@ -24,8 +34,6 @@ export function readLogFile(logFile) {
|
|
|
24
34
|
|
|
25
35
|
try {
|
|
26
36
|
const content = readFileSync(logFile, 'utf-8');
|
|
27
|
-
// Windows 上若 writer 使用 os.EOL,分隔符会变 \r\n---\r\n。固定 LF 切会失败 → 整文件
|
|
28
|
-
// 解析成一条乱码或漏。CRLF-tolerant split 把两边都 cover 住。
|
|
29
37
|
const entries = content.split(/\r?\n---\r?\n/).filter(line => line.trim());
|
|
30
38
|
const parsed = entries.map(entry => {
|
|
31
39
|
try {
|
|
@@ -34,7 +42,6 @@ export function readLogFile(logFile) {
|
|
|
34
42
|
return null;
|
|
35
43
|
}
|
|
36
44
|
}).filter(Boolean);
|
|
37
|
-
// 去重:同一 timestamp+url 的条目,后出现的(带 response)覆盖先出现的(在途)
|
|
38
45
|
const map = new Map();
|
|
39
46
|
for (const entry of parsed) {
|
|
40
47
|
const key = `${entry.timestamp}|${entry.url}`;
|
|
@@ -47,8 +54,6 @@ export function readLogFile(logFile) {
|
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
// SSE 单客户端 backpressure 容忍上限:连续未排空 > 此时长则视为 dead 客户端剔除。
|
|
51
|
-
// 与 server.js 同名常量值保持一致(避免循环依赖,此处单独 mirror)。
|
|
52
57
|
const SSE_BACKPRESSURE_TIMEOUT_MS = 5000;
|
|
53
58
|
|
|
54
59
|
function _removeClient(clients, client) {
|
|
@@ -56,14 +61,7 @@ function _removeClient(clients, client) {
|
|
|
56
61
|
if (idx !== -1) clients.splice(idx, 1);
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
/**
|
|
60
|
-
* 向单个 SSE client 安全写入 payload。
|
|
61
|
-
* - 写错或 client.destroyed/!writable:立即从 clients 数组移除
|
|
62
|
-
* - write 返回 false(写缓冲满):标记时间戳,超过 SSE_BACKPRESSURE_TIMEOUT_MS 仍未排空则剔除并 end()
|
|
63
|
-
* - drain 后重置 _sseBackpressureSince=0,下次 backpressure 重新计时
|
|
64
|
-
*/
|
|
65
64
|
function _safeSseWrite(clients, client, payload) {
|
|
66
|
-
// 仅在显式标记 destroyed/writable=false 时剔除;undefined(如老 mock)按"活"处理。
|
|
67
65
|
if (client.destroyed === true || client.writable === false) {
|
|
68
66
|
_removeClient(clients, client);
|
|
69
67
|
return false;
|
|
@@ -88,25 +86,13 @@ function _safeSseWrite(clients, client, payload) {
|
|
|
88
86
|
return true;
|
|
89
87
|
}
|
|
90
88
|
|
|
91
|
-
/**
|
|
92
|
-
* Send an SSE entry to all connected clients.
|
|
93
|
-
* @param {Array} clients - SSE client array
|
|
94
|
-
* @param {object} entry - parsed log entry
|
|
95
|
-
*/
|
|
96
89
|
export function sendToClients(clients, entry) {
|
|
97
90
|
const payload = `data: ${JSON.stringify(entry)}\n\n`;
|
|
98
|
-
// 倒序遍历允许循环内安全 splice
|
|
99
91
|
for (let i = clients.length - 1; i >= 0; i--) {
|
|
100
92
|
_safeSseWrite(clients, clients[i], payload);
|
|
101
93
|
}
|
|
102
94
|
}
|
|
103
95
|
|
|
104
|
-
/**
|
|
105
|
-
* Send a named SSE event to all connected clients.
|
|
106
|
-
* @param {Array} clients - SSE client array
|
|
107
|
-
* @param {string} eventName - SSE event name
|
|
108
|
-
* @param {object} data - event payload
|
|
109
|
-
*/
|
|
110
96
|
export function sendEventToClients(clients, eventName, data) {
|
|
111
97
|
const payload = `event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
112
98
|
for (let i = clients.length - 1; i >= 0; i--) {
|
|
@@ -114,11 +100,6 @@ export function sendEventToClients(clients, eventName, data) {
|
|
|
114
100
|
}
|
|
115
101
|
}
|
|
116
102
|
|
|
117
|
-
/**
|
|
118
|
-
* 旋转处理器专用:发送已序列化的 load_chunk segment 数据。
|
|
119
|
-
* @param {Array} clients - SSE client array
|
|
120
|
-
* @param {string} dataJson - segment 已被调用方 JSON.stringify
|
|
121
|
-
*/
|
|
122
103
|
export function sendChunkToClients(clients, dataJson) {
|
|
123
104
|
const payload = `event: load_chunk\ndata: ${dataJson}\n\n`;
|
|
124
105
|
for (let i = clients.length - 1; i >= 0; i--) {
|
|
@@ -126,24 +107,180 @@ export function sendChunkToClients(clients, dataJson) {
|
|
|
126
107
|
}
|
|
127
108
|
}
|
|
128
109
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
110
|
+
// --- 轮转切换(抽取公共逻辑) ---
|
|
111
|
+
|
|
112
|
+
function _switchToRotatedFile(logFile, currentLogFile, clients, opts) {
|
|
113
|
+
_unwatchSingleFile(logFile);
|
|
114
|
+
const total = countLogEntries(currentLogFile);
|
|
115
|
+
sendEventToClients(clients, 'load_start', { total, incremental: false });
|
|
116
|
+
streamReconstructedEntries(currentLogFile, (segment) => {
|
|
117
|
+
sendChunkToClients(clients, JSON.stringify(segment));
|
|
118
|
+
});
|
|
119
|
+
sendEventToClients(clients, 'load_end', {});
|
|
120
|
+
watchLogFile({ ...opts, logFile: currentLogFile });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- 增量读 + 解析 + 广播(独立于触发机制) ---
|
|
124
|
+
|
|
125
|
+
function _readDelta(state) {
|
|
126
|
+
const { logFile, opts, reconstructor } = state;
|
|
127
|
+
const { clients, getClaudePid, runParallelHook, notifyStatsWorker, getLogFile } = opts;
|
|
128
|
+
try {
|
|
129
|
+
const currentSize = statSync(logFile).size;
|
|
130
|
+
|
|
131
|
+
if (currentSize < state.lastByteOffset) {
|
|
132
|
+
state.lastByteOffset = 0;
|
|
133
|
+
state.pendingTail = '';
|
|
134
|
+
reconstructor.reset();
|
|
135
|
+
|
|
136
|
+
const currentLogFile = getLogFile();
|
|
137
|
+
if (currentLogFile !== logFile && !watchedFiles.has(currentLogFile)) {
|
|
138
|
+
_switchToRotatedFile(logFile, currentLogFile, clients, opts);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (currentSize <= state.lastByteOffset) return;
|
|
144
|
+
|
|
145
|
+
const bytesToRead = currentSize - state.lastByteOffset;
|
|
146
|
+
const buf = Buffer.alloc(bytesToRead);
|
|
147
|
+
const fd = openSync(logFile, 'r');
|
|
148
|
+
try {
|
|
149
|
+
readSync(fd, buf, 0, bytesToRead, state.lastByteOffset);
|
|
150
|
+
} finally {
|
|
151
|
+
closeSync(fd);
|
|
152
|
+
}
|
|
153
|
+
state.lastByteOffset = currentSize;
|
|
154
|
+
|
|
155
|
+
const raw = state.pendingTail + buf.toString('utf-8');
|
|
156
|
+
const parts = raw.split('\n---\n');
|
|
157
|
+
state.pendingTail = parts.pop() || '';
|
|
158
|
+
|
|
159
|
+
if (parts.length === 0 && state.pendingTail.trim()) {
|
|
160
|
+
try {
|
|
161
|
+
JSON.parse(state.pendingTail);
|
|
162
|
+
parts.push(state.pendingTail);
|
|
163
|
+
state.pendingTail = '';
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const validParts = parts.filter(p => p.trim());
|
|
168
|
+
if (validParts.length > 0) {
|
|
169
|
+
validParts.forEach(entry => {
|
|
170
|
+
try {
|
|
171
|
+
const parsed = JSON.parse(entry);
|
|
172
|
+
if (!parsed.pid) parsed.pid = getClaudePid();
|
|
173
|
+
reconstructor.reconstruct(parsed);
|
|
174
|
+
try { enrichEntry(parsed); } catch {}
|
|
175
|
+
sendToClients(clients, parsed);
|
|
176
|
+
runParallelHook('onNewEntry', parsed).catch(() => {});
|
|
177
|
+
if (isMainAgentEntry(parsed) && !parsed.inProgress) {
|
|
178
|
+
const cached = extractCachedContent(parsed);
|
|
179
|
+
if (cached) sendEventToClients(clients, 'kv_cache_content', cached);
|
|
180
|
+
const usage = parsed.response?.body?.usage;
|
|
181
|
+
if (usage) {
|
|
182
|
+
const contextSize = getContextSizeForModel(parsed.body?.model);
|
|
183
|
+
const cwData = buildContextWindowEvent(usage, contextSize);
|
|
184
|
+
if (cwData) sendEventToClients(clients, 'context_window', cwData);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch {}
|
|
188
|
+
});
|
|
189
|
+
notifyStatsWorker(logFile);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const currentLogFile = getLogFile();
|
|
193
|
+
if (currentLogFile !== logFile && !watchedFiles.has(currentLogFile)) {
|
|
194
|
+
_switchToRotatedFile(logFile, currentLogFile, clients, opts);
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
// File not yet created or transient read error
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- 目录级 fs.watch 管理 ---
|
|
202
|
+
|
|
203
|
+
function _getOrCreateDirWatcher(dir) {
|
|
204
|
+
if (_dirWatchers.has(dir)) return _dirWatchers.get(dir);
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const files = new Map();
|
|
208
|
+
const watcher = watch(dir, (eventType, filename) => {
|
|
209
|
+
if (!filename) {
|
|
210
|
+
for (const [, fileState] of files) {
|
|
211
|
+
_scheduleDebouncedRead(fileState);
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const fileState = files.get(filename);
|
|
216
|
+
if (fileState) _scheduleDebouncedRead(fileState);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
watcher.on('error', () => {
|
|
220
|
+
for (const [, fileState] of files) {
|
|
221
|
+
_fallbackToPolling(fileState);
|
|
222
|
+
}
|
|
223
|
+
try { watcher.close(); } catch {}
|
|
224
|
+
_dirWatchers.delete(dir);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const entry = { watcher, files };
|
|
228
|
+
_dirWatchers.set(dir, entry);
|
|
229
|
+
return entry;
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function _scheduleDebouncedRead(fileState) {
|
|
236
|
+
if (fileState.debounceTimer) return;
|
|
237
|
+
fileState.debounceTimer = setTimeout(() => {
|
|
238
|
+
fileState.debounceTimer = null;
|
|
239
|
+
_readDelta(fileState);
|
|
240
|
+
}, FSWATCH_DEBOUNCE_MS);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function _fallbackToPolling(fileState) {
|
|
244
|
+
if (fileState.polling) return;
|
|
245
|
+
fileState.polling = true;
|
|
246
|
+
watchFile(fileState.logFile, { interval: 500 }, () => {
|
|
247
|
+
_readDelta(fileState);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function _unwatchSingleFile(logFile) {
|
|
252
|
+
const fileState = watchedFiles.get(logFile);
|
|
253
|
+
watchedFiles.delete(logFile);
|
|
254
|
+
|
|
255
|
+
if (!fileState) return;
|
|
256
|
+
|
|
257
|
+
if (fileState.debounceTimer) clearTimeout(fileState.debounceTimer);
|
|
258
|
+
if (fileState.safetyTimer) clearInterval(fileState.safetyTimer);
|
|
259
|
+
|
|
260
|
+
if (fileState.polling) {
|
|
261
|
+
try { unwatchFile(logFile); } catch {}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const dir = dirname(logFile);
|
|
266
|
+
const filename = basename(logFile);
|
|
267
|
+
const dirEntry = _dirWatchers.get(dir);
|
|
268
|
+
if (dirEntry) {
|
|
269
|
+
dirEntry.files.delete(filename);
|
|
270
|
+
if (dirEntry.files.size === 0) {
|
|
271
|
+
try { dirEntry.watcher.close(); } catch {}
|
|
272
|
+
_dirWatchers.delete(dir);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// --- 公开 API ---
|
|
278
|
+
|
|
139
279
|
export function watchLogFile(opts) {
|
|
140
|
-
const { logFile
|
|
280
|
+
const { logFile } = opts;
|
|
141
281
|
if (watchedFiles.has(logFile)) return;
|
|
142
282
|
|
|
143
|
-
// Track byte offset instead of string length — avoids full-file re-read on every poll
|
|
144
283
|
let lastByteOffset = 0;
|
|
145
|
-
let pendingTail = ''; // incomplete entry carried across polls
|
|
146
|
-
// Delta storage: 增量重建器,用于逐条重建 mainAgent delta 条目
|
|
147
284
|
const _reconstructor = createIncrementalReconstructor();
|
|
148
285
|
try {
|
|
149
286
|
if (existsSync(logFile)) {
|
|
@@ -151,150 +288,60 @@ export function watchLogFile(opts) {
|
|
|
151
288
|
}
|
|
152
289
|
} catch {}
|
|
153
290
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// 文件被清空可能是轮转信号,立即检查是否已切换到新文件
|
|
166
|
-
const currentLogFile = getLogFile();
|
|
167
|
-
if (currentLogFile !== logFile && !watchedFiles.has(currentLogFile)) {
|
|
168
|
-
unwatchFile(logFile);
|
|
169
|
-
watchedFiles.delete(logFile);
|
|
170
|
-
|
|
171
|
-
// 流式分段广播,避免全量加载 OOM;走 _safeSseWrite 包装做 backpressure / dead-client 清理
|
|
172
|
-
const rotTotal = countLogEntries(currentLogFile);
|
|
173
|
-
sendEventToClients(clients, 'load_start', { total: rotTotal, incremental: false });
|
|
174
|
-
streamReconstructedEntries(currentLogFile, (segment) => {
|
|
175
|
-
sendChunkToClients(clients, JSON.stringify(segment));
|
|
176
|
-
});
|
|
177
|
-
sendEventToClients(clients, 'load_end', {});
|
|
178
|
-
watchLogFile({ ...opts, logFile: currentLogFile });
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
291
|
+
const fileState = {
|
|
292
|
+
logFile,
|
|
293
|
+
opts,
|
|
294
|
+
reconstructor: _reconstructor,
|
|
295
|
+
lastByteOffset,
|
|
296
|
+
pendingTail: '',
|
|
297
|
+
debounceTimer: null,
|
|
298
|
+
safetyTimer: null,
|
|
299
|
+
polling: false,
|
|
300
|
+
};
|
|
182
301
|
|
|
183
|
-
|
|
302
|
+
watchedFiles.set(logFile, fileState);
|
|
184
303
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
readSync(fd, buf, 0, bytesToRead, lastByteOffset);
|
|
191
|
-
} finally {
|
|
192
|
-
closeSync(fd);
|
|
193
|
-
}
|
|
194
|
-
lastByteOffset = currentSize;
|
|
304
|
+
if (FORCE_POLL) {
|
|
305
|
+
_fallbackToPolling(fileState);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
195
308
|
|
|
196
|
-
|
|
197
|
-
|
|
309
|
+
const dir = dirname(logFile);
|
|
310
|
+
const filename = basename(logFile);
|
|
311
|
+
const dirEntry = _getOrCreateDirWatcher(dir);
|
|
198
312
|
|
|
199
|
-
|
|
200
|
-
|
|
313
|
+
if (!dirEntry) {
|
|
314
|
+
_fallbackToPolling(fileState);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
201
317
|
|
|
202
|
-
|
|
203
|
-
// (happens when the file ends without a trailing \n---\n)
|
|
204
|
-
if (parts.length === 0 && pendingTail.trim()) {
|
|
205
|
-
try {
|
|
206
|
-
JSON.parse(pendingTail);
|
|
207
|
-
// Valid JSON — treat as complete entry
|
|
208
|
-
parts.push(pendingTail);
|
|
209
|
-
pendingTail = '';
|
|
210
|
-
} catch {
|
|
211
|
-
// Incomplete — keep in pendingTail for next poll
|
|
212
|
-
}
|
|
213
|
-
}
|
|
318
|
+
dirEntry.files.set(filename, fileState);
|
|
214
319
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const parsed = JSON.parse(entry);
|
|
220
|
-
if (!parsed.pid) {
|
|
221
|
-
parsed.pid = getClaudePid();
|
|
222
|
-
}
|
|
223
|
-
// Delta storage: reconstruct before push — 确保前端收到完整 messages
|
|
224
|
-
_reconstructor.reconstruct(parsed);
|
|
225
|
-
// ExitPlanMode V2 input 服务端补全(详见 enrich-plan-input.js#enrichEntry JSDoc)。
|
|
226
|
-
// 同步实现:候选扫描天然廉价(无 ExitPlanMode 块直接 0ms 返回);命中
|
|
227
|
-
// 路径由 transcript 64MB 上限 + miss 30s TTL + path mtime 校验三层兜
|
|
228
|
-
// 住,最坏 ~150ms。如未来 hit 比例显著上升再考虑 setImmediate 拆分。
|
|
229
|
-
try { enrichEntry(parsed); } catch { /* 静默回退 */ }
|
|
230
|
-
sendToClients(clients, parsed);
|
|
231
|
-
runParallelHook('onNewEntry', parsed).catch(() => {});
|
|
232
|
-
if (isMainAgentEntry(parsed) && !parsed.inProgress) {
|
|
233
|
-
const cached = extractCachedContent(parsed);
|
|
234
|
-
if (cached) {
|
|
235
|
-
sendEventToClients(clients, 'kv_cache_content', cached);
|
|
236
|
-
}
|
|
237
|
-
const usage = parsed.response?.body?.usage;
|
|
238
|
-
if (usage) {
|
|
239
|
-
const contextSize = getContextSizeForModel(parsed.body?.model);
|
|
240
|
-
const cwData = buildContextWindowEvent(usage, contextSize);
|
|
241
|
-
if (cwData) {
|
|
242
|
-
sendEventToClients(clients, 'context_window', cwData);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
} catch (err) {
|
|
247
|
-
// Skip invalid entries
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
notifyStatsWorker(logFile);
|
|
251
|
-
}
|
|
320
|
+
fileState.safetyTimer = setInterval(() => {
|
|
321
|
+
_readDelta(fileState);
|
|
322
|
+
}, SAFETY_POLL_MS);
|
|
323
|
+
}
|
|
252
324
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const data = JSON.stringify(segment);
|
|
267
|
-
clients.forEach(client => {
|
|
268
|
-
try { client.write(`event: load_chunk\ndata: ${data}\n\n`); } catch { }
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
clients.forEach(client => {
|
|
272
|
-
try { client.write(`event: load_end\ndata: {}\n\n`); } catch { }
|
|
273
|
-
});
|
|
274
|
-
watchLogFile({ ...opts, logFile: currentLogFile });
|
|
275
|
-
}
|
|
276
|
-
} catch (err) {
|
|
277
|
-
// File not yet created, will retry on next poll
|
|
278
|
-
}
|
|
279
|
-
});
|
|
325
|
+
export function unwatchLogFile(logFile) {
|
|
326
|
+
_unwatchSingleFile(logFile);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function unwatchAll() {
|
|
330
|
+
for (const logFile of watchedFiles.keys()) {
|
|
331
|
+
_unwatchSingleFile(logFile);
|
|
332
|
+
}
|
|
333
|
+
for (const [, entry] of _dirWatchers) {
|
|
334
|
+
try { entry.watcher.close(); } catch {}
|
|
335
|
+
}
|
|
336
|
+
_dirWatchers.clear();
|
|
337
|
+
watchedFiles.clear();
|
|
280
338
|
}
|
|
281
339
|
|
|
282
|
-
/**
|
|
283
|
-
* Start watching the current log file + install statusLine + context window.
|
|
284
|
-
* @param {object} opts
|
|
285
|
-
* @param {string} opts.logFile - current LOG_FILE
|
|
286
|
-
* @param {Array} opts.clients - SSE clients array
|
|
287
|
-
* @param {Function} opts.getClaudePid
|
|
288
|
-
* @param {Function} opts.runParallelHook
|
|
289
|
-
* @param {Function} opts.notifyStatsWorker
|
|
290
|
-
* @param {Function} opts.getLogFile
|
|
291
|
-
*/
|
|
292
340
|
export function startWatching(opts) {
|
|
293
341
|
const { clients, ...watchOpts } = opts;
|
|
294
342
|
watchLogFile({ ...watchOpts, clients });
|
|
295
343
|
}
|
|
296
344
|
|
|
297
|
-
/** Get the watchedFiles Map (for cleanup in stopViewer). */
|
|
298
345
|
export function getWatchedFiles() {
|
|
299
346
|
return watchedFiles;
|
|
300
347
|
}
|
|
@@ -75,7 +75,7 @@ export async function installPluginFromUrl(pluginsDir, fileUrl, extractNameScrip
|
|
|
75
75
|
writeFileSync(tmpFile, content, 'utf-8');
|
|
76
76
|
try {
|
|
77
77
|
const result = await new Promise((resolve, reject) => {
|
|
78
|
-
execFile('node', [extractNameScript, tmpFile], { timeout: 5000 }, (err, stdout) => {
|
|
78
|
+
execFile('node', [extractNameScript, tmpFile], { timeout: 5000, windowsHide: true }, (err, stdout) => {
|
|
79
79
|
if (err) return reject(err);
|
|
80
80
|
resolve(stdout);
|
|
81
81
|
});
|
package/server/lib/updater.js
CHANGED
|
@@ -107,7 +107,7 @@ export function isAnyCcvBusy({ currentPid, busy, portRange, lsofImpl } = {}) {
|
|
|
107
107
|
|
|
108
108
|
const [start, end] = Array.isArray(portRange) && portRange.length === 2 ? portRange : [7008, 7099];
|
|
109
109
|
const pid = typeof currentPid === 'number' ? currentPid : process.pid;
|
|
110
|
-
const runLsof = lsofImpl || ((cmd) => execSync(cmd, { timeout: 2000, encoding: 'utf-8' }));
|
|
110
|
+
const runLsof = lsofImpl || ((cmd) => execSync(cmd, { timeout: 2000, encoding: 'utf-8', windowsHide: true }));
|
|
111
111
|
|
|
112
112
|
try {
|
|
113
113
|
const out = String(runLsof(`lsof -iTCP:${start}-${end} -sTCP:LISTEN -P -n -Fp`));
|
|
@@ -216,7 +216,9 @@ export async function checkAndUpdate(options = {}) {
|
|
|
216
216
|
const child = spawnImpl(
|
|
217
217
|
'npm',
|
|
218
218
|
['install', '-g', `cc-viewer@${remoteVersion}`, '--no-audit', '--no-fund'],
|
|
219
|
-
|
|
219
|
+
// windowsHide:Windows 下 shell 模式经 cmd.exe 跑 npm.cmd(console-subsystem),
|
|
220
|
+
// 不隐藏会在后台更新期间常驻一个可见控制台窗口;POSIX 上为 no-op。
|
|
221
|
+
{ detached: true, stdio: 'ignore', shell: process.platform === 'win32', windowsHide: true }
|
|
220
222
|
);
|
|
221
223
|
if (child && typeof child.unref === 'function') child.unref();
|
|
222
224
|
} catch (err) {
|
package/server/pty-manager.js
CHANGED
|
@@ -178,7 +178,7 @@ export async function spawnClaude(proxyPort, cwd, extraArgs = [], claudePath = n
|
|
|
178
178
|
if (process.versions.electron) {
|
|
179
179
|
const { execSync } = await import('node:child_process');
|
|
180
180
|
try {
|
|
181
|
-
nodePath = execSync(process.platform === 'win32' ? 'where node' : 'which node', { encoding: 'utf-8' }).trim();
|
|
181
|
+
nodePath = execSync(process.platform === 'win32' ? 'where node' : 'which node', { encoding: 'utf-8', windowsHide: true }).trim();
|
|
182
182
|
if (process.platform === 'win32') nodePath = nodePath.split('\n')[0].trim();
|
|
183
183
|
} catch {
|
|
184
184
|
nodePath = process.platform === 'win32' ? 'node' : '/usr/local/bin/node';
|
|
@@ -223,7 +223,7 @@ function askHook(req, res, parsedUrl, isLocal, deps) {
|
|
|
223
223
|
// Phase 3: short-poll handoff endpoint. ask-bridge GET /api/ask-hook/:id/result?wait=30000
|
|
224
224
|
// 在 wait ms 内若答案/cancel 到达 → 立即返;否则返 204 让 client 重发。
|
|
225
225
|
// 内存有 entry → 注册 listener;内存无 → 查 disk consume(server 重启场景)。
|
|
226
|
-
function askHookResult(req, res, parsedUrl, isLocal, deps) {
|
|
226
|
+
async function askHookResult(req, res, parsedUrl, isLocal, deps) {
|
|
227
227
|
const url = parsedUrl.pathname;
|
|
228
228
|
try {
|
|
229
229
|
// URL 形如 /api/ask-hook/<id>/result?wait=30000;id 受白名单约束(与 POST 同源)
|
|
@@ -242,7 +242,7 @@ function askHookResult(req, res, parsedUrl, isLocal, deps) {
|
|
|
242
242
|
// 用 consumeIfFinal 单次 withLock 内判 status 决定是否 delete —— 旧设计的
|
|
243
243
|
// "consume + 若 pending 再 setEntry 写回" 两段是 race window:中间被 markAnswered 命中后,
|
|
244
244
|
// setEntry 走 status guard 已经不会覆盖;但不删的 pending 也无须重写一遍。
|
|
245
|
-
const diskEntry = askStoreConsumeIfFinal(id);
|
|
245
|
+
const diskEntry = await askStoreConsumeIfFinal(id);
|
|
246
246
|
if (diskEntry && diskEntry.status === 'answered') {
|
|
247
247
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
248
248
|
res.end(JSON.stringify({ answers: diskEntry.answers || {} }));
|
|
@@ -80,6 +80,8 @@ function dingtalkConfigPost(req, res, parsedUrl, isLocal, deps) {
|
|
|
80
80
|
allowStaffIds: incoming.allowStaffIds,
|
|
81
81
|
maxChunkChars: incoming.maxChunkChars,
|
|
82
82
|
blockOnSkipPermissions: incoming.blockOnSkipPermissions,
|
|
83
|
+
ackCard: incoming.ackCard,
|
|
84
|
+
cardTemplateId: incoming.cardTemplateId,
|
|
83
85
|
});
|
|
84
86
|
// 驱动进程管理器(替代旧的在进程 reloadBridge):启用→重启 worker,停用→停 worker。
|
|
85
87
|
try {
|
|
@@ -614,7 +614,7 @@ function openFile(req, res, parsedUrl, isLocal, deps) {
|
|
|
614
614
|
if (plat === 'darwin') {
|
|
615
615
|
execFile('open', [fullPath], () => {});
|
|
616
616
|
} else if (plat === 'win32') {
|
|
617
|
-
execFile('cmd.exe', ['/c', 'start', '', fullPath], () => {});
|
|
617
|
+
execFile('cmd.exe', ['/c', 'start', '', fullPath], { windowsHide: true }, () => {});
|
|
618
618
|
} else {
|
|
619
619
|
execFile('xdg-open', [fullPath], () => {});
|
|
620
620
|
}
|
|
@@ -842,7 +842,7 @@ function createDir(req, res, parsedUrl, isLocal, deps) {
|
|
|
842
842
|
function openLogDir(req, res) {
|
|
843
843
|
const dir = LOG_FILE ? dirname(LOG_FILE) : LOG_DIR;
|
|
844
844
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open';
|
|
845
|
-
execFile(cmd, [dir], () => {});
|
|
845
|
+
execFile(cmd, [dir], { windowsHide: true }, () => {});
|
|
846
846
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
847
847
|
res.end(JSON.stringify({ ok: true, dir }));
|
|
848
848
|
}
|
|
@@ -851,7 +851,7 @@ function openProfileDir(req, res) {
|
|
|
851
851
|
const dir = dirname(PROFILE_PATH);
|
|
852
852
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
853
853
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open';
|
|
854
|
-
execFile(cmd, [dir], () => {});
|
|
854
|
+
execFile(cmd, [dir], { windowsHide: true }, () => {});
|
|
855
855
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
856
856
|
res.end(JSON.stringify({ ok: true, dir }));
|
|
857
857
|
}
|
|
@@ -859,7 +859,7 @@ function openProfileDir(req, res) {
|
|
|
859
859
|
function openProjectDir(req, res) {
|
|
860
860
|
const dir = process.env.CCV_PROJECT_DIR || process.cwd();
|
|
861
861
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open';
|
|
862
|
-
execFile(cmd, [dir], () => {});
|
|
862
|
+
execFile(cmd, [dir], { windowsHide: true }, () => {});
|
|
863
863
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
864
864
|
res.end(JSON.stringify({ ok: true, dir }));
|
|
865
865
|
}
|
|
@@ -4,6 +4,23 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { PACKAGE_JSON } from '../_paths.js';
|
|
5
5
|
import { LOG_DIR } from '../../findcc.js';
|
|
6
6
|
import { _projectName } from '../interceptor.js';
|
|
7
|
+
import { detectHomebrewInstall } from '../lib/updater.js';
|
|
8
|
+
|
|
9
|
+
// 判定当前 cc-viewer 的安装渠道,供前端精准匹配升级命令。
|
|
10
|
+
// - electron:桌面版(in-process server),走 GitHub Releases 重新下载安装包。
|
|
11
|
+
// - brew:Homebrew Cellar 布局命中 → `brew upgrade cc-viewer`(npm install -g 会跟 Cellar 打架)。
|
|
12
|
+
// - npm:默认兜底 → `npm install -g cc-viewer --registry=...`(指定官方源避免镜像滞后拿到旧版本)。
|
|
13
|
+
// deps 仅供单测注入;失败安全:detect 抛异常时回落到 npm(不会误导 brew 用户走 npm)。
|
|
14
|
+
export function getInstallMethod({
|
|
15
|
+
electron = process.versions && process.versions.electron,
|
|
16
|
+
detect = detectHomebrewInstall,
|
|
17
|
+
} = {}) {
|
|
18
|
+
if (electron) return 'electron';
|
|
19
|
+
try {
|
|
20
|
+
if (detect()) return 'brew';
|
|
21
|
+
} catch { /* 探测失败 → 回落 npm */ }
|
|
22
|
+
return 'npm';
|
|
23
|
+
}
|
|
7
24
|
|
|
8
25
|
function projectName(req, res) {
|
|
9
26
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -20,7 +37,7 @@ function versionInfo(req, res) {
|
|
|
20
37
|
try {
|
|
21
38
|
const pkg = JSON.parse(readFileSync(PACKAGE_JSON, 'utf-8'));
|
|
22
39
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
23
|
-
res.end(JSON.stringify({ version: pkg.version }));
|
|
40
|
+
res.end(JSON.stringify({ version: pkg.version, installMethod: getInstallMethod() }));
|
|
24
41
|
} catch {
|
|
25
42
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
26
43
|
res.end(JSON.stringify({ error: 'Failed to read version' }));
|