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.
Files changed (75) hide show
  1. package/README.md +400 -0
  2. package/index.js +51 -21
  3. package/package.json +3 -2
  4. package/commands/log/core/constants.js +0 -237
  5. package/commands/log/core/display.js +0 -370
  6. package/commands/log/core/search.js +0 -330
  7. package/commands/log/core/tail.js +0 -216
  8. package/commands/log/core/utils.js +0 -424
  9. package/commands/log.js +0 -298
  10. package/commands/sandbox/core/log-bridge.js +0 -119
  11. package/commands/sandbox/core/replay/analyzer.js +0 -311
  12. package/commands/sandbox/core/replay/batch-orchestrator.js +0 -536
  13. package/commands/sandbox/core/replay/batch-task.js +0 -369
  14. package/commands/sandbox/core/replay/concurrent-display.js +0 -70
  15. package/commands/sandbox/core/replay/concurrent-orchestrator.js +0 -170
  16. package/commands/sandbox/core/replay/data-source.js +0 -86
  17. package/commands/sandbox/core/replay/display.js +0 -231
  18. package/commands/sandbox/core/replay/executor.js +0 -634
  19. package/commands/sandbox/core/replay/history-fetcher.js +0 -124
  20. package/commands/sandbox/core/replay/index.js +0 -338
  21. package/commands/sandbox/core/replay/loghouse-data-source.js +0 -177
  22. package/commands/sandbox/core/replay/pid-mapping.js +0 -26
  23. package/commands/sandbox/core/replay/request.js +0 -109
  24. package/commands/sandbox/core/replay/worker.js +0 -166
  25. package/commands/sandbox/core/session.js +0 -346
  26. package/commands/sandbox/log-bridge.js +0 -2
  27. package/commands/sandbox/ray.js +0 -2
  28. package/commands/sandbox/replay/analyzer.js +0 -311
  29. package/commands/sandbox/replay/batch-orchestrator.js +0 -536
  30. package/commands/sandbox/replay/batch-task.js +0 -369
  31. package/commands/sandbox/replay/concurrent-display.js +0 -70
  32. package/commands/sandbox/replay/concurrent-orchestrator.js +0 -170
  33. package/commands/sandbox/replay/display.js +0 -231
  34. package/commands/sandbox/replay/executor.js +0 -634
  35. package/commands/sandbox/replay/history-fetcher.js +0 -118
  36. package/commands/sandbox/replay/index.js +0 -338
  37. package/commands/sandbox/replay/pid-mapping.js +0 -26
  38. package/commands/sandbox/replay/request.js +0 -109
  39. package/commands/sandbox/replay/worker.js +0 -166
  40. package/commands/sandbox/replay.js +0 -2
  41. package/commands/sandbox/session.js +0 -2
  42. package/commands/sandbox-original.js +0 -1393
  43. package/commands/sandbox.js +0 -499
  44. package/help/help.json +0 -1071
  45. package/help/middleware.js +0 -71
  46. package/help/renderer.js +0 -800
  47. package/lib/plugin-context.js +0 -40
  48. package/sdks/sandbox/core/client.js +0 -845
  49. package/sdks/sandbox/core/config.js +0 -70
  50. package/sdks/sandbox/core/types.js +0 -74
  51. package/sdks/sandbox/httpLogger.js +0 -251
  52. package/sdks/sandbox/index.js +0 -9
  53. package/utils/asciiArt.js +0 -138
  54. package/utils/bun-compat.js +0 -59
  55. package/utils/ciPipelines.js +0 -138
  56. package/utils/cli.js +0 -17
  57. package/utils/command-router.js +0 -79
  58. package/utils/configManager.js +0 -503
  59. package/utils/dependency-resolver.js +0 -135
  60. package/utils/eagleeye_traceid.js +0 -151
  61. package/utils/envDetector.js +0 -78
  62. package/utils/execution_logger.js +0 -415
  63. package/utils/featureManager.js +0 -68
  64. package/utils/firstTimeTip.js +0 -44
  65. package/utils/hook-manager.js +0 -125
  66. package/utils/http-logger.js +0 -264
  67. package/utils/i18n.js +0 -139
  68. package/utils/image-progress.js +0 -159
  69. package/utils/logger.js +0 -154
  70. package/utils/plugin-loader.js +0 -124
  71. package/utils/plugin-manager.js +0 -348
  72. package/utils/ray_cli_wrapper.js +0 -746
  73. package/utils/sandbox-client.js +0 -419
  74. package/utils/terminal.js +0 -32
  75. 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 };