@fastgpt-sdk/sandbox-adapter 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.
- package/README.md +163 -0
- package/dist/adapters/BaseSandboxAdapter.d.ts +59 -0
- package/dist/adapters/FastGPTSandboxAdapter/index.d.ts +85 -0
- package/dist/adapters/MinimalProviderAdapter.d.ts +52 -0
- package/dist/adapters/OpenSandboxAdapter.d.ts +229 -0
- package/dist/adapters/index.d.ts +26 -0
- package/dist/errors/CommandExecutionError.d.ts +16 -0
- package/dist/errors/ConnectionError.d.ts +8 -0
- package/dist/errors/FeatureNotSupportedError.d.ts +10 -0
- package/dist/errors/FileOperationError.d.ts +13 -0
- package/dist/errors/SandboxException.d.ts +17 -0
- package/dist/errors/SandboxStateError.d.ts +9 -0
- package/dist/errors/TimeoutError.d.ts +15 -0
- package/dist/errors/index.d.ts +7 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1225 -0
- package/dist/interfaces/ICommandExecution.d.ts +41 -0
- package/dist/interfaces/IFileSystem.d.ts +99 -0
- package/dist/interfaces/IHealthCheck.d.ts +23 -0
- package/dist/interfaces/ISandbox.d.ts +23 -0
- package/dist/interfaces/ISandboxLifecycle.d.ts +54 -0
- package/dist/interfaces/index.d.ts +5 -0
- package/dist/polyfill/CommandPolyfillService.d.ts +122 -0
- package/dist/types/execution.d.ts +61 -0
- package/dist/types/filesystem.d.ts +106 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/sandbox.d.ts +92 -0
- package/dist/utils/base64.d.ts +20 -0
- package/dist/utils/streams.d.ts +26 -0
- package/package.json +87 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1225 @@
|
|
|
1
|
+
// src/adapters/FastGPTSandboxAdapter/index.ts
|
|
2
|
+
import {
|
|
3
|
+
createSDK
|
|
4
|
+
} from "@fastgpt-sdk/sandbox-server";
|
|
5
|
+
|
|
6
|
+
// src/errors/SandboxException.ts
|
|
7
|
+
class SandboxException extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(message, code = "INTERNAL_UNKNOWN_ERROR", cause) {
|
|
10
|
+
super(message, { cause });
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = "SandboxException";
|
|
13
|
+
Object.setPrototypeOf(this, SandboxException.prototype);
|
|
14
|
+
}
|
|
15
|
+
toJSON() {
|
|
16
|
+
return {
|
|
17
|
+
name: this.name,
|
|
18
|
+
message: this.message,
|
|
19
|
+
code: this.code,
|
|
20
|
+
cause: this.cause,
|
|
21
|
+
stack: this.stack
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/errors/CommandExecutionError.ts
|
|
27
|
+
class CommandExecutionError extends SandboxException {
|
|
28
|
+
command;
|
|
29
|
+
exitCode;
|
|
30
|
+
stdout;
|
|
31
|
+
stderr;
|
|
32
|
+
commandError;
|
|
33
|
+
constructor(message, command, exitCodeOrCause, stdout, stderr) {
|
|
34
|
+
super(message, "COMMAND_FAILED", exitCodeOrCause instanceof Error ? exitCodeOrCause : undefined);
|
|
35
|
+
this.command = command;
|
|
36
|
+
this.name = "CommandExecutionError";
|
|
37
|
+
Object.setPrototypeOf(this, CommandExecutionError.prototype);
|
|
38
|
+
if (exitCodeOrCause instanceof Error) {
|
|
39
|
+
this.commandError = exitCodeOrCause;
|
|
40
|
+
} else {
|
|
41
|
+
this.exitCode = exitCodeOrCause;
|
|
42
|
+
this.stdout = stdout;
|
|
43
|
+
this.stderr = stderr;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
getCombinedOutput() {
|
|
47
|
+
let output = this.stdout || "";
|
|
48
|
+
if (this.stderr) {
|
|
49
|
+
output += output ? `
|
|
50
|
+
${this.stderr}` : this.stderr;
|
|
51
|
+
}
|
|
52
|
+
return output;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// src/errors/ConnectionError.ts
|
|
56
|
+
class ConnectionError extends SandboxException {
|
|
57
|
+
endpoint;
|
|
58
|
+
constructor(message, endpoint, cause) {
|
|
59
|
+
super(message, "CONNECTION_ERROR", cause);
|
|
60
|
+
this.endpoint = endpoint;
|
|
61
|
+
this.name = "ConnectionError";
|
|
62
|
+
Object.setPrototypeOf(this, ConnectionError.prototype);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// src/errors/FeatureNotSupportedError.ts
|
|
66
|
+
class FeatureNotSupportedError extends SandboxException {
|
|
67
|
+
feature;
|
|
68
|
+
provider;
|
|
69
|
+
constructor(message, feature, provider) {
|
|
70
|
+
super(`Feature not supported by ${provider}: ${message}`, "FEATURE_NOT_SUPPORTED");
|
|
71
|
+
this.feature = feature;
|
|
72
|
+
this.provider = provider;
|
|
73
|
+
this.name = "FeatureNotSupportedError";
|
|
74
|
+
Object.setPrototypeOf(this, FeatureNotSupportedError.prototype);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// src/errors/FileOperationError.ts
|
|
78
|
+
class FileOperationError extends SandboxException {
|
|
79
|
+
path;
|
|
80
|
+
fileErrorCode;
|
|
81
|
+
constructor(message, path, fileErrorCode, cause) {
|
|
82
|
+
super(message, fileErrorCode, cause);
|
|
83
|
+
this.path = path;
|
|
84
|
+
this.fileErrorCode = fileErrorCode;
|
|
85
|
+
this.name = "FileOperationError";
|
|
86
|
+
Object.setPrototypeOf(this, FileOperationError.prototype);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// src/errors/SandboxStateError.ts
|
|
90
|
+
class SandboxStateError extends SandboxException {
|
|
91
|
+
currentState;
|
|
92
|
+
requiredState;
|
|
93
|
+
constructor(message, currentState, requiredState) {
|
|
94
|
+
super(`Invalid sandbox state: ${message} (current: ${currentState}${requiredState ? `, required: ${requiredState}` : ""})`, "INVALID_STATE");
|
|
95
|
+
this.currentState = currentState;
|
|
96
|
+
this.requiredState = requiredState;
|
|
97
|
+
this.name = "SandboxStateError";
|
|
98
|
+
Object.setPrototypeOf(this, SandboxStateError.prototype);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// src/errors/TimeoutError.ts
|
|
102
|
+
class TimeoutError extends SandboxException {
|
|
103
|
+
timeoutMs;
|
|
104
|
+
operation;
|
|
105
|
+
constructor(message, timeoutMs, operation) {
|
|
106
|
+
super(message, "TIMEOUT");
|
|
107
|
+
this.timeoutMs = timeoutMs;
|
|
108
|
+
this.operation = operation;
|
|
109
|
+
this.name = "TimeoutError";
|
|
110
|
+
Object.setPrototypeOf(this, TimeoutError.prototype);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class SandboxReadyTimeoutError extends SandboxException {
|
|
115
|
+
constructor(sandboxId, timeoutMs) {
|
|
116
|
+
super(`Sandbox ${sandboxId} did not become ready within ${timeoutMs}ms`, "READY_TIMEOUT");
|
|
117
|
+
this.name = "SandboxReadyTimeoutError";
|
|
118
|
+
Object.setPrototypeOf(this, SandboxReadyTimeoutError.prototype);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// src/utils/base64.ts
|
|
122
|
+
function bytesToBase64(bytes) {
|
|
123
|
+
const binary = Array.from(bytes).map((b) => String.fromCharCode(b)).join("");
|
|
124
|
+
return btoa(binary);
|
|
125
|
+
}
|
|
126
|
+
function base64ToBytes(base64) {
|
|
127
|
+
const binary = atob(base64.trim());
|
|
128
|
+
const bytes = new Uint8Array(binary.length);
|
|
129
|
+
for (let i = 0;i < binary.length; i++) {
|
|
130
|
+
bytes[i] = binary.charCodeAt(i);
|
|
131
|
+
}
|
|
132
|
+
return bytes;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/polyfill/CommandPolyfillService.ts
|
|
136
|
+
class CommandPolyfillService {
|
|
137
|
+
executor;
|
|
138
|
+
constructor(executor) {
|
|
139
|
+
this.executor = executor;
|
|
140
|
+
}
|
|
141
|
+
async readFile(path) {
|
|
142
|
+
try {
|
|
143
|
+
const result = await this.executor.execute(`cat "${this.escapePath(path)}" | base64 -w 0`);
|
|
144
|
+
if (result.exitCode !== 0) {
|
|
145
|
+
throw this.createFileError(path, result.stderr);
|
|
146
|
+
}
|
|
147
|
+
return base64ToBytes(result.stdout);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error instanceof FileOperationError) {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
if (error instanceof CommandExecutionError) {
|
|
153
|
+
throw this.createFileError(path, error.stderr || "");
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async readFileRange(path, start, end) {
|
|
159
|
+
const length = end ? end - start : "";
|
|
160
|
+
const cmd = `dd if="${this.escapePath(path)}" bs=1 skip=${start} count=${length} 2>/dev/null | base64 -w 0`;
|
|
161
|
+
const result = await this.executor.execute(cmd);
|
|
162
|
+
if (result.exitCode !== 0) {
|
|
163
|
+
throw this.createFileError(path, result.stderr);
|
|
164
|
+
}
|
|
165
|
+
return base64ToBytes(result.stdout);
|
|
166
|
+
}
|
|
167
|
+
async writeFile(path, data) {
|
|
168
|
+
const base64 = bytesToBase64(data);
|
|
169
|
+
const chunkSize = 1024;
|
|
170
|
+
await this.createParentDirectory(path);
|
|
171
|
+
let first = true;
|
|
172
|
+
for (let i = 0;i < base64.length; i += chunkSize) {
|
|
173
|
+
const chunk = base64.slice(i, i + chunkSize);
|
|
174
|
+
const redirect = first ? ">" : ">>";
|
|
175
|
+
const result = await this.executor.execute(`echo "${chunk}" | base64 -d ${redirect} "${this.escapePath(path)}"`);
|
|
176
|
+
if (result.exitCode !== 0) {
|
|
177
|
+
throw this.createFileError(path, result.stderr);
|
|
178
|
+
}
|
|
179
|
+
first = false;
|
|
180
|
+
}
|
|
181
|
+
return data.length;
|
|
182
|
+
}
|
|
183
|
+
async writeTextFile(path, content) {
|
|
184
|
+
await this.createParentDirectory(path);
|
|
185
|
+
const escapedContent = content.replace(/\\/g, "\\\\").replace(/\$/g, "\\$");
|
|
186
|
+
const result = await this.executor.execute(`cat > "${this.escapePath(path)}" << 'POLYFILL_EOF'
|
|
187
|
+
${escapedContent}
|
|
188
|
+
POLYFILL_EOF`);
|
|
189
|
+
if (result.exitCode !== 0) {
|
|
190
|
+
throw this.createFileError(path, result.stderr);
|
|
191
|
+
}
|
|
192
|
+
return content.length;
|
|
193
|
+
}
|
|
194
|
+
async deleteFiles(paths) {
|
|
195
|
+
const results = [];
|
|
196
|
+
for (const path of paths) {
|
|
197
|
+
try {
|
|
198
|
+
const result = await this.executor.execute(`rm -f "${this.escapePath(path)}"`);
|
|
199
|
+
results.push({
|
|
200
|
+
path,
|
|
201
|
+
success: result.exitCode === 0,
|
|
202
|
+
error: result.exitCode !== 0 ? new Error(result.stderr) : undefined
|
|
203
|
+
});
|
|
204
|
+
} catch (error) {
|
|
205
|
+
results.push({
|
|
206
|
+
path,
|
|
207
|
+
success: false,
|
|
208
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return results;
|
|
213
|
+
}
|
|
214
|
+
async createDirectories(paths, options) {
|
|
215
|
+
for (const path of paths) {
|
|
216
|
+
const result = await this.executor.execute(`mkdir -p "${this.escapePath(path)}"`);
|
|
217
|
+
if (result.exitCode !== 0) {
|
|
218
|
+
throw new FileOperationError(`Failed to create directory: ${result.stderr}`, path, "PATH_NOT_DIRECTORY");
|
|
219
|
+
}
|
|
220
|
+
if (options?.mode) {
|
|
221
|
+
await this.executor.execute(`chmod ${options.mode.toString(8)} "${this.escapePath(path)}"`);
|
|
222
|
+
}
|
|
223
|
+
if (options?.owner || options?.group) {
|
|
224
|
+
const owner = options.owner || "";
|
|
225
|
+
const group = options.group ? `:${options.group}` : "";
|
|
226
|
+
await this.executor.execute(`chown ${owner}${group} "${this.escapePath(path)}" 2>/dev/null || true`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async deleteDirectories(paths, options) {
|
|
231
|
+
const flagParts = [];
|
|
232
|
+
if (options?.recursive !== false)
|
|
233
|
+
flagParts.push("r");
|
|
234
|
+
if (options?.force !== false)
|
|
235
|
+
flagParts.push("f");
|
|
236
|
+
const flags = flagParts.length > 0 ? `-${flagParts.join("")}` : "";
|
|
237
|
+
for (const path of paths) {
|
|
238
|
+
const result = await this.executor.execute(`rm ${flags} "${this.escapePath(path)}"`.trim());
|
|
239
|
+
if (result.exitCode !== 0) {
|
|
240
|
+
throw new FileOperationError(`Failed to delete directory: ${result.stderr}`, path, "PATH_IS_DIRECTORY");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async listDirectory(path) {
|
|
245
|
+
const result = await this.executor.execute(`ls -la "${this.escapePath(path)}" --time-style=+"%Y-%m-%dT%H:%M:%S" 2>/dev/null || echo "DIRECTORY_NOT_FOUND"`);
|
|
246
|
+
if (result.stdout.includes("DIRECTORY_NOT_FOUND")) {
|
|
247
|
+
throw new FileOperationError("Directory not found", path, "FILE_NOT_FOUND");
|
|
248
|
+
}
|
|
249
|
+
return this.parseLsOutput(result.stdout, path);
|
|
250
|
+
}
|
|
251
|
+
async createParentDirectory(filePath) {
|
|
252
|
+
const lastSlash = filePath.lastIndexOf("/");
|
|
253
|
+
if (lastSlash > 0) {
|
|
254
|
+
const parentDir = filePath.slice(0, lastSlash);
|
|
255
|
+
await this.executor.execute(`mkdir -p "${this.escapePath(parentDir)}"`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async getFileInfo(paths) {
|
|
259
|
+
const infoMap = new Map;
|
|
260
|
+
for (const path of paths) {
|
|
261
|
+
try {
|
|
262
|
+
const result = await this.executor.execute(`stat -c '%s|%Y|%W|%a|%U|%G|%F' "${this.escapePath(path)}" 2>/dev/null || echo "STAT_FAILED"`);
|
|
263
|
+
if (result.stdout.includes("STAT_FAILED")) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const parts = result.stdout.trim().split("|");
|
|
267
|
+
if (parts.length >= 7) {
|
|
268
|
+
infoMap.set(path, {
|
|
269
|
+
path,
|
|
270
|
+
size: Number.parseInt(parts[0], 10) || undefined,
|
|
271
|
+
modifiedAt: parts[1] ? new Date(Number.parseInt(parts[1], 10) * 1000) : undefined,
|
|
272
|
+
createdAt: parts[2] ? new Date(Number.parseInt(parts[2], 10) * 1000) : undefined,
|
|
273
|
+
mode: Number.parseInt(parts[3], 8) || undefined,
|
|
274
|
+
owner: parts[4] || undefined,
|
|
275
|
+
group: parts[5] || undefined,
|
|
276
|
+
isDirectory: parts[6].includes("directory"),
|
|
277
|
+
isFile: parts[6].includes("regular file"),
|
|
278
|
+
isSymlink: parts[6].includes("symbolic link")
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
} catch {}
|
|
282
|
+
}
|
|
283
|
+
return infoMap;
|
|
284
|
+
}
|
|
285
|
+
async setPermissions(entries) {
|
|
286
|
+
for (const entry of entries) {
|
|
287
|
+
if (entry.mode !== undefined) {
|
|
288
|
+
await this.executor.execute(`chmod ${entry.mode.toString(8)} "${this.escapePath(entry.path)}"`);
|
|
289
|
+
}
|
|
290
|
+
if (entry.owner || entry.group) {
|
|
291
|
+
const owner = entry.owner || "";
|
|
292
|
+
const group = entry.group ? `:${entry.group}` : "";
|
|
293
|
+
await this.executor.execute(`chown ${owner}${group} "${this.escapePath(entry.path)}" 2>/dev/null || true`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async search(pattern, path = ".") {
|
|
298
|
+
const escapedPattern = pattern.replace(/'/g, `'"'"'`);
|
|
299
|
+
const escapedPath = this.escapePath(path);
|
|
300
|
+
let result = await this.executor.execute(`find "${escapedPath}" -name '${escapedPattern}' -print 2>/dev/null || echo "FIND_FAILED"`);
|
|
301
|
+
if (!result.stdout.includes("FIND_FAILED")) {
|
|
302
|
+
return result.stdout.split(`
|
|
303
|
+
`).filter((p) => p.trim()).map((p) => ({ path: p }));
|
|
304
|
+
}
|
|
305
|
+
result = await this.executor.execute(`ls -R "${escapedPath}" 2>/dev/null | grep -E "${escapedPattern}" || true`);
|
|
306
|
+
return result.stdout.split(`
|
|
307
|
+
`).filter((p) => p.trim()).map((p) => ({ path: `${path}/${p}` }));
|
|
308
|
+
}
|
|
309
|
+
async moveFiles(entries) {
|
|
310
|
+
for (const { source, destination } of entries) {
|
|
311
|
+
const result = await this.executor.execute(`mv "${this.escapePath(source)}" "${this.escapePath(destination)}"`);
|
|
312
|
+
if (result.exitCode !== 0) {
|
|
313
|
+
throw new FileOperationError(`Failed to move file: ${result.stderr}`, source, "TRANSFER_ERROR");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async replaceContent(entries) {
|
|
318
|
+
for (const { path, oldContent, newContent } of entries) {
|
|
319
|
+
const escapedOld = oldContent.replace(/\\/g, "\\\\\\\\").replace(/\//g, "\\/").replace(/&/g, "\\&");
|
|
320
|
+
const escapedNew = newContent.replace(/\\/g, "\\\\\\\\").replace(/\//g, "\\/").replace(/&/g, "\\&");
|
|
321
|
+
const result = await this.executor.execute(`sed -i 's/${escapedOld}/${escapedNew}/g' "${this.escapePath(path)}"`);
|
|
322
|
+
if (result.exitCode !== 0) {
|
|
323
|
+
throw new FileOperationError(`Failed to replace content: ${result.stderr}`, path, "INVALID_PATH");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async ping() {
|
|
328
|
+
try {
|
|
329
|
+
const result = await this.executor.execute('echo "PING"');
|
|
330
|
+
return result.exitCode === 0 && result.stdout.includes("PING");
|
|
331
|
+
} catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async getMetrics() {
|
|
336
|
+
const timestamp = Date.now();
|
|
337
|
+
const cpuResult = await this.executor.execute('nproc 2>/dev/null || echo "1"');
|
|
338
|
+
const cpuCount = Number.parseInt(cpuResult.stdout.trim(), 10) || 1;
|
|
339
|
+
const memResult = await this.executor.execute('cat /proc/meminfo 2>/dev/null || echo "FAILED"');
|
|
340
|
+
let memoryTotalMiB = 0;
|
|
341
|
+
let memoryUsedMiB = 0;
|
|
342
|
+
if (!memResult.stdout.includes("FAILED")) {
|
|
343
|
+
const totalMatch = memResult.stdout.match(/MemTotal:\s+(\d+)\s+kB/);
|
|
344
|
+
const availableMatch = memResult.stdout.match(/MemAvailable:\s+(\d+)\s+kB/);
|
|
345
|
+
if (totalMatch) {
|
|
346
|
+
memoryTotalMiB = Math.floor(Number.parseInt(totalMatch[1], 10) / 1024);
|
|
347
|
+
}
|
|
348
|
+
if (totalMatch && availableMatch) {
|
|
349
|
+
const total = Number.parseInt(totalMatch[1], 10);
|
|
350
|
+
const available = Number.parseInt(availableMatch[1], 10);
|
|
351
|
+
memoryUsedMiB = Math.floor((total - available) / 1024);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const cpuUsedPercentage = 0;
|
|
355
|
+
return {
|
|
356
|
+
cpuCount,
|
|
357
|
+
cpuUsedPercentage,
|
|
358
|
+
memoryTotalMiB,
|
|
359
|
+
memoryUsedMiB,
|
|
360
|
+
timestamp
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
escapePath(path) {
|
|
364
|
+
return path.replace(/"/g, "\\\"");
|
|
365
|
+
}
|
|
366
|
+
parseLsOutput(output, basePath) {
|
|
367
|
+
const lines = output.split(`
|
|
368
|
+
`);
|
|
369
|
+
const entries = [];
|
|
370
|
+
for (const line of lines) {
|
|
371
|
+
if (line.startsWith("total") || !line.trim()) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const match = line.match(/^([-dl])([-rwxsStT]{9})\s+\d+\s+\S+\s+\S+\s+(\d+)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\s+(.+)$/);
|
|
375
|
+
if (match) {
|
|
376
|
+
const [, type, , size, dateStr, name] = match;
|
|
377
|
+
if (name === "." || name === "..") {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
const isDirectory = type === "d";
|
|
381
|
+
const isSymlink = type === "l";
|
|
382
|
+
entries.push({
|
|
383
|
+
name,
|
|
384
|
+
path: `${basePath}/${name}`,
|
|
385
|
+
isDirectory: isDirectory || isSymlink,
|
|
386
|
+
isFile: type === "-",
|
|
387
|
+
size: Number.parseInt(size, 10) || undefined,
|
|
388
|
+
modifiedAt: new Date(dateStr)
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return entries;
|
|
393
|
+
}
|
|
394
|
+
createFileError(path, stderr) {
|
|
395
|
+
const lowerStderr = stderr.toLowerCase();
|
|
396
|
+
if (lowerStderr.includes("no such file") || lowerStderr.includes("does not exist")) {
|
|
397
|
+
return new FileOperationError(stderr, path, "FILE_NOT_FOUND");
|
|
398
|
+
}
|
|
399
|
+
if (lowerStderr.includes("permission denied")) {
|
|
400
|
+
return new FileOperationError(stderr, path, "PERMISSION_DENIED");
|
|
401
|
+
}
|
|
402
|
+
if (lowerStderr.includes("is a directory")) {
|
|
403
|
+
return new FileOperationError(stderr, path, "PATH_IS_DIRECTORY");
|
|
404
|
+
}
|
|
405
|
+
if (lowerStderr.includes("not a directory")) {
|
|
406
|
+
return new FileOperationError(stderr, path, "PATH_NOT_DIRECTORY");
|
|
407
|
+
}
|
|
408
|
+
return new FileOperationError(stderr, path, "TRANSFER_ERROR");
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/adapters/BaseSandboxAdapter.ts
|
|
413
|
+
class BaseSandboxAdapter {
|
|
414
|
+
_status = { state: "Creating" };
|
|
415
|
+
polyfillService;
|
|
416
|
+
constructor() {
|
|
417
|
+
this.polyfillService = new CommandPolyfillService(this);
|
|
418
|
+
}
|
|
419
|
+
get status() {
|
|
420
|
+
return this._status;
|
|
421
|
+
}
|
|
422
|
+
async waitUntilReady(timeoutMs = 120000) {
|
|
423
|
+
const startTime = Date.now();
|
|
424
|
+
const checkInterval = 1000;
|
|
425
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
426
|
+
const isReady = await this.ping();
|
|
427
|
+
if (isReady) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
await this.sleep(checkInterval);
|
|
431
|
+
}
|
|
432
|
+
throw new SandboxReadyTimeoutError(this.id, timeoutMs);
|
|
433
|
+
}
|
|
434
|
+
async renewExpiration(_additionalSeconds) {
|
|
435
|
+
throw new FeatureNotSupportedError("Sandbox expiration renewal not supported by this provider", "renewExpiration", this.provider);
|
|
436
|
+
}
|
|
437
|
+
async executeStream(command, handlers, options) {
|
|
438
|
+
const result = await this.execute(command, options);
|
|
439
|
+
if (handlers.onStdout && result.stdout) {
|
|
440
|
+
await handlers.onStdout({ text: result.stdout });
|
|
441
|
+
}
|
|
442
|
+
if (handlers.onStderr && result.stderr) {
|
|
443
|
+
await handlers.onStderr({ text: result.stderr });
|
|
444
|
+
}
|
|
445
|
+
if (handlers.onComplete) {
|
|
446
|
+
await handlers.onComplete(result);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async executeBackground(_command, _options) {
|
|
450
|
+
throw new FeatureNotSupportedError("Background execution not supported by this provider", "executeBackground", this.provider);
|
|
451
|
+
}
|
|
452
|
+
async interrupt(_sessionId) {
|
|
453
|
+
throw new FeatureNotSupportedError("Command interruption not supported by this provider", "interrupt", this.provider);
|
|
454
|
+
}
|
|
455
|
+
async readFiles(paths, options) {
|
|
456
|
+
const polyfillService = this.requirePolyfillService("readFiles", "File read not supported by this provider");
|
|
457
|
+
const results = [];
|
|
458
|
+
for (const path of paths) {
|
|
459
|
+
try {
|
|
460
|
+
let content;
|
|
461
|
+
if (options?.range) {
|
|
462
|
+
const [startValue, endValue] = options.range.split("-");
|
|
463
|
+
const start = Number.parseInt(startValue, 10);
|
|
464
|
+
const end = endValue ? Number.parseInt(endValue, 10) : undefined;
|
|
465
|
+
if (Number.isNaN(start) || endValue && Number.isNaN(end)) {
|
|
466
|
+
throw new Error(`Invalid range: ${options.range}`);
|
|
467
|
+
}
|
|
468
|
+
content = await polyfillService.readFileRange(path, start, end);
|
|
469
|
+
} else {
|
|
470
|
+
content = await polyfillService.readFile(path);
|
|
471
|
+
}
|
|
472
|
+
results.push({ path, content, error: null });
|
|
473
|
+
} catch (error) {
|
|
474
|
+
results.push({
|
|
475
|
+
path,
|
|
476
|
+
content: new Uint8Array,
|
|
477
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return results;
|
|
482
|
+
}
|
|
483
|
+
async writeFiles(entries) {
|
|
484
|
+
const polyfillService = this.requirePolyfillService("writeFiles", "File write not supported by this provider");
|
|
485
|
+
const results = [];
|
|
486
|
+
for (const entry of entries) {
|
|
487
|
+
try {
|
|
488
|
+
let bytesWritten;
|
|
489
|
+
if (typeof entry.data === "string") {
|
|
490
|
+
bytesWritten = await polyfillService.writeTextFile(entry.path, entry.data);
|
|
491
|
+
} else if (entry.data instanceof Uint8Array) {
|
|
492
|
+
bytesWritten = await polyfillService.writeFile(entry.path, entry.data);
|
|
493
|
+
} else if (entry.data instanceof ArrayBuffer) {
|
|
494
|
+
bytesWritten = await polyfillService.writeFile(entry.path, new Uint8Array(entry.data));
|
|
495
|
+
} else if (entry.data instanceof Blob) {
|
|
496
|
+
const arrayBuffer = await entry.data.arrayBuffer();
|
|
497
|
+
bytesWritten = await polyfillService.writeFile(entry.path, new Uint8Array(arrayBuffer));
|
|
498
|
+
} else {
|
|
499
|
+
const chunks = [];
|
|
500
|
+
const reader = entry.data.getReader();
|
|
501
|
+
while (true) {
|
|
502
|
+
const { done, value } = await reader.read();
|
|
503
|
+
if (done) {
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
chunks.push(value);
|
|
507
|
+
}
|
|
508
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
509
|
+
const combined = new Uint8Array(totalLength);
|
|
510
|
+
let offset = 0;
|
|
511
|
+
for (const chunk of chunks) {
|
|
512
|
+
combined.set(chunk, offset);
|
|
513
|
+
offset += chunk.length;
|
|
514
|
+
}
|
|
515
|
+
bytesWritten = await polyfillService.writeFile(entry.path, combined);
|
|
516
|
+
}
|
|
517
|
+
results.push({ path: entry.path, bytesWritten, error: null });
|
|
518
|
+
} catch (error) {
|
|
519
|
+
results.push({
|
|
520
|
+
path: entry.path,
|
|
521
|
+
bytesWritten: 0,
|
|
522
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return results;
|
|
527
|
+
}
|
|
528
|
+
async deleteFiles(paths) {
|
|
529
|
+
const polyfillService = this.requirePolyfillService("deleteFiles", "File delete not supported by this provider");
|
|
530
|
+
const polyfillResults = await polyfillService.deleteFiles(paths);
|
|
531
|
+
return polyfillResults.map((r) => ({
|
|
532
|
+
path: r.path,
|
|
533
|
+
success: r.success,
|
|
534
|
+
error: r.error || null
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
async moveFiles(entries) {
|
|
538
|
+
const polyfillService = this.requirePolyfillService("moveFiles", "File move not supported by this provider");
|
|
539
|
+
await polyfillService.moveFiles(entries.map((e) => ({ source: e.source, destination: e.destination })));
|
|
540
|
+
}
|
|
541
|
+
async replaceContent(entries) {
|
|
542
|
+
const polyfillService = this.requirePolyfillService("replaceContent", "Content replace not supported by this provider");
|
|
543
|
+
await polyfillService.replaceContent(entries);
|
|
544
|
+
}
|
|
545
|
+
async createDirectories(paths, options) {
|
|
546
|
+
const polyfillService = this.requirePolyfillService("createDirectories", "Directory creation not supported by this provider");
|
|
547
|
+
await polyfillService.createDirectories(paths, options);
|
|
548
|
+
}
|
|
549
|
+
async deleteDirectories(paths, options) {
|
|
550
|
+
const polyfillService = this.requirePolyfillService("deleteDirectories", "Directory deletion not supported by this provider");
|
|
551
|
+
await polyfillService.deleteDirectories(paths, options);
|
|
552
|
+
}
|
|
553
|
+
async listDirectory(path) {
|
|
554
|
+
const polyfillService = this.requirePolyfillService("listDirectory", "Directory listing not supported by this provider");
|
|
555
|
+
return polyfillService.listDirectory(path);
|
|
556
|
+
}
|
|
557
|
+
async* readFileStream(path) {
|
|
558
|
+
this.requirePolyfillService("readFileStream", "File stream read not supported by this provider");
|
|
559
|
+
const readChunk = async (range) => {
|
|
560
|
+
const results = await this.readFiles([path], range ? { range } : undefined);
|
|
561
|
+
const fileResult = results[0];
|
|
562
|
+
if (!fileResult) {
|
|
563
|
+
throw new Error("No file result returned");
|
|
564
|
+
}
|
|
565
|
+
if (fileResult.error) {
|
|
566
|
+
throw fileResult.error;
|
|
567
|
+
}
|
|
568
|
+
return fileResult.content;
|
|
569
|
+
};
|
|
570
|
+
let size;
|
|
571
|
+
try {
|
|
572
|
+
const infoMap = await this.getFileInfo([path]);
|
|
573
|
+
size = infoMap.get(path)?.size;
|
|
574
|
+
} catch {}
|
|
575
|
+
if (typeof size !== "number") {
|
|
576
|
+
yield await readChunk();
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const chunkSize = 64 * 1024;
|
|
580
|
+
for (let offset = 0;offset < size; offset += chunkSize) {
|
|
581
|
+
const end = Math.min(offset + chunkSize, size);
|
|
582
|
+
const content = await readChunk(`${offset}-${end}`);
|
|
583
|
+
yield content;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async writeFileStream(path, stream) {
|
|
587
|
+
const reader = stream.getReader();
|
|
588
|
+
const chunks = [];
|
|
589
|
+
while (true) {
|
|
590
|
+
const { done, value } = await reader.read();
|
|
591
|
+
if (done) {
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
chunks.push(value);
|
|
595
|
+
}
|
|
596
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
597
|
+
const combined = new Uint8Array(totalLength);
|
|
598
|
+
let offset = 0;
|
|
599
|
+
for (const chunk of chunks) {
|
|
600
|
+
combined.set(chunk, offset);
|
|
601
|
+
offset += chunk.length;
|
|
602
|
+
}
|
|
603
|
+
await this.writeFiles([{ path, data: combined }]);
|
|
604
|
+
}
|
|
605
|
+
async getFileInfo(paths) {
|
|
606
|
+
const polyfillService = this.requirePolyfillService("getFileInfo", "File info not supported by this provider");
|
|
607
|
+
return polyfillService.getFileInfo(paths);
|
|
608
|
+
}
|
|
609
|
+
async setPermissions(entries) {
|
|
610
|
+
const polyfillService = this.requirePolyfillService("setPermissions", "Permission setting not supported by this provider");
|
|
611
|
+
await polyfillService.setPermissions(entries);
|
|
612
|
+
}
|
|
613
|
+
async search(pattern, path) {
|
|
614
|
+
const polyfillService = this.requirePolyfillService("search", "File search not supported by this provider");
|
|
615
|
+
return polyfillService.search(pattern, path);
|
|
616
|
+
}
|
|
617
|
+
async ping() {
|
|
618
|
+
const polyfillService = this.requirePolyfillService("ping", "Health check not supported by this provider");
|
|
619
|
+
return polyfillService.ping();
|
|
620
|
+
}
|
|
621
|
+
async getMetrics() {
|
|
622
|
+
const polyfillService = this.requirePolyfillService("getMetrics", "Metrics not supported by this provider");
|
|
623
|
+
return polyfillService.getMetrics();
|
|
624
|
+
}
|
|
625
|
+
sleep(ms) {
|
|
626
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
627
|
+
}
|
|
628
|
+
requirePolyfillService(feature, message) {
|
|
629
|
+
if (!this.polyfillService) {
|
|
630
|
+
throw new FeatureNotSupportedError(message, feature, this.provider);
|
|
631
|
+
}
|
|
632
|
+
return this.polyfillService;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/adapters/FastGPTSandboxAdapter/index.ts
|
|
637
|
+
function mapContainerStatus(state) {
|
|
638
|
+
switch (state) {
|
|
639
|
+
case "Running":
|
|
640
|
+
return { state: "Running" };
|
|
641
|
+
case "Creating":
|
|
642
|
+
return { state: "Creating" };
|
|
643
|
+
case "Paused":
|
|
644
|
+
return { state: "Paused" };
|
|
645
|
+
case "Error":
|
|
646
|
+
return { state: "Error" };
|
|
647
|
+
case "Unknown":
|
|
648
|
+
default:
|
|
649
|
+
return { state: "Error", reason: "Unknown state" };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
class FastGPTSandboxAdapter extends BaseSandboxAdapter {
|
|
654
|
+
config;
|
|
655
|
+
provider = "fastgpt";
|
|
656
|
+
sdk;
|
|
657
|
+
_id = "";
|
|
658
|
+
constructor(config) {
|
|
659
|
+
super();
|
|
660
|
+
this.config = config;
|
|
661
|
+
this.sdk = createSDK(config.baseUrl, config.token);
|
|
662
|
+
this._id = config.containerName;
|
|
663
|
+
}
|
|
664
|
+
get id() {
|
|
665
|
+
return this._id;
|
|
666
|
+
}
|
|
667
|
+
async getInfo() {
|
|
668
|
+
try {
|
|
669
|
+
const info = await this.sdk.container.get(this._id);
|
|
670
|
+
if (!info) {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
this._status = mapContainerStatus(info.status.state);
|
|
674
|
+
return {
|
|
675
|
+
id: info.name,
|
|
676
|
+
image: {
|
|
677
|
+
repository: info.image.imageName
|
|
678
|
+
},
|
|
679
|
+
entrypoint: [],
|
|
680
|
+
status: mapContainerStatus(info.status.state),
|
|
681
|
+
createdAt: info.createdAt ? new Date(info.createdAt) : new Date
|
|
682
|
+
};
|
|
683
|
+
} catch (error) {
|
|
684
|
+
throw new CommandExecutionError("Failed to get sandbox info", "getInfo", error instanceof Error ? error : undefined);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
async create(_config) {
|
|
688
|
+
try {
|
|
689
|
+
const exists = await this.getInfo();
|
|
690
|
+
if (exists) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
this._status = { state: "Creating" };
|
|
694
|
+
await this.sdk.container.create({ name: this._id });
|
|
695
|
+
await this.waitUntilReady();
|
|
696
|
+
this._status = { state: "Running" };
|
|
697
|
+
} catch (error) {
|
|
698
|
+
throw new ConnectionError("Failed to create sandbox", this.config.baseUrl, error);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async start() {
|
|
702
|
+
try {
|
|
703
|
+
await this.sdk.container.start(this._id);
|
|
704
|
+
this._status = { state: "Running" };
|
|
705
|
+
} catch (error) {
|
|
706
|
+
throw new CommandExecutionError("Failed to start sandbox", "start", error instanceof Error ? error : undefined);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async stop() {
|
|
710
|
+
try {
|
|
711
|
+
await this.sdk.container.pause(this._id);
|
|
712
|
+
this._status = { state: "Paused" };
|
|
713
|
+
} catch (error) {
|
|
714
|
+
throw new CommandExecutionError("Failed to stop sandbox", "stop", error instanceof Error ? error : undefined);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async pause() {
|
|
718
|
+
try {
|
|
719
|
+
await this.sdk.container.pause(this._id);
|
|
720
|
+
this._status = { state: "Paused" };
|
|
721
|
+
} catch (error) {
|
|
722
|
+
throw new CommandExecutionError("Failed to pause sandbox", "pause", error instanceof Error ? error : undefined);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async resume() {
|
|
726
|
+
try {
|
|
727
|
+
await this.sdk.container.start(this._id);
|
|
728
|
+
this._status = { state: "Running" };
|
|
729
|
+
} catch (error) {
|
|
730
|
+
throw new CommandExecutionError("Failed to resume sandbox", "resume", error instanceof Error ? error : undefined);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async delete() {
|
|
734
|
+
try {
|
|
735
|
+
await this.sdk.container.delete(this._id);
|
|
736
|
+
this._status = { state: "Deleted" };
|
|
737
|
+
} catch (error) {
|
|
738
|
+
throw new CommandExecutionError("Failed to delete sandbox", "delete", error instanceof Error ? error : undefined);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async close() {
|
|
742
|
+
return this.delete();
|
|
743
|
+
}
|
|
744
|
+
async execute(command, options) {
|
|
745
|
+
try {
|
|
746
|
+
await this.waitUntilReady();
|
|
747
|
+
const response = await this.sdk.sandbox.exec(this._id, {
|
|
748
|
+
command,
|
|
749
|
+
cwd: options?.workingDirectory
|
|
750
|
+
});
|
|
751
|
+
return {
|
|
752
|
+
stdout: response.stdout,
|
|
753
|
+
stderr: response.stderr,
|
|
754
|
+
exitCode: response.exitCode
|
|
755
|
+
};
|
|
756
|
+
} catch (error) {
|
|
757
|
+
throw new CommandExecutionError(`Command execution failed: ${command}`, command, error instanceof Error ? error : undefined);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
async ping() {
|
|
761
|
+
try {
|
|
762
|
+
return await this.sdk.sandbox.health(this._id);
|
|
763
|
+
} catch {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/adapters/MinimalProviderAdapter.ts
|
|
770
|
+
class MinimalProviderAdapter extends BaseSandboxAdapter {
|
|
771
|
+
config;
|
|
772
|
+
provider = "minimal";
|
|
773
|
+
_id = "";
|
|
774
|
+
connection;
|
|
775
|
+
constructor(config) {
|
|
776
|
+
super();
|
|
777
|
+
this.config = config;
|
|
778
|
+
}
|
|
779
|
+
get id() {
|
|
780
|
+
return this._id;
|
|
781
|
+
}
|
|
782
|
+
get status() {
|
|
783
|
+
return this._status;
|
|
784
|
+
}
|
|
785
|
+
async create(config) {
|
|
786
|
+
if (this.config?.connectionFactory) {
|
|
787
|
+
this.connection = await this.config.connectionFactory();
|
|
788
|
+
this._id = this.connection.id;
|
|
789
|
+
this._status = { state: "Running" };
|
|
790
|
+
if (config.entrypoint && config.entrypoint.length > 0) {
|
|
791
|
+
await this.execute(config.entrypoint.join(" "));
|
|
792
|
+
}
|
|
793
|
+
} else {
|
|
794
|
+
throw new Error("Connection factory not provided");
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async connect(connection) {
|
|
798
|
+
this.connection = connection;
|
|
799
|
+
this._id = connection.id;
|
|
800
|
+
this._status = await connection.getStatus();
|
|
801
|
+
}
|
|
802
|
+
async start() {
|
|
803
|
+
this._status = { state: "Running" };
|
|
804
|
+
}
|
|
805
|
+
async stop() {
|
|
806
|
+
await this.execute("exit 0").catch(() => {});
|
|
807
|
+
this._status = { state: "Deleted" };
|
|
808
|
+
}
|
|
809
|
+
async pause() {
|
|
810
|
+
throw new FeatureNotSupportedError("Pause not supported by minimal provider", "pause", this.provider);
|
|
811
|
+
}
|
|
812
|
+
async resume() {
|
|
813
|
+
throw new FeatureNotSupportedError("Resume not supported by minimal provider", "resume", this.provider);
|
|
814
|
+
}
|
|
815
|
+
async delete() {
|
|
816
|
+
await this.stop();
|
|
817
|
+
await this.connection?.close();
|
|
818
|
+
}
|
|
819
|
+
async getInfo() {
|
|
820
|
+
return {
|
|
821
|
+
id: this._id,
|
|
822
|
+
image: { repository: "minimal", tag: "latest" },
|
|
823
|
+
entrypoint: [],
|
|
824
|
+
status: this._status,
|
|
825
|
+
createdAt: new Date
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
async close() {
|
|
829
|
+
await this.connection?.close();
|
|
830
|
+
}
|
|
831
|
+
async execute(command, options) {
|
|
832
|
+
if (!this.connection) {
|
|
833
|
+
throw new Error("Not connected to minimal provider");
|
|
834
|
+
}
|
|
835
|
+
let finalCommand = command;
|
|
836
|
+
if (options?.workingDirectory) {
|
|
837
|
+
finalCommand = `cd "${options.workingDirectory}" && ${command}`;
|
|
838
|
+
}
|
|
839
|
+
if (options?.timeoutMs && options.timeoutMs > 0) {
|
|
840
|
+
const timeoutSec = Math.ceil(options.timeoutMs / 1000);
|
|
841
|
+
finalCommand = `timeout ${timeoutSec} sh -c '${finalCommand.replace(/'/g, `'"'"'`)}'`;
|
|
842
|
+
}
|
|
843
|
+
if (options?.env && Object.keys(options.env).length > 0) {
|
|
844
|
+
const envVars = Object.entries(options.env).map(([k, v]) => `${k}="${v.replace(/"/g, '"')}"`).join(" ");
|
|
845
|
+
finalCommand = `export ${envVars} && ${finalCommand}`;
|
|
846
|
+
}
|
|
847
|
+
const result = await this.connection.execute(finalCommand);
|
|
848
|
+
return {
|
|
849
|
+
stdout: result.stdout,
|
|
850
|
+
stderr: result.stderr,
|
|
851
|
+
exitCode: result.exitCode
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/adapters/OpenSandboxAdapter.ts
|
|
857
|
+
import { ConnectionConfig, Sandbox } from "@alibaba-group/opensandbox";
|
|
858
|
+
class OpenSandboxAdapter extends BaseSandboxAdapter {
|
|
859
|
+
connectionConfig;
|
|
860
|
+
provider = "opensandbox";
|
|
861
|
+
runtime;
|
|
862
|
+
_sandbox;
|
|
863
|
+
_connection;
|
|
864
|
+
_id = "";
|
|
865
|
+
_connectionState = "disconnected";
|
|
866
|
+
constructor(connectionConfig = {}) {
|
|
867
|
+
super();
|
|
868
|
+
this.connectionConfig = connectionConfig;
|
|
869
|
+
this.runtime = connectionConfig.runtime ?? "docker";
|
|
870
|
+
this._connection = this.createConnectionConfig();
|
|
871
|
+
}
|
|
872
|
+
get id() {
|
|
873
|
+
return this._id;
|
|
874
|
+
}
|
|
875
|
+
get connectionState() {
|
|
876
|
+
return this._connectionState;
|
|
877
|
+
}
|
|
878
|
+
get sandbox() {
|
|
879
|
+
if (!this._sandbox) {
|
|
880
|
+
throw new SandboxStateError("Sandbox not initialized. Call create() or connect() first.", this._connectionState, "connected");
|
|
881
|
+
}
|
|
882
|
+
return this._sandbox;
|
|
883
|
+
}
|
|
884
|
+
createConnectionConfig() {
|
|
885
|
+
const { baseUrl, apiKey } = this.connectionConfig;
|
|
886
|
+
if (!baseUrl) {
|
|
887
|
+
return new ConnectionConfig({ apiKey });
|
|
888
|
+
}
|
|
889
|
+
return new ConnectionConfig({
|
|
890
|
+
domain: baseUrl,
|
|
891
|
+
apiKey
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
convertImageSpec(image) {
|
|
895
|
+
const parts = [image.repository];
|
|
896
|
+
if (image.tag) {
|
|
897
|
+
parts.push(":", image.tag);
|
|
898
|
+
}
|
|
899
|
+
if (image.digest) {
|
|
900
|
+
parts.push("@", image.digest);
|
|
901
|
+
}
|
|
902
|
+
return parts.join("");
|
|
903
|
+
}
|
|
904
|
+
parseImageSpec(image) {
|
|
905
|
+
const atIndex = image.indexOf("@");
|
|
906
|
+
if (atIndex > -1) {
|
|
907
|
+
const repository = image.slice(0, atIndex);
|
|
908
|
+
const digest = image.slice(atIndex + 1);
|
|
909
|
+
return { repository, digest };
|
|
910
|
+
}
|
|
911
|
+
const colonIndex = image.indexOf(":");
|
|
912
|
+
if (colonIndex > -1) {
|
|
913
|
+
const repository = image.slice(0, colonIndex);
|
|
914
|
+
const tag = image.slice(colonIndex + 1);
|
|
915
|
+
return { repository, tag };
|
|
916
|
+
}
|
|
917
|
+
return { repository: image };
|
|
918
|
+
}
|
|
919
|
+
convertResourceLimits(resourceLimits) {
|
|
920
|
+
if (!resourceLimits) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const result = {};
|
|
924
|
+
if (resourceLimits.cpuCount !== undefined) {
|
|
925
|
+
result.cpu = resourceLimits.cpuCount.toString();
|
|
926
|
+
}
|
|
927
|
+
if (resourceLimits.memoryMiB !== undefined) {
|
|
928
|
+
result.memory = `${resourceLimits.memoryMiB}Mi`;
|
|
929
|
+
}
|
|
930
|
+
if (resourceLimits.diskGiB !== undefined) {
|
|
931
|
+
result.disk = `${resourceLimits.diskGiB}Gi`;
|
|
932
|
+
}
|
|
933
|
+
return result;
|
|
934
|
+
}
|
|
935
|
+
parseResourceLimits(resource) {
|
|
936
|
+
if (!resource) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const result = {};
|
|
940
|
+
const cpu = resource.cpu;
|
|
941
|
+
if (cpu) {
|
|
942
|
+
const cpuCount = Number.parseInt(cpu, 10);
|
|
943
|
+
if (!Number.isNaN(cpuCount)) {
|
|
944
|
+
result.cpuCount = cpuCount;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const memory = resource.memory;
|
|
948
|
+
if (memory) {
|
|
949
|
+
const match = memory.match(/^(\d+)(Mi|Gi)$/);
|
|
950
|
+
if (match) {
|
|
951
|
+
const value = Number.parseInt(match[1] || "0", 10);
|
|
952
|
+
if (match[2] === "Mi") {
|
|
953
|
+
result.memoryMiB = value;
|
|
954
|
+
} else {
|
|
955
|
+
result.memoryMiB = value * 1024;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const disk = resource.disk;
|
|
960
|
+
if (disk) {
|
|
961
|
+
const match = disk.match(/^(\d+)Gi$/);
|
|
962
|
+
if (match) {
|
|
963
|
+
const value = Number.parseInt(match[1] || "0", 10);
|
|
964
|
+
result.diskGiB = value;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return result;
|
|
968
|
+
}
|
|
969
|
+
async create(config) {
|
|
970
|
+
this._connectionState = "connecting";
|
|
971
|
+
try {
|
|
972
|
+
const image = this.convertImageSpec(config.image);
|
|
973
|
+
const resource = this.convertResourceLimits(config.resourceLimits);
|
|
974
|
+
this._sandbox = await Sandbox.create({
|
|
975
|
+
connectionConfig: this._connection,
|
|
976
|
+
image,
|
|
977
|
+
entrypoint: config.entrypoint,
|
|
978
|
+
timeoutSeconds: config.timeout,
|
|
979
|
+
resource,
|
|
980
|
+
env: config.env,
|
|
981
|
+
metadata: config.metadata
|
|
982
|
+
});
|
|
983
|
+
this._id = this._sandbox.id;
|
|
984
|
+
this._status = { state: "Running" };
|
|
985
|
+
this._connectionState = "connected";
|
|
986
|
+
} catch (error) {
|
|
987
|
+
this._connectionState = "disconnected";
|
|
988
|
+
throw new ConnectionError("Failed to create sandbox", this.connectionConfig.baseUrl, error);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
async connect(sandboxId) {
|
|
992
|
+
this._connectionState = "connecting";
|
|
993
|
+
try {
|
|
994
|
+
this._sandbox = await Sandbox.connect({
|
|
995
|
+
sandboxId,
|
|
996
|
+
connectionConfig: this._connection
|
|
997
|
+
});
|
|
998
|
+
this._id = this._sandbox.id;
|
|
999
|
+
this._status = { state: "Running" };
|
|
1000
|
+
this._connectionState = "connected";
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
this._connectionState = "disconnected";
|
|
1003
|
+
throw new ConnectionError(`Failed to connect to sandbox ${sandboxId}`, this.connectionConfig.baseUrl, error);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
async start() {
|
|
1007
|
+
if (this._status.state === "Paused") {
|
|
1008
|
+
await this.resume();
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
async stop() {
|
|
1012
|
+
try {
|
|
1013
|
+
await this.sandbox.kill();
|
|
1014
|
+
this._status = { state: "Deleted" };
|
|
1015
|
+
this._connectionState = "disconnected";
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
throw new CommandExecutionError("Failed to stop sandbox", "stop", error instanceof Error ? error : undefined);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async pause() {
|
|
1021
|
+
try {
|
|
1022
|
+
await this.sandbox.pause();
|
|
1023
|
+
this._status = { state: "Paused" };
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
if (error && typeof error === "object" && "code" in error && error.code === "SANDBOX::API_NOT_SUPPORTED") {
|
|
1026
|
+
throw new FeatureNotSupportedError("Pause operation is not supported by this runtime (e.g., Kubernetes)", "pause", this.provider);
|
|
1027
|
+
}
|
|
1028
|
+
throw new CommandExecutionError("Failed to pause sandbox", "pause", error instanceof Error ? error : undefined);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
async resume() {
|
|
1032
|
+
try {
|
|
1033
|
+
this._sandbox = await this.sandbox.resume();
|
|
1034
|
+
this._id = this.sandbox.id;
|
|
1035
|
+
this._status = { state: "Running" };
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
if (error && typeof error === "object" && "code" in error && error.code === "SANDBOX::API_NOT_SUPPORTED") {
|
|
1038
|
+
throw new FeatureNotSupportedError("Resume operation is not supported by this runtime (e.g., Kubernetes)", "resume", this.provider);
|
|
1039
|
+
}
|
|
1040
|
+
throw new CommandExecutionError("Failed to resume sandbox", "resume", error instanceof Error ? error : undefined);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
async delete() {
|
|
1044
|
+
try {
|
|
1045
|
+
await this.sandbox.kill();
|
|
1046
|
+
this._status = { state: "Deleted" };
|
|
1047
|
+
this._connectionState = "disconnected";
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
throw new CommandExecutionError("Failed to delete sandbox", "delete", error instanceof Error ? error : undefined);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
async getInfo() {
|
|
1053
|
+
try {
|
|
1054
|
+
const info = await this.sandbox.getInfo();
|
|
1055
|
+
return {
|
|
1056
|
+
id: info.id,
|
|
1057
|
+
image: typeof info.image === "string" ? this.parseImageSpec(info.image) : ("uri" in info.image) ? this.parseImageSpec(info.image.uri) : info.image,
|
|
1058
|
+
entrypoint: info.entrypoint,
|
|
1059
|
+
metadata: info.metadata,
|
|
1060
|
+
status: info.status,
|
|
1061
|
+
createdAt: info.createdAt,
|
|
1062
|
+
expiresAt: info.expiresAt,
|
|
1063
|
+
resourceLimits: this.parseResourceLimits(info.resourceLimits)
|
|
1064
|
+
};
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
throw new CommandExecutionError("Failed to get sandbox info", "getInfo", error instanceof Error ? error : undefined);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
async close() {
|
|
1070
|
+
try {
|
|
1071
|
+
await this._sandbox?.close();
|
|
1072
|
+
} finally {
|
|
1073
|
+
this._sandbox = undefined;
|
|
1074
|
+
this._id = "";
|
|
1075
|
+
this._connectionState = "closed";
|
|
1076
|
+
this._status = { state: "Deleted" };
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async renewExpiration(additionalSeconds) {
|
|
1080
|
+
try {
|
|
1081
|
+
await this.sandbox.renew(additionalSeconds);
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
throw new CommandExecutionError("Failed to renew sandbox expiration", "renew", error instanceof Error ? error : undefined);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
async execute(command, options) {
|
|
1087
|
+
try {
|
|
1088
|
+
const execution = await this.sandbox.commands.run(command, {
|
|
1089
|
+
workingDirectory: options?.workingDirectory,
|
|
1090
|
+
background: options?.background
|
|
1091
|
+
});
|
|
1092
|
+
const stdout = execution.logs.stdout.map((msg) => msg.text).join(`
|
|
1093
|
+
`);
|
|
1094
|
+
const stderr = execution.logs.stderr.map((msg) => msg.text).join(`
|
|
1095
|
+
`);
|
|
1096
|
+
const exitCode = 0;
|
|
1097
|
+
const stdoutLength = execution.logs.stdout.reduce((sum, msg) => sum + msg.text.length, 0);
|
|
1098
|
+
const stderrLength = execution.logs.stderr.reduce((sum, msg) => sum + msg.text.length, 0);
|
|
1099
|
+
const MaxOutputSize = 1024 * 1024;
|
|
1100
|
+
const truncated = stdoutLength >= MaxOutputSize || stderrLength >= MaxOutputSize;
|
|
1101
|
+
return {
|
|
1102
|
+
stdout,
|
|
1103
|
+
stderr,
|
|
1104
|
+
exitCode,
|
|
1105
|
+
truncated
|
|
1106
|
+
};
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
if (error instanceof SandboxStateError) {
|
|
1109
|
+
throw error;
|
|
1110
|
+
}
|
|
1111
|
+
throw new CommandExecutionError(`Command execution failed: ${command}`, command, error instanceof Error ? error : undefined);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
async executeStream(command, handlers, options) {
|
|
1115
|
+
try {
|
|
1116
|
+
const sdkHandlers = {
|
|
1117
|
+
...handlers.onStderr ? { onStderr: handlers.onStderr } : {},
|
|
1118
|
+
...handlers.onStdout ? { onStdout: handlers.onStdout } : {},
|
|
1119
|
+
...handlers.onError ? {
|
|
1120
|
+
onError: async (err) => {
|
|
1121
|
+
const error = new Error(err.value || err.name || "Execution error");
|
|
1122
|
+
error.name = err.name || "ExecutionError";
|
|
1123
|
+
if (err.traceback?.length) {
|
|
1124
|
+
error.stack = err.traceback.join(`
|
|
1125
|
+
`);
|
|
1126
|
+
}
|
|
1127
|
+
await handlers.onError?.(error);
|
|
1128
|
+
}
|
|
1129
|
+
} : {}
|
|
1130
|
+
};
|
|
1131
|
+
const execution = await this.sandbox.commands.run(command, {
|
|
1132
|
+
workingDirectory: options?.workingDirectory,
|
|
1133
|
+
background: options?.background
|
|
1134
|
+
}, sdkHandlers);
|
|
1135
|
+
if (handlers.onComplete) {
|
|
1136
|
+
const stdout = execution.logs.stdout.map((msg) => msg.text).join(`
|
|
1137
|
+
`);
|
|
1138
|
+
const stderr = execution.logs.stderr.map((msg) => msg.text).join(`
|
|
1139
|
+
`);
|
|
1140
|
+
const exitCode = 0;
|
|
1141
|
+
const stdoutLength = execution.logs.stdout.reduce((sum, msg) => sum + msg.text.length, 0);
|
|
1142
|
+
const stderrLength = execution.logs.stderr.reduce((sum, msg) => sum + msg.text.length, 0);
|
|
1143
|
+
const MaxOutputSize = 1024 * 1024;
|
|
1144
|
+
const truncated = stdoutLength >= MaxOutputSize || stderrLength >= MaxOutputSize;
|
|
1145
|
+
await handlers.onComplete({ stdout, stderr, exitCode, truncated });
|
|
1146
|
+
}
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
throw new CommandExecutionError(`Streaming command execution failed: ${command}`, command, error instanceof Error ? error : undefined);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
async executeBackground(command, options) {
|
|
1152
|
+
try {
|
|
1153
|
+
const execution = await this.sandbox.commands.run(command, {
|
|
1154
|
+
workingDirectory: options?.workingDirectory,
|
|
1155
|
+
background: true
|
|
1156
|
+
});
|
|
1157
|
+
if (!execution.id) {
|
|
1158
|
+
throw new CommandExecutionError("Background execution did not return a session ID", command);
|
|
1159
|
+
}
|
|
1160
|
+
const sessionId = execution.id;
|
|
1161
|
+
const sandbox = this.sandbox;
|
|
1162
|
+
return {
|
|
1163
|
+
sessionId,
|
|
1164
|
+
kill: async () => {
|
|
1165
|
+
try {
|
|
1166
|
+
await sandbox.commands.interrupt(sessionId);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
throw new CommandExecutionError(`Failed to kill background session ${sessionId}`, "interrupt", error instanceof Error ? error : undefined);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
if (error instanceof CommandExecutionError) {
|
|
1174
|
+
throw error;
|
|
1175
|
+
}
|
|
1176
|
+
throw new CommandExecutionError(`Background command execution failed: ${command}`, command, error instanceof Error ? error : undefined);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
async interrupt(sessionId) {
|
|
1180
|
+
try {
|
|
1181
|
+
await this.sandbox.commands.interrupt(sessionId);
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
throw new CommandExecutionError(`Failed to interrupt session ${sessionId}`, "interrupt", error instanceof Error ? error : undefined);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
async ping() {
|
|
1187
|
+
try {
|
|
1188
|
+
return await this.sandbox.health.ping();
|
|
1189
|
+
} catch {
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
async getMetrics() {
|
|
1194
|
+
return this.sandbox.metrics.getMetrics();
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/adapters/index.ts
|
|
1199
|
+
var createSandbox = ({ provider, config }) => {
|
|
1200
|
+
switch (provider) {
|
|
1201
|
+
case "opensandbox":
|
|
1202
|
+
return new OpenSandboxAdapter(config);
|
|
1203
|
+
case "minimal":
|
|
1204
|
+
return new MinimalProviderAdapter(config);
|
|
1205
|
+
case "fastgpt":
|
|
1206
|
+
return new FastGPTSandboxAdapter(config);
|
|
1207
|
+
default:
|
|
1208
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
export {
|
|
1212
|
+
createSandbox,
|
|
1213
|
+
TimeoutError,
|
|
1214
|
+
SandboxStateError,
|
|
1215
|
+
SandboxReadyTimeoutError,
|
|
1216
|
+
SandboxException,
|
|
1217
|
+
OpenSandboxAdapter,
|
|
1218
|
+
MinimalProviderAdapter,
|
|
1219
|
+
FileOperationError,
|
|
1220
|
+
FeatureNotSupportedError,
|
|
1221
|
+
FastGPTSandboxAdapter,
|
|
1222
|
+
ConnectionError,
|
|
1223
|
+
CommandExecutionError,
|
|
1224
|
+
BaseSandboxAdapter
|
|
1225
|
+
};
|