rl-rockcli 0.0.8 → 0.0.10

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 (162) hide show
  1. package/commands/attach/basic-repl.js +212 -0
  2. package/commands/attach/cleanup-history.js +189 -0
  3. package/commands/attach/cleanup-manager.js +163 -0
  4. package/commands/attach/copy-ui/copyRepl.js +195 -0
  5. package/commands/attach/copy-ui/index.js +7 -0
  6. package/commands/attach/copy-ui/render/outputBlock.js +25 -0
  7. package/commands/attach/copy-ui/viewport/viewport.js +23 -0
  8. package/commands/attach/copy-ui/viewport/wheel.js +14 -0
  9. package/commands/attach/history-manager.js +507 -0
  10. package/commands/attach/history-session.js +48 -0
  11. package/commands/attach/ink-repl/InkREPL.js +1507 -0
  12. package/commands/attach/ink-repl/builtinCommands.js +1253 -0
  13. package/commands/attach/ink-repl/components/ConnectingScreen.js +76 -0
  14. package/commands/attach/ink-repl/components/Console.js +191 -0
  15. package/commands/attach/ink-repl/components/DetailView.js +148 -0
  16. package/commands/attach/ink-repl/components/DropdownMenu.js +86 -0
  17. package/commands/attach/ink-repl/components/InputArea.js +125 -0
  18. package/commands/attach/ink-repl/components/InputLine.js +18 -0
  19. package/commands/attach/ink-repl/components/OutputArea.js +22 -0
  20. package/commands/attach/ink-repl/components/OutputItem.js +96 -0
  21. package/commands/attach/ink-repl/components/ShellLayout.js +61 -0
  22. package/commands/attach/ink-repl/components/Spinner.js +79 -0
  23. package/commands/attach/ink-repl/components/StatusBar.js +106 -0
  24. package/commands/attach/ink-repl/components/WelcomeBanner.js +48 -0
  25. package/commands/attach/ink-repl/contexts/LayoutContext.js +12 -0
  26. package/commands/attach/ink-repl/contexts/ThemeContext.js +43 -0
  27. package/commands/attach/ink-repl/hooks/useFunctionKeys.js +70 -0
  28. package/commands/attach/ink-repl/hooks/useMouse.js +162 -0
  29. package/commands/attach/ink-repl/hooks/useResources.js +132 -0
  30. package/commands/attach/ink-repl/hooks/useSpinner.js +49 -0
  31. package/commands/attach/ink-repl/index.js +112 -0
  32. package/commands/attach/ink-repl/package.json +3 -0
  33. package/commands/attach/ink-repl/replState.js +947 -0
  34. package/commands/attach/ink-repl/shortcuts/defaultKeybindings.js +138 -0
  35. package/commands/attach/ink-repl/shortcuts/index.js +332 -0
  36. package/commands/attach/ink-repl/themes/defaultDark.js +18 -0
  37. package/commands/attach/ink-repl/themes/defaultLight.js +18 -0
  38. package/commands/attach/ink-repl/themes/index.js +4 -0
  39. package/commands/attach/ink-repl/themes/themeManager.js +45 -0
  40. package/commands/attach/ink-repl/themes/themeTokens.js +15 -0
  41. package/commands/attach/ink-repl/utils/atCompletion.js +346 -0
  42. package/commands/attach/ink-repl/utils/clipboard.js +50 -0
  43. package/commands/attach/ink-repl/utils/consoleLogger.js +81 -0
  44. package/commands/attach/ink-repl/utils/exitCodeHandler.js +49 -0
  45. package/commands/attach/ink-repl/utils/exitCodeTips.js +56 -0
  46. package/commands/attach/ink-repl/utils/formatTime.js +12 -0
  47. package/commands/attach/ink-repl/utils/outputSelection.js +120 -0
  48. package/commands/attach/ink-repl/utils/outputViewport.js +77 -0
  49. package/commands/attach/ink-repl/utils/paginatedFileLoading.js +76 -0
  50. package/commands/attach/ink-repl/utils/paramHint.js +60 -0
  51. package/commands/attach/ink-repl/utils/parseError.js +174 -0
  52. package/commands/attach/ink-repl/utils/pathCompletion.js +167 -0
  53. package/commands/attach/ink-repl/utils/remotePathSafety.js +56 -0
  54. package/commands/attach/ink-repl/utils/replSelection.js +205 -0
  55. package/commands/attach/ink-repl/utils/responseFormatter.js +127 -0
  56. package/commands/attach/ink-repl/utils/textWrap.js +117 -0
  57. package/commands/attach/ink-repl/utils/truncate.js +115 -0
  58. package/commands/attach/opentui-repl/App.tsx +891 -0
  59. package/commands/attach/opentui-repl/builtinCommands.ts +80 -0
  60. package/commands/attach/opentui-repl/components/ConfirmDialog.tsx +116 -0
  61. package/commands/attach/opentui-repl/components/ConnectingScreen.tsx +131 -0
  62. package/commands/attach/opentui-repl/components/Console.tsx +73 -0
  63. package/commands/attach/opentui-repl/components/DetailView.tsx +45 -0
  64. package/commands/attach/opentui-repl/components/DropdownMenu.tsx +130 -0
  65. package/commands/attach/opentui-repl/components/ExecutionStatus.tsx +66 -0
  66. package/commands/attach/opentui-repl/components/Header.tsx +24 -0
  67. package/commands/attach/opentui-repl/components/OutputArea.tsx +25 -0
  68. package/commands/attach/opentui-repl/components/OutputBlock.tsx +108 -0
  69. package/commands/attach/opentui-repl/components/PromptInput.tsx +109 -0
  70. package/commands/attach/opentui-repl/components/StatusBar.tsx +63 -0
  71. package/commands/attach/opentui-repl/components/Toast.tsx +65 -0
  72. package/commands/attach/opentui-repl/components/WelcomeBanner.tsx +41 -0
  73. package/commands/attach/opentui-repl/contexts/ReplContext.tsx +137 -0
  74. package/commands/attach/opentui-repl/contexts/SessionContext.tsx +32 -0
  75. package/commands/attach/opentui-repl/contexts/ThemeContext.tsx +70 -0
  76. package/commands/attach/opentui-repl/contexts/ToastContext.tsx +69 -0
  77. package/commands/attach/opentui-repl/contexts/toast-logic.js +71 -0
  78. package/commands/attach/opentui-repl/hooks/useResources.ts +102 -0
  79. package/commands/attach/opentui-repl/hooks/useSpinner.ts +46 -0
  80. package/commands/attach/opentui-repl/index.js +99 -0
  81. package/commands/attach/opentui-repl/keybindings.ts +39 -0
  82. package/commands/attach/opentui-repl/package.json +3 -0
  83. package/commands/attach/opentui-repl/render.tsx +72 -0
  84. package/commands/attach/opentui-repl/tsconfig.json +12 -0
  85. package/commands/attach/repl.js +791 -0
  86. package/commands/attach/sandbox-id-resolver.js +56 -0
  87. package/commands/attach/session-manager.js +307 -0
  88. package/commands/attach/ui-mode.js +146 -0
  89. package/commands/log/core/constants.js +237 -0
  90. package/commands/log/core/display.js +370 -0
  91. package/commands/log/core/search.js +330 -0
  92. package/commands/log/core/tail.js +216 -0
  93. package/commands/log/core/utils.js +424 -0
  94. package/commands/log.js +298 -0
  95. package/commands/sandbox/core/log-bridge.js +119 -0
  96. package/commands/sandbox/core/replay/analyzer.js +311 -0
  97. package/commands/sandbox/core/replay/batch-orchestrator.js +536 -0
  98. package/commands/sandbox/core/replay/batch-task.js +369 -0
  99. package/commands/sandbox/core/replay/concurrent-display.js +70 -0
  100. package/commands/sandbox/core/replay/concurrent-orchestrator.js +170 -0
  101. package/commands/sandbox/core/replay/data-source.js +86 -0
  102. package/commands/sandbox/core/replay/display.js +231 -0
  103. package/commands/sandbox/core/replay/executor.js +634 -0
  104. package/commands/sandbox/core/replay/history-fetcher.js +124 -0
  105. package/commands/sandbox/core/replay/index.js +338 -0
  106. package/commands/sandbox/core/replay/loghouse-data-source.js +177 -0
  107. package/commands/sandbox/core/replay/pid-mapping.js +26 -0
  108. package/commands/sandbox/core/replay/request.js +109 -0
  109. package/commands/sandbox/core/replay/worker.js +166 -0
  110. package/commands/sandbox/core/session.js +346 -0
  111. package/commands/sandbox/log-bridge.js +2 -0
  112. package/commands/sandbox/ray.js +2 -0
  113. package/commands/sandbox/replay/analyzer.js +311 -0
  114. package/commands/sandbox/replay/batch-orchestrator.js +536 -0
  115. package/commands/sandbox/replay/batch-task.js +369 -0
  116. package/commands/sandbox/replay/concurrent-display.js +70 -0
  117. package/commands/sandbox/replay/concurrent-orchestrator.js +170 -0
  118. package/commands/sandbox/replay/display.js +231 -0
  119. package/commands/sandbox/replay/executor.js +634 -0
  120. package/commands/sandbox/replay/history-fetcher.js +118 -0
  121. package/commands/sandbox/replay/index.js +338 -0
  122. package/commands/sandbox/replay/pid-mapping.js +26 -0
  123. package/commands/sandbox/replay/request.js +109 -0
  124. package/commands/sandbox/replay/worker.js +166 -0
  125. package/commands/sandbox/replay.js +2 -0
  126. package/commands/sandbox/session.js +2 -0
  127. package/commands/sandbox-original.js +1393 -0
  128. package/commands/sandbox.js +499 -0
  129. package/help/help.json +1071 -0
  130. package/help/middleware.js +71 -0
  131. package/help/renderer.js +800 -0
  132. package/index.js +5 -15
  133. package/lib/plugin-context.js +40 -0
  134. package/package.json +2 -2
  135. package/sdks/sandbox/core/client.js +845 -0
  136. package/sdks/sandbox/core/config.js +70 -0
  137. package/sdks/sandbox/core/types.js +74 -0
  138. package/sdks/sandbox/httpLogger.js +251 -0
  139. package/sdks/sandbox/index.js +9 -0
  140. package/utils/asciiArt.js +138 -0
  141. package/utils/bun-compat.js +59 -0
  142. package/utils/ciPipelines.js +138 -0
  143. package/utils/cli.js +17 -0
  144. package/utils/command-router.js +79 -0
  145. package/utils/configManager.js +503 -0
  146. package/utils/dependency-resolver.js +135 -0
  147. package/utils/eagleeye_traceid.js +151 -0
  148. package/utils/envDetector.js +78 -0
  149. package/utils/execution_logger.js +415 -0
  150. package/utils/featureManager.js +68 -0
  151. package/utils/firstTimeTip.js +44 -0
  152. package/utils/hook-manager.js +125 -0
  153. package/utils/http-logger.js +264 -0
  154. package/utils/i18n.js +139 -0
  155. package/utils/image-progress.js +159 -0
  156. package/utils/logger.js +154 -0
  157. package/utils/plugin-loader.js +124 -0
  158. package/utils/plugin-manager.js +348 -0
  159. package/utils/ray_cli_wrapper.js +746 -0
  160. package/utils/sandbox-client.js +419 -0
  161. package/utils/terminal.js +32 -0
  162. package/utils/tips.js +106 -0
@@ -0,0 +1,369 @@
1
+ /**
2
+ * 批量回放任务状态常量
3
+ */
4
+ const TaskStatus = {
5
+ PENDING: 'pending', // 待处理
6
+ FETCHING: 'fetching', // 正在获取历史
7
+ READY: 'ready', // 历史已就绪,待执行
8
+ RUNNING: 'running', // 正在执行回放
9
+ COMPLETED: 'completed', // 执行完成
10
+ FAILED: 'failed', // 执行失败
11
+ SKIPPED: 'skipped' // 跳过(历史获取失败等)
12
+ };
13
+
14
+ /**
15
+ * 错误发生阶段
16
+ */
17
+ const ErrorPhase = {
18
+ PARSE: 'parse', // 解析输入
19
+ HISTORY_FETCH: 'history_fetch', // 获取历史
20
+ REPLAY: 'replay' // 执行回放
21
+ };
22
+
23
+ /**
24
+ * 单个沙箱任务
25
+ * 管理单个沙箱的回放状态和结果
26
+ */
27
+ class SandboxTask {
28
+ /**
29
+ * @param {number} index - 任务索引(0-based)
30
+ * @param {Object} sandboxConfig - 沙箱配置
31
+ * @param {Object} defaults - 默认配置(从输入文件的 defaults 字段)
32
+ */
33
+ constructor(index, sandboxConfig, defaults = {}) {
34
+ this.index = index;
35
+ this.sandboxId = sandboxConfig.sandboxId;
36
+ this.name = sandboxConfig.name || sandboxConfig.sandboxId.substring(0, 8);
37
+
38
+ // 合并配置(沙箱配置覆盖默认配置)
39
+ this.cluster = sandboxConfig.cluster || defaults.cluster || null;
40
+ this.userId = sandboxConfig.userId || defaults.userId || null;
41
+ this.experimentId = sandboxConfig.experimentId || defaults.experimentId || null;
42
+ this.mode = sandboxConfig.mode || defaults.mode || 'smart';
43
+ this.timing = sandboxConfig.timing || defaults.timing || 'none';
44
+
45
+ // 元数据
46
+ this.tags = sandboxConfig.tags || {};
47
+ this.uris = sandboxConfig.uris || [];
48
+
49
+ // 合并过滤器(默认 + 沙箱特定)
50
+ this.filters = [
51
+ ...(defaults.filters || []),
52
+ ...(sandboxConfig.filters || [])
53
+ ];
54
+
55
+ // 状态
56
+ this.status = TaskStatus.PENDING;
57
+
58
+ // 历史数据
59
+ this.requests = sandboxConfig.requests || null; // 预加载的请求列表
60
+ this.historyData = null; // 完整的历史数据(包含 startParams, targetCluster 等)
61
+
62
+ // 回放结果
63
+ this.newSandboxId = null; // 新创建的沙箱 ID
64
+ this.replayResult = null; // ReplayExecutor 返回的结果
65
+ this.pidMappings = []; // PID 映射表
66
+
67
+ // 错误信息
68
+ this.error = null;
69
+ this.errorPhase = null;
70
+
71
+ // 时间记录
72
+ this.historyFetchStartTime = null;
73
+ this.historyFetchEndTime = null;
74
+ this.replayStartTime = null;
75
+ this.replayEndTime = null;
76
+
77
+ // 日志文件
78
+ this.logFile = null;
79
+ }
80
+
81
+ /**
82
+ * 是否有预加载的请求列表
83
+ * @returns {boolean}
84
+ */
85
+ hasRequests() {
86
+ return !!(this.requests && Array.isArray(this.requests) && this.requests.length > 0);
87
+ }
88
+
89
+ /**
90
+ * 设置历史获取开始
91
+ */
92
+ startHistoryFetch() {
93
+ this.status = TaskStatus.FETCHING;
94
+ this.historyFetchStartTime = Date.now();
95
+ }
96
+
97
+ /**
98
+ * 设置历史获取完成
99
+ * @param {Object} historyData - 历史数据
100
+ */
101
+ completeHistoryFetch(historyData) {
102
+ this.historyData = historyData;
103
+ this.requests = historyData.requests || [];
104
+ this.status = TaskStatus.READY;
105
+ this.historyFetchEndTime = Date.now();
106
+ }
107
+
108
+ /**
109
+ * 设置历史获取失败
110
+ * @param {Error} error - 错误对象
111
+ */
112
+ failHistoryFetch(error) {
113
+ this.status = TaskStatus.FAILED;
114
+ this.error = error.message || String(error);
115
+ this.errorPhase = ErrorPhase.HISTORY_FETCH;
116
+ this.historyFetchEndTime = Date.now();
117
+ }
118
+
119
+ /**
120
+ * 设置回放开始
121
+ */
122
+ startReplay() {
123
+ this.status = TaskStatus.RUNNING;
124
+ this.replayStartTime = Date.now();
125
+ }
126
+
127
+ /**
128
+ * 设置回放完成
129
+ * @param {Object} result - 回放结果
130
+ * @param {string} newSandboxId - 新沙箱 ID
131
+ * @param {Array} pidMappings - PID 映射
132
+ */
133
+ completeReplay(result, newSandboxId, pidMappings = []) {
134
+ // 如果有失败的请求,标记为 FAILED 而不是 COMPLETED
135
+ if (result && result.failed > 0) {
136
+ this.status = TaskStatus.FAILED;
137
+ this.error = `${result.failed} request(s) failed during replay`;
138
+ this.errorPhase = ErrorPhase.REPLAY;
139
+ } else {
140
+ this.status = TaskStatus.COMPLETED;
141
+ }
142
+ this.replayResult = result;
143
+ this.newSandboxId = newSandboxId;
144
+ this.pidMappings = pidMappings;
145
+ this.replayEndTime = Date.now();
146
+ }
147
+
148
+ /**
149
+ * 设置回放失败
150
+ * @param {Error} error - 错误对象
151
+ */
152
+ failReplay(error) {
153
+ this.status = TaskStatus.FAILED;
154
+ this.error = error.message || String(error);
155
+ this.errorPhase = ErrorPhase.REPLAY;
156
+ this.replayEndTime = Date.now();
157
+ }
158
+
159
+ /**
160
+ * 获取历史获取耗时(毫秒)
161
+ * @returns {number|null}
162
+ */
163
+ getHistoryFetchDuration() {
164
+ if (this.historyFetchStartTime && this.historyFetchEndTime) {
165
+ return this.historyFetchEndTime - this.historyFetchStartTime;
166
+ }
167
+ return null;
168
+ }
169
+
170
+ /**
171
+ * 获取回放耗时(毫秒)
172
+ * @returns {number|null}
173
+ */
174
+ getReplayDuration() {
175
+ if (this.replayStartTime && this.replayEndTime) {
176
+ return this.replayEndTime - this.replayStartTime;
177
+ }
178
+ return null;
179
+ }
180
+
181
+ /**
182
+ * 转换为结果对象
183
+ * @returns {SandboxResult}
184
+ */
185
+ toResult() {
186
+ return new SandboxResult(this);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * 单个沙箱的执行结果
192
+ */
193
+ class SandboxResult {
194
+ /**
195
+ * @param {SandboxTask} task - 任务对象
196
+ */
197
+ constructor(task) {
198
+ this.index = task.index;
199
+ this.sandboxId = task.sandboxId;
200
+ this.name = task.name;
201
+ this.tags = task.tags;
202
+ this.status = task.status;
203
+ this.newSandboxId = task.newSandboxId;
204
+
205
+ // 历史获取结果
206
+ this.historyFetch = {
207
+ success: task.status !== TaskStatus.FAILED || task.errorPhase !== ErrorPhase.HISTORY_FETCH,
208
+ requestCount: task.requests ? task.requests.length : 0,
209
+ duration: task.getHistoryFetchDuration(),
210
+ error: task.errorPhase === ErrorPhase.HISTORY_FETCH ? task.error : null
211
+ };
212
+
213
+ // 回放结果
214
+ if (task.replayResult) {
215
+ this.replay = {
216
+ originalRequests: task.replayResult.total || 0,
217
+ executedRequests: (task.replayResult.success || 0) + (task.replayResult.failed || 0),
218
+ success: task.replayResult.success || 0,
219
+ failed: task.replayResult.failed || 0,
220
+ skipped: task.replayResult.skipped || 0,
221
+ duration: task.getReplayDuration()
222
+ };
223
+ } else {
224
+ this.replay = null;
225
+ }
226
+
227
+ // PID 映射
228
+ this.pidMappings = task.pidMappings;
229
+
230
+ // 错误信息
231
+ this.errors = [];
232
+ if (task.error) {
233
+ this.errors.push({
234
+ phase: task.errorPhase,
235
+ message: task.error
236
+ });
237
+ }
238
+
239
+ // 日志文件
240
+ this.logFile = task.logFile;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * 批量回放结果汇总
246
+ */
247
+ class BatchReplayResult {
248
+ /**
249
+ * @param {Object} options - 配置选项
250
+ */
251
+ constructor(options = {}) {
252
+ this.summary = {
253
+ totalSandboxes: 0,
254
+ successful: 0,
255
+ failed: 0,
256
+ skipped: 0,
257
+ startTime: null,
258
+ endTime: null,
259
+ totalDuration: null,
260
+ concurrency: options.concurrency || 1
261
+ };
262
+
263
+ this.sandboxResults = [];
264
+ this.failedSandboxes = [];
265
+
266
+ this.metadata = {
267
+ inputFile: options.inputFile || null,
268
+ inputMetadata: options.inputMetadata || null,
269
+ cliVersion: options.cliVersion || null,
270
+ mode: options.mode || 'smart',
271
+ timing: options.timing || 'none'
272
+ };
273
+ }
274
+
275
+ /**
276
+ * 设置开始时间
277
+ */
278
+ start() {
279
+ this.summary.startTime = new Date().toISOString();
280
+ }
281
+
282
+ /**
283
+ * 设置结束时间并计算总耗时
284
+ */
285
+ end() {
286
+ this.summary.endTime = new Date().toISOString();
287
+
288
+ if (this.summary.startTime && this.summary.endTime) {
289
+ const startMs = new Date(this.summary.startTime).getTime();
290
+ const endMs = new Date(this.summary.endTime).getTime();
291
+ const durationMs = endMs - startMs;
292
+ this.summary.totalDuration = formatDuration(durationMs);
293
+ }
294
+ }
295
+
296
+ /**
297
+ * 添加沙箱结果
298
+ * @param {SandboxResult} result - 沙箱结果
299
+ */
300
+ addResult(result) {
301
+ this.sandboxResults.push(result);
302
+ this.summary.totalSandboxes++;
303
+
304
+ if (result.status === TaskStatus.COMPLETED) {
305
+ this.summary.successful++;
306
+ } else if (result.status === TaskStatus.FAILED) {
307
+ this.summary.failed++;
308
+ this.failedSandboxes.push({
309
+ sandboxId: result.sandboxId,
310
+ name: result.name,
311
+ error: result.errors.length > 0 ? result.errors[0].message : 'Unknown error',
312
+ phase: result.errors.length > 0 ? result.errors[0].phase : 'unknown'
313
+ });
314
+ } else if (result.status === TaskStatus.SKIPPED) {
315
+ this.summary.skipped++;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * 从任务列表生成结果
321
+ * @param {SandboxTask[]} tasks - 任务列表
322
+ */
323
+ generateFromTasks(tasks) {
324
+ for (const task of tasks) {
325
+ this.addResult(task.toResult());
326
+ }
327
+ }
328
+
329
+ /**
330
+ * 转换为 JSON 对象
331
+ * @returns {Object}
332
+ */
333
+ toJSON() {
334
+ return {
335
+ summary: this.summary,
336
+ sandboxResults: this.sandboxResults,
337
+ failedSandboxes: this.failedSandboxes,
338
+ metadata: this.metadata
339
+ };
340
+ }
341
+ }
342
+
343
+ /**
344
+ * 格式化持续时间
345
+ * @param {number} ms - 毫秒
346
+ * @returns {string}
347
+ */
348
+ function formatDuration(ms) {
349
+ if (ms === null || ms === undefined) return '-';
350
+
351
+ if (ms < 1000) {
352
+ return `${ms}ms`;
353
+ } else if (ms < 60000) {
354
+ return `${(ms / 1000).toFixed(1)}s`;
355
+ } else {
356
+ const minutes = Math.floor(ms / 60000);
357
+ const seconds = Math.floor((ms % 60000) / 1000);
358
+ return `${minutes}m ${seconds}s`;
359
+ }
360
+ }
361
+
362
+ module.exports = {
363
+ TaskStatus,
364
+ ErrorPhase,
365
+ SandboxTask,
366
+ SandboxResult,
367
+ BatchReplayResult,
368
+ formatDuration
369
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 格式化持续时间
3
+ * @param {number} ms - 毫秒
4
+ * @returns {string}
5
+ */
6
+ function formatDuration(ms) {
7
+ if (ms == null || isNaN(ms)) return 'N/A';
8
+ if (ms < 1000) return `${ms}ms`;
9
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
10
+ const minutes = Math.floor(ms / 60000);
11
+ const seconds = Math.round((ms % 60000) / 1000);
12
+ return `${minutes}m${seconds}s`;
13
+ }
14
+
15
+ /**
16
+ * 显示并发回放结果摘要
17
+ * @param {ConcurrentReplayResult} result
18
+ * @param {Object} options
19
+ * @param {boolean} options.quiet - 静默模式
20
+ */
21
+ function displayConcurrentResultsSummary(result, options = {}) {
22
+ if (options.quiet) return;
23
+
24
+ console.log('\n═══════════════════════════════════════════════════════════════');
25
+ console.log('📊 Concurrent Replay Summary');
26
+ console.log('═══════════════════════════════════════════════════════════════');
27
+
28
+ console.log(`\nConcurrency: ${result.concurrency} sandboxes`);
29
+ console.log(`Status: ${result.allSucceeded ? '✅ All succeeded' : '⚠️ Some failed'}`);
30
+
31
+ // 显示每个 worker 的结果
32
+ console.log('\nPer-Sandbox Results:');
33
+ console.log('┌─────────┬────────────────────────────────────────┬─────────┬─────────┬─────────┐');
34
+ console.log('│ Worker │ Sandbox ID │ Success │ Failed │ Duration│');
35
+ console.log('├─────────┼────────────────────────────────────────┼─────────┼─────────┼─────────┤');
36
+
37
+ result.workers.forEach(w => {
38
+ const status = w.status === 'completed' ? '✅' : '❌';
39
+ const sandboxId = (w.sandboxId || 'N/A').padEnd(38).substring(0, 38);
40
+ const success = String(w.results?.success || 0).padStart(7);
41
+ const failed = String(w.results?.failed || 0).padStart(7);
42
+ const duration = formatDuration(w.duration).padStart(7);
43
+
44
+ console.log(`│ ${status} ${String(w.workerId).padEnd(5)} │ ${sandboxId} │ ${success} │ ${failed} │ ${duration} │`);
45
+
46
+ if (w.error) {
47
+ const errorMsg = w.error.length > 60 ? w.error.substring(0, 57) + '...' : w.error;
48
+ console.log(`│ │ Error: ${errorMsg.padEnd(52)} │`);
49
+ }
50
+ });
51
+
52
+ console.log('└─────────┴────────────────────────────────────────┴─────────┴─────────┴─────────┘');
53
+
54
+ // 显示汇总
55
+ console.log('\nAggregated Results:');
56
+ console.log(`├── ✅ Total Successful: ${result.aggregated.success}`);
57
+ console.log(`├── ❌ Total Failed: ${result.aggregated.failed}`);
58
+ console.log(`└── ⏭️ Total Skipped: ${result.aggregated.skipped}`);
59
+
60
+ // 显示日志文件
61
+ console.log('\nLog Files:');
62
+ result.workers.forEach((w, index) => {
63
+ const prefix = index === result.workers.length - 1 ? '└──' : '├──';
64
+ console.log(`${prefix} Worker ${w.workerId}: ${w.logFile}`);
65
+ });
66
+
67
+ console.log('═══════════════════════════════════════════════════════════════\n');
68
+ }
69
+
70
+ module.exports = { displayConcurrentResultsSummary, formatDuration };
@@ -0,0 +1,170 @@
1
+ const { ReplayWorker } = require('./worker');
2
+
3
+ /**
4
+ * 并发回放协调器
5
+ * 管理多个 ReplayWorker 的并发执行
6
+ */
7
+ class ConcurrentReplayOrchestrator {
8
+ /**
9
+ * @param {Object} options
10
+ * @param {number} options.concurrency - 并发数
11
+ * @param {Object} options.config - 沙箱配置
12
+ * @param {boolean} options.quiet - 静默模式
13
+ * @param {string} options.logFile - 日志文件基础路径
14
+ * @param {string} options.failureStrategy - 失败策略: 'continue' | 'stop-all'
15
+ */
16
+ constructor(options) {
17
+ this.concurrency = options.concurrency || 1;
18
+ this.config = options.config;
19
+ this.quiet = options.quiet || false;
20
+ this.logFile = options.logFile || 'replay.log';
21
+ this.failureStrategy = options.failureStrategy || 'continue';
22
+
23
+ this.workers = [];
24
+ this.progressIntervalId = null;
25
+ }
26
+
27
+ /**
28
+ * 执行并发回放
29
+ * @param {ReplayPlan} plan - 回放计划
30
+ * @param {Object} executorOptions - 执行器选项
31
+ * @returns {Promise<ConcurrentReplayResult>}
32
+ */
33
+ async execute(plan, executorOptions) {
34
+ // 1. 显示并发回放摘要
35
+ this.displayConcurrentSummary(plan);
36
+
37
+ // 2. 创建所有 workers
38
+ for (let i = 1; i <= this.concurrency; i++) {
39
+ const worker = new ReplayWorker(i, this.config, plan, {
40
+ ...executorOptions,
41
+ logFile: this.logFile
42
+ });
43
+ this.workers.push(worker);
44
+ }
45
+
46
+ // 3. 启动进度监控(如果非静默模式)
47
+ if (!this.quiet) {
48
+ this.startProgressMonitor();
49
+ }
50
+
51
+ // 4. 并发执行所有 workers
52
+ let workerResults;
53
+
54
+ if (this.failureStrategy === 'stop-all') {
55
+ // 使用 Promise.all,任一失败则全部停止
56
+ try {
57
+ workerResults = await Promise.all(
58
+ this.workers.map(w => w.execute())
59
+ );
60
+ } catch (error) {
61
+ // 如果 Promise.all 抛出异常,收集已有结果
62
+ workerResults = this.workers.map(w => w.getWorkerResult());
63
+ }
64
+ } else {
65
+ // 使用 Promise.allSettled,允许部分失败
66
+ const settled = await Promise.allSettled(
67
+ this.workers.map(w => w.execute())
68
+ );
69
+ workerResults = settled.map((result, index) => {
70
+ if (result.status === 'fulfilled') {
71
+ return result.value;
72
+ } else {
73
+ return this.workers[index].getWorkerResult();
74
+ }
75
+ });
76
+ }
77
+
78
+ // 5. 停止进度监控
79
+ this.stopProgressMonitor();
80
+
81
+ // 6. 汇总结果
82
+ return this.aggregateResults(workerResults);
83
+ }
84
+
85
+ /**
86
+ * 显示并发回放摘要
87
+ * @param {ReplayPlan} plan
88
+ */
89
+ displayConcurrentSummary(plan) {
90
+ if (this.quiet) return;
91
+
92
+ console.log(`\n🔀 Concurrent Replay Mode`);
93
+ console.log(` Concurrency: ${this.concurrency} sandboxes`);
94
+ console.log(` Requests per sandbox: ${plan.executeCount}`);
95
+ console.log(` Total requests: ${plan.executeCount * this.concurrency}`);
96
+ console.log(`\n⏳ Starting ${this.concurrency} sandboxes...\n`);
97
+ }
98
+
99
+ /**
100
+ * 启动进度监控
101
+ */
102
+ startProgressMonitor() {
103
+ this.progressIntervalId = setInterval(() => {
104
+ this.displayProgress();
105
+ }, 10000); // 每 10 秒更新一次进度
106
+ }
107
+
108
+ /**
109
+ * 停止进度监控
110
+ */
111
+ stopProgressMonitor() {
112
+ if (this.progressIntervalId) {
113
+ clearInterval(this.progressIntervalId);
114
+ this.progressIntervalId = null;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * 显示当前进度
120
+ */
121
+ displayProgress() {
122
+ const statuses = this.workers.map(w => {
123
+ const progress = w.getProgress();
124
+ const icon = progress.status === 'completed' ? '✅' :
125
+ progress.status === 'failed' ? '❌' :
126
+ progress.status === 'running' ? '⏳' : '⏸️';
127
+ const sandboxId = progress.sandboxId || '--------';
128
+ return ` Worker ${progress.workerId}: ${icon} sandbox=${sandboxId}... ${progress.success}/${progress.total}`;
129
+ });
130
+
131
+ console.log('\n--- Progress Update ---');
132
+ statuses.forEach(s => console.log(s));
133
+ console.log('-----------------------\n');
134
+ }
135
+
136
+ /**
137
+ * 汇总执行结果
138
+ * @param {WorkerResult[]} workerResults
139
+ * @returns {ConcurrentReplayResult}
140
+ */
141
+ aggregateResults(workerResults) {
142
+ const totalResults = {
143
+ total: 0,
144
+ success: 0,
145
+ failed: 0,
146
+ skipped: 0
147
+ };
148
+
149
+ workerResults.forEach(r => {
150
+ if (r.results) {
151
+ totalResults.total += r.results.total;
152
+ totalResults.success += r.results.success;
153
+ totalResults.failed += r.results.failed;
154
+ totalResults.skipped += r.results.skipped;
155
+ }
156
+ });
157
+
158
+ return {
159
+ concurrency: this.concurrency,
160
+ workers: workerResults,
161
+ aggregated: totalResults,
162
+ allSucceeded: workerResults.every(r => r.status === 'completed'),
163
+ sandboxIds: workerResults
164
+ .filter(r => r.sandboxId)
165
+ .map(r => ({ workerId: r.workerId, sandboxId: r.sandboxId }))
166
+ };
167
+ }
168
+ }
169
+
170
+ module.exports = { ConcurrentReplayOrchestrator };