rl-rockcli 0.0.2 → 0.0.4
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/README.md +400 -0
- package/index.js +51 -21
- package/package.json +3 -2
- package/commands/log/core/constants.js +0 -237
- package/commands/log/core/display.js +0 -370
- package/commands/log/core/search.js +0 -330
- package/commands/log/core/tail.js +0 -216
- package/commands/log/core/utils.js +0 -424
- package/commands/log.js +0 -298
- package/commands/sandbox/core/log-bridge.js +0 -119
- package/commands/sandbox/core/replay/analyzer.js +0 -311
- package/commands/sandbox/core/replay/batch-orchestrator.js +0 -536
- package/commands/sandbox/core/replay/batch-task.js +0 -369
- package/commands/sandbox/core/replay/concurrent-display.js +0 -70
- package/commands/sandbox/core/replay/concurrent-orchestrator.js +0 -170
- package/commands/sandbox/core/replay/data-source.js +0 -86
- package/commands/sandbox/core/replay/display.js +0 -231
- package/commands/sandbox/core/replay/executor.js +0 -634
- package/commands/sandbox/core/replay/history-fetcher.js +0 -124
- package/commands/sandbox/core/replay/index.js +0 -338
- package/commands/sandbox/core/replay/loghouse-data-source.js +0 -177
- package/commands/sandbox/core/replay/pid-mapping.js +0 -26
- package/commands/sandbox/core/replay/request.js +0 -109
- package/commands/sandbox/core/replay/worker.js +0 -166
- package/commands/sandbox/core/session.js +0 -346
- package/commands/sandbox/log-bridge.js +0 -2
- package/commands/sandbox/ray.js +0 -2
- package/commands/sandbox/replay/analyzer.js +0 -311
- package/commands/sandbox/replay/batch-orchestrator.js +0 -536
- package/commands/sandbox/replay/batch-task.js +0 -369
- package/commands/sandbox/replay/concurrent-display.js +0 -70
- package/commands/sandbox/replay/concurrent-orchestrator.js +0 -170
- package/commands/sandbox/replay/display.js +0 -231
- package/commands/sandbox/replay/executor.js +0 -634
- package/commands/sandbox/replay/history-fetcher.js +0 -118
- package/commands/sandbox/replay/index.js +0 -338
- package/commands/sandbox/replay/pid-mapping.js +0 -26
- package/commands/sandbox/replay/request.js +0 -109
- package/commands/sandbox/replay/worker.js +0 -166
- package/commands/sandbox/replay.js +0 -2
- package/commands/sandbox/session.js +0 -2
- package/commands/sandbox-original.js +0 -1393
- package/commands/sandbox.js +0 -499
- package/help/help.json +0 -1071
- package/help/middleware.js +0 -71
- package/help/renderer.js +0 -800
- package/lib/plugin-context.js +0 -40
- package/sdks/sandbox/core/client.js +0 -845
- package/sdks/sandbox/core/config.js +0 -70
- package/sdks/sandbox/core/types.js +0 -74
- package/sdks/sandbox/httpLogger.js +0 -251
- package/sdks/sandbox/index.js +0 -9
- package/utils/asciiArt.js +0 -138
- package/utils/bun-compat.js +0 -59
- package/utils/ciPipelines.js +0 -138
- package/utils/cli.js +0 -17
- package/utils/command-router.js +0 -79
- package/utils/configManager.js +0 -503
- package/utils/dependency-resolver.js +0 -135
- package/utils/eagleeye_traceid.js +0 -151
- package/utils/envDetector.js +0 -78
- package/utils/execution_logger.js +0 -415
- package/utils/featureManager.js +0 -68
- package/utils/firstTimeTip.js +0 -44
- package/utils/hook-manager.js +0 -125
- package/utils/http-logger.js +0 -264
- package/utils/i18n.js +0 -139
- package/utils/image-progress.js +0 -159
- package/utils/logger.js +0 -154
- package/utils/plugin-loader.js +0 -124
- package/utils/plugin-manager.js +0 -348
- package/utils/ray_cli_wrapper.js +0 -746
- package/utils/sandbox-client.js +0 -419
- package/utils/terminal.js +0 -32
- package/utils/tips.js +0 -106
|
@@ -1,634 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const logger = require('../../../../utils/logger');
|
|
5
|
-
const { RequestAction, RequestCategory } = require('./request');
|
|
6
|
-
const { extractPidFromOutput } = require('./pid-mapping');
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 回放执行器
|
|
10
|
-
*/
|
|
11
|
-
class ReplayExecutor {
|
|
12
|
-
/**
|
|
13
|
-
* @param {Object} client - SandboxClient 实例
|
|
14
|
-
* @param {Object} options
|
|
15
|
-
* @param {string} options.timing - 时间控制: 'original' | 'fixed' | 'none'
|
|
16
|
-
* @param {number} options.interval - 固定间隔(秒)
|
|
17
|
-
* @param {boolean} options.quiet - 静默模式
|
|
18
|
-
* @param {string} options.logFile - 日志文件路径
|
|
19
|
-
* @param {boolean} options.verboseLog - 详细日志(记录 request/response)
|
|
20
|
-
*/
|
|
21
|
-
constructor(client, options = {}) {
|
|
22
|
-
this.client = client;
|
|
23
|
-
this.options = {
|
|
24
|
-
timing: options.timing || 'none',
|
|
25
|
-
interval: options.interval || 5,
|
|
26
|
-
quiet: options.quiet || false,
|
|
27
|
-
logFile: options.logFile || 'replay.log',
|
|
28
|
-
verboseLog: options.verboseLog || false
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
this.currentSandboxId = null;
|
|
32
|
-
this.logStream = null;
|
|
33
|
-
this.criticalError = null; // 关键错误(如 start_async 失败),会终止回放
|
|
34
|
-
|
|
35
|
-
// 从 client.config 提取 CLI 覆盖的 headers(优先级最高)
|
|
36
|
-
this.cliOverrideHeaders = {};
|
|
37
|
-
if (client.config) {
|
|
38
|
-
if (client.config.cluster) {
|
|
39
|
-
this.cliOverrideHeaders['x-cluster'] = client.config.cluster;
|
|
40
|
-
}
|
|
41
|
-
if (client.config.userId) {
|
|
42
|
-
this.cliOverrideHeaders['X-User-Id'] = client.config.userId;
|
|
43
|
-
}
|
|
44
|
-
if (client.config.experimentId) {
|
|
45
|
-
this.cliOverrideHeaders['X-Experiment-Id'] = client.config.experimentId;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// 执行结果统计
|
|
50
|
-
this.results = {
|
|
51
|
-
total: 0,
|
|
52
|
-
success: 0,
|
|
53
|
-
failed: 0,
|
|
54
|
-
skipped: 0
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* 合并 headers,CLI 参数优先
|
|
60
|
-
* @param {Object} reqHeaders - 请求中的 headers
|
|
61
|
-
* @returns {Object} - 合并后的 headers
|
|
62
|
-
*/
|
|
63
|
-
mergeHeaders(reqHeaders) {
|
|
64
|
-
return {
|
|
65
|
-
...reqHeaders,
|
|
66
|
-
...this.cliOverrideHeaders
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 初始化日志文件
|
|
72
|
-
*/
|
|
73
|
-
initLogFile() {
|
|
74
|
-
this.logStream = fs.createWriteStream(this.options.logFile, { flags: 'a' });
|
|
75
|
-
this.logToFile('=== Replay Session Started ===');
|
|
76
|
-
this.logToFile(`Mode: ${this.options.timing}`);
|
|
77
|
-
this.logToFile(`Interval: ${this.options.interval}s`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* 关闭日志文件
|
|
82
|
-
*/
|
|
83
|
-
closeLogFile() {
|
|
84
|
-
if (this.logStream) {
|
|
85
|
-
this.logToFile('=== Replay Session Ended ===');
|
|
86
|
-
this.logStream.end();
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* 执行回放计划
|
|
92
|
-
* @param {ReplayPlan} plan
|
|
93
|
-
* @returns {Object} 执行结果
|
|
94
|
-
*/
|
|
95
|
-
async execute(plan) {
|
|
96
|
-
this.plan = plan;
|
|
97
|
-
this.results.total = plan.requests.length;
|
|
98
|
-
this.results.skipped = plan.skippedCount + plan.mergedCount;
|
|
99
|
-
|
|
100
|
-
this.initLogFile();
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
for (let i = 0; i < plan.requests.length; i++) {
|
|
104
|
-
const req = plan.requests[i];
|
|
105
|
-
const progress = `[${i + 1}/${plan.requests.length}]`;
|
|
106
|
-
|
|
107
|
-
// 如果有关键错误,跳过后续请求
|
|
108
|
-
if (this.criticalError) {
|
|
109
|
-
this.results.skipped++;
|
|
110
|
-
this.logToFile(`${progress} SKIPPED: ${req.method} ${req.uri} (critical error: ${this.criticalError})`);
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const startTime = Date.now();
|
|
115
|
-
try {
|
|
116
|
-
await this.executeRequest(req, progress);
|
|
117
|
-
this.results.success++;
|
|
118
|
-
} catch (error) {
|
|
119
|
-
this.results.failed++;
|
|
120
|
-
this.logError(progress, req, error);
|
|
121
|
-
|
|
122
|
-
// 检查是否是关键错误(start_async 失败)
|
|
123
|
-
if (req.uri.includes('start_async')) {
|
|
124
|
-
this.criticalError = error.message;
|
|
125
|
-
this.log(` 🛑 Critical error: sandbox creation failed, stopping replay`);
|
|
126
|
-
this.logToFile(` CRITICAL: sandbox creation failed, remaining requests will be skipped`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
const duration = Date.now() - startTime;
|
|
130
|
-
|
|
131
|
-
// 记录到日志文件
|
|
132
|
-
this.logToFile(`${progress} ${req.method} ${req.uri} - Duration: ${duration}ms`);
|
|
133
|
-
|
|
134
|
-
// 时间控制
|
|
135
|
-
if (i < plan.requests.length - 1 && !this.criticalError) {
|
|
136
|
-
await this.waitBetweenRequests(req, plan.requests[i + 1]);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
} finally {
|
|
140
|
-
this.closeLogFile();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return this.results;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* 执行单个请求
|
|
148
|
-
* @param {ReplayRequest} req
|
|
149
|
-
* @param {string} progress
|
|
150
|
-
*/
|
|
151
|
-
async executeRequest(req, progress) {
|
|
152
|
-
// 请求间分隔符
|
|
153
|
-
this.log('===');
|
|
154
|
-
this.logToFile('===');
|
|
155
|
-
|
|
156
|
-
// 替换 sandbox_id
|
|
157
|
-
this.replaceSandboxId(req);
|
|
158
|
-
|
|
159
|
-
switch (req.action) {
|
|
160
|
-
case RequestAction.EXECUTE:
|
|
161
|
-
return this.executeNormal(req, progress);
|
|
162
|
-
|
|
163
|
-
case RequestAction.EXECUTE_WITH_PROCESS_WAIT:
|
|
164
|
-
return this.executeWithProcessWait(req, progress);
|
|
165
|
-
|
|
166
|
-
default:
|
|
167
|
-
throw new Error(`Unknown action: ${req.action}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* 普通执行
|
|
173
|
-
* @param {ReplayRequest} req
|
|
174
|
-
* @param {string} progress
|
|
175
|
-
*/
|
|
176
|
-
async executeNormal(req, progress) {
|
|
177
|
-
this.log(`${progress} ${req.method} ${req.uri}`);
|
|
178
|
-
|
|
179
|
-
if (req.category === RequestCategory.UPLOAD) {
|
|
180
|
-
return this.executeUpload(req, progress);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// 合并 headers(CLI 参数优先)
|
|
184
|
-
const mergedHeaders = this.mergeHeaders(req.headers);
|
|
185
|
-
|
|
186
|
-
// 记录请求到日志文件(始终记录)
|
|
187
|
-
this.logToFile(` [REQUEST] ${req.method} ${req.uri}`);
|
|
188
|
-
if (mergedHeaders && Object.keys(mergedHeaders).length > 0) {
|
|
189
|
-
this.logToFile(` [REQUEST HEADERS] ${this.safeStringify(mergedHeaders)}`);
|
|
190
|
-
}
|
|
191
|
-
if (req.requestBody) {
|
|
192
|
-
this.logToFile(` [REQUEST BODY] ${this.safeStringify(req.requestBody)}`);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const response = await this.client.makeRequest({
|
|
196
|
-
method: req.method,
|
|
197
|
-
uri: req.uri,
|
|
198
|
-
requestBody: req.requestBody,
|
|
199
|
-
headers: mergedHeaders
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const status = response.status || (response.data?.status === 'Success' ? 200 : 500);
|
|
203
|
-
|
|
204
|
-
// 记录响应到日志文件(始终记录)
|
|
205
|
-
this.logToFile(` [RESPONSE] Status: ${status}`);
|
|
206
|
-
if (response.headers) {
|
|
207
|
-
this.logToFile(` [RESPONSE HEADERS] ${this.safeStringify(response.headers)}`);
|
|
208
|
-
}
|
|
209
|
-
if (response.data) {
|
|
210
|
-
this.logToFile(` [RESPONSE DATA] ${this.safeStringify(response.data)}`);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// 检查状态码是否成功(必须先检查状态码,再处理特定逻辑)
|
|
214
|
-
if (status >= 400) {
|
|
215
|
-
const errorMsg = response.data?.message || response.data?.error || `HTTP ${status}`;
|
|
216
|
-
this.log(` ❌ Status: ${status} - ${errorMsg}`);
|
|
217
|
-
throw new Error(`Request failed with status ${status}: ${errorMsg}`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// 检查是否是 start_async 请求
|
|
221
|
-
if (req.uri.includes('start_async')) {
|
|
222
|
-
const sandboxId = response.data?.result?.sandbox_id;
|
|
223
|
-
if (sandboxId) {
|
|
224
|
-
this.currentSandboxId = sandboxId;
|
|
225
|
-
this.log(` 📝 sandbox_id: ${this.currentSandboxId}`);
|
|
226
|
-
this.logToFile(` Extracted sandbox_id: ${this.currentSandboxId}`);
|
|
227
|
-
|
|
228
|
-
// 等待沙箱 alive
|
|
229
|
-
this.log(` ⏳ Waiting for sandbox alive...`);
|
|
230
|
-
this.logToFile(` Waiting for sandbox alive...`);
|
|
231
|
-
await this.waitForSandboxAlive();
|
|
232
|
-
this.log(` ✅ Sandbox is alive!`);
|
|
233
|
-
this.logToFile(` Sandbox is alive!`);
|
|
234
|
-
} else {
|
|
235
|
-
// start_async 成功但没有 sandbox_id,这是异常情况
|
|
236
|
-
this.log(` ⚠️ Warning: start_async succeeded but no sandbox_id in response`);
|
|
237
|
-
this.logToFile(` Warning: start_async succeeded but no sandbox_id in response`);
|
|
238
|
-
this.logToFile(` Response: ${this.safeStringify(response.data)}`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
this.log(` ✅ Status: ${status}`);
|
|
243
|
-
return response;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* 执行上传请求
|
|
248
|
-
* @param {ReplayRequest} req
|
|
249
|
-
* @param {string} progress
|
|
250
|
-
*/
|
|
251
|
-
async executeUpload(req, progress) {
|
|
252
|
-
const fileData = req.requestBody?.file;
|
|
253
|
-
if (!fileData) {
|
|
254
|
-
throw new Error('Upload request missing file data');
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const filename = fileData.filename || 'unknown';
|
|
258
|
-
const content = fileData.content || '';
|
|
259
|
-
const targetPath = req.requestBody.target_path || `/tmp/${filename}`;
|
|
260
|
-
|
|
261
|
-
this.log(` Uploading: ${filename} -> ${targetPath}`);
|
|
262
|
-
|
|
263
|
-
// 解码文件内容
|
|
264
|
-
let fileBuffer;
|
|
265
|
-
if (fileData.isBase64) {
|
|
266
|
-
fileBuffer = Buffer.from(content, 'base64');
|
|
267
|
-
} else {
|
|
268
|
-
fileBuffer = this.parseEscapedBinary(content);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// 写入临时文件
|
|
272
|
-
const tempDir = os.tmpdir();
|
|
273
|
-
const tempFilePath = path.join(tempDir, filename);
|
|
274
|
-
fs.writeFileSync(tempFilePath, fileBuffer);
|
|
275
|
-
|
|
276
|
-
try {
|
|
277
|
-
// 使用 SDK 上传
|
|
278
|
-
this.client._sandboxId = this.currentSandboxId;
|
|
279
|
-
const result = await this.client.uploadFile(tempFilePath, targetPath);
|
|
280
|
-
|
|
281
|
-
if (result.success) {
|
|
282
|
-
this.log(` ✅ Upload success`);
|
|
283
|
-
} else {
|
|
284
|
-
throw new Error(result.message);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return result;
|
|
288
|
-
} finally {
|
|
289
|
-
// 清理临时文件
|
|
290
|
-
if (fs.existsSync(tempFilePath)) {
|
|
291
|
-
fs.unlinkSync(tempFilePath);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* 执行 nohup 命令并等待进程结束
|
|
298
|
-
* @param {ReplayRequest} req
|
|
299
|
-
* @param {string} progress
|
|
300
|
-
*/
|
|
301
|
-
async executeWithProcessWait(req, progress) {
|
|
302
|
-
const command = req.getCommand();
|
|
303
|
-
this.log(`${progress} ${req.method} ${req.uri}`);
|
|
304
|
-
this.log(` Command: ${this.truncate(command, 70)}`);
|
|
305
|
-
|
|
306
|
-
// 合并 headers(CLI 参数优先)
|
|
307
|
-
const mergedHeaders = this.mergeHeaders(req.headers);
|
|
308
|
-
|
|
309
|
-
// 记录请求到日志文件(始终记录)
|
|
310
|
-
this.logToFile(` [REQUEST] ${req.method} ${req.uri}`);
|
|
311
|
-
if (mergedHeaders && Object.keys(mergedHeaders).length > 0) {
|
|
312
|
-
this.logToFile(` [REQUEST HEADERS] ${this.safeStringify(mergedHeaders)}`);
|
|
313
|
-
}
|
|
314
|
-
this.logToFile(` [REQUEST BODY] ${this.safeStringify(req.requestBody)}`);
|
|
315
|
-
|
|
316
|
-
const response = await this.client.makeRequest({
|
|
317
|
-
method: req.method,
|
|
318
|
-
uri: req.uri,
|
|
319
|
-
requestBody: req.requestBody,
|
|
320
|
-
headers: mergedHeaders
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
const status = response.status || (response.data?.status === 'Success' ? 200 : 500);
|
|
324
|
-
|
|
325
|
-
// 记录响应到日志文件(始终记录)
|
|
326
|
-
this.logToFile(` [RESPONSE] Status: ${status}`);
|
|
327
|
-
if (response.headers) {
|
|
328
|
-
this.logToFile(` [RESPONSE HEADERS] ${this.safeStringify(response.headers)}`);
|
|
329
|
-
}
|
|
330
|
-
if (response.data) {
|
|
331
|
-
this.logToFile(` [RESPONSE DATA] ${this.safeStringify(response.data)}`);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// 检查状态码是否成功
|
|
335
|
-
if (status >= 400) {
|
|
336
|
-
const errorMsg = response.data?.message || response.data?.error || `HTTP ${status}`;
|
|
337
|
-
this.log(` ❌ Status: ${status} - ${errorMsg}`);
|
|
338
|
-
throw new Error(`Request failed with status ${status}: ${errorMsg}`);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
this.log(` ✅ Status: ${status}`);
|
|
342
|
-
|
|
343
|
-
// 提取 PID 并等待进程结束
|
|
344
|
-
const output = response.data?.result?.output || '';
|
|
345
|
-
const pid = extractPidFromOutput(output);
|
|
346
|
-
|
|
347
|
-
if (pid) {
|
|
348
|
-
this.log(` 📌 PID: ${pid}`);
|
|
349
|
-
this.logToFile(` PID captured: ${pid}`);
|
|
350
|
-
|
|
351
|
-
// 等待进程结束
|
|
352
|
-
this.log(` ⏳ Waiting for process ${pid} to complete...`);
|
|
353
|
-
this.logToFile(` Waiting for process ${pid} to complete...`);
|
|
354
|
-
await this.waitForProcessComplete(pid, mergedHeaders);
|
|
355
|
-
this.log(` ✅ Process ${pid} completed`);
|
|
356
|
-
this.logToFile(` Process ${pid} completed`);
|
|
357
|
-
} else {
|
|
358
|
-
this.log(` ⚠️ No PID found in output, skipping process wait`);
|
|
359
|
-
this.logToFile(` No PID found in output, skipping process wait`);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return response;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* 等待进程结束
|
|
367
|
-
* @param {string} pid - 进程 ID
|
|
368
|
-
* @param {Object} headers - 请求头
|
|
369
|
-
* @param {number} timeout - 超时时间(毫秒)
|
|
370
|
-
*/
|
|
371
|
-
async waitForProcessComplete(pid, headers, timeout = 600000) {
|
|
372
|
-
const startTime = Date.now();
|
|
373
|
-
let pollCount = 0;
|
|
374
|
-
|
|
375
|
-
while (Date.now() - startTime < timeout) {
|
|
376
|
-
try {
|
|
377
|
-
pollCount++;
|
|
378
|
-
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
379
|
-
|
|
380
|
-
// 使用 kill -0 检查进程是否存在
|
|
381
|
-
const response = await this.client.makeRequest({
|
|
382
|
-
method: 'POST',
|
|
383
|
-
uri: '/apis/envs/sandbox/v1/run_in_session',
|
|
384
|
-
requestBody: {
|
|
385
|
-
sandbox_id: this.currentSandboxId,
|
|
386
|
-
container_name: this.currentSandboxId,
|
|
387
|
-
session: 'agent',
|
|
388
|
-
action_type: 'bash',
|
|
389
|
-
command: `kill -0 ${pid}`,
|
|
390
|
-
check: 'silent'
|
|
391
|
-
},
|
|
392
|
-
headers
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
const exitCode = response.data?.result?.exit_code;
|
|
396
|
-
this.logToFile(` [PROCESS_CHECK #${pollCount}] kill -0 ${pid} -> exit_code=${exitCode}, elapsed=${elapsed}s`);
|
|
397
|
-
|
|
398
|
-
// exit_code 非 0 表示进程已结束
|
|
399
|
-
if (exitCode !== 0) {
|
|
400
|
-
this.logToFile(` [PROCESS_CHECK] Process ${pid} completed after ${pollCount} polls, ${elapsed}s`);
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// 等待 3 秒后重试
|
|
405
|
-
await this.sleep(3000);
|
|
406
|
-
} catch (error) {
|
|
407
|
-
// 请求失败也可能表示进程已结束
|
|
408
|
-
this.logToFile(` [PROCESS_CHECK] Error checking process: ${error.message}`);
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// 超时
|
|
414
|
-
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
415
|
-
this.log(` ⚠️ Process wait timeout after ${elapsed}s`);
|
|
416
|
-
this.logToFile(` Process wait timeout after ${elapsed}s, ${pollCount} polls`);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* 替换请求中的 sandbox_id
|
|
421
|
-
* @param {ReplayRequest} req
|
|
422
|
-
*/
|
|
423
|
-
replaceSandboxId(req) {
|
|
424
|
-
if (this.currentSandboxId) {
|
|
425
|
-
if (req.requestBody?.sandbox_id) {
|
|
426
|
-
req.requestBody.sandbox_id = this.currentSandboxId;
|
|
427
|
-
}
|
|
428
|
-
if (req.requestBody?.container_name) {
|
|
429
|
-
req.requestBody.container_name = this.currentSandboxId;
|
|
430
|
-
}
|
|
431
|
-
if (req.uri && req.uri.includes('sandbox_id=')) {
|
|
432
|
-
req.uri = req.uri.replace(/sandbox_id=[^&]+/, `sandbox_id=${this.currentSandboxId}`);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* 等待沙箱 alive
|
|
439
|
-
* @param {number} timeout - 超时时间(毫秒)
|
|
440
|
-
*/
|
|
441
|
-
async waitForSandboxAlive(timeout = 120000) {
|
|
442
|
-
const startTime = Date.now();
|
|
443
|
-
this.client._sandboxId = this.currentSandboxId;
|
|
444
|
-
let pollCount = 0;
|
|
445
|
-
let lastError = null;
|
|
446
|
-
let lastStatus = null;
|
|
447
|
-
|
|
448
|
-
while (Date.now() - startTime < timeout) {
|
|
449
|
-
try {
|
|
450
|
-
pollCount++;
|
|
451
|
-
const status = await this.client.getStatus();
|
|
452
|
-
lastStatus = status;
|
|
453
|
-
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
454
|
-
|
|
455
|
-
// 记录每次轮询到日志文件(始终记录)
|
|
456
|
-
this.logToFile(` [GET_STATUS #${pollCount}] is_alive=${status.is_alive}, elapsed=${elapsed}s`);
|
|
457
|
-
if (status.status) {
|
|
458
|
-
this.logToFile(` [GET_STATUS #${pollCount}] stages: ${this.safeStringify(status.status)}`);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (status.is_alive) {
|
|
462
|
-
this.logToFile(` [GET_STATUS] Sandbox alive after ${pollCount} polls, ${elapsed}s`);
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// 检查失败状态
|
|
467
|
-
if (status.status) {
|
|
468
|
-
for (const [stage, details] of Object.entries(status.status)) {
|
|
469
|
-
if (details.status === 'failed' || details.status === 'timeout') {
|
|
470
|
-
// 忽略 ray_schedule 失败
|
|
471
|
-
if (stage === 'ray_schedule') continue;
|
|
472
|
-
this.logToFile(` [GET_STATUS] Sandbox failed at ${stage}: ${details.message || 'Unknown error'}`);
|
|
473
|
-
throw new Error(`Sandbox failed at ${stage}: ${details.message || 'Unknown error'}`);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
await this.sleep(3000);
|
|
479
|
-
} catch (error) {
|
|
480
|
-
if (error.message.includes('Sandbox failed')) {
|
|
481
|
-
throw error;
|
|
482
|
-
}
|
|
483
|
-
// 记录错误但保存以便超时时使用
|
|
484
|
-
lastError = error;
|
|
485
|
-
this.logToFile(` [GET_STATUS] Error: ${error.message}`);
|
|
486
|
-
// 如果是 HTTP 错误,附加响应信息
|
|
487
|
-
if (error.response?.data) {
|
|
488
|
-
this.logToFile(` Response: ${this.safeStringify(error.response.data)}`);
|
|
489
|
-
}
|
|
490
|
-
logger.warn(`Failed to check sandbox status: ${error.message}`);
|
|
491
|
-
throw error;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// 超时时提供更详细的错误信息
|
|
496
|
-
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
497
|
-
let errorMessage = `Sandbox did not become alive within ${timeout}ms (${elapsed}s, ${pollCount} polls)`;
|
|
498
|
-
|
|
499
|
-
if (lastError) {
|
|
500
|
-
errorMessage += `. Last error: ${lastError.message}`;
|
|
501
|
-
} else if (lastStatus?.status) {
|
|
502
|
-
// 附加最后的 stage 信息
|
|
503
|
-
const stageInfo = Object.entries(lastStatus.status)
|
|
504
|
-
.map(([k, v]) => `${k}=${v.status}`)
|
|
505
|
-
.join(', ');
|
|
506
|
-
errorMessage += `. Last stages: ${stageInfo}`;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
throw new Error(errorMessage);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* 请求间等待
|
|
514
|
-
* @param {ReplayRequest} currentReq
|
|
515
|
-
* @param {ReplayRequest} nextReq
|
|
516
|
-
*/
|
|
517
|
-
async waitBetweenRequests(currentReq, nextReq) {
|
|
518
|
-
// start_async 后不需要额外等待(已经等待 alive)
|
|
519
|
-
if (currentReq.uri?.includes('start_async')) {
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
switch (this.options.timing) {
|
|
524
|
-
case 'original':
|
|
525
|
-
if (currentReq.responseTime && nextReq.responseTime) {
|
|
526
|
-
const gap = nextReq.responseTime - currentReq.responseTime;
|
|
527
|
-
if (gap > 0 && gap < 60000) {
|
|
528
|
-
if (!this.options.quiet) {
|
|
529
|
-
this.log(` ⏸️ Waiting ${(gap / 1000).toFixed(1)}s (original timing)...`);
|
|
530
|
-
}
|
|
531
|
-
await this.sleep(gap);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
break;
|
|
535
|
-
|
|
536
|
-
case 'fixed':
|
|
537
|
-
if (this.options.interval > 0) {
|
|
538
|
-
if (!this.options.quiet) {
|
|
539
|
-
this.log(` ⏸️ Waiting ${this.options.interval}s...`);
|
|
540
|
-
}
|
|
541
|
-
await this.sleep(this.options.interval * 1000);
|
|
542
|
-
}
|
|
543
|
-
break;
|
|
544
|
-
|
|
545
|
-
case 'none':
|
|
546
|
-
default:
|
|
547
|
-
// 不等待
|
|
548
|
-
break;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// 辅助方法
|
|
553
|
-
log(message) {
|
|
554
|
-
if (!this.options.quiet) {
|
|
555
|
-
console.log(message);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
logToFile(message) {
|
|
560
|
-
if (this.logStream) {
|
|
561
|
-
const timestamp = new Date().toISOString();
|
|
562
|
-
this.logStream.write(`[${timestamp}] ${message}\n`);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
logError(progress, req, error) {
|
|
567
|
-
this.log(`${progress} ${req.method} ${req.uri}`);
|
|
568
|
-
this.log(` ❌ Failed: ${error.message}`);
|
|
569
|
-
this.logToFile(`${progress} FAILED: ${error.message}`);
|
|
570
|
-
|
|
571
|
-
// 记录更多错误细节
|
|
572
|
-
if (error.response?.data) {
|
|
573
|
-
this.logToFile(` Response: ${JSON.stringify(error.response.data)}`);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
truncate(str, maxLen) {
|
|
578
|
-
if (!str) return '';
|
|
579
|
-
if (str.length <= maxLen) return str;
|
|
580
|
-
return str.substring(0, maxLen - 3) + '...';
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* 安全的 JSON 序列化(处理大对象和循环引用)
|
|
585
|
-
* @param {*} obj
|
|
586
|
-
* @param {number} maxLen - 最大长度
|
|
587
|
-
* @returns {string}
|
|
588
|
-
*/
|
|
589
|
-
safeStringify(obj, maxLen = 2000) {
|
|
590
|
-
try {
|
|
591
|
-
const str = JSON.stringify(obj);
|
|
592
|
-
if (str.length > maxLen) {
|
|
593
|
-
return str.substring(0, maxLen) + '... (truncated)';
|
|
594
|
-
}
|
|
595
|
-
return str;
|
|
596
|
-
} catch (error) {
|
|
597
|
-
return `[Stringify Error: ${error.message}]`;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
sleep(ms) {
|
|
602
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
parseEscapedBinary(str) {
|
|
606
|
-
const buffer = [];
|
|
607
|
-
let i = 0;
|
|
608
|
-
while (i < str.length) {
|
|
609
|
-
if (str[i] === '\\' && i + 1 < str.length) {
|
|
610
|
-
if (str[i + 1] === 'x' && i + 3 < str.length) {
|
|
611
|
-
const hexCode = str.substr(i + 2, 2);
|
|
612
|
-
const byteValue = parseInt(hexCode, 16);
|
|
613
|
-
buffer.push(byteValue);
|
|
614
|
-
i += 4;
|
|
615
|
-
} else {
|
|
616
|
-
const char = str[i + 1];
|
|
617
|
-
switch (char) {
|
|
618
|
-
case 'n': buffer.push(0x0A); i += 2; break;
|
|
619
|
-
case 'r': buffer.push(0x0D); i += 2; break;
|
|
620
|
-
case 't': buffer.push(0x09); i += 2; break;
|
|
621
|
-
case '\\': buffer.push(0x5C); i += 2; break;
|
|
622
|
-
default: buffer.push(char.charCodeAt(0)); i += 2; break;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
} else {
|
|
626
|
-
buffer.push(str.charCodeAt(i));
|
|
627
|
-
i++;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
return Buffer.from(buffer);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
module.exports = { ReplayExecutor };
|