rl-rockcli 0.0.7 → 0.0.8

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