cc-viewer 1.5.3 → 1.5.5
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/index-48freCdh.css +41 -0
- package/dist/assets/index-J-HTEXmo.js +669 -0
- package/dist/index.html +2 -2
- package/i18n.js +18 -18
- package/package.json +1 -1
- package/proxy.js +2 -0
- package/pty-manager.js +42 -1
- package/server.js +90 -1
- package/dist/assets/index-BTCSaxml.js +0 -606
- package/dist/assets/index-CWx2rxKd.css +0 -41
package/dist/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
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-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-J-HTEXmo.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-48freCdh.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/i18n.js
CHANGED
|
@@ -567,24 +567,24 @@ const i18nData = {
|
|
|
567
567
|
"uk": "Імпорт локальних логів"
|
|
568
568
|
},
|
|
569
569
|
"ui.loadLocalJsonl": {
|
|
570
|
-
"zh": "
|
|
571
|
-
"en": "
|
|
572
|
-
"zh-TW": "
|
|
573
|
-
"ko": "
|
|
574
|
-
"ja": "
|
|
575
|
-
"de": "
|
|
576
|
-
"es": "
|
|
577
|
-
"fr": "
|
|
578
|
-
"it": "
|
|
579
|
-
"da": "
|
|
580
|
-
"pl": "
|
|
581
|
-
"ru": "
|
|
582
|
-
"ar": "
|
|
583
|
-
"no": "
|
|
584
|
-
"pt-BR": "
|
|
585
|
-
"th": "
|
|
586
|
-
"tr": "
|
|
587
|
-
"uk": "
|
|
570
|
+
"zh": "上传日志并解析",
|
|
571
|
+
"en": "Upload and parse log",
|
|
572
|
+
"zh-TW": "上傳日誌並解析",
|
|
573
|
+
"ko": "로그 업로드 및 분석",
|
|
574
|
+
"ja": "ログをアップロードして解析",
|
|
575
|
+
"de": "Log hochladen und analysieren",
|
|
576
|
+
"es": "Subir y analizar registro",
|
|
577
|
+
"fr": "Télécharger et analyser le journal",
|
|
578
|
+
"it": "Carica e analizza il log",
|
|
579
|
+
"da": "Upload og analysér log",
|
|
580
|
+
"pl": "Prześlij i przeanalizuj log",
|
|
581
|
+
"ru": "Загрузить и разобрать журнал",
|
|
582
|
+
"ar": "تحميل وتحليل السجل",
|
|
583
|
+
"no": "Last opp og analyser logg",
|
|
584
|
+
"pt-BR": "Enviar e analisar log",
|
|
585
|
+
"th": "อัปโหลดและวิเคราะห์ล็อก",
|
|
586
|
+
"tr": "Günlüğü yükle ve ayrıştır",
|
|
587
|
+
"uk": "Завантажити та проаналізувати журнал"
|
|
588
588
|
},
|
|
589
589
|
"ui.fileTooLarge": {
|
|
590
590
|
"zh": "文件大小不能超过 500MB",
|
package/package.json
CHANGED
package/proxy.js
CHANGED
|
@@ -127,6 +127,8 @@ export function startProxy() {
|
|
|
127
127
|
const { Readable, pipeline } = await import('node:stream');
|
|
128
128
|
// @ts-ignore
|
|
129
129
|
const nodeStream = Readable.fromWeb(response.body);
|
|
130
|
+
// 持久 error handler 兜底:防止 pipeline 清理后延迟到达的 error 事件导致进程崩溃
|
|
131
|
+
nodeStream.on('error', () => {});
|
|
130
132
|
// pipeline handles stream errors; without this, unhandled 'error' events crash the process.
|
|
131
133
|
pipeline(nodeStream, res, (err) => {
|
|
132
134
|
if (err && process.env.CCV_DEBUG) {
|
package/pty-manager.js
CHANGED
|
@@ -14,6 +14,45 @@ const MAX_BUFFER = 200000;
|
|
|
14
14
|
let batchBuffer = '';
|
|
15
15
|
let batchScheduled = false;
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* 在 outputBuffer 截断时,找到安全的截断位置,
|
|
19
|
+
* 避免从 ANSI 转义序列中间开始导致终端状态紊乱。
|
|
20
|
+
* 策略:从截断点向后扫描,跳过可能被截断的不完整转义序列。
|
|
21
|
+
*/
|
|
22
|
+
function findSafeSliceStart(buf, rawStart) {
|
|
23
|
+
// 从 rawStart 开始,向后最多扫描 64 字节寻找安全起点
|
|
24
|
+
const scanLimit = Math.min(rawStart + 64, buf.length);
|
|
25
|
+
let i = rawStart;
|
|
26
|
+
while (i < scanLimit) {
|
|
27
|
+
const ch = buf.charCodeAt(i);
|
|
28
|
+
// 如果当前字符是 ESC (0x1b),可能是新转义序列的开头,
|
|
29
|
+
// 但也可能是被截断的序列的中间部分,跳过整个序列
|
|
30
|
+
if (ch === 0x1b) {
|
|
31
|
+
// 找到 ESC,向后寻找序列结束符(字母字符)
|
|
32
|
+
let j = i + 1;
|
|
33
|
+
while (j < scanLimit && !((buf.charCodeAt(j) >= 0x40 && buf.charCodeAt(j) <= 0x7e) && j > i + 1)) {
|
|
34
|
+
j++;
|
|
35
|
+
}
|
|
36
|
+
if (j < scanLimit) {
|
|
37
|
+
// 找到完整序列末尾,从下一个字符开始是安全的
|
|
38
|
+
return j + 1;
|
|
39
|
+
}
|
|
40
|
+
// 序列不完整,继续扫描
|
|
41
|
+
i = j;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// 如果字符是 CSI 参数字符 (0x30-0x3f) 或中间字符 (0x20-0x2f),
|
|
45
|
+
// 说明我们在转义序列中间,继续向后
|
|
46
|
+
if ((ch >= 0x20 && ch <= 0x3f)) {
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
// 普通可见字符或控制字符(非转义相关),这是安全位置
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
return i < buf.length ? i : rawStart;
|
|
54
|
+
}
|
|
55
|
+
|
|
17
56
|
function flushBatch() {
|
|
18
57
|
batchScheduled = false;
|
|
19
58
|
if (!batchBuffer) return;
|
|
@@ -87,7 +126,9 @@ export async function spawnClaude(proxyPort, cwd, extraArgs = [], claudePath = n
|
|
|
87
126
|
ptyProcess.onData((data) => {
|
|
88
127
|
outputBuffer += data;
|
|
89
128
|
if (outputBuffer.length > MAX_BUFFER) {
|
|
90
|
-
|
|
129
|
+
const rawStart = outputBuffer.length - MAX_BUFFER;
|
|
130
|
+
const safeStart = findSafeSliceStart(outputBuffer, rawStart);
|
|
131
|
+
outputBuffer = outputBuffer.slice(safeStart);
|
|
91
132
|
}
|
|
92
133
|
batchBuffer += data;
|
|
93
134
|
if (!batchScheduled) {
|
package/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { createConnection } from 'node:net';
|
|
3
3
|
import { randomBytes } from 'node:crypto';
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, watchFile, unwatchFile, statSync, readdirSync, renameSync, unlinkSync, openSync, readSync, closeSync, realpathSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, watchFile, unwatchFile, statSync, readdirSync, renameSync, unlinkSync, openSync, readSync, closeSync, realpathSync, mkdirSync, createReadStream } from 'node:fs';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { dirname, join, extname } from 'node:path';
|
|
7
7
|
import { homedir, userInfo, platform, networkInterfaces } from 'node:os';
|
|
@@ -1141,6 +1141,49 @@ async function handleRequest(req, res) {
|
|
|
1141
1141
|
return;
|
|
1142
1142
|
}
|
|
1143
1143
|
|
|
1144
|
+
// 下载指定本地日志文件(原始 JSONL 格式)
|
|
1145
|
+
if (url === '/api/download-log' && method === 'GET') {
|
|
1146
|
+
const file = parsedUrl.searchParams.get('file');
|
|
1147
|
+
if (!file || file.includes('..')) {
|
|
1148
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1149
|
+
res.end(JSON.stringify({ error: 'Invalid file name' }));
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (!file.endsWith('.jsonl')) {
|
|
1153
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1154
|
+
res.end(JSON.stringify({ error: 'Invalid file type' }));
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
const filePath = join(LOG_DIR, file);
|
|
1158
|
+
try {
|
|
1159
|
+
if (!existsSync(filePath)) {
|
|
1160
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1161
|
+
res.end(JSON.stringify({ error: 'File not found' }));
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const realPath = realpathSync(filePath);
|
|
1165
|
+
const realLogDir = realpathSync(LOG_DIR);
|
|
1166
|
+
if (!realPath.startsWith(realLogDir)) {
|
|
1167
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1168
|
+
res.end(JSON.stringify({ error: 'Access denied' }));
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const fileName = file.split('/').pop();
|
|
1172
|
+
const stat = statSync(realPath);
|
|
1173
|
+
res.writeHead(200, {
|
|
1174
|
+
'Content-Type': 'application/octet-stream',
|
|
1175
|
+
'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
|
|
1176
|
+
'Content-Length': stat.size,
|
|
1177
|
+
});
|
|
1178
|
+
const stream = createReadStream(realPath);
|
|
1179
|
+
stream.pipe(res);
|
|
1180
|
+
} catch (err) {
|
|
1181
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1182
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1183
|
+
}
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1144
1187
|
// 读取指定本地日志文件(支持 project/file 路径)
|
|
1145
1188
|
if (url === '/api/local-log' && method === 'GET') {
|
|
1146
1189
|
const file = parsedUrl.searchParams.get('file');
|
|
@@ -1187,6 +1230,52 @@ async function handleRequest(req, res) {
|
|
|
1187
1230
|
return;
|
|
1188
1231
|
}
|
|
1189
1232
|
|
|
1233
|
+
// 删除日志文件
|
|
1234
|
+
if (url === '/api/delete-logs' && method === 'POST') {
|
|
1235
|
+
let body = '';
|
|
1236
|
+
req.on('data', chunk => { body += chunk; });
|
|
1237
|
+
req.on('end', () => {
|
|
1238
|
+
try {
|
|
1239
|
+
const { files } = JSON.parse(body);
|
|
1240
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
1241
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1242
|
+
res.end(JSON.stringify({ error: 'No files specified' }));
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const results = [];
|
|
1246
|
+
for (const file of files) {
|
|
1247
|
+
if (!file || file.includes('..') || !file.endsWith('.jsonl')) {
|
|
1248
|
+
results.push({ file, error: 'Invalid file name' });
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
const filePath = join(LOG_DIR, file);
|
|
1252
|
+
try {
|
|
1253
|
+
if (!existsSync(filePath)) {
|
|
1254
|
+
results.push({ file, error: 'Not found' });
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const realPath = realpathSync(filePath);
|
|
1258
|
+
const realLogDir = realpathSync(LOG_DIR);
|
|
1259
|
+
if (!realPath.startsWith(realLogDir)) {
|
|
1260
|
+
results.push({ file, error: 'Access denied' });
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
unlinkSync(realPath);
|
|
1264
|
+
results.push({ file, ok: true });
|
|
1265
|
+
} catch (err) {
|
|
1266
|
+
results.push({ file, error: err.message });
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1270
|
+
res.end(JSON.stringify({ results }));
|
|
1271
|
+
} catch {
|
|
1272
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1273
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1190
1279
|
// 合并日志文件
|
|
1191
1280
|
if (url === '/api/merge-logs' && method === 'POST') {
|
|
1192
1281
|
let body = '';
|