rl-rockcli 0.0.1

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.
@@ -0,0 +1,472 @@
1
+ /**
2
+ * Sandbox Client for managing sandbox environments
3
+ * 开源版 Sandbox SDK 客户端
4
+ */
5
+
6
+ const axios = require('axios');
7
+ const { SandboxConfig } = require('./config');
8
+ const logger = require('../../utils/logger');
9
+
10
+ /**
11
+ * Simple HTTP logger for SDK
12
+ */
13
+ const httpLogger = {
14
+ logRequest: (method, url, data) => {
15
+ logger.debug(`[HTTP] ${method} ${url}`);
16
+ if (data) logger.debug(`[HTTP] Request: ${JSON.stringify(data).substring(0, 200)}`);
17
+ },
18
+ logResponse: (method, url, status, duration, data, traceId) => {
19
+ logger.debug(`[HTTP] ${method} ${url} - ${status} (${duration}ms)`);
20
+ if (traceId) logger.debug(`[HTTP] TraceId: ${traceId}`);
21
+ },
22
+ logError: (method, url, error, duration, traceId) => {
23
+ logger.error(`[HTTP] ${method} ${url} - Error: ${error.message} (${duration}ms)`);
24
+ }
25
+ };
26
+
27
+ /**
28
+ * Sandbox Client for managing sandbox environments
29
+ */
30
+ class SandboxClient {
31
+ constructor(config, options = {}) {
32
+ if (config instanceof SandboxConfig) {
33
+ this.config = config;
34
+ } else {
35
+ this.config = new SandboxConfig(config);
36
+ }
37
+ this.config.validate({ requireImage: options.requireImage !== false });
38
+
39
+ this._sandboxId = null;
40
+ this._hostName = null;
41
+ this._hostIp = null;
42
+ this._url = `${this.config.baseUrl}/apis/envs/sandbox/v1`;
43
+ this._onLog = options.onLog || null;
44
+
45
+ // Create axios instance with interceptors for HTTP logging
46
+ this._axios = axios.create();
47
+ this._setupInterceptors();
48
+ }
49
+
50
+ _log(level, message) {
51
+ if (this._onLog) {
52
+ this._onLog(level, message);
53
+ } else {
54
+ logger[level](message);
55
+ }
56
+ }
57
+
58
+ _setupInterceptors() {
59
+ this._axios.interceptors.request.use((config) => {
60
+ config._startTime = Date.now();
61
+ httpLogger.logRequest(
62
+ config.method?.toUpperCase() || 'GET',
63
+ config.url,
64
+ config.data
65
+ );
66
+ return config;
67
+ });
68
+
69
+ this._axios.interceptors.response.use(
70
+ (response) => {
71
+ const duration = Date.now() - (response.config._startTime || Date.now());
72
+ const traceId = this._extractTraceId(response.headers);
73
+ httpLogger.logResponse(
74
+ response.config.method?.toUpperCase() || 'GET',
75
+ response.config.url,
76
+ response.status,
77
+ duration,
78
+ response.data,
79
+ traceId
80
+ );
81
+ return response;
82
+ },
83
+ (error) => {
84
+ const duration = Date.now() - (error.config?._startTime || Date.now());
85
+ const traceId = error.response ? this._extractTraceId(error.response.headers) : null;
86
+ httpLogger.logError(
87
+ error.config?.method?.toUpperCase() || 'GET',
88
+ error.config?.url || 'unknown',
89
+ error,
90
+ duration,
91
+ traceId
92
+ );
93
+ return Promise.reject(error);
94
+ }
95
+ );
96
+ }
97
+
98
+ _extractTraceId(headers) {
99
+ if (!headers) return null;
100
+ const traceIdHeaders = ['x-trace-id', 'x-request-id', 'traceid', 'trace-id', 'request-id'];
101
+ for (const headerName of traceIdHeaders) {
102
+ const value = headers[headerName];
103
+ if (value) return value;
104
+ }
105
+ return null;
106
+ }
107
+
108
+ get sandboxId() {
109
+ return this._sandboxId;
110
+ }
111
+
112
+ get hostName() {
113
+ return this._hostName;
114
+ }
115
+
116
+ get hostIp() {
117
+ return this._hostIp;
118
+ }
119
+
120
+ _buildHeaders() {
121
+ const headers = {
122
+ 'Content-Type': 'application/json',
123
+ };
124
+
125
+ if (this.config.xrlAuthorization) {
126
+ headers['XRL-Authorization'] = `Bearer ${this.config.xrlAuthorization}`;
127
+ }
128
+
129
+ if (this.config.extraHeaders) {
130
+ Object.assign(headers, this.config.extraHeaders);
131
+ }
132
+
133
+ return headers;
134
+ }
135
+
136
+ /**
137
+ * Start a new sandbox instance
138
+ */
139
+ async start() {
140
+ const url = `${this._url}/start_async`;
141
+ const headers = this._buildHeaders();
142
+ const data = {
143
+ image: this.config.image,
144
+ auto_clear_time: Math.floor(this.config.autoClearSeconds / 60),
145
+ auto_clear_time_minutes: Math.floor(this.config.autoClearSeconds / 60),
146
+ startup_timeout: this.config.startupTimeout,
147
+ memory: this.config.memory,
148
+ cpus: this.config.cpus,
149
+ };
150
+
151
+ logger.debug(`[Sandbox SDK] POST ${url}`);
152
+ const response = await this._axios.post(url, data, { headers });
153
+
154
+ if (response.data.status !== 'Success') {
155
+ const error = response.data.result || response.data;
156
+ throw new Error(`Failed to start sandbox: ${JSON.stringify(error)}`);
157
+ }
158
+
159
+ const result = response.data.result;
160
+ this._sandboxId = result.sandbox_id;
161
+ this._hostName = result.host_name;
162
+ this._hostIp = result.host_ip;
163
+
164
+ let isAlive = false;
165
+ if (this.config.waitForAlive) {
166
+ await this._waitForAlive();
167
+ isAlive = true;
168
+ }
169
+
170
+ return {
171
+ sandboxId: this._sandboxId,
172
+ hostName: this._hostName,
173
+ hostIp: this._hostIp,
174
+ isAlive: isAlive,
175
+ };
176
+ }
177
+
178
+ async _waitForAlive() {
179
+ const startTime = Date.now();
180
+ const timeout = this.config.startupTimeout * 1000;
181
+
182
+ this._log('info', 'Waiting for sandbox to be ready...');
183
+
184
+ while (Date.now() - startTime < timeout) {
185
+ const status = await this.getStatus();
186
+
187
+ if (status.is_alive) {
188
+ this._log('info', `Sandbox is ready (ID: ${this._sandboxId})`);
189
+ return;
190
+ }
191
+
192
+ await new Promise(resolve => setTimeout(resolve, 3000));
193
+ }
194
+
195
+ throw new Error(`Sandbox did not become alive within ${this.config.startupTimeout}s`);
196
+ }
197
+
198
+ /**
199
+ * Get sandbox status
200
+ */
201
+ async getStatus() {
202
+ const url = `${this._url}/get_status?sandbox_id=${this._sandboxId}`;
203
+ const headers = this._buildHeaders();
204
+
205
+ const response = await this._axios.get(url, { headers });
206
+
207
+ if (response.data.status !== 'Success') {
208
+ throw new Error(`Failed to get status: ${JSON.stringify(response.data)}`);
209
+ }
210
+
211
+ return response.data.result;
212
+ }
213
+
214
+ /**
215
+ * Check if sandbox is alive
216
+ */
217
+ async isAlive() {
218
+ try {
219
+ const status = await this.getStatus();
220
+ return {
221
+ isAlive: status.is_alive,
222
+ message: status.host_name || '',
223
+ };
224
+ } catch (error) {
225
+ throw new Error(`Failed to check isAlive: ${error.message}`);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Execute a command in the sandbox
231
+ */
232
+ async execute(command) {
233
+ const url = `${this._url}/execute`;
234
+ const headers = this._buildHeaders();
235
+
236
+ let commandArray;
237
+ if (Array.isArray(command)) {
238
+ commandArray = command;
239
+ } else {
240
+ commandArray = ['sh', '-c', command];
241
+ }
242
+
243
+ const data = {
244
+ command: commandArray,
245
+ sandbox_id: this._sandboxId,
246
+ };
247
+
248
+ const response = await this._axios.post(url, data, { headers });
249
+
250
+ if (response.data.status !== 'Success') {
251
+ throw new Error(`Failed to execute command: ${JSON.stringify(response.data)}`);
252
+ }
253
+
254
+ return response.data.result;
255
+ }
256
+
257
+ /**
258
+ * Stop the sandbox
259
+ */
260
+ async stop() {
261
+ if (!this._sandboxId) {
262
+ logger.warn('No sandbox ID to stop');
263
+ return;
264
+ }
265
+
266
+ try {
267
+ const url = `${this._url}/stop`;
268
+ const headers = this._buildHeaders();
269
+ const data = { sandbox_id: this._sandboxId };
270
+
271
+ await this._axios.post(url, data, { headers });
272
+ logger.info(`Sandbox ${this._sandboxId} stopped successfully`);
273
+ } catch (error) {
274
+ logger.warn(`Failed to stop sandbox: ${error.message}`);
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Create a bash session
280
+ */
281
+ async createSession(options = {}) {
282
+ const url = `${this._url}/create_session`;
283
+ const headers = this._buildHeaders();
284
+
285
+ const data = {
286
+ sandbox_id: this._sandboxId,
287
+ session: options.session || 'default',
288
+ session_type: options.sessionType || 'bash',
289
+ startup_source: options.startupSource || [],
290
+ env_enable: options.envEnable || false,
291
+ };
292
+
293
+ const response = await this._axios.post(url, data, { headers });
294
+
295
+ if (response.data.status !== 'Success') {
296
+ throw new Error(`Failed to create session: ${JSON.stringify(response.data)}`);
297
+ }
298
+
299
+ return response.data.result;
300
+ }
301
+
302
+ /**
303
+ * Run a command in a session
304
+ */
305
+ async runInSession(action) {
306
+ const url = `${this._url}/run_in_session`;
307
+ const headers = this._buildHeaders();
308
+
309
+ const data = {
310
+ sandbox_id: this._sandboxId,
311
+ command: action.command,
312
+ session: action.session || 'default',
313
+ timeout: action.timeout || null,
314
+ is_interactive_command: action.isInteractiveCommand || false,
315
+ check: action.check || 'raise',
316
+ };
317
+
318
+ const axiosConfig = { headers };
319
+
320
+ if (action.axiosTimeout) {
321
+ axiosConfig.timeout = action.axiosTimeout;
322
+ } else if (action.timeout) {
323
+ axiosConfig.timeout = action.timeout * 1000;
324
+ }
325
+
326
+ if (action.signal) {
327
+ axiosConfig.signal = action.signal;
328
+ }
329
+
330
+ const response = await this._axios.post(url, data, axiosConfig);
331
+
332
+ if (response.data.status !== 'Success') {
333
+ throw new Error(`Failed to run in session: ${JSON.stringify(response.data)}`);
334
+ }
335
+
336
+ return response.data.result;
337
+ }
338
+
339
+ /**
340
+ * Close a session
341
+ */
342
+ async closeSession(session = 'default') {
343
+ const url = `${this._url}/close_session`;
344
+ const headers = this._buildHeaders();
345
+
346
+ const data = {
347
+ sandbox_id: this._sandboxId,
348
+ session: session,
349
+ };
350
+
351
+ const response = await this._axios.post(url, data, { headers });
352
+
353
+ if (response.data.status !== 'Success') {
354
+ throw new Error(`Failed to close session: ${JSON.stringify(response.data)}`);
355
+ }
356
+
357
+ return response.data.result;
358
+ }
359
+
360
+ /**
361
+ * Upload a file to the sandbox
362
+ */
363
+ async uploadFile(localPath, targetPath) {
364
+ const fs = require('fs');
365
+ const path = require('path');
366
+
367
+ try {
368
+ if (!fs.existsSync(localPath)) {
369
+ return { success: false, message: `File not found: ${localPath}` };
370
+ }
371
+
372
+ const url = `${this._url}/upload`;
373
+ const headers = this._buildHeaders();
374
+ delete headers['Content-Type'];
375
+
376
+ const form = new FormData();
377
+ const fileBuffer = fs.readFileSync(localPath);
378
+ const blob = new Blob([fileBuffer], { type: 'application/octet-stream' });
379
+
380
+ form.append('file', blob, path.basename(localPath));
381
+ form.append('target_path', targetPath);
382
+ form.append('sandbox_id', this._sandboxId);
383
+ form.append('container_name', this._sandboxId);
384
+
385
+ const response = await fetch(url, {
386
+ method: 'POST',
387
+ headers,
388
+ body: form,
389
+ });
390
+
391
+ const data = await response.json();
392
+
393
+ if (data.status !== 'Success') {
394
+ return { success: false, message: `Failed to upload file: ${JSON.stringify(data)}` };
395
+ }
396
+
397
+ return { success: true, message: `Successfully uploaded ${path.basename(localPath)} to ${targetPath}` };
398
+ } catch (error) {
399
+ return { success: false, message: `Upload failed: ${error.message}` };
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Download a file from the sandbox
405
+ */
406
+ async downloadFile(filePath) {
407
+ const url = `${this._url}/read_file`;
408
+ const headers = this._buildHeaders();
409
+
410
+ const data = {
411
+ path: filePath,
412
+ sandbox_id: this._sandboxId,
413
+ };
414
+
415
+ const response = await this._axios.post(url, data, { headers });
416
+
417
+ if (response.data.status !== 'Success') {
418
+ throw new Error(`Failed to download file: ${JSON.stringify(response.data)}`);
419
+ }
420
+
421
+ return response.data.result;
422
+ }
423
+
424
+ /**
425
+ * Write content to a file in the sandbox
426
+ */
427
+ async writeFile(content, path) {
428
+ const url = `${this._url}/write_file`;
429
+ const headers = this._buildHeaders();
430
+
431
+ const data = {
432
+ content,
433
+ path,
434
+ sandbox_id: this._sandboxId,
435
+ };
436
+
437
+ const response = await this._axios.post(url, data, { headers });
438
+
439
+ if (response.data.status !== 'Success') {
440
+ return { success: false, message: `Failed to write file ${path}` };
441
+ }
442
+
443
+ return { success: true, message: `Successfully wrote content to file ${path}` };
444
+ }
445
+
446
+ /**
447
+ * Read a file from the sandbox
448
+ */
449
+ async readFile(filePath, startLine, endLine) {
450
+ if (startLine < 1 || endLine < startLine) {
451
+ throw new Error(`Invalid line range: startLine=${startLine}, endLine=${endLine}`);
452
+ }
453
+
454
+ const command = `sed -n ${startLine},${endLine}p ${filePath}`;
455
+ const result = await this.execute(command);
456
+
457
+ if (result.exit_code !== 0) {
458
+ throw new Error(`Failed to read file ${filePath}: ${result.stderr}`);
459
+ }
460
+
461
+ return { content: result.stdout };
462
+ }
463
+
464
+ /**
465
+ * Close the sandbox
466
+ */
467
+ async close() {
468
+ await this.stop();
469
+ }
470
+ }
471
+
472
+ module.exports = { SandboxClient };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Sandbox SDK Configuration
3
+ * 开源版配置 - 默认 localhost:8080,支持环境变量配置
4
+ */
5
+
6
+ // 开源版默认镜像(不放在配置文件中,直接定义常量)
7
+ const DEFAULT_IMAGE = 'python:3.11';
8
+
9
+ /**
10
+ * Sandbox configuration class
11
+ * 开源版配置类 - 不包含环境特定的默认值
12
+ */
13
+ class SandboxConfig {
14
+ constructor(options = {}) {
15
+ // Technical defaults (not environment-specific)
16
+ this.autoClearSeconds = options.autoClearSeconds || 300; // 5 minutes
17
+ this.startupTimeout = options.startupTimeout || 120; // 2 minutes
18
+ this.memory = options.memory || '8g';
19
+ this.cpus = options.cpus !== undefined ? options.cpus : 2.0;
20
+
21
+ // Environment-specific settings (no defaults, must be provided)
22
+ this.baseUrl = options.baseUrl || process.env.ROCK_BASE_URL || 'http://localhost:8080';
23
+ this.xrlAuthorization = options.xrlAuthorization || process.env.ROCK_API_KEY;
24
+ this.image = options.image;
25
+ this.cluster = options.cluster;
26
+ this.namespace = options.namespace || null;
27
+ this.userId = options.userId || null;
28
+ this.experimentId = options.experimentId || null;
29
+ this.extraHeaders = options.extraHeaders || {};
30
+ this.waitForAlive = options.waitForAlive !== undefined ? options.waitForAlive : false;
31
+ }
32
+
33
+ validate(options = {}) {
34
+ const { requireImage = true } = options;
35
+
36
+ if (!this.baseUrl) {
37
+ throw new Error('baseUrl is required');
38
+ }
39
+ if (requireImage && !this.image) {
40
+ throw new Error('image is required');
41
+ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Get sandbox configuration for opensource version
47
+ * 开源版配置获取函数 - 返回开源版默认值
48
+ * @returns {Object} Sandbox config with default values
49
+ */
50
+ function getOpenSourceSandboxConfig() {
51
+ return {
52
+ baseUrl: process.env.ROCK_BASE_URL || 'http://localhost:8080',
53
+ image: DEFAULT_IMAGE,
54
+ };
55
+ }
56
+
57
+ module.exports = { SandboxConfig, getOpenSourceSandboxConfig, DEFAULT_IMAGE };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Sandbox SDK
3
+ * 开源版 Sandbox SDK 入口
4
+ */
5
+
6
+ const { SandboxConfig, getOpenSourceSandboxConfig } = require('./config');
7
+ const { SandboxClient } = require('./client');
8
+
9
+ module.exports = {
10
+ SandboxConfig,
11
+ SandboxClient,
12
+ getOpenSourceSandboxConfig,
13
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Sandbox SDK Response Types
3
+ */
4
+
5
+ module.exports = {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 工具函数导出
3
+ */
4
+
5
+ const { parseTime } = require('./time');
6
+
7
+ module.exports = {
8
+ parseTime,
9
+ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * 日志工具模块
3
+ * 基于 winston 实现,支持通过 verbose 级别控制日志输出
4
+ */
5
+
6
+ const winston = require('winston');
7
+
8
+ // 日志级别枚举
9
+ const LogLevel = {
10
+ ERROR: 0,
11
+ WARNING: 1,
12
+ INFO: 2,
13
+ DEBUG: 3
14
+ };
15
+
16
+ // 默认 verbose 级别 (0 = 只输出 error)
17
+ let verboseLevel = 0;
18
+
19
+ // TUI 模式标志:当启用时,抑制所有日志输出以避免干扰 TUI 渲染
20
+ let tuiModeEnabled = false;
21
+
22
+ // 创建 winston logger 实例
23
+ const logger = winston.createLogger({
24
+ levels: {
25
+ error: 0,
26
+ warn: 1,
27
+ info: 2,
28
+ debug: 3
29
+ },
30
+ transports: [
31
+ new winston.transports.Console({
32
+ stderrLevels: ['error', 'warn'],
33
+ consoleWarnLevels: ['warn'],
34
+ format: winston.format.combine(
35
+ winston.format.printf(({ level, message, ...meta }) => {
36
+ const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : '';
37
+ return `${message}${metaStr}`;
38
+ })
39
+ )
40
+ })
41
+ ]
42
+ });
43
+
44
+ // 初始设置为 info 级别
45
+ logger.level = 'info';
46
+
47
+ function setVerboseLevel(level) {
48
+ verboseLevel = Math.min(Math.max(level, 0), 3);
49
+ const levelMap = ['info', 'info', 'debug', 'debug'];
50
+ logger.level = levelMap[verboseLevel];
51
+ }
52
+
53
+ function getVerboseLevel() {
54
+ return verboseLevel;
55
+ }
56
+
57
+ function shouldLog(level) {
58
+ return level <= verboseLevel;
59
+ }
60
+
61
+ function setTUIMode(enabled) {
62
+ tuiModeEnabled = !!enabled;
63
+ }
64
+
65
+ function getTUIMode() {
66
+ return tuiModeEnabled;
67
+ }
68
+
69
+ function error(...args) {
70
+ if (tuiModeEnabled) return;
71
+ logger.error(args.join(' '));
72
+ }
73
+
74
+ function warn(...args) {
75
+ if (tuiModeEnabled) return;
76
+ logger.warn(args.join(' '));
77
+ }
78
+
79
+ function info(...args) {
80
+ if (tuiModeEnabled) return;
81
+ logger.info(args.join(' '));
82
+ }
83
+
84
+ function debug(...args) {
85
+ if (tuiModeEnabled) return;
86
+ logger.debug(args.join(' '));
87
+ }
88
+
89
+ function parseVerboseLevel(argv) {
90
+ const vCount = argv.v || 0;
91
+ return Math.min(vCount, 3);
92
+ }
93
+
94
+ module.exports = {
95
+ LogLevel,
96
+ setVerboseLevel,
97
+ getVerboseLevel,
98
+ shouldLog,
99
+ error,
100
+ warn,
101
+ info,
102
+ debug,
103
+ parseVerboseLevel,
104
+ setTUIMode,
105
+ getTUIMode
106
+ };