@myskyline_ai/ccdebug 0.2.1
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/LICENSE +201 -0
- package/README.md +129 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +674 -0
- package/dist/html-generator.d.ts +24 -0
- package/dist/html-generator.d.ts.map +1 -0
- package/dist/html-generator.js +141 -0
- package/dist/index-generator.d.ts +29 -0
- package/dist/index-generator.d.ts.map +1 -0
- package/dist/index-generator.js +271 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/interceptor-loader.js +59 -0
- package/dist/interceptor.d.ts +46 -0
- package/dist/interceptor.d.ts.map +1 -0
- package/dist/interceptor.js +555 -0
- package/dist/log-file-manager.d.ts +15 -0
- package/dist/log-file-manager.d.ts.map +1 -0
- package/dist/log-file-manager.js +41 -0
- package/dist/shared-conversation-processor.d.ts +114 -0
- package/dist/shared-conversation-processor.d.ts.map +1 -0
- package/dist/shared-conversation-processor.js +663 -0
- package/dist/token-extractor.js +28 -0
- package/dist/types.d.ts +95 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/frontend/dist/index.global.js +1522 -0
- package/frontend/dist/styles.css +985 -0
- package/frontend/template.html +19 -0
- package/package.json +83 -0
- package/web/debug.html +14 -0
- package/web/dist/assets/index-BIP9r3RA.js +48 -0
- package/web/dist/assets/index-BIP9r3RA.js.map +1 -0
- package/web/dist/assets/index-De3gn-G-.css +1 -0
- package/web/dist/favicon.svg +4 -0
- package/web/dist/index.html +15 -0
- package/web/index.html +14 -0
- package/web/package.json +47 -0
- package/web/server/conversation-parser.d.ts +47 -0
- package/web/server/conversation-parser.d.ts.map +1 -0
- package/web/server/conversation-parser.js +564 -0
- package/web/server/conversation-parser.js.map +1 -0
- package/web/server/index.d.ts +16 -0
- package/web/server/index.d.ts.map +1 -0
- package/web/server/index.js +60 -0
- package/web/server/index.js.map +1 -0
- package/web/server/log-file-manager.d.ts +98 -0
- package/web/server/log-file-manager.d.ts.map +1 -0
- package/web/server/log-file-manager.js +512 -0
- package/web/server/log-file-manager.js.map +1 -0
- package/web/server/src/types/index.d.ts +68 -0
- package/web/server/src/types/index.d.ts.map +1 -0
- package/web/server/src/types/index.js +3 -0
- package/web/server/src/types/index.js.map +1 -0
- package/web/server/test-path.js +48 -0
- package/web/server/web-server.d.ts +41 -0
- package/web/server/web-server.d.ts.map +1 -0
- package/web/server/web-server.js +807 -0
- package/web/server/web-server.js.map +1 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.WebServer = void 0;
|
|
37
|
+
const express = require('express');
|
|
38
|
+
const { createServer } = require('http');
|
|
39
|
+
const { Server: SocketIOServer } = require('socket.io');
|
|
40
|
+
const cors = require('cors');
|
|
41
|
+
const path = require('path');
|
|
42
|
+
const fs = require('fs');
|
|
43
|
+
// const Anthropic = require('@anthropic-ai/sdk');
|
|
44
|
+
const { LogFileManager } = require('./log-file-manager.js');
|
|
45
|
+
const { ConversationParser } = require('./conversation-parser.js');
|
|
46
|
+
// import { ApiResponse, FilesApiResponse, ProjectInfo, ConversationData } from '../src/types/index.js';
|
|
47
|
+
// 统一调试日志开关:设置环境变量 CCDEBUG_DEBUG=1 时才输出详细日志
|
|
48
|
+
const DEBUG_LOGS = process.env.CCDEBUG_DEBUG === '1';
|
|
49
|
+
const dlog = (...args) => { if (DEBUG_LOGS)
|
|
50
|
+
console.log(...args); };
|
|
51
|
+
class WebServer {
|
|
52
|
+
/**
|
|
53
|
+
* 按优先级获取ANTHROPIC_AUTH_TOKEN
|
|
54
|
+
* 1. 从项目目录的.claude/settings.local.json获取
|
|
55
|
+
* 2. 从全局Claude目录的settings.local.json获取
|
|
56
|
+
* 3. 从全局环境变量获取
|
|
57
|
+
*/
|
|
58
|
+
getAuthToken() {
|
|
59
|
+
// 1. 从项目目录的.claude/settings.local.json获取
|
|
60
|
+
const projectSettingsPath = path.join(this.config.projectDir, '.claude/settings.local.json');
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(projectSettingsPath)) {
|
|
63
|
+
const projectSettings = JSON.parse(fs.readFileSync(projectSettingsPath, 'utf-8'));
|
|
64
|
+
if (projectSettings.env && projectSettings.env.ANTHROPIC_AUTH_TOKEN) {
|
|
65
|
+
dlog('从项目设置文件获取ANTHROPIC_AUTH_TOKEN');
|
|
66
|
+
return projectSettings.env.ANTHROPIC_AUTH_TOKEN;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
console.error('读取项目设置文件失败:', error);
|
|
72
|
+
}
|
|
73
|
+
// 2. 从全局Claude目录的settings.local.json获取
|
|
74
|
+
const globalSettingsPath = path.join(process.env.HOME || '', '.claude/settings.local.json');
|
|
75
|
+
try {
|
|
76
|
+
if (fs.existsSync(globalSettingsPath)) {
|
|
77
|
+
const globalSettings = JSON.parse(fs.readFileSync(globalSettingsPath, 'utf-8'));
|
|
78
|
+
if (globalSettings.env && globalSettings.env.ANTHROPIC_AUTH_TOKEN) {
|
|
79
|
+
dlog('从全局设置文件获取ANTHROPIC_AUTH_TOKEN');
|
|
80
|
+
return globalSettings.env.ANTHROPIC_AUTH_TOKEN;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
console.error('读取全局设置文件失败:', error);
|
|
86
|
+
}
|
|
87
|
+
// 3. 从全局环境变量获取
|
|
88
|
+
if (process.env.ANTHROPIC_AUTH_TOKEN) {
|
|
89
|
+
dlog('从环境变量获取ANTHROPIC_AUTH_TOKEN');
|
|
90
|
+
return process.env.ANTHROPIC_AUTH_TOKEN;
|
|
91
|
+
}
|
|
92
|
+
console.warn('未能获取ANTHROPIC_AUTH_TOKEN');
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
constructor(config) {
|
|
96
|
+
this.isStarted = false;
|
|
97
|
+
this.config = config;
|
|
98
|
+
this.app = express();
|
|
99
|
+
this.server = createServer(this.app);
|
|
100
|
+
// 初始化 Anthropic 客户端
|
|
101
|
+
// this.anthropic = new Anthropic({
|
|
102
|
+
// apiKey: process.env.ANTHROPIC_API_KEY,
|
|
103
|
+
// });
|
|
104
|
+
this.logFileManager = new LogFileManager();
|
|
105
|
+
this.conversationParser = new ConversationParser();
|
|
106
|
+
//默认从当前项目的.claude-trace/cclog目录获取cc日志文件,如果cclog不存在或没有jsonl文件,再从.claude/projects下获取
|
|
107
|
+
this.logDir = path.join(config.projectDir, '.claude-trace/cclog');
|
|
108
|
+
// 检查目录是否存在以及目录中是否有jsonl文件
|
|
109
|
+
let useDefaultLogDir = true;
|
|
110
|
+
if (fs.existsSync(this.logDir)) {
|
|
111
|
+
try {
|
|
112
|
+
const files = fs.readdirSync(this.logDir);
|
|
113
|
+
const hasJsonlFiles = files.some(file => file.endsWith('.jsonl'));
|
|
114
|
+
if (hasJsonlFiles) {
|
|
115
|
+
useDefaultLogDir = false;
|
|
116
|
+
dlog(`使用默认日志目录: ${this.logDir},找到 ${files.filter(f => f.endsWith('.jsonl')).length} 个jsonl文件`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.error('读取日志目录失败:', error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// 如果目录不存在或没有jsonl文件,则使用resolveLogDirectory
|
|
124
|
+
if (useDefaultLogDir) {
|
|
125
|
+
this.logDir = this.logFileManager.resolveLogDirectory(config.projectDir);
|
|
126
|
+
dlog(`使用备用日志目录: ${this.logDir}`);
|
|
127
|
+
}
|
|
128
|
+
// 设置ConversationParser的日志目录
|
|
129
|
+
this.conversationParser.setLogDirectory(this.logDir);
|
|
130
|
+
this.setupBasicMiddleware();
|
|
131
|
+
this.setupRoutes();
|
|
132
|
+
this.setupStaticFiles();
|
|
133
|
+
// this.setupWebSocket();
|
|
134
|
+
// this.setupFileWatcher();
|
|
135
|
+
}
|
|
136
|
+
setupBasicMiddleware() {
|
|
137
|
+
// CORS配置
|
|
138
|
+
this.app.use(cors());
|
|
139
|
+
// JSON解析
|
|
140
|
+
this.app.use(express.json());
|
|
141
|
+
}
|
|
142
|
+
setupStaticFiles() {
|
|
143
|
+
// 静态文件服务 - 优先使用构建后的public目录
|
|
144
|
+
if (this.config.staticDir) {
|
|
145
|
+
const publicDir = path.join(this.config.staticDir, 'public');
|
|
146
|
+
// 如果存在public目录,优先使用它
|
|
147
|
+
if (fs.existsSync(publicDir)) {
|
|
148
|
+
dlog('Using built static files from:', publicDir);
|
|
149
|
+
this.app.use(express.static(publicDir));
|
|
150
|
+
}
|
|
151
|
+
// 同时也提供dist根目录的静态文件(用于assets等)
|
|
152
|
+
this.app.use(express.static(this.config.staticDir));
|
|
153
|
+
}
|
|
154
|
+
// SPA路由支持 - 必须放在最后,并且只处理非API请求
|
|
155
|
+
this.app.get('*', (req, res) => {
|
|
156
|
+
// 如果是API请求,不处理
|
|
157
|
+
if (req.path.startsWith('/api/')) {
|
|
158
|
+
return res.status(404).json({ success: false, error: 'API endpoint not found' });
|
|
159
|
+
}
|
|
160
|
+
if (this.config.staticDir) {
|
|
161
|
+
// 优先使用构建后的版本 dist/public/index.html
|
|
162
|
+
const publicIndexPath = path.join(this.config.staticDir, 'public', 'index.html');
|
|
163
|
+
if (fs.existsSync(publicIndexPath)) {
|
|
164
|
+
dlog('Serving built HTML from:', publicIndexPath);
|
|
165
|
+
return res.sendFile(publicIndexPath);
|
|
166
|
+
}
|
|
167
|
+
// 备选方案:使用 dist/index.html
|
|
168
|
+
const indexPath = path.join(this.config.staticDir, 'index.html');
|
|
169
|
+
if (fs.existsSync(indexPath)) {
|
|
170
|
+
dlog('Serving fallback HTML from:', indexPath);
|
|
171
|
+
return res.sendFile(indexPath);
|
|
172
|
+
}
|
|
173
|
+
console.error('No index.html found in static directory:', this.config.staticDir);
|
|
174
|
+
res.status(404).send('Index file not found');
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
res.status(404).send('Static directory not configured');
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
setupRoutes() {
|
|
182
|
+
// 项目信息API
|
|
183
|
+
this.app.get('/api/project/info', async (req, res) => {
|
|
184
|
+
try {
|
|
185
|
+
const projectInfo = {
|
|
186
|
+
path: this.config.projectDir,
|
|
187
|
+
logDir: this.logDir
|
|
188
|
+
};
|
|
189
|
+
const response = {
|
|
190
|
+
success: true,
|
|
191
|
+
data: projectInfo
|
|
192
|
+
};
|
|
193
|
+
res.json(response);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
console.error('获取项目信息失败:', error);
|
|
197
|
+
const response = {
|
|
198
|
+
success: false,
|
|
199
|
+
error: error instanceof Error ? error.message : '未知错误'
|
|
200
|
+
};
|
|
201
|
+
res.status(500).json(response);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
// 主日志列表API
|
|
205
|
+
this.app.get('/api/main-logs', async (req, res) => {
|
|
206
|
+
try {
|
|
207
|
+
const mainLogs = await this.logFileManager.getMainLogSummaries(this.logDir);
|
|
208
|
+
const response = {
|
|
209
|
+
success: true,
|
|
210
|
+
data: {
|
|
211
|
+
mainLogs: mainLogs,
|
|
212
|
+
logDir: this.logDir
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
res.json(response);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
console.error('获取主日志列表失败:', error);
|
|
219
|
+
const response = {
|
|
220
|
+
success: false,
|
|
221
|
+
error: error instanceof Error ? error.message : '未知错误'
|
|
222
|
+
};
|
|
223
|
+
res.status(500).json(response);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
// 文件列表API
|
|
227
|
+
this.app.get('/api/files', async (req, res) => {
|
|
228
|
+
try {
|
|
229
|
+
const sessionId = req.query.sessionId;
|
|
230
|
+
const mainLogId = req.query.mainLogId;
|
|
231
|
+
let files = await this.logFileManager.getAvailableLogFiles(this.logDir);
|
|
232
|
+
// 如果提供了mainLogId参数,返回主日志及其子agent日志
|
|
233
|
+
if (mainLogId) {
|
|
234
|
+
// 获取主日志
|
|
235
|
+
const mainLog = files.find(f => f.id === mainLogId);
|
|
236
|
+
if (mainLog) {
|
|
237
|
+
// 获取子agent日志
|
|
238
|
+
const agentLogs = await this.logFileManager.getAgentLogsForSession(this.logDir, mainLogId);
|
|
239
|
+
// 合并主日志和子agent日志
|
|
240
|
+
files = [mainLog, ...agentLogs];
|
|
241
|
+
dlog(`按mainLogId ${mainLogId} 过滤后找到 ${files.length} 个文件(1个主日志 + ${agentLogs.length}个子agent日志)`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
files = [];
|
|
245
|
+
dlog(`未找到mainLogId为 ${mainLogId} 的主日志`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// 如果提供了sessionId参数,过滤匹配的文件
|
|
249
|
+
else if (sessionId) {
|
|
250
|
+
files = files.filter(file => file.id.includes(sessionId) || file.name.includes(sessionId));
|
|
251
|
+
dlog(`按sessionId ${sessionId} 过滤后找到 ${files.length} 个文件`);
|
|
252
|
+
}
|
|
253
|
+
const filesData = {
|
|
254
|
+
files: files,
|
|
255
|
+
latest: files.length > 0 ? files[0].id : null,
|
|
256
|
+
projectDir: this.config.projectDir,
|
|
257
|
+
logDir: this.logDir,
|
|
258
|
+
sessionId: sessionId || null,
|
|
259
|
+
mainLogId: mainLogId || null
|
|
260
|
+
};
|
|
261
|
+
const response = {
|
|
262
|
+
success: true,
|
|
263
|
+
data: filesData
|
|
264
|
+
};
|
|
265
|
+
res.json(response);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
console.error('获取文件列表失败:', error);
|
|
269
|
+
const response = {
|
|
270
|
+
success: false,
|
|
271
|
+
error: error instanceof Error ? error.message : '未知错误'
|
|
272
|
+
};
|
|
273
|
+
res.status(500).json(response);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
// 对话数据API
|
|
277
|
+
this.app.get('/api/conversations/:fileId', async (req, res) => {
|
|
278
|
+
try {
|
|
279
|
+
const fileId = req.params.fileId;
|
|
280
|
+
if (!fileId) {
|
|
281
|
+
const response = {
|
|
282
|
+
success: false,
|
|
283
|
+
error: '缺少fileId参数'
|
|
284
|
+
};
|
|
285
|
+
return res.status(400).json(response);
|
|
286
|
+
}
|
|
287
|
+
const conversationData = await this.conversationParser.parseFile(fileId);
|
|
288
|
+
const response = {
|
|
289
|
+
success: true,
|
|
290
|
+
data: conversationData
|
|
291
|
+
};
|
|
292
|
+
res.json(response);
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
console.error('获取对话数据失败:', error);
|
|
296
|
+
const response = {
|
|
297
|
+
success: false,
|
|
298
|
+
error: error instanceof Error ? error.message : '未知错误'
|
|
299
|
+
};
|
|
300
|
+
res.status(500).json(response);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
// 健康检查API
|
|
304
|
+
this.app.get('/api/health', (req, res) => {
|
|
305
|
+
const response = {
|
|
306
|
+
success: true,
|
|
307
|
+
message: 'Web服务器运行正常'
|
|
308
|
+
};
|
|
309
|
+
res.json(response);
|
|
310
|
+
});
|
|
311
|
+
// 调试API:获取trace文件列表
|
|
312
|
+
this.app.get('/api/debug/trace-files', async (req, res) => {
|
|
313
|
+
try {
|
|
314
|
+
const traceDir = path.join(this.config.projectDir, '.claude-trace');
|
|
315
|
+
if (!fs.existsSync(traceDir)) {
|
|
316
|
+
return res.json({ files: [], traceDir });
|
|
317
|
+
}
|
|
318
|
+
const files = fs.readdirSync(traceDir).map(file => {
|
|
319
|
+
const filePath = path.join(traceDir, file);
|
|
320
|
+
const stats = fs.statSync(filePath);
|
|
321
|
+
return {
|
|
322
|
+
name: file,
|
|
323
|
+
size: stats.size,
|
|
324
|
+
modified: stats.mtime
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
res.json({ files, traceDir });
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
331
|
+
res.status(500).json({ error: errorMessage });
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
// 获取会话步骤详情
|
|
335
|
+
this.app.get('/api/conversations/:fileId/steps/:stepId', async (req, res) => {
|
|
336
|
+
try {
|
|
337
|
+
const { fileId, stepId } = req.params;
|
|
338
|
+
if (!fileId || !stepId) {
|
|
339
|
+
const response = {
|
|
340
|
+
success: false,
|
|
341
|
+
error: '缺少fileId或stepId参数'
|
|
342
|
+
};
|
|
343
|
+
return res.status(400).json(response);
|
|
344
|
+
}
|
|
345
|
+
const conversationData = await this.conversationParser.parseFile(fileId);
|
|
346
|
+
const targetStep = conversationData.steps.find(step => step.id === stepId);
|
|
347
|
+
if (!targetStep) {
|
|
348
|
+
const response = {
|
|
349
|
+
success: false,
|
|
350
|
+
error: '未找到对应的步骤'
|
|
351
|
+
};
|
|
352
|
+
return res.status(404).json(response);
|
|
353
|
+
}
|
|
354
|
+
const response = {
|
|
355
|
+
success: true,
|
|
356
|
+
data: targetStep
|
|
357
|
+
};
|
|
358
|
+
res.json(response);
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
console.error('获取步骤详情失败:', error);
|
|
362
|
+
console.error('错误详情:', {
|
|
363
|
+
fileId: req.params.fileId,
|
|
364
|
+
stepId: req.params.stepId,
|
|
365
|
+
errorMessage: error instanceof Error ? error.message : '未知错误',
|
|
366
|
+
errorStack: error instanceof Error ? error.stack : undefined
|
|
367
|
+
});
|
|
368
|
+
const response = {
|
|
369
|
+
success: false,
|
|
370
|
+
error: error instanceof Error ? error.message : '未知错误'
|
|
371
|
+
};
|
|
372
|
+
res.status(500).json(response);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
// 获取LLM日志
|
|
376
|
+
this.app.get('/api/conversations/:fileId/llm-logs/:messageId', async (req, res) => {
|
|
377
|
+
try {
|
|
378
|
+
const { fileId, messageId } = req.params;
|
|
379
|
+
dlog(`查找LLM日志: fileId=${fileId}, messageId=${messageId}`);
|
|
380
|
+
// 1. 直接在日志目录中查找对应的文件
|
|
381
|
+
const traceDir = path.join(this.config.projectDir, '.claude-trace', 'tracelog');
|
|
382
|
+
let targetFilePath = null;
|
|
383
|
+
let targetFileId = fileId;
|
|
384
|
+
//如果是agent-*文件,则从this.logDir目录读取此文件,找到第一个条记录的sessionId属性
|
|
385
|
+
if (fileId.startsWith('agent-')) {
|
|
386
|
+
const logFilePath = path.join(this.logDir, `${fileId}.jsonl`);
|
|
387
|
+
if (fs.existsSync(logFilePath)) {
|
|
388
|
+
const fileContent = await fs.readFileSync(logFilePath, 'utf-8');
|
|
389
|
+
const lines = fileContent.split('\n').filter(line => line.trim() !== '');
|
|
390
|
+
if (lines.length > 0) {
|
|
391
|
+
const firstRecord = JSON.parse(lines[0]);
|
|
392
|
+
if (firstRecord.sessionId) {
|
|
393
|
+
targetFileId = firstRecord.sessionId;
|
|
394
|
+
dlog(`根据agent-*文件找到sessionId: ${targetFileId}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// 查找sessionId.jsonl文件
|
|
400
|
+
const jsonlFilePath = path.join(traceDir, `${targetFileId}.jsonl`);
|
|
401
|
+
if (fs.existsSync(jsonlFilePath)) {
|
|
402
|
+
targetFilePath = jsonlFilePath;
|
|
403
|
+
dlog(`找到LLM日志文件: ${targetFilePath}`);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
throw new Error(`找不到LLM日志文件: ${targetFileId}`);
|
|
407
|
+
}
|
|
408
|
+
// 2. 读取并解析文件内容
|
|
409
|
+
const fileContent = await fs.promises.readFile(targetFilePath, 'utf-8');
|
|
410
|
+
let matchedRecord = null;
|
|
411
|
+
if (targetFilePath.endsWith('.jsonl')) {
|
|
412
|
+
// 处理.jsonl文件:逐行解析JSON
|
|
413
|
+
const lines = fileContent.split('\n').filter(line => line.trim() !== '');
|
|
414
|
+
for (const line of lines) {
|
|
415
|
+
try {
|
|
416
|
+
const record = JSON.parse(line);
|
|
417
|
+
// 3. 在response.body_raw中搜索messageId
|
|
418
|
+
if (record.response && record.response.body_raw) {
|
|
419
|
+
// 解析SSE数据,查找message_start事件中的messageId
|
|
420
|
+
try {
|
|
421
|
+
const sseEvents = this.parseSSEData(record.response.body_raw);
|
|
422
|
+
for (const event of sseEvents) {
|
|
423
|
+
if (event.type === 'message_start' && event.message && event.message.id) {
|
|
424
|
+
dlog(`找到message_start事件,messageId: ${event.message.id}, 查找的messageId: ${messageId}`);
|
|
425
|
+
if (event.message.id === messageId || messageId === 'step_1') {
|
|
426
|
+
// 如果messageId匹配,或者请求的是step_1(默认值),则返回这条记录
|
|
427
|
+
matchedRecord = record;
|
|
428
|
+
dlog(`匹配成功,返回记录`);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (matchedRecord)
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
catch (parseError) {
|
|
437
|
+
console.warn(`SSE解析失败: ${parseError}`);
|
|
438
|
+
// 如果SSE解析失败,回退到简单的字符串搜索
|
|
439
|
+
if (record.response.body_raw.includes(messageId)) {
|
|
440
|
+
matchedRecord = record;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch (parseError) {
|
|
447
|
+
console.warn(`解析JSON行失败: ${parseError}`);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (!matchedRecord) {
|
|
453
|
+
throw new Error(`在文件 ${fileId} 中找不到messageId: ${messageId}`);
|
|
454
|
+
}
|
|
455
|
+
dlog(`找到匹配的LLM记录`);
|
|
456
|
+
//从traceDir中查找llm_requests目录尝试获取LLM请求数据文件,如果找到,覆盖LLM请求数据
|
|
457
|
+
const llmRequestsDir = path.join(traceDir, 'llm_requests');
|
|
458
|
+
const llmRequestFilePath = path.join(llmRequestsDir, `${messageId}.json`);
|
|
459
|
+
if (fs.existsSync(llmRequestFilePath)) {
|
|
460
|
+
try {
|
|
461
|
+
const llmRequestContent = await fs.promises.readFile(llmRequestFilePath, 'utf-8');
|
|
462
|
+
matchedRecord.request = JSON.parse(llmRequestContent);
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
throw new Error(`覆盖请求数据失败: ${error}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// 4. 构建返回数据
|
|
469
|
+
const processedRecord = {
|
|
470
|
+
request: matchedRecord.request || matchedRecord,
|
|
471
|
+
response: matchedRecord.response || {},
|
|
472
|
+
logged_at: matchedRecord.logged_at || matchedRecord.timestamp || new Date().toISOString()
|
|
473
|
+
};
|
|
474
|
+
// 5. 解析response.body_raw(如果存在)
|
|
475
|
+
if (matchedRecord.response && matchedRecord.response.body_raw) {
|
|
476
|
+
try {
|
|
477
|
+
let res = matchedRecord.response;
|
|
478
|
+
// 解析SSE格式数据
|
|
479
|
+
const sseEvents = this.parseSSEData(res.body_raw);
|
|
480
|
+
res.body_data = this.transformSSEEvents(sseEvents);
|
|
481
|
+
dlog(`解析到 ${sseEvents.length} 个SSE事件`);
|
|
482
|
+
// 提取content_block_delta的文本内容
|
|
483
|
+
const textParts = [];
|
|
484
|
+
for (const event of sseEvents) {
|
|
485
|
+
if (event.type === 'content_block_delta' && event.delta && event.delta.type === 'text_delta' && event.delta.text) {
|
|
486
|
+
textParts.push(event.delta.text);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
dlog(`提取到 ${textParts.length} 个文本片段`);
|
|
490
|
+
if (textParts.length > 0) {
|
|
491
|
+
res.body_text = textParts.join('');
|
|
492
|
+
dlog(`合并后的文本长度: ${res.body_text.length}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch (parseError) {
|
|
496
|
+
console.warn(`解析body_raw失败: ${parseError}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const response = {
|
|
500
|
+
success: true,
|
|
501
|
+
data: processedRecord
|
|
502
|
+
};
|
|
503
|
+
res.json(response);
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
console.error('获取LLM日志失败:', error);
|
|
507
|
+
const response = {
|
|
508
|
+
success: false,
|
|
509
|
+
error: error instanceof Error ? error.message : '未知错误'
|
|
510
|
+
};
|
|
511
|
+
res.status(500).json(response);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
// 保存 LLM 请求数据的 API
|
|
515
|
+
this.app.post('/api/llm-requests/:messageId', async (req, res) => {
|
|
516
|
+
try {
|
|
517
|
+
const { messageId } = req.params;
|
|
518
|
+
const requestData = req.body;
|
|
519
|
+
if (!messageId) {
|
|
520
|
+
const response = {
|
|
521
|
+
success: false,
|
|
522
|
+
error: '缺少消息ID参数'
|
|
523
|
+
};
|
|
524
|
+
return res.status(400).json(response);
|
|
525
|
+
}
|
|
526
|
+
// 确保 llm_requests 目录存在
|
|
527
|
+
const llmRequestsDir = path.join(this.config.projectDir, '.claude-trace', 'llm_requests');
|
|
528
|
+
if (!fs.existsSync(llmRequestsDir)) {
|
|
529
|
+
fs.mkdirSync(llmRequestsDir, { recursive: true });
|
|
530
|
+
}
|
|
531
|
+
// 保存文件
|
|
532
|
+
const filePath = path.join(llmRequestsDir, `${messageId}.json`);
|
|
533
|
+
fs.writeFileSync(filePath, JSON.stringify(requestData, null, 2));
|
|
534
|
+
const response = {
|
|
535
|
+
success: true,
|
|
536
|
+
data: {
|
|
537
|
+
message: '保存成功',
|
|
538
|
+
filePath: filePath
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
res.json(response);
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
console.error('保存LLM请求数据失败:', error);
|
|
545
|
+
const response = {
|
|
546
|
+
success: false,
|
|
547
|
+
error: error instanceof Error ? error.message : '未知错误'
|
|
548
|
+
};
|
|
549
|
+
res.status(500).json(response);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
// 重新发起 LLM 请求的 API
|
|
553
|
+
this.app.post('/api/llm-request', async (req, res) => {
|
|
554
|
+
try {
|
|
555
|
+
// 构建与原始请求一致的请求参数
|
|
556
|
+
const requestHeaders = req.body.headers;
|
|
557
|
+
const requestBody = req.body.body;
|
|
558
|
+
const authToken = this.getAuthToken();
|
|
559
|
+
if (!authToken) {
|
|
560
|
+
throw new Error('无法获取ANTHROPIC_AUTH_TOKEN,请检查设置文件或环境变量');
|
|
561
|
+
}
|
|
562
|
+
requestHeaders['authorization'] = `Bearer ${authToken}`;
|
|
563
|
+
requestBody.stream = false;
|
|
564
|
+
// 发起HTTP请求到Anthropic API
|
|
565
|
+
const fetch = (await Promise.resolve().then(() => __importStar(require('node-fetch')))).default;
|
|
566
|
+
const https = await Promise.resolve().then(() => __importStar(require('https')));
|
|
567
|
+
const agent = new https.Agent({
|
|
568
|
+
rejectUnauthorized: process.env.NODE_ENV === 'production' ? true : false
|
|
569
|
+
});
|
|
570
|
+
const response = await fetch(req.body.url, {
|
|
571
|
+
method: 'POST',
|
|
572
|
+
headers: requestHeaders,
|
|
573
|
+
body: JSON.stringify(requestBody),
|
|
574
|
+
agent: agent
|
|
575
|
+
});
|
|
576
|
+
if (!response.ok) {
|
|
577
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
578
|
+
}
|
|
579
|
+
// 获取响应体文本
|
|
580
|
+
const responseJson = await response.json();
|
|
581
|
+
// 构建响应对象
|
|
582
|
+
const apiResponse = {
|
|
583
|
+
success: true,
|
|
584
|
+
data: {
|
|
585
|
+
response: responseJson,
|
|
586
|
+
timestamp: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
res.json(apiResponse);
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
console.error('LLM 请求失败:', error);
|
|
593
|
+
const errorResponse = {
|
|
594
|
+
success: false,
|
|
595
|
+
error: error instanceof Error ? error.message : '未知错误'
|
|
596
|
+
};
|
|
597
|
+
res.status(500).json(errorResponse);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
setupWebSocket() {
|
|
602
|
+
this.io.on('connection', (socket) => {
|
|
603
|
+
dlog('客户端连接:', socket.id);
|
|
604
|
+
// 发送连接确认
|
|
605
|
+
socket.emit('connection', {
|
|
606
|
+
message: '连接成功',
|
|
607
|
+
timestamp: new Date().toISOString()
|
|
608
|
+
});
|
|
609
|
+
socket.on('disconnect', () => {
|
|
610
|
+
dlog('客户端断开连接:', socket.id);
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
setupFileWatcher() {
|
|
615
|
+
this.fileWatcher = this.logFileManager.watchLogDirectory(this.logDir, async (eventType, filename, filepath) => {
|
|
616
|
+
dlog(`文件变化: ${eventType} - ${filename}`);
|
|
617
|
+
try {
|
|
618
|
+
// 获取更新后的文件列表
|
|
619
|
+
const files = await this.logFileManager.getAvailableLogFiles(this.logDir);
|
|
620
|
+
// 如果是文件修改,尝试解析新的对话数据
|
|
621
|
+
let updatedConversations = null;
|
|
622
|
+
if (eventType === 'change' && filepath) {
|
|
623
|
+
try {
|
|
624
|
+
const fileId = filename.replace('.jsonl', '');
|
|
625
|
+
const lines = await this.logFileManager.readLogFile(filepath);
|
|
626
|
+
updatedConversations = this.conversationParser.parseConversations(lines);
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
console.warn(`解析更新的对话数据失败: ${error}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// 通知所有客户端文件更新
|
|
633
|
+
this.io.emit('file:updated', {
|
|
634
|
+
event: 'file:updated',
|
|
635
|
+
data: {
|
|
636
|
+
eventType,
|
|
637
|
+
filename,
|
|
638
|
+
fileId: filename.replace('.jsonl', ''),
|
|
639
|
+
files,
|
|
640
|
+
conversations: updatedConversations,
|
|
641
|
+
timestamp: new Date().toISOString()
|
|
642
|
+
},
|
|
643
|
+
timestamp: new Date().toISOString()
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
console.error('处理文件变化事件失败:', error);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
async start() {
|
|
652
|
+
return new Promise((resolve, reject) => {
|
|
653
|
+
try {
|
|
654
|
+
this.server.listen(this.config.port, '0.0.0.0', () => {
|
|
655
|
+
console.log(`Web服务器启动成功:`);
|
|
656
|
+
console.log(`- 端口: ${this.config.port}`);
|
|
657
|
+
console.log(`- 项目目录: ${this.config.projectDir}`);
|
|
658
|
+
console.log(`- 日志目录: ${this.logDir}`);
|
|
659
|
+
console.log(`- 本地访问地址: http://localhost:${this.config.port}`);
|
|
660
|
+
console.log(`- 远程访问地址: http://<服务器IP>:${this.config.port}`);
|
|
661
|
+
resolve();
|
|
662
|
+
});
|
|
663
|
+
this.server.on('error', (error) => {
|
|
664
|
+
console.error('Web服务器启动失败:', error);
|
|
665
|
+
reject(error);
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
console.error('启动Web服务器失败:', error);
|
|
670
|
+
reject(error);
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
async stop() {
|
|
675
|
+
return new Promise((resolve) => {
|
|
676
|
+
// // 停止文件监听
|
|
677
|
+
// if (this.fileWatcher) {
|
|
678
|
+
// this.fileWatcher.close();
|
|
679
|
+
// }
|
|
680
|
+
// // 关闭WebSocket连接
|
|
681
|
+
// this.io.close();
|
|
682
|
+
// 关闭HTTP服务器
|
|
683
|
+
this.server.close(() => {
|
|
684
|
+
console.log('Web服务器已停止');
|
|
685
|
+
resolve();
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
getUrl() {
|
|
690
|
+
return `http://localhost:${this.config.port}`;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* 解析SSE格式的数据
|
|
694
|
+
* @param sseData SSE格式的原始数据
|
|
695
|
+
* @returns 解析后的事件数组
|
|
696
|
+
*/
|
|
697
|
+
parseSSEData(sseData) {
|
|
698
|
+
const events = [];
|
|
699
|
+
const lines = sseData.split('\n');
|
|
700
|
+
let currentEvent = {};
|
|
701
|
+
for (const line of lines) {
|
|
702
|
+
const trimmedLine = line.trim();
|
|
703
|
+
if (trimmedLine === '') {
|
|
704
|
+
// 空行表示事件结束
|
|
705
|
+
if (currentEvent.event && currentEvent.data) {
|
|
706
|
+
try {
|
|
707
|
+
// 解析data字段中的JSON
|
|
708
|
+
const parsedData = JSON.parse(currentEvent.data);
|
|
709
|
+
events.push(parsedData);
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
console.warn('解析SSE事件数据失败:', error, currentEvent.data);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
currentEvent = {};
|
|
716
|
+
}
|
|
717
|
+
else if (trimmedLine.startsWith('event:')) {
|
|
718
|
+
currentEvent.event = trimmedLine.substring(6).trim();
|
|
719
|
+
}
|
|
720
|
+
else if (trimmedLine.startsWith('data:')) {
|
|
721
|
+
currentEvent.data = trimmedLine.substring(5).trim();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
// 处理最后一个事件(如果没有以空行结尾)
|
|
725
|
+
if (currentEvent.event && currentEvent.data) {
|
|
726
|
+
try {
|
|
727
|
+
const parsedData = JSON.parse(currentEvent.data);
|
|
728
|
+
events.push(parsedData);
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
console.warn('解析SSE事件数据失败:', error, currentEvent.data);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return events;
|
|
735
|
+
}
|
|
736
|
+
transformSSEEvents(events) {
|
|
737
|
+
// 转换events数组为更可读的JSON对象
|
|
738
|
+
const convertedMessages = [];
|
|
739
|
+
let currentTextContent = '';
|
|
740
|
+
let currentToolUse = null;
|
|
741
|
+
let currentToolInput = '';
|
|
742
|
+
for (const event of events) {
|
|
743
|
+
if (event.type === 'content_block_delta') {
|
|
744
|
+
if (event.delta?.type === 'text_delta') {
|
|
745
|
+
// 收集文本内容
|
|
746
|
+
currentTextContent += event.delta.text;
|
|
747
|
+
}
|
|
748
|
+
else if (event.delta?.type === 'input_json_delta') {
|
|
749
|
+
// 收集工具输入JSON
|
|
750
|
+
currentToolInput += event.delta.partial_json;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
else if (event.type === 'content_block_start') {
|
|
754
|
+
if (event.content_block?.type === 'tool_use') {
|
|
755
|
+
// 开始一个新的工具使用
|
|
756
|
+
currentToolUse = {
|
|
757
|
+
type: 'tool_use',
|
|
758
|
+
id: event.content_block.id,
|
|
759
|
+
name: event.content_block.name,
|
|
760
|
+
input: {}
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
else if (event.type === 'content_block_stop') {
|
|
765
|
+
// 内容块结束,处理累积的内容
|
|
766
|
+
if (currentTextContent.trim()) {
|
|
767
|
+
convertedMessages.push({
|
|
768
|
+
type: 'text',
|
|
769
|
+
text: currentTextContent
|
|
770
|
+
});
|
|
771
|
+
currentTextContent = '';
|
|
772
|
+
}
|
|
773
|
+
if (currentToolUse && currentToolInput) {
|
|
774
|
+
try {
|
|
775
|
+
currentToolUse.input = JSON.parse(currentToolInput);
|
|
776
|
+
convertedMessages.push(currentToolUse);
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
console.warn('解析工具输入JSON失败:', error, currentToolInput);
|
|
780
|
+
}
|
|
781
|
+
currentToolUse = null;
|
|
782
|
+
currentToolInput = '';
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
// 处理剩余的文本内容
|
|
787
|
+
if (currentTextContent.trim()) {
|
|
788
|
+
convertedMessages.push({
|
|
789
|
+
type: 'text',
|
|
790
|
+
text: currentTextContent
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
// 处理剩余的工具使用
|
|
794
|
+
if (currentToolUse && currentToolInput) {
|
|
795
|
+
try {
|
|
796
|
+
currentToolUse.input = JSON.parse(currentToolInput);
|
|
797
|
+
convertedMessages.push(currentToolUse);
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
console.warn('解析工具输入JSON失败:', error, currentToolInput);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return convertedMessages;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
exports.WebServer = WebServer;
|
|
807
|
+
//# sourceMappingURL=web-server.js.map
|