@fastgpt-sdk/sandbox-adapter 0.0.35 → 0.0.36

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.
@@ -61,6 +61,12 @@ export declare abstract class BaseSandboxAdapter implements ISandbox {
61
61
  listDirectory(path: string): Promise<DirectoryEntry[]>;
62
62
  readFileStream(path: string): AsyncIterable<Uint8Array>;
63
63
  writeFileStream(path: string, stream: ReadableStream<Uint8Array>): Promise<void>;
64
+ /**
65
+ * Stream a ReadableStream into a file via a temporary file that is
66
+ * atomically renamed on success. If the stream fails mid-write, the
67
+ * original file is left untouched and the temp file is cleaned up.
68
+ */
69
+ private writeStreamToFile;
64
70
  getFileInfo(paths: string[]): Promise<Map<string, FileInfo>>;
65
71
  setPermissions(entries: PermissionEntry[]): Promise<void>;
66
72
  search(pattern: string, path?: string): Promise<SearchResult[]>;
package/dist/index.cjs CHANGED
@@ -45023,13 +45023,34 @@ class CommandPolyfillService {
45023
45023
  constructor(executor) {
45024
45024
  this.executor = executor;
45025
45025
  }
45026
+ static READ_CHUNK_SIZE = 256 * 1024;
45026
45027
  async readFile(path) {
45027
45028
  try {
45028
- const result = await this.executor.execute(`cat "${this.escapePath(path)}" | base64 -w 0`);
45029
- if (result.exitCode !== 0) {
45030
- throw this.createFileError(path, result.stderr);
45029
+ const size = await this.statSize(path);
45030
+ if (size === undefined) {
45031
+ const result = await this.executor.execute(`cat "${this.escapePath(path)}" | base64 -w 0`);
45032
+ if (result.exitCode !== 0) {
45033
+ throw this.createFileError(path, result.stderr);
45034
+ }
45035
+ return base64ToBytes(result.stdout);
45036
+ }
45037
+ if (size === 0)
45038
+ return new Uint8Array;
45039
+ const chunks = [];
45040
+ let totalLength = 0;
45041
+ for (let offset = 0;offset < size; offset += CommandPolyfillService.READ_CHUNK_SIZE) {
45042
+ const end = Math.min(offset + CommandPolyfillService.READ_CHUNK_SIZE, size);
45043
+ const chunk = await this.readFileRange(path, offset, end);
45044
+ chunks.push(chunk);
45045
+ totalLength += chunk.length;
45031
45046
  }
45032
- return base64ToBytes(result.stdout);
45047
+ const combined = new Uint8Array(totalLength);
45048
+ let writeOffset = 0;
45049
+ for (const chunk of chunks) {
45050
+ combined.set(chunk, writeOffset);
45051
+ writeOffset += chunk.length;
45052
+ }
45053
+ return combined;
45033
45054
  } catch (error) {
45034
45055
  if (error instanceof FileOperationError) {
45035
45056
  throw error;
@@ -45040,9 +45061,18 @@ class CommandPolyfillService {
45040
45061
  throw error;
45041
45062
  }
45042
45063
  }
45064
+ async statSize(path) {
45065
+ const result = await this.executor.execute(`stat -c '%s' "${this.escapePath(path)}" 2>/dev/null || stat -f '%z' "${this.escapePath(path)}" 2>/dev/null || echo STAT_FAILED`);
45066
+ const stdout = result.stdout.trim();
45067
+ if (!stdout || stdout.includes("STAT_FAILED"))
45068
+ return;
45069
+ const parsed = Number.parseInt(stdout, 10);
45070
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
45071
+ }
45043
45072
  async readFileRange(path, start, end) {
45044
- const length = end ? end - start : "";
45045
- const cmd = `dd if="${this.escapePath(path)}" bs=1 skip=${start} count=${length} 2>/dev/null | base64 -w 0`;
45073
+ const escaped = this.escapePath(path);
45074
+ const tailPos = start + 1;
45075
+ const cmd = end !== undefined ? `tail -c +${tailPos} "${escaped}" | head -c ${end - start} | base64 -w 0` : `tail -c +${tailPos} "${escaped}" | base64 -w 0`;
45046
45076
  const result = await this.executor.execute(cmd);
45047
45077
  if (result.exitCode !== 0) {
45048
45078
  throw this.createFileError(path, result.stderr);
@@ -45050,10 +45080,25 @@ class CommandPolyfillService {
45050
45080
  return base64ToBytes(result.stdout);
45051
45081
  }
45052
45082
  async writeFile(path, data) {
45083
+ await this.appendBytes(path, data, { truncate: true });
45084
+ return data.length;
45085
+ }
45086
+ async appendBytes(path, data, options) {
45087
+ if (options?.truncate) {
45088
+ await this.createParentDirectory(path);
45089
+ }
45090
+ if (data.length === 0) {
45091
+ if (options?.truncate) {
45092
+ const result = await this.executor.execute(`: > "${this.escapePath(path)}"`);
45093
+ if (result.exitCode !== 0) {
45094
+ throw this.createFileError(path, result.stderr);
45095
+ }
45096
+ }
45097
+ return;
45098
+ }
45053
45099
  const base64 = bytesToBase64(data);
45054
45100
  const chunkSize = 1024;
45055
- await this.createParentDirectory(path);
45056
- let first = true;
45101
+ let first = Boolean(options?.truncate);
45057
45102
  for (let i = 0;i < base64.length; i += chunkSize) {
45058
45103
  const chunk = base64.slice(i, i + chunkSize);
45059
45104
  const redirect = first ? ">" : ">>";
@@ -45063,7 +45108,6 @@ class CommandPolyfillService {
45063
45108
  }
45064
45109
  first = false;
45065
45110
  }
45066
- return data.length;
45067
45111
  }
45068
45112
  async writeTextFile(path, content) {
45069
45113
  await this.createParentDirectory(path);
@@ -45406,23 +45450,7 @@ class BaseSandboxAdapter {
45406
45450
  const arrayBuffer = await entry.data.arrayBuffer();
45407
45451
  bytesWritten = await polyfillService.writeFile(entry.path, new Uint8Array(arrayBuffer));
45408
45452
  } else {
45409
- const chunks = [];
45410
- const reader = entry.data.getReader();
45411
- while (true) {
45412
- const { done, value } = await reader.read();
45413
- if (done) {
45414
- break;
45415
- }
45416
- chunks.push(value);
45417
- }
45418
- const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
45419
- const combined = new Uint8Array(totalLength);
45420
- let offset = 0;
45421
- for (const chunk of chunks) {
45422
- combined.set(chunk, offset);
45423
- offset += chunk.length;
45424
- }
45425
- bytesWritten = await polyfillService.writeFile(entry.path, combined);
45453
+ bytesWritten = await this.writeStreamToFile(polyfillService, entry.path, entry.data);
45426
45454
  }
45427
45455
  results.push({ path: entry.path, bytesWritten, error: null });
45428
45456
  } catch (error) {
@@ -45497,23 +45525,36 @@ class BaseSandboxAdapter {
45497
45525
  }
45498
45526
  }
45499
45527
  async writeFileStream(path, stream) {
45528
+ const polyfillService = this.requirePolyfillService("writeFileStream", "File stream write not supported by this provider");
45529
+ await this.writeStreamToFile(polyfillService, this.normalizePath(path), stream);
45530
+ }
45531
+ async writeStreamToFile(polyfillService, path, stream) {
45532
+ const tmpPath = `${path}.tmp.${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
45500
45533
  const reader = stream.getReader();
45501
- const chunks = [];
45502
- while (true) {
45503
- const { done, value } = await reader.read();
45504
- if (done) {
45505
- break;
45534
+ let totalBytes = 0;
45535
+ let first = true;
45536
+ try {
45537
+ while (true) {
45538
+ const { done, value } = await reader.read();
45539
+ if (done)
45540
+ break;
45541
+ if (!value || value.length === 0)
45542
+ continue;
45543
+ await polyfillService.appendBytes(tmpPath, value, first ? { truncate: true } : undefined);
45544
+ totalBytes += value.length;
45545
+ first = false;
45506
45546
  }
45507
- chunks.push(value);
45508
- }
45509
- const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
45510
- const combined = new Uint8Array(totalLength);
45511
- let offset = 0;
45512
- for (const chunk of chunks) {
45513
- combined.set(chunk, offset);
45514
- offset += chunk.length;
45547
+ if (first) {
45548
+ await polyfillService.appendBytes(tmpPath, new Uint8Array, { truncate: true });
45549
+ }
45550
+ await polyfillService.moveFiles([{ source: tmpPath, destination: path }]);
45551
+ } catch (error) {
45552
+ await polyfillService.deleteFiles([tmpPath]).catch(() => {});
45553
+ throw error;
45554
+ } finally {
45555
+ reader.releaseLock();
45515
45556
  }
45516
- await this.writeFiles([{ path, data: combined }]);
45557
+ return totalBytes;
45517
45558
  }
45518
45559
  async getFileInfo(paths) {
45519
45560
  const polyfillService = this.requirePolyfillService("getFileInfo", "File info not supported by this provider");
@@ -47909,7 +47950,79 @@ var Sandbox = class _Sandbox {
47909
47950
  }
47910
47951
  };
47911
47952
 
47953
+ // src/utils/outputBuffer.ts
47954
+ class BoundedOutputBuffer {
47955
+ maxBytes;
47956
+ chunks = [];
47957
+ currentBytes = 0;
47958
+ _totalBytes = 0;
47959
+ _truncated = false;
47960
+ separatorBuf;
47961
+ constructor(maxBytes, separator) {
47962
+ this.maxBytes = maxBytes;
47963
+ if (!(maxBytes > 0)) {
47964
+ throw new Error(`BoundedOutputBuffer: maxBytes must be > 0 (got ${maxBytes})`);
47965
+ }
47966
+ if (separator) {
47967
+ this.separatorBuf = Buffer.from(separator, "utf8");
47968
+ }
47969
+ }
47970
+ append(text) {
47971
+ if (!text)
47972
+ return;
47973
+ const needsSep = this.separatorBuf && this.chunks.length > 0;
47974
+ const textBuf = Buffer.from(text, "utf8");
47975
+ const sepLen = needsSep ? this.separatorBuf.length : 0;
47976
+ this._totalBytes += textBuf.length + sepLen;
47977
+ if (textBuf.length + sepLen > this.maxBytes) {
47978
+ this._truncated = true;
47979
+ const keep = Math.min(textBuf.length, this.maxBytes);
47980
+ this.chunks = [
47981
+ keep < textBuf.length ? Buffer.from(textBuf.subarray(textBuf.length - keep)) : textBuf
47982
+ ];
47983
+ this.currentBytes = keep;
47984
+ return;
47985
+ }
47986
+ const chunk = needsSep ? Buffer.concat([this.separatorBuf, textBuf]) : textBuf;
47987
+ this.chunks.push(chunk);
47988
+ this.currentBytes += chunk.length;
47989
+ if (this.currentBytes <= this.maxBytes)
47990
+ return;
47991
+ this._truncated = true;
47992
+ while (this.chunks.length > 1 && this.currentBytes - this.chunks[0].length >= this.maxBytes) {
47993
+ this.currentBytes -= this.chunks[0].length;
47994
+ this.chunks.shift();
47995
+ }
47996
+ if (this.currentBytes > this.maxBytes && this.chunks.length > 0) {
47997
+ const overflow = this.currentBytes - this.maxBytes;
47998
+ this.chunks[0] = this.chunks[0].subarray(overflow);
47999
+ this.currentBytes -= overflow;
48000
+ }
48001
+ if (this.separatorBuf && this.chunks.length > 0) {
48002
+ const first = this.chunks[0];
48003
+ const sep = this.separatorBuf;
48004
+ if (first.subarray(0, sep.length).equals(sep)) {
48005
+ this.chunks[0] = first.subarray(sep.length);
48006
+ this.currentBytes -= sep.length;
48007
+ }
48008
+ }
48009
+ }
48010
+ get totalBytes() {
48011
+ return this._totalBytes;
48012
+ }
48013
+ get truncated() {
48014
+ return this._truncated;
48015
+ }
48016
+ toString() {
48017
+ if (this.chunks.length === 0)
48018
+ return "";
48019
+ return Buffer.concat(this.chunks, this.currentBytes).toString("utf8");
48020
+ }
48021
+ }
48022
+
47912
48023
  // src/adapters/OpenSandboxAdapter/index.ts
48024
+ var DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
48025
+
47913
48026
  class OpenSandboxAdapter extends BaseSandboxAdapter {
47914
48027
  connectionConfig;
47915
48028
  createConfig;
@@ -48293,21 +48406,30 @@ class OpenSandboxAdapter extends BaseSandboxAdapter {
48293
48406
  return results;
48294
48407
  }
48295
48408
  async execute(command, options) {
48409
+ const maxBytes = options?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
48410
+ const stdoutBuf = new BoundedOutputBuffer(maxBytes, `
48411
+ `);
48412
+ const stderrBuf = new BoundedOutputBuffer(maxBytes, `
48413
+ `);
48296
48414
  try {
48297
48415
  const execution = await this.sandbox.commands.run(command, {
48298
48416
  workingDirectory: this.normalizePath(options?.workingDirectory),
48299
48417
  background: options?.background
48418
+ }, {
48419
+ onStdout: (msg) => {
48420
+ stdoutBuf.append(msg.text);
48421
+ },
48422
+ onStderr: (msg) => {
48423
+ stderrBuf.append(msg.text);
48424
+ }
48300
48425
  });
48301
- const stdout = execution.logs.stdout.map((msg) => msg.text).join(`
48302
- `);
48303
- const stderr = execution.logs.stderr.map((msg) => msg.text).join(`
48304
- `);
48305
48426
  const exitCode = this.extractExitCode(execution);
48306
- const stdoutLength = execution.logs.stdout.reduce((sum, msg) => sum + msg.text.length, 0);
48307
- const stderrLength = execution.logs.stderr.reduce((sum, msg) => sum + msg.text.length, 0);
48308
- const MaxOutputSize = 1024 * 1024;
48309
- const truncated = stdoutLength >= MaxOutputSize || stderrLength >= MaxOutputSize;
48310
- return { stdout, stderr, exitCode, truncated };
48427
+ return {
48428
+ stdout: stdoutBuf.toString(),
48429
+ stderr: stderrBuf.toString(),
48430
+ exitCode,
48431
+ truncated: stdoutBuf.truncated || stderrBuf.truncated
48432
+ };
48311
48433
  } catch (error) {
48312
48434
  if (error instanceof SandboxStateError)
48313
48435
  throw error;
@@ -48315,10 +48437,26 @@ class OpenSandboxAdapter extends BaseSandboxAdapter {
48315
48437
  }
48316
48438
  }
48317
48439
  async executeStream(command, handlers, options) {
48440
+ const wantsComplete = Boolean(handlers.onComplete);
48441
+ const maxBytes = options?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
48442
+ const stdoutBuf = wantsComplete ? new BoundedOutputBuffer(maxBytes, `
48443
+ `) : undefined;
48444
+ const stderrBuf = wantsComplete ? new BoundedOutputBuffer(maxBytes, `
48445
+ `) : undefined;
48318
48446
  try {
48319
48447
  const sdkHandlers = {
48320
- ...handlers.onStderr ? { onStderr: handlers.onStderr } : {},
48321
- ...handlers.onStdout ? { onStdout: handlers.onStdout } : {},
48448
+ ...handlers.onStdout || wantsComplete ? {
48449
+ onStdout: async (msg) => {
48450
+ stdoutBuf?.append(msg.text);
48451
+ await handlers.onStdout?.(msg);
48452
+ }
48453
+ } : {},
48454
+ ...handlers.onStderr || wantsComplete ? {
48455
+ onStderr: async (msg) => {
48456
+ stderrBuf?.append(msg.text);
48457
+ await handlers.onStderr?.(msg);
48458
+ }
48459
+ } : {},
48322
48460
  ...handlers.onError ? {
48323
48461
  onError: async (err) => {
48324
48462
  const error = new Error(err.value || err.name || "Execution error");
@@ -48335,17 +48473,14 @@ class OpenSandboxAdapter extends BaseSandboxAdapter {
48335
48473
  workingDirectory: this.normalizePath(options?.workingDirectory),
48336
48474
  background: options?.background
48337
48475
  }, sdkHandlers);
48338
- if (handlers.onComplete) {
48339
- const stdout = execution.logs.stdout.map((msg) => msg.text).join(`
48340
- `);
48341
- const stderr = execution.logs.stderr.map((msg) => msg.text).join(`
48342
- `);
48476
+ if (wantsComplete) {
48343
48477
  const exitCode = this.extractExitCode(execution);
48344
- const stdoutLength = execution.logs.stdout.reduce((sum, msg) => sum + msg.text.length, 0);
48345
- const stderrLength = execution.logs.stderr.reduce((sum, msg) => sum + msg.text.length, 0);
48346
- const MaxOutputSize = 1024 * 1024;
48347
- const truncated = stdoutLength >= MaxOutputSize || stderrLength >= MaxOutputSize;
48348
- await handlers.onComplete({ stdout, stderr, exitCode, truncated });
48478
+ await handlers.onComplete({
48479
+ stdout: stdoutBuf.toString(),
48480
+ stderr: stderrBuf.toString(),
48481
+ exitCode,
48482
+ truncated: stdoutBuf.truncated || stderrBuf.truncated
48483
+ });
48349
48484
  }
48350
48485
  } catch (error) {
48351
48486
  throw new CommandExecutionError(`Streaming command execution failed: ${command}`, command, error instanceof Error ? error : undefined);
package/dist/index.js CHANGED
@@ -45007,13 +45007,34 @@ class CommandPolyfillService {
45007
45007
  constructor(executor) {
45008
45008
  this.executor = executor;
45009
45009
  }
45010
+ static READ_CHUNK_SIZE = 256 * 1024;
45010
45011
  async readFile(path) {
45011
45012
  try {
45012
- const result = await this.executor.execute(`cat "${this.escapePath(path)}" | base64 -w 0`);
45013
- if (result.exitCode !== 0) {
45014
- throw this.createFileError(path, result.stderr);
45013
+ const size = await this.statSize(path);
45014
+ if (size === undefined) {
45015
+ const result = await this.executor.execute(`cat "${this.escapePath(path)}" | base64 -w 0`);
45016
+ if (result.exitCode !== 0) {
45017
+ throw this.createFileError(path, result.stderr);
45018
+ }
45019
+ return base64ToBytes(result.stdout);
45020
+ }
45021
+ if (size === 0)
45022
+ return new Uint8Array;
45023
+ const chunks = [];
45024
+ let totalLength = 0;
45025
+ for (let offset = 0;offset < size; offset += CommandPolyfillService.READ_CHUNK_SIZE) {
45026
+ const end = Math.min(offset + CommandPolyfillService.READ_CHUNK_SIZE, size);
45027
+ const chunk = await this.readFileRange(path, offset, end);
45028
+ chunks.push(chunk);
45029
+ totalLength += chunk.length;
45015
45030
  }
45016
- return base64ToBytes(result.stdout);
45031
+ const combined = new Uint8Array(totalLength);
45032
+ let writeOffset = 0;
45033
+ for (const chunk of chunks) {
45034
+ combined.set(chunk, writeOffset);
45035
+ writeOffset += chunk.length;
45036
+ }
45037
+ return combined;
45017
45038
  } catch (error) {
45018
45039
  if (error instanceof FileOperationError) {
45019
45040
  throw error;
@@ -45024,9 +45045,18 @@ class CommandPolyfillService {
45024
45045
  throw error;
45025
45046
  }
45026
45047
  }
45048
+ async statSize(path) {
45049
+ const result = await this.executor.execute(`stat -c '%s' "${this.escapePath(path)}" 2>/dev/null || stat -f '%z' "${this.escapePath(path)}" 2>/dev/null || echo STAT_FAILED`);
45050
+ const stdout = result.stdout.trim();
45051
+ if (!stdout || stdout.includes("STAT_FAILED"))
45052
+ return;
45053
+ const parsed = Number.parseInt(stdout, 10);
45054
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
45055
+ }
45027
45056
  async readFileRange(path, start, end) {
45028
- const length = end ? end - start : "";
45029
- const cmd = `dd if="${this.escapePath(path)}" bs=1 skip=${start} count=${length} 2>/dev/null | base64 -w 0`;
45057
+ const escaped = this.escapePath(path);
45058
+ const tailPos = start + 1;
45059
+ const cmd = end !== undefined ? `tail -c +${tailPos} "${escaped}" | head -c ${end - start} | base64 -w 0` : `tail -c +${tailPos} "${escaped}" | base64 -w 0`;
45030
45060
  const result = await this.executor.execute(cmd);
45031
45061
  if (result.exitCode !== 0) {
45032
45062
  throw this.createFileError(path, result.stderr);
@@ -45034,10 +45064,25 @@ class CommandPolyfillService {
45034
45064
  return base64ToBytes(result.stdout);
45035
45065
  }
45036
45066
  async writeFile(path, data) {
45067
+ await this.appendBytes(path, data, { truncate: true });
45068
+ return data.length;
45069
+ }
45070
+ async appendBytes(path, data, options) {
45071
+ if (options?.truncate) {
45072
+ await this.createParentDirectory(path);
45073
+ }
45074
+ if (data.length === 0) {
45075
+ if (options?.truncate) {
45076
+ const result = await this.executor.execute(`: > "${this.escapePath(path)}"`);
45077
+ if (result.exitCode !== 0) {
45078
+ throw this.createFileError(path, result.stderr);
45079
+ }
45080
+ }
45081
+ return;
45082
+ }
45037
45083
  const base64 = bytesToBase64(data);
45038
45084
  const chunkSize = 1024;
45039
- await this.createParentDirectory(path);
45040
- let first = true;
45085
+ let first = Boolean(options?.truncate);
45041
45086
  for (let i = 0;i < base64.length; i += chunkSize) {
45042
45087
  const chunk = base64.slice(i, i + chunkSize);
45043
45088
  const redirect = first ? ">" : ">>";
@@ -45047,7 +45092,6 @@ class CommandPolyfillService {
45047
45092
  }
45048
45093
  first = false;
45049
45094
  }
45050
- return data.length;
45051
45095
  }
45052
45096
  async writeTextFile(path, content) {
45053
45097
  await this.createParentDirectory(path);
@@ -45390,23 +45434,7 @@ class BaseSandboxAdapter {
45390
45434
  const arrayBuffer = await entry.data.arrayBuffer();
45391
45435
  bytesWritten = await polyfillService.writeFile(entry.path, new Uint8Array(arrayBuffer));
45392
45436
  } else {
45393
- const chunks = [];
45394
- const reader = entry.data.getReader();
45395
- while (true) {
45396
- const { done, value } = await reader.read();
45397
- if (done) {
45398
- break;
45399
- }
45400
- chunks.push(value);
45401
- }
45402
- const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
45403
- const combined = new Uint8Array(totalLength);
45404
- let offset = 0;
45405
- for (const chunk of chunks) {
45406
- combined.set(chunk, offset);
45407
- offset += chunk.length;
45408
- }
45409
- bytesWritten = await polyfillService.writeFile(entry.path, combined);
45437
+ bytesWritten = await this.writeStreamToFile(polyfillService, entry.path, entry.data);
45410
45438
  }
45411
45439
  results.push({ path: entry.path, bytesWritten, error: null });
45412
45440
  } catch (error) {
@@ -45481,23 +45509,36 @@ class BaseSandboxAdapter {
45481
45509
  }
45482
45510
  }
45483
45511
  async writeFileStream(path, stream) {
45512
+ const polyfillService = this.requirePolyfillService("writeFileStream", "File stream write not supported by this provider");
45513
+ await this.writeStreamToFile(polyfillService, this.normalizePath(path), stream);
45514
+ }
45515
+ async writeStreamToFile(polyfillService, path, stream) {
45516
+ const tmpPath = `${path}.tmp.${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
45484
45517
  const reader = stream.getReader();
45485
- const chunks = [];
45486
- while (true) {
45487
- const { done, value } = await reader.read();
45488
- if (done) {
45489
- break;
45518
+ let totalBytes = 0;
45519
+ let first = true;
45520
+ try {
45521
+ while (true) {
45522
+ const { done, value } = await reader.read();
45523
+ if (done)
45524
+ break;
45525
+ if (!value || value.length === 0)
45526
+ continue;
45527
+ await polyfillService.appendBytes(tmpPath, value, first ? { truncate: true } : undefined);
45528
+ totalBytes += value.length;
45529
+ first = false;
45490
45530
  }
45491
- chunks.push(value);
45492
- }
45493
- const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
45494
- const combined = new Uint8Array(totalLength);
45495
- let offset = 0;
45496
- for (const chunk of chunks) {
45497
- combined.set(chunk, offset);
45498
- offset += chunk.length;
45531
+ if (first) {
45532
+ await polyfillService.appendBytes(tmpPath, new Uint8Array, { truncate: true });
45533
+ }
45534
+ await polyfillService.moveFiles([{ source: tmpPath, destination: path }]);
45535
+ } catch (error) {
45536
+ await polyfillService.deleteFiles([tmpPath]).catch(() => {});
45537
+ throw error;
45538
+ } finally {
45539
+ reader.releaseLock();
45499
45540
  }
45500
- await this.writeFiles([{ path, data: combined }]);
45541
+ return totalBytes;
45501
45542
  }
45502
45543
  async getFileInfo(paths) {
45503
45544
  const polyfillService = this.requirePolyfillService("getFileInfo", "File info not supported by this provider");
@@ -47893,7 +47934,79 @@ var Sandbox = class _Sandbox {
47893
47934
  }
47894
47935
  };
47895
47936
 
47937
+ // src/utils/outputBuffer.ts
47938
+ class BoundedOutputBuffer {
47939
+ maxBytes;
47940
+ chunks = [];
47941
+ currentBytes = 0;
47942
+ _totalBytes = 0;
47943
+ _truncated = false;
47944
+ separatorBuf;
47945
+ constructor(maxBytes, separator) {
47946
+ this.maxBytes = maxBytes;
47947
+ if (!(maxBytes > 0)) {
47948
+ throw new Error(`BoundedOutputBuffer: maxBytes must be > 0 (got ${maxBytes})`);
47949
+ }
47950
+ if (separator) {
47951
+ this.separatorBuf = Buffer.from(separator, "utf8");
47952
+ }
47953
+ }
47954
+ append(text) {
47955
+ if (!text)
47956
+ return;
47957
+ const needsSep = this.separatorBuf && this.chunks.length > 0;
47958
+ const textBuf = Buffer.from(text, "utf8");
47959
+ const sepLen = needsSep ? this.separatorBuf.length : 0;
47960
+ this._totalBytes += textBuf.length + sepLen;
47961
+ if (textBuf.length + sepLen > this.maxBytes) {
47962
+ this._truncated = true;
47963
+ const keep = Math.min(textBuf.length, this.maxBytes);
47964
+ this.chunks = [
47965
+ keep < textBuf.length ? Buffer.from(textBuf.subarray(textBuf.length - keep)) : textBuf
47966
+ ];
47967
+ this.currentBytes = keep;
47968
+ return;
47969
+ }
47970
+ const chunk = needsSep ? Buffer.concat([this.separatorBuf, textBuf]) : textBuf;
47971
+ this.chunks.push(chunk);
47972
+ this.currentBytes += chunk.length;
47973
+ if (this.currentBytes <= this.maxBytes)
47974
+ return;
47975
+ this._truncated = true;
47976
+ while (this.chunks.length > 1 && this.currentBytes - this.chunks[0].length >= this.maxBytes) {
47977
+ this.currentBytes -= this.chunks[0].length;
47978
+ this.chunks.shift();
47979
+ }
47980
+ if (this.currentBytes > this.maxBytes && this.chunks.length > 0) {
47981
+ const overflow = this.currentBytes - this.maxBytes;
47982
+ this.chunks[0] = this.chunks[0].subarray(overflow);
47983
+ this.currentBytes -= overflow;
47984
+ }
47985
+ if (this.separatorBuf && this.chunks.length > 0) {
47986
+ const first = this.chunks[0];
47987
+ const sep = this.separatorBuf;
47988
+ if (first.subarray(0, sep.length).equals(sep)) {
47989
+ this.chunks[0] = first.subarray(sep.length);
47990
+ this.currentBytes -= sep.length;
47991
+ }
47992
+ }
47993
+ }
47994
+ get totalBytes() {
47995
+ return this._totalBytes;
47996
+ }
47997
+ get truncated() {
47998
+ return this._truncated;
47999
+ }
48000
+ toString() {
48001
+ if (this.chunks.length === 0)
48002
+ return "";
48003
+ return Buffer.concat(this.chunks, this.currentBytes).toString("utf8");
48004
+ }
48005
+ }
48006
+
47896
48007
  // src/adapters/OpenSandboxAdapter/index.ts
48008
+ var DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
48009
+
47897
48010
  class OpenSandboxAdapter extends BaseSandboxAdapter {
47898
48011
  connectionConfig;
47899
48012
  createConfig;
@@ -48277,21 +48390,30 @@ class OpenSandboxAdapter extends BaseSandboxAdapter {
48277
48390
  return results;
48278
48391
  }
48279
48392
  async execute(command, options) {
48393
+ const maxBytes = options?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
48394
+ const stdoutBuf = new BoundedOutputBuffer(maxBytes, `
48395
+ `);
48396
+ const stderrBuf = new BoundedOutputBuffer(maxBytes, `
48397
+ `);
48280
48398
  try {
48281
48399
  const execution = await this.sandbox.commands.run(command, {
48282
48400
  workingDirectory: this.normalizePath(options?.workingDirectory),
48283
48401
  background: options?.background
48402
+ }, {
48403
+ onStdout: (msg) => {
48404
+ stdoutBuf.append(msg.text);
48405
+ },
48406
+ onStderr: (msg) => {
48407
+ stderrBuf.append(msg.text);
48408
+ }
48284
48409
  });
48285
- const stdout = execution.logs.stdout.map((msg) => msg.text).join(`
48286
- `);
48287
- const stderr = execution.logs.stderr.map((msg) => msg.text).join(`
48288
- `);
48289
48410
  const exitCode = this.extractExitCode(execution);
48290
- const stdoutLength = execution.logs.stdout.reduce((sum, msg) => sum + msg.text.length, 0);
48291
- const stderrLength = execution.logs.stderr.reduce((sum, msg) => sum + msg.text.length, 0);
48292
- const MaxOutputSize = 1024 * 1024;
48293
- const truncated = stdoutLength >= MaxOutputSize || stderrLength >= MaxOutputSize;
48294
- return { stdout, stderr, exitCode, truncated };
48411
+ return {
48412
+ stdout: stdoutBuf.toString(),
48413
+ stderr: stderrBuf.toString(),
48414
+ exitCode,
48415
+ truncated: stdoutBuf.truncated || stderrBuf.truncated
48416
+ };
48295
48417
  } catch (error) {
48296
48418
  if (error instanceof SandboxStateError)
48297
48419
  throw error;
@@ -48299,10 +48421,26 @@ class OpenSandboxAdapter extends BaseSandboxAdapter {
48299
48421
  }
48300
48422
  }
48301
48423
  async executeStream(command, handlers, options) {
48424
+ const wantsComplete = Boolean(handlers.onComplete);
48425
+ const maxBytes = options?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
48426
+ const stdoutBuf = wantsComplete ? new BoundedOutputBuffer(maxBytes, `
48427
+ `) : undefined;
48428
+ const stderrBuf = wantsComplete ? new BoundedOutputBuffer(maxBytes, `
48429
+ `) : undefined;
48302
48430
  try {
48303
48431
  const sdkHandlers = {
48304
- ...handlers.onStderr ? { onStderr: handlers.onStderr } : {},
48305
- ...handlers.onStdout ? { onStdout: handlers.onStdout } : {},
48432
+ ...handlers.onStdout || wantsComplete ? {
48433
+ onStdout: async (msg) => {
48434
+ stdoutBuf?.append(msg.text);
48435
+ await handlers.onStdout?.(msg);
48436
+ }
48437
+ } : {},
48438
+ ...handlers.onStderr || wantsComplete ? {
48439
+ onStderr: async (msg) => {
48440
+ stderrBuf?.append(msg.text);
48441
+ await handlers.onStderr?.(msg);
48442
+ }
48443
+ } : {},
48306
48444
  ...handlers.onError ? {
48307
48445
  onError: async (err) => {
48308
48446
  const error = new Error(err.value || err.name || "Execution error");
@@ -48319,17 +48457,14 @@ class OpenSandboxAdapter extends BaseSandboxAdapter {
48319
48457
  workingDirectory: this.normalizePath(options?.workingDirectory),
48320
48458
  background: options?.background
48321
48459
  }, sdkHandlers);
48322
- if (handlers.onComplete) {
48323
- const stdout = execution.logs.stdout.map((msg) => msg.text).join(`
48324
- `);
48325
- const stderr = execution.logs.stderr.map((msg) => msg.text).join(`
48326
- `);
48460
+ if (wantsComplete) {
48327
48461
  const exitCode = this.extractExitCode(execution);
48328
- const stdoutLength = execution.logs.stdout.reduce((sum, msg) => sum + msg.text.length, 0);
48329
- const stderrLength = execution.logs.stderr.reduce((sum, msg) => sum + msg.text.length, 0);
48330
- const MaxOutputSize = 1024 * 1024;
48331
- const truncated = stdoutLength >= MaxOutputSize || stderrLength >= MaxOutputSize;
48332
- await handlers.onComplete({ stdout, stderr, exitCode, truncated });
48462
+ await handlers.onComplete({
48463
+ stdout: stdoutBuf.toString(),
48464
+ stderr: stderrBuf.toString(),
48465
+ exitCode,
48466
+ truncated: stdoutBuf.truncated || stderrBuf.truncated
48467
+ });
48333
48468
  }
48334
48469
  } catch (error) {
48335
48470
  throw new CommandExecutionError(`Streaming command execution failed: ${command}`, command, error instanceof Error ? error : undefined);
@@ -13,12 +13,33 @@ export declare class CommandPolyfillService {
13
13
  private readonly executor;
14
14
  constructor(executor: ICommandExecution);
15
15
  /**
16
- * Read a file via base64 encoding.
17
- * Uses: cat <file> | base64
16
+ * Chunk size used when reading files through command execution. Each chunk
17
+ * produces a base64-encoded stdout of roughly `READ_CHUNK_SIZE * 4 / 3`
18
+ * bytes, which must fit within the executor's stdout byte cap (default
19
+ * 1 MiB) with room to spare.
20
+ */
21
+ private static readonly READ_CHUNK_SIZE;
22
+ /**
23
+ * Read a file in chunks via `dd | base64`. Uses `stat` to discover the file
24
+ * size, then issues range reads so that no single command's stdout exceeds
25
+ * the executor's bounded output limit.
26
+ *
27
+ * Falls back to a single `cat | base64` read when `stat` fails (e.g. when
28
+ * the sandbox lacks GNU stat). That fallback is bounded by the caller's
29
+ * `maxOutputBytes`, so very large files require stat-based chunking.
18
30
  */
19
31
  readFile(path: string): Promise<Uint8Array>;
20
32
  /**
21
- * Read a portion of a file via dd + base64.
33
+ * Return the file size in bytes, or undefined if stat fails (e.g. the file
34
+ * does not exist or stat is unavailable).
35
+ */
36
+ private statSize;
37
+ /**
38
+ * Read a portion of a file via `tail -c +N | head -c M | base64`.
39
+ *
40
+ * `tail -c +N` emits bytes starting at position N (1-indexed). `head -c M`
41
+ * caps the length. Both are POSIX-ish and avoid the `dd bs=1` one-syscall-
42
+ * per-byte trap that made the old implementation unusable for large reads.
22
43
  */
23
44
  readFileRange(path: string, start: number, end?: number): Promise<Uint8Array>;
24
45
  /**
@@ -26,6 +47,17 @@ export declare class CommandPolyfillService {
26
47
  * Uses: echo <base64> | base64 -d > <file>
27
48
  */
28
49
  writeFile(path: string, data: Uint8Array): Promise<number>;
50
+ /**
51
+ * Append `data` to `path`, chunking the base64 payload to stay under the
52
+ * shell command line length limit. Set `truncate` to rewrite the file
53
+ * from scratch on the first append.
54
+ *
55
+ * Parent directory creation runs once when `truncate` is set, so streaming
56
+ * writes pay the mkdir cost only on the first chunk.
57
+ */
58
+ appendBytes(path: string, data: Uint8Array, options?: {
59
+ truncate?: boolean;
60
+ }): Promise<void>;
29
61
  /**
30
62
  * Write a text file directly.
31
63
  */
@@ -12,6 +12,16 @@ export interface ExecuteOptions {
12
12
  env?: Record<string, string>;
13
13
  /** Abort signal for cancellation */
14
14
  signal?: AbortSignal;
15
+ /**
16
+ * Maximum number of bytes to retain in stdout / stderr for the returned
17
+ * {@link ExecuteResult}. Output beyond this limit is dropped (oldest first)
18
+ * and `truncated` is set to true. Streaming handlers (`onStdout` / `onStderr`)
19
+ * still receive every chunk regardless of this limit.
20
+ *
21
+ * Defaults to 1 MiB per stream. Set a larger value for commands whose full
22
+ * output you need, bearing in mind the memory cost.
23
+ */
24
+ maxOutputBytes?: number;
15
25
  }
16
26
  /**
17
27
  * Result of command execution.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Bounded, append-only text buffer that keeps only the tail when the total
3
+ * byte count exceeds `maxBytes`. The head is dropped first, because for
4
+ * command output the tail is almost always the most useful part (error
5
+ * messages, summaries, stack traces, final lines).
6
+ *
7
+ * Chunks are stored as UTF-8 `Buffer`s so byte counting is O(1) per chunk
8
+ * and slicing is exact. Decoding only happens in {@link toString}, which
9
+ * gracefully handles any broken leading multi-byte sequence created by a
10
+ * mid-character slice.
11
+ *
12
+ * Memory is O(maxBytes) regardless of how much data is appended over the
13
+ * lifetime of the buffer.
14
+ */
15
+ export declare class BoundedOutputBuffer {
16
+ private readonly maxBytes;
17
+ private chunks;
18
+ private currentBytes;
19
+ private _totalBytes;
20
+ private _truncated;
21
+ private readonly separatorBuf;
22
+ /**
23
+ * @param maxBytes Maximum retained bytes.
24
+ * @param separator Optional string inserted between consecutive appends
25
+ * (e.g. `'\n'` to replicate the old `join('\n')` behaviour).
26
+ */
27
+ constructor(maxBytes: number, separator?: string);
28
+ append(text: string): void;
29
+ /** Total bytes appended over the buffer's lifetime (not just the bytes currently stored). */
30
+ get totalBytes(): number;
31
+ /** True once any data has been dropped. */
32
+ get truncated(): boolean;
33
+ toString(): string;
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastgpt-sdk/sandbox-adapter",
3
- "version": "0.0.35",
3
+ "version": "0.0.36",
4
4
  "description": "Unified abstraction layer for cloud sandbox providers with adapter pattern and feature polyfilling",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",