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.
- package/index.js +15 -5
- package/package.json +2 -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
package/utils/ray_cli_wrapper.js
DELETED
|
@@ -1,746 +0,0 @@
|
|
|
1
|
-
const { spawn } = require('child_process');
|
|
2
|
-
const fs = require('fs-extra');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const https = require('https');
|
|
5
|
-
const http = require('http');
|
|
6
|
-
const logger = require('./logger');
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Ray CLI 封装类
|
|
10
|
-
* 负责调用系统中的 ray-cli 并进行参数转换
|
|
11
|
-
*/
|
|
12
|
-
class RayCliWrapper {
|
|
13
|
-
constructor(options = {}) {
|
|
14
|
-
this.verbose = options.verbose || 0;
|
|
15
|
-
this.configPath = path.join(__dirname, '../config/ray_cluster_mapping.json');
|
|
16
|
-
this.clusterConfig = null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 检查 ray-cli 是否已安装
|
|
21
|
-
* @returns {Promise<boolean>}
|
|
22
|
-
*/
|
|
23
|
-
async checkRayCliInstalled() {
|
|
24
|
-
// 如果设置了 ROCK_CLI_MOCK_RAY 环境变量,跳过检查(用于测试)
|
|
25
|
-
if (process.env.ROCK_CLI_MOCK_RAY === '1') {
|
|
26
|
-
return Promise.resolve(true);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return new Promise((resolve, reject) => {
|
|
30
|
-
const checkProcess = spawn('which', ['ray'], {
|
|
31
|
-
shell: true
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
checkProcess.on('close', (code) => {
|
|
35
|
-
if (code === 0) {
|
|
36
|
-
resolve(true);
|
|
37
|
-
} else {
|
|
38
|
-
reject(new Error(
|
|
39
|
-
'ray-cli not found. Please install it: pip install -U "ray[default]"'
|
|
40
|
-
));
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
checkProcess.on('error', () => {
|
|
45
|
-
reject(new Error(
|
|
46
|
-
'ray-cli not found. Please install it: pip install -U "ray[default]"'
|
|
47
|
-
));
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 加载集群配置
|
|
54
|
-
* @returns {Object} 集群配置对象
|
|
55
|
-
*/
|
|
56
|
-
loadClusterConfig() {
|
|
57
|
-
try {
|
|
58
|
-
if (!fs.existsSync(this.configPath)) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
`Config file not found: ${this.configPath}`
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const content = fs.readFileSync(this.configPath, 'utf8');
|
|
65
|
-
this.clusterConfig = JSON.parse(content);
|
|
66
|
-
|
|
67
|
-
if (!this.clusterConfig.clusters) {
|
|
68
|
-
throw new Error('Invalid config: missing "clusters" field');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return this.clusterConfig;
|
|
72
|
-
|
|
73
|
-
} catch (error) {
|
|
74
|
-
throw new Error(`Failed to load cluster config: ${error.message}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* 获取集群地址
|
|
80
|
-
* @param {string} clusterName - 集群名称
|
|
81
|
-
* @returns {string} Ray Dashboard 地址
|
|
82
|
-
*/
|
|
83
|
-
getClusterAddress(clusterName) {
|
|
84
|
-
if (!this.clusterConfig) {
|
|
85
|
-
this.loadClusterConfig();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const cluster = this.clusterConfig.clusters[clusterName];
|
|
89
|
-
if (!cluster) {
|
|
90
|
-
const available = Object.keys(this.clusterConfig.clusters).join(', ');
|
|
91
|
-
throw new Error(
|
|
92
|
-
`Cluster '${clusterName}' not found in config. Available: ${available}`
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return cluster.address;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* 处理自定义参数并构建 ray-cli 参数
|
|
101
|
-
* @param {Object} argv - yargs 参数对象
|
|
102
|
-
* @returns {Array<string>} 命令参数数组
|
|
103
|
-
*/
|
|
104
|
-
buildRayArgs(argv) {
|
|
105
|
-
const args = [];
|
|
106
|
-
|
|
107
|
-
// 1. 收集位置参数(action...)
|
|
108
|
-
if (argv.action && Array.isArray(argv.action)) {
|
|
109
|
-
// yargs 收集的多个位置参数
|
|
110
|
-
args.push(...argv.action);
|
|
111
|
-
} else if (argv._ && argv._.length > 1) {
|
|
112
|
-
// 从 argv._ 中提取(兼容)
|
|
113
|
-
args.push(...argv._.slice(1));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// 2. 处理 --cluster (优先级高)
|
|
117
|
-
if (argv.cluster) {
|
|
118
|
-
const address = this.getClusterAddress(argv.cluster);
|
|
119
|
-
args.push('--address', address);
|
|
120
|
-
} else if (argv.address) {
|
|
121
|
-
args.push('--address', argv.address);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 3. 处理 --sandbox-id (优先级高)
|
|
125
|
-
if (argv.sandboxId) {
|
|
126
|
-
args.push('--filter', `name=sandbox-${argv.sandboxId}`);
|
|
127
|
-
} else if (argv.filter) {
|
|
128
|
-
args.push('--filter', argv.filter);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 4. 透传其他参数(仅透传 ray-cli 支持的参数)
|
|
132
|
-
// 不透传 rock-cli 全局参数
|
|
133
|
-
const skipParams = [
|
|
134
|
-
'cluster', 'sandbox-id', 'sandboxId', 'address', 'filter',
|
|
135
|
-
'verbose', 'v', '_', '$0',
|
|
136
|
-
'base-url', 'baseUrl', 'api-key', 'apiKey',
|
|
137
|
-
'help', 'h', 'version', 'action'
|
|
138
|
-
];
|
|
139
|
-
|
|
140
|
-
for (const [key, value] of Object.entries(argv)) {
|
|
141
|
-
if (skipParams.includes(key)) continue;
|
|
142
|
-
if (value === undefined || value === null) continue;
|
|
143
|
-
|
|
144
|
-
const argName = key.length === 1 ? `-${key}` : `--${key}`;
|
|
145
|
-
|
|
146
|
-
if (typeof value === 'boolean') {
|
|
147
|
-
if (value) args.push(argName);
|
|
148
|
-
} else if (Array.isArray(value)) {
|
|
149
|
-
value.forEach(v => args.push(argName, String(v)));
|
|
150
|
-
} else {
|
|
151
|
-
args.push(argName, String(value));
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return args;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* 执行自定义命令(通过 HTTP API)
|
|
160
|
-
* @param {string} command - 命令名称
|
|
161
|
-
* @param {Object} argv - yargs 参数对象
|
|
162
|
-
* @returns {Promise<Object>} 执行结果
|
|
163
|
-
*/
|
|
164
|
-
async executeCustomCommand(command, argv) {
|
|
165
|
-
// get 命令有自己的参数验证逻辑
|
|
166
|
-
if (command === 'get') {
|
|
167
|
-
return this.executeGetCommand(argv);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// 其他命令需要验证必需参数
|
|
171
|
-
if (!argv.cluster && !argv.address) {
|
|
172
|
-
throw new Error('Either --cluster or --address is required');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// 获取集群地址
|
|
176
|
-
let baseUrl;
|
|
177
|
-
if (argv.cluster) {
|
|
178
|
-
baseUrl = this.getClusterAddress(argv.cluster);
|
|
179
|
-
} else {
|
|
180
|
-
baseUrl = argv.address;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (command === 'status') {
|
|
184
|
-
return this.fetchClusterStatus(baseUrl, argv.format);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
throw new Error(`Unknown custom command: ${command}`);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* 处理 get 命令
|
|
192
|
-
* @param {Object} argv - 包含 action: ['get', 'sandbox', '<id>']
|
|
193
|
-
* @returns {Promise<Object>} 执行结果
|
|
194
|
-
*/
|
|
195
|
-
async executeGetCommand(argv) {
|
|
196
|
-
const resource = argv.action && argv.action[1];
|
|
197
|
-
const id = argv.action && argv.action[2];
|
|
198
|
-
|
|
199
|
-
if (resource === 'sandbox') {
|
|
200
|
-
return this.executeGetSandbox(id, argv);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
success: false,
|
|
205
|
-
exitCode: 1,
|
|
206
|
-
stdout: '',
|
|
207
|
-
stderr: `Error: Unknown resource type: ${resource || '(none)'}\nSupported: sandbox\nUsage: rock-cli ray get sandbox <sandbox-id> --cluster=<cluster>`
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* 获取 sandbox 信息
|
|
213
|
-
* @param {string} sandboxId - Sandbox ID
|
|
214
|
-
* @param {Object} argv - yargs 参数对象
|
|
215
|
-
* @returns {Promise<Object>} 执行结果
|
|
216
|
-
*/
|
|
217
|
-
async executeGetSandbox(sandboxId, argv) {
|
|
218
|
-
// 1. 验证必需参数
|
|
219
|
-
if (!sandboxId) {
|
|
220
|
-
return {
|
|
221
|
-
success: false,
|
|
222
|
-
exitCode: 1,
|
|
223
|
-
stdout: '',
|
|
224
|
-
stderr: 'Error: Missing required argument: sandbox-id\nUsage: rock-cli ray get sandbox <sandbox-id> --cluster=<cluster>'
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (!argv.cluster && !argv.address) {
|
|
229
|
-
return {
|
|
230
|
-
success: false,
|
|
231
|
-
exitCode: 1,
|
|
232
|
-
stdout: '',
|
|
233
|
-
stderr: 'Error: Missing required option: --cluster\nUsage: rock-cli ray get sandbox <sandbox-id> --cluster=<cluster>'
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const clusterName = argv.cluster || argv.address;
|
|
238
|
-
|
|
239
|
-
// 2. 查询 actor 信息
|
|
240
|
-
if (this.verbose >= 2) {
|
|
241
|
-
logger.debug(`Querying actor for sandbox: ${sandboxId}`);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const actorResult = await this.execute({
|
|
245
|
-
action: ['list', 'actors'],
|
|
246
|
-
cluster: argv.cluster,
|
|
247
|
-
address: argv.address,
|
|
248
|
-
format: 'json',
|
|
249
|
-
sandboxId: sandboxId
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
if (!actorResult.success) {
|
|
253
|
-
return {
|
|
254
|
-
success: false,
|
|
255
|
-
exitCode: 1,
|
|
256
|
-
stdout: '',
|
|
257
|
-
stderr: `Error: Failed to query actor: ${actorResult.stderr}`
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// 3. 解析 actor 响应
|
|
262
|
-
let actors;
|
|
263
|
-
try {
|
|
264
|
-
actors = JSON.parse(actorResult.stdout);
|
|
265
|
-
} catch (e) {
|
|
266
|
-
// ray-cli 返回 "No resource in the cluster" 等非 JSON 格式表示资源不存在
|
|
267
|
-
if (actorResult.stdout.includes('No resource')) {
|
|
268
|
-
return {
|
|
269
|
-
success: false,
|
|
270
|
-
exitCode: 1,
|
|
271
|
-
stdout: '',
|
|
272
|
-
stderr: `Error: Sandbox '${sandboxId}' not found in cluster '${clusterName}'`
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
return {
|
|
276
|
-
success: false,
|
|
277
|
-
exitCode: 1,
|
|
278
|
-
stdout: '',
|
|
279
|
-
stderr: `Error: Failed to parse actor response: ${e.message}`
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (!actors || actors.length === 0) {
|
|
284
|
-
return {
|
|
285
|
-
success: false,
|
|
286
|
-
exitCode: 1,
|
|
287
|
-
stdout: '',
|
|
288
|
-
stderr: `Error: Sandbox '${sandboxId}' not found in cluster '${clusterName}'`
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const actor = actors[0];
|
|
293
|
-
const nodeId = actor.node_id;
|
|
294
|
-
|
|
295
|
-
if (!nodeId) {
|
|
296
|
-
return {
|
|
297
|
-
success: false,
|
|
298
|
-
exitCode: 1,
|
|
299
|
-
stdout: '',
|
|
300
|
-
stderr: `Error: Actor found but node_id is missing for sandbox '${sandboxId}'`
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// 4. 查询 node 信息(使用 filter 精确查询)
|
|
305
|
-
if (this.verbose >= 2) {
|
|
306
|
-
logger.debug(`Querying node: ${nodeId}`);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const nodeResult = await this.executeWithFilter({
|
|
310
|
-
action: ['list', 'nodes'],
|
|
311
|
-
cluster: argv.cluster,
|
|
312
|
-
address: argv.address,
|
|
313
|
-
format: 'json',
|
|
314
|
-
filter: `node_id=${nodeId}`
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
if (!nodeResult.success) {
|
|
318
|
-
return {
|
|
319
|
-
success: false,
|
|
320
|
-
exitCode: 1,
|
|
321
|
-
stdout: '',
|
|
322
|
-
stderr: `Error: Failed to query node: ${nodeResult.stderr}`
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// 5. 解析 node 响应
|
|
327
|
-
let nodes;
|
|
328
|
-
try {
|
|
329
|
-
nodes = JSON.parse(nodeResult.stdout);
|
|
330
|
-
} catch (e) {
|
|
331
|
-
return {
|
|
332
|
-
success: false,
|
|
333
|
-
exitCode: 1,
|
|
334
|
-
stdout: '',
|
|
335
|
-
stderr: `Error: Failed to parse node response: ${e.message}`
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (!nodes || nodes.length === 0) {
|
|
340
|
-
return {
|
|
341
|
-
success: false,
|
|
342
|
-
exitCode: 1,
|
|
343
|
-
stdout: '',
|
|
344
|
-
stderr: `Error: Node '${nodeId}' not found for sandbox '${sandboxId}'`
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const node = nodes[0];
|
|
349
|
-
|
|
350
|
-
// 6. 格式化输出
|
|
351
|
-
const outputFormat = argv.format || 'table';
|
|
352
|
-
if (outputFormat === 'json') {
|
|
353
|
-
return this.formatSandboxJsonOutput(sandboxId, clusterName, actor, node);
|
|
354
|
-
} else if (argv.detail) {
|
|
355
|
-
return this.formatSandboxDetailOutput(sandboxId, clusterName, actor, node);
|
|
356
|
-
} else {
|
|
357
|
-
return this.formatSandboxTableOutput(sandboxId, clusterName, actor, node);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* 执行带有自定义 filter 的 ray-cli 命令
|
|
363
|
-
* 与 execute() 不同,此方法直接使用传入的 filter,不做 sandboxId 转换
|
|
364
|
-
* @param {Object} argv - 参数对象
|
|
365
|
-
* @returns {Promise<Object>} 执行结果
|
|
366
|
-
*/
|
|
367
|
-
async executeWithFilter(argv) {
|
|
368
|
-
// 验证必需参数
|
|
369
|
-
if (!argv.cluster && !argv.address) {
|
|
370
|
-
throw new Error('Either --cluster or --address is required');
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// 1. 检查 ray-cli
|
|
374
|
-
await this.checkRayCliInstalled();
|
|
375
|
-
|
|
376
|
-
// 2. 构建命令参数
|
|
377
|
-
const args = [];
|
|
378
|
-
|
|
379
|
-
// 添加 action
|
|
380
|
-
if (argv.action && Array.isArray(argv.action)) {
|
|
381
|
-
args.push(...argv.action);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// 添加 address
|
|
385
|
-
if (argv.cluster) {
|
|
386
|
-
const address = this.getClusterAddress(argv.cluster);
|
|
387
|
-
args.push('--address', address);
|
|
388
|
-
} else if (argv.address) {
|
|
389
|
-
args.push('--address', argv.address);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// 添加 filter(直接使用)
|
|
393
|
-
if (argv.filter) {
|
|
394
|
-
args.push('--filter', argv.filter);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// 添加 format
|
|
398
|
-
if (argv.format) {
|
|
399
|
-
args.push('--format', argv.format);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// 3. 输出调试信息
|
|
403
|
-
if (this.verbose >= 3) {
|
|
404
|
-
logger.debug(`Executing: ray ${args.join(' ')}`);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// 4. 执行命令
|
|
408
|
-
return new Promise((resolve, reject) => {
|
|
409
|
-
let stdout = '';
|
|
410
|
-
let stderr = '';
|
|
411
|
-
|
|
412
|
-
const rayProcess = spawn('ray', args, {
|
|
413
|
-
shell: false,
|
|
414
|
-
env: process.env
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
rayProcess.stdout.on('data', (data) => {
|
|
418
|
-
stdout += data.toString();
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
rayProcess.stderr.on('data', (data) => {
|
|
422
|
-
stderr += data.toString();
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
rayProcess.on('close', (code) => {
|
|
426
|
-
resolve({
|
|
427
|
-
success: code === 0,
|
|
428
|
-
exitCode: code,
|
|
429
|
-
stdout: stdout.trim(),
|
|
430
|
-
stderr: stderr.trim()
|
|
431
|
-
});
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
rayProcess.on('error', (error) => {
|
|
435
|
-
reject(new Error(`Failed to execute ray-cli: ${error.message}`));
|
|
436
|
-
});
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* 格式化 JSON 输出(完整信息)
|
|
442
|
-
*/
|
|
443
|
-
formatSandboxJsonOutput(sandboxId, cluster, actor, node) {
|
|
444
|
-
// 过滤掉 null 值
|
|
445
|
-
const filterNull = (obj) => {
|
|
446
|
-
return Object.fromEntries(
|
|
447
|
-
Object.entries(obj).filter(([_, v]) => v !== null)
|
|
448
|
-
);
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
const output = {
|
|
452
|
-
sandbox_id: sandboxId,
|
|
453
|
-
cluster: cluster,
|
|
454
|
-
actor: filterNull(actor),
|
|
455
|
-
node: filterNull(node)
|
|
456
|
-
};
|
|
457
|
-
|
|
458
|
-
return {
|
|
459
|
-
success: true,
|
|
460
|
-
exitCode: 0,
|
|
461
|
-
stdout: JSON.stringify(output, null, 2),
|
|
462
|
-
stderr: ''
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* 格式化精简表格输出
|
|
468
|
-
*/
|
|
469
|
-
formatSandboxTableOutput(sandboxId, cluster, actor, node) {
|
|
470
|
-
const lines = [
|
|
471
|
-
`Sandbox: ${sandboxId}`,
|
|
472
|
-
`Cluster: ${cluster}`,
|
|
473
|
-
'',
|
|
474
|
-
'sandbox_id actor_state node_ip node_state',
|
|
475
|
-
'-------------------------------- ----------- ------------ ----------',
|
|
476
|
-
`${sandboxId.substring(0, 32).padEnd(32)} ${(actor.state || 'N/A').padEnd(11)} ${(node.node_ip || 'N/A').padEnd(12)} ${node.state || 'N/A'}`
|
|
477
|
-
];
|
|
478
|
-
|
|
479
|
-
return {
|
|
480
|
-
success: true,
|
|
481
|
-
exitCode: 0,
|
|
482
|
-
stdout: lines.join('\n'),
|
|
483
|
-
stderr: ''
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* 格式化详细输出(--detail)
|
|
489
|
-
*/
|
|
490
|
-
formatSandboxDetailOutput(sandboxId, cluster, actor, node) {
|
|
491
|
-
// 格式化资源信息
|
|
492
|
-
const formatResources = (resources) => {
|
|
493
|
-
if (!resources) return 'N/A';
|
|
494
|
-
const parts = [];
|
|
495
|
-
if (resources.CPU) parts.push(`CPU: ${resources.CPU}`);
|
|
496
|
-
if (resources.memory) parts.push(`memory: ${resources.memory}`);
|
|
497
|
-
if (resources.object_store_memory) parts.push(`object_store: ${resources.object_store_memory}`);
|
|
498
|
-
if (resources['xrl-sandbox']) parts.push(`xrl-sandbox: ${resources['xrl-sandbox']}`);
|
|
499
|
-
return parts.join(', ') || 'N/A';
|
|
500
|
-
};
|
|
501
|
-
|
|
502
|
-
// 格式化标签信息
|
|
503
|
-
const formatLabels = (labels) => {
|
|
504
|
-
if (!labels) return 'N/A';
|
|
505
|
-
const entries = Object.entries(labels);
|
|
506
|
-
if (entries.length === 0) return 'N/A';
|
|
507
|
-
return entries.map(([k, v]) => `${k}=${v}`).join(', ');
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
// 格式化资源请求
|
|
511
|
-
const formatRequiredResources = (resources) => {
|
|
512
|
-
if (!resources) return null;
|
|
513
|
-
const parts = [];
|
|
514
|
-
for (const [k, v] of Object.entries(resources)) {
|
|
515
|
-
parts.push(`${k}: ${v}`);
|
|
516
|
-
}
|
|
517
|
-
return parts.join(', ') || null;
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
// 构建 Actor 信息行
|
|
521
|
-
const actorLines = [
|
|
522
|
-
`actor_id: ${actor.actor_id || 'N/A'}`,
|
|
523
|
-
`name: ${actor.name || 'N/A'}`,
|
|
524
|
-
`class_name: ${actor.class_name || 'N/A'}`,
|
|
525
|
-
`state: ${actor.state || 'N/A'}`,
|
|
526
|
-
`pid: ${actor.pid || 'N/A'}`,
|
|
527
|
-
`ray_namespace: ${actor.ray_namespace || 'N/A'}`,
|
|
528
|
-
`job_id: ${actor.job_id || 'N/A'}`,
|
|
529
|
-
`node_id: ${actor.node_id || 'N/A'}`
|
|
530
|
-
];
|
|
531
|
-
|
|
532
|
-
// 添加可选字段(仅非 null 时显示)
|
|
533
|
-
if (actor.is_detached !== null && actor.is_detached !== undefined) {
|
|
534
|
-
actorLines.push(`is_detached: ${actor.is_detached}`);
|
|
535
|
-
}
|
|
536
|
-
if (actor.repr_name) {
|
|
537
|
-
actorLines.push(`repr_name: ${actor.repr_name}`);
|
|
538
|
-
}
|
|
539
|
-
if (actor.placement_group_id) {
|
|
540
|
-
actorLines.push(`placement_group_id: ${actor.placement_group_id}`);
|
|
541
|
-
}
|
|
542
|
-
const requiredRes = formatRequiredResources(actor.required_resources);
|
|
543
|
-
if (requiredRes) {
|
|
544
|
-
actorLines.push(`resources: ${requiredRes}`);
|
|
545
|
-
}
|
|
546
|
-
if (actor.num_restarts !== null && actor.num_restarts !== undefined) {
|
|
547
|
-
actorLines.push(`num_restarts: ${actor.num_restarts}`);
|
|
548
|
-
}
|
|
549
|
-
if (actor.death_cause) {
|
|
550
|
-
actorLines.push(`death_cause: ${actor.death_cause}`);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const lines = [
|
|
554
|
-
`Sandbox: ${sandboxId}`,
|
|
555
|
-
`Cluster: ${cluster}`,
|
|
556
|
-
'',
|
|
557
|
-
'=== Actor Information ===',
|
|
558
|
-
...actorLines,
|
|
559
|
-
'',
|
|
560
|
-
'=== Node Information ===',
|
|
561
|
-
`node_id: ${node.node_id || 'N/A'}`,
|
|
562
|
-
`node_ip: ${node.node_ip || 'N/A'}`,
|
|
563
|
-
`node_name: ${node.node_name || 'N/A'}`,
|
|
564
|
-
`state: ${node.state || 'N/A'}`,
|
|
565
|
-
`is_head_node: ${node.is_head_node !== undefined ? node.is_head_node : 'N/A'}`,
|
|
566
|
-
`resources: ${formatResources(node.resources_total)}`,
|
|
567
|
-
`labels: ${formatLabels(node.labels)}`
|
|
568
|
-
];
|
|
569
|
-
|
|
570
|
-
return {
|
|
571
|
-
success: true,
|
|
572
|
-
exitCode: 0,
|
|
573
|
-
stdout: lines.join('\n'),
|
|
574
|
-
stderr: ''
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* 获取集群状态(通过 HTTP API)
|
|
580
|
-
* @param {string} baseUrl - Ray Dashboard 地址
|
|
581
|
-
* @param {string} format - 输出格式(默认为文本格式)
|
|
582
|
-
* @returns {Promise<Object>} 执行结果
|
|
583
|
-
*/
|
|
584
|
-
async fetchClusterStatus(baseUrl, format) {
|
|
585
|
-
// format: 默认或任何非json值 -> format=1(文本格式)
|
|
586
|
-
// format: json -> format=0(JSON格式)
|
|
587
|
-
const formatParam = format === 'json' ? '0' : '1';
|
|
588
|
-
|
|
589
|
-
// 处理 baseUrl ,移除尾部斜杠
|
|
590
|
-
const cleanBaseUrl = baseUrl.replace(/\/$/, '');
|
|
591
|
-
const url = `${cleanBaseUrl}/api/cluster_status?format=${formatParam}`;
|
|
592
|
-
|
|
593
|
-
if (this.verbose >= 3) {
|
|
594
|
-
logger.debug(`Fetching cluster status from: ${url}`);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
return new Promise((resolve, reject) => {
|
|
598
|
-
const urlObj = new URL(url);
|
|
599
|
-
const protocol = urlObj.protocol === 'https:' ? https : http;
|
|
600
|
-
|
|
601
|
-
const options = {
|
|
602
|
-
hostname: urlObj.hostname,
|
|
603
|
-
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
604
|
-
path: urlObj.pathname + urlObj.search,
|
|
605
|
-
method: 'GET',
|
|
606
|
-
headers: {
|
|
607
|
-
'User-Agent': 'rock-cli'
|
|
608
|
-
},
|
|
609
|
-
rejectUnauthorized: false // 忽略 SSL 证书验证
|
|
610
|
-
};
|
|
611
|
-
|
|
612
|
-
protocol.get(options, (res) => {
|
|
613
|
-
// 检查 HTTP 状态码
|
|
614
|
-
if (res.statusCode !== 200) {
|
|
615
|
-
let errorData = '';
|
|
616
|
-
res.on('data', (chunk) => {
|
|
617
|
-
errorData += chunk;
|
|
618
|
-
});
|
|
619
|
-
res.on('end', () => {
|
|
620
|
-
reject(new Error(`HTTP ${res.statusCode}: ${errorData || res.statusMessage}`));
|
|
621
|
-
});
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
let data = '';
|
|
626
|
-
|
|
627
|
-
// 设置编码
|
|
628
|
-
res.setEncoding('utf8');
|
|
629
|
-
|
|
630
|
-
res.on('data', (chunk) => {
|
|
631
|
-
data += chunk;
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
res.on('end', () => {
|
|
635
|
-
try {
|
|
636
|
-
if (this.verbose >= 3) {
|
|
637
|
-
logger.debug(`Raw response: ${data.substring(0, 200)}...`);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
const response = JSON.parse(data);
|
|
641
|
-
|
|
642
|
-
if (!response.result) {
|
|
643
|
-
return resolve({
|
|
644
|
-
success: false,
|
|
645
|
-
exitCode: 1,
|
|
646
|
-
stdout: '',
|
|
647
|
-
stderr: response.msg || 'Failed to fetch cluster status'
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// format=1: 返回格式化的文本
|
|
652
|
-
if (formatParam === '1') {
|
|
653
|
-
resolve({
|
|
654
|
-
success: true,
|
|
655
|
-
exitCode: 0,
|
|
656
|
-
stdout: response.data.clusterStatus || '',
|
|
657
|
-
stderr: ''
|
|
658
|
-
});
|
|
659
|
-
} else {
|
|
660
|
-
// format=0: 返回 JSON
|
|
661
|
-
resolve({
|
|
662
|
-
success: true,
|
|
663
|
-
exitCode: 0,
|
|
664
|
-
stdout: JSON.stringify(response.data, null, 2),
|
|
665
|
-
stderr: ''
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
} catch (error) {
|
|
669
|
-
reject(new Error(`Failed to parse cluster status response: ${error.message}`));
|
|
670
|
-
}
|
|
671
|
-
});
|
|
672
|
-
}).on('error', (error) => {
|
|
673
|
-
reject(new Error(`Failed to fetch cluster status: ${error.message}`));
|
|
674
|
-
});
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* 执行 ray-cli 命令
|
|
680
|
-
* @param {Object} argv - yargs 参数对象
|
|
681
|
-
* @returns {Promise<Object>} 执行结果
|
|
682
|
-
*/
|
|
683
|
-
async execute(argv) {
|
|
684
|
-
// 验证必需参数
|
|
685
|
-
if (!argv.cluster && !argv.address) {
|
|
686
|
-
throw new Error('Either --cluster or --address is required');
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// 1. 检查 ray-cli
|
|
690
|
-
await this.checkRayCliInstalled();
|
|
691
|
-
|
|
692
|
-
// 2. 构建命令参数
|
|
693
|
-
const args = this.buildRayArgs(argv);
|
|
694
|
-
|
|
695
|
-
// 3. 输出调试信息
|
|
696
|
-
if (this.verbose >= 3) {
|
|
697
|
-
logger.debug(`Executing: ray ${args.join(' ')}`);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// 4. 如果是 mock 模式,直接返回成功(用于测试)
|
|
701
|
-
if (process.env.ROCK_CLI_MOCK_RAY === '1') {
|
|
702
|
-
return Promise.resolve({
|
|
703
|
-
success: true,
|
|
704
|
-
exitCode: 0,
|
|
705
|
-
stdout: 'Mock ray output',
|
|
706
|
-
stderr: ''
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// 5. 执行命令
|
|
711
|
-
return new Promise((resolve, reject) => {
|
|
712
|
-
let stdout = '';
|
|
713
|
-
let stderr = '';
|
|
714
|
-
|
|
715
|
-
const rayProcess = spawn('ray', args, {
|
|
716
|
-
shell: false,
|
|
717
|
-
env: process.env
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
// 捕获输出
|
|
721
|
-
rayProcess.stdout.on('data', (data) => {
|
|
722
|
-
stdout += data.toString();
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
rayProcess.stderr.on('data', (data) => {
|
|
726
|
-
stderr += data.toString();
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
// 处理退出
|
|
730
|
-
rayProcess.on('close', (code) => {
|
|
731
|
-
resolve({
|
|
732
|
-
success: code === 0,
|
|
733
|
-
exitCode: code,
|
|
734
|
-
stdout: stdout.trim(),
|
|
735
|
-
stderr: stderr.trim()
|
|
736
|
-
});
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
rayProcess.on('error', (error) => {
|
|
740
|
-
reject(new Error(`Failed to execute ray-cli: ${error.message}`));
|
|
741
|
-
});
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
module.exports = { RayCliWrapper };
|