mcpField
D
daytona-integration

daytona-integration

github.com/seanchiuai云与 DevOps038 次安装40 周前更新
代码仓库

关于 daytona-integration

面向使用 Daytona SDK 管理远程沙箱的开发者的 TypeScript 集成指南,涵盖沙箱生命周期、进程执行、文件传输和预览服务器模式,并提供配置与错误处理建议。

主要功能

  1. 01创建和管理沙箱
  2. 02配置运行时、资源与环境变量
  3. 03运行命令、代码和交互会话
  4. 04通过沙箱文件系统传输文件
  5. 05处理重试与预览服务器

适用场景

  1. 01在沙箱中运行开发命令
  2. 02为并行 Agent 创建独立沙箱
  3. 03上传项目文件并取回输出
  4. 04启动预览服务器并获取 URL

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/seanchiuai/multishot --skill daytona-integration

安装后在 Claude Code 中输入/daytona-integration确认已加载。

详细介绍

Daytona 集成

来源:Daytona TypeScript SDK 文档

安装

npm install @daytonaio/sdk

SDK 设置

import { Daytona } from '@daytonaio/sdk'

const daytona = new Daytona({
  apiKey: process.env.DAYTONA_API_KEY,          // Required for auth
  apiUrl: 'https://app.daytona.io/api',         // Default API endpoint
  target: 'us',                                  // Optional: region preference
  // Alternative auth:
  // jwtToken: 'jwt-token',                      // Requires organizationId
  // organizationId: 'org-id'
})
DaytonaConfig 接口
属性类型必填描述
apiKeystring否*API 密钥身份验证
apiUrlstring否API 端点(默认:https://app.daytona.io/api)
jwtTokenstring否*JWT 身份验证(需要 organizationId)
organizationIdstring否使用 JWT 时必需
targetstring否沙盒位置偏好

*必须提供 apiKey 或 jwtToken 之一。

Daytona 类方法

create()

创建新的沙盒。

const sandbox = await daytona.create({
  language: 'typescript',      // 'python' | 'typescript' | 'javascript'
  envVars: {                   // Environment variables
    CLAUDE_CODE_OAUTH_TOKEN: token,
    NODE_ENV: 'development'
  },
  autoStopInterval: 15,        // Minutes idle before stop (default: 15)
  autoArchiveInterval: 10080,  // Minutes before archive (default: 7 days)
  autoDeleteInterval: 43200,   // Minutes before deletion
  labels: { project: 'mvp' },  // Metadata tags
  ephemeral: false             // Auto-cleanup on stop
}, {
  timeout: 60                  // Creation timeout in seconds
})

返回: Promise<Sandbox>

CreateSandboxParams
属性类型默认值描述
language'python' | 'typescript' | 'javascript''python'运行时语言
envVarsRecord<string, string>{}环境变量
autoStopIntervalnumber15空闲多少分钟后停止
autoArchiveIntervalnumber10080多少分钟后归档
autoDeleteIntervalnumber-多少分钟后删除
labelsRecord<string, string>-元数据标签
ephemeralbooleanfalse停止时自动清理
volumesVolumeMount[]-卷挂载
imagestring | Image-自定义 Docker 镜像
resourcesResources-CPU/内存/磁盘分配
snapshotstring-从快照 ID 创建
get()

通过 ID 或名称获取沙盒。

const sandbox = await daytona.get('sandbox-id-or-name')

返回: Promise<Sandbox>

list()

列出沙盒,可选进行筛选。

const result = await daytona.list(
  { project: 'mvp' },  // Filter by labels
  1,                   // Page number
  10                   // Items per page
)
// result.sandboxes: Sandbox[]
// result.total: number

返回: Promise<PaginatedSandboxes>

findOne()

查找第一个符合筛选条件的沙盒。

const sandbox = await daytona.findOne({
  id: 'sandbox-id',
  name: 'sandbox-name',
  labels: { project: 'mvp' }
})

返回: Promise<Sandbox>

delete()

永久删除沙盒。

await daytona.delete(sandbox, 60)  // sandbox object, timeout seconds
start() / stop()
await daytona.start(sandbox, 60)  // Start and await ready, timeout seconds
await daytona.stop(sandbox)       // Stop execution

Sandbox 类

属性
属性类型描述
idstring唯一的沙盒标识符
namestring沙盒名称
stateSandboxState'started' | 'stopped' | ...
cpunumberCPU 数量
memorynumber内存大小(GiB)
disknumber磁盘空间(GiB)
gpunumberGPU 数量
envRecord<string, string>环境变量
labelsRecord<string, string>元数据
processProcess进程执行接口
fsFileSystem文件系统接口
gitGitGit 操作接口
方法
// Lifecycle
await sandbox.start(timeout?)
await sandbox.stop(timeout?)
await sandbox.delete(timeout)
await sandbox.archive()
await sandbox.recover(timeout?)

// State
await sandbox.refreshData()
await sandbox.waitUntilStarted(timeout?)
await sandbox.waitUntilStopped(timeout?)

// Configuration
await sandbox.setLabels({ key: 'value' })
await sandbox.setAutostopInterval(minutes)
await sandbox.setAutoDeleteInterval(minutes)
await sandbox.setAutoArchiveInterval(minutes)

// Paths
const workDir = await sandbox.getWorkDir()
const homeDir = await sandbox.getUserHomeDir()

// Access
const previewUrl = await sandbox.getPreviewLink(port)
const signedUrl = await sandbox.getSignedPreviewUrl(port, expiresInSeconds?)

Process 类(sandbox.process)

executeCommand()

执行 Shell 命令。

const response = await sandbox.process.executeCommand(
  'npm install',           // command
  '/workspace',            // cwd (optional)
  { NODE_ENV: 'prod' },    // env (optional)
  300                      // timeout in seconds (optional, 0=indefinite)
)

返回: Promise<ExecuteResponse>

严重警告 - 后台进程: 使用 timeout=0 调用 executeCommand() 无法正常运行后台服务。尽管文档称“0=无限期”,但 Promise 会立即解析,且不会启动受跟踪的进程。请改用基于会话的执行方式来运行后台进程(参见下文“基于会话的执行”部分)。

// ❌ WRONG - Server won't actually run
await sandbox.process.executeCommand('npm run dev', '/workspace', undefined, 0)

// ✅ CORRECT - Use sessions for background processes
await sandbox.process.createSession('preview-server')
await sandbox.process.executeSessionCommand('preview-server', {
  command: 'npm run dev',
  async: true
}, 0)
ExecuteResponse 接口
属性类型描述
exitCodenumber进程退出状态
resultstringstdout 内容
artifactsExecutionArtifacts附加数据(stdout、图表)
codeRun()

使用适当的运行时执行代码。

const response = await sandbox.process.codeRun(
  'console.log("Hello")',  // code
  { argv: [], env: {} },   // params (optional)
  60                       // timeout in seconds (optional)
)
基于会话的执行(用于流式传输)
// Create a persistent session
await sandbox.process.createSession('my-session')

// Execute with async log callbacks
await sandbox.process.executeSessionCommand('my-session', {
  command: 'npm start',
  async: true
}, 300)

// Get logs with streaming callbacks
await sandbox.process.getSessionCommandLogs(
  'my-session',
  'command-id',
  (stdout: string) => { /* handle stdout chunk */ },
  (stderr: string) => { /* handle stderr chunk */ }
)

// Cleanup
await sandbox.process.deleteSession('my-session')
PTY(交互式终端)
// Create PTY session
const pty = await sandbox.process.createPty({ cols: 80, rows: 24 })

// Connect via WebSocket
await sandbox.process.connectPty(pty.sessionId, {
  onData: (data) => { /* terminal output */ },
  onExit: (code) => { /* process exited */ }
})

// Resize
await sandbox.process.resizePtySession(pty.sessionId, 120, 40)

// Kill
await sandbox.process.killPtySession(pty.sessionId)

文件系统(sandbox.fs)

// Download file (returns Buffer)
const buffer = await sandbox.fs.downloadFile('/workspace/file.txt')
const content = buffer.toString()

// Upload file (Buffer or local path)
await sandbox.fs.uploadFile(Buffer.from('content'), '/workspace/file.txt')
await sandbox.fs.uploadFile('/local/path.txt', '/workspace/file.txt')

// List directory (returns FileInfo[])
const files = await sandbox.fs.listFiles('/workspace')
// files[i].name, files[i].size, files[i].isDir

// Create directory
await sandbox.fs.createFolder('/workspace/new-dir', '755')

// Delete file/directory
await sandbox.fs.deleteFile('/workspace/file.txt')
await sandbox.fs.deleteFile('/workspace/dir', true)  // recursive

// File details
const info = await sandbox.fs.getFileDetails('/workspace/file.txt')

// Search
const results = await sandbox.fs.searchFiles('/workspace', '*.ts')
const matches = await sandbox.fs.findFiles('/workspace', 'pattern')

并行创建沙盒(4 个 Agent)

const agentIds = ['agent-a', 'agent-b', 'agent-c', 'agent-d']

const sandboxes = await Promise.all(
  agentIds.map(async (agentId) => {
    const sandbox = await daytona.create({
      language: 'typescript',
      envVars: { CLAUDE_CODE_OAUTH_TOKEN: token },
      labels: { agentId },
      autoStopInterval: 60
    })
    return { agentId, sandbox }
  })
)

文件传输模式

从沙盒提取(选择胜出者)
// Create tar in sandbox
await sandbox.process.executeCommand(
  'tar -czf /tmp/project.tar.gz -C /workspace .',
  undefined,
  undefined,
  60
)

// Download
const tarBuffer = await sandbox.fs.downloadFile('/tmp/project.tar.gz')

// Save locally
import { writeFileSync } from 'fs'
writeFileSync('~/.multishot/runs/run-id/winner.tar.gz', tarBuffer)
注入沙盒(下一轮)
import { readFileSync } from 'fs'

// Upload tar
const tarBuffer = readFileSync('~/.multishot/runs/run-id/winner.tar.gz')
await sandbox.fs.uploadFile(tarBuffer, '/tmp/project.tar.gz')

// Extract
await sandbox.process.executeCommand(
  'tar -xzf /tmp/project.tar.gz -C /workspace',
  undefined,
  undefined,
  60
)

错误处理

网络错误重试模式

执行会发起 API 调用的命令(Claude Code、npm 安装等)时,请为暂时性网络错误实现重试逻辑:

async function execWithRetry(
  command: string,
  maxRetries: number = 3
): Promise<ExecuteResponse> {
  let lastError: Error | null = null

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await sandbox.process.executeCommand(
        command,
        workDir,
        undefined,
        900
      )

      if (response.exitCode !== 0) {
        throw new Error(`Process exited with code ${response.exitCode}`)
      }

      return response // Success

    } catch (err) {
      lastError = err instanceof Error ? err : new Error(String(err))

      // Check if error is retryable (network issues)
      const isRetryable =
        lastError.message.includes('ECONNRESET') ||
        lastError.message.includes('ETIMEDOUT') ||
        lastError.message.includes('ENOTFOUND') ||
        lastError.message.includes('EAI_AGAIN') ||
        lastError.message.includes('socket hang up')

      if (!isRetryable) {
        throw lastError // Non-network error - fail immediately
      }

      if (attempt < maxRetries) {
        // Exponential backoff: 5s, 10s, 20s
        const delayMs = 5000 * Math.pow(2, attempt - 1)
        console.warn(`Retry ${attempt}/${maxRetries} in ${delayMs / 1000}s...`)
        await new Promise(resolve => setTimeout(resolve, delayMs))
      } else {
        throw new Error(`Network error after ${maxRetries} retries: ${lastError.message}`)
      }
    }
  }

  throw lastError || new Error('Unknown error')
}

可重试的错误:

  • ECONNRESET - 连接被强制关闭
  • ETIMEDOUT - 连接超时
  • ENOTFOUND - DNS 查询失败
  • EAI_AGAIN - DNS 暂时性故障
  • socket hang up - 连接中断

不可重试的错误(立即失败):

  • 身份验证失败
  • 无效命令
  • 权限错误
  • 验证错误

优势:

  • 防止暂时性网络中断浪费沙盒资源
  • 并行 Agent 执行时至关重要(4 个 Agent 意味着网络故障概率更高)
  • 指数退避可避免给故障服务造成过大压力
创建沙盒
try {
  const sandbox = await daytona.create({ language: 'typescript' })
} catch (error) {
  if (error.code === 'SANDBOX_CREATION_FAILED') {
    // Retry once
    await new Promise(r => setTimeout(r, 2000))
    const sandbox = await daytona.create({ language: 'typescript' })
  }
}

// Always cleanup in finally
try {
  // ... work with sandbox
} finally {
  await sandbox.delete(30).catch(console.error)
}

MVP 实现说明

当前实现使用带重试逻辑的 executeCommand()(而非基于会话的流式传输):

// SandboxManager.execClaudeCommand() pattern with retries
for (let attempt = 1; attempt <= maxRetries; attempt++) {
  try {
    const response = await sandbox.process.executeCommand(
      command,
      workDir,
      undefined,
      900 // 15 minute timeout
    )
    if (response.result) onStdout?.(response.result)
    return // Success
  } catch (err) {
    // Retry logic for ECONNRESET, ETIMEDOUT, etc.
    // Exponential backoff: 5s, 10s, 20s
  }
}

预览服务器使用基于会话的执行方式运行后台进程:

await sandbox.process.createSession(sessionId)
await sandbox.process.executeSessionCommand(sessionId, {
  command: config.command,
  async: true
}, 0)

预览 URL 的获取使用签名 URL:

const preview = await sandbox.getSignedPreviewUrl(port, 3600)
return preview.url

预览优化模式

配置缓存

通过缓存项目类型检测结果来减少文件系统操作:

class SandboxManager {
  private previewConfigs: Map<string, PreviewConfig> = new Map()

  async detectProjectType(agentId: string): Promise<PreviewConfig> {
    // Check cache first
    const cached = this.previewConfigs.get(agentId)
    if (cached) {
      console.log(`Using cached preview config: ${cached.type}`)
      return cached
    }

    // Detect project type (filesystem operations)
    const config = await this.performDetection(agentId)

    // Cache result
    this.previewConfigs.set(agentId, config)
    return config
  }

  clearPreviewCache(): void {
    this.previewConfigs.clear()
  }

  async destroySandbox(agentId: string): Promise<void> {
    // Clear cached config when sandbox destroyed
    this.previewConfigs.delete(agentId)
    // ... destroy sandbox
  }
}

优势:

  • 重复的预览请求会复用缓存的配置(无需访问文件系统)
  • 新一轮运行时会清除缓存,以检测最新的项目结构
  • 销毁沙盒时会清除相应 Agent 的缓存
复用会话

通过复用已有会话来避免重复启动预览服务器:

interface SandboxInfo {
  sandbox: Sandbox
  agentId: string
  previewSessionId?: string
}

async startPreviewServer(
  agentId: string,
  config: PreviewConfig
): Promise<void> {
  const info = this.sandboxes.get(agentId)
  const sessionId = `preview-${agentId}`

  // Reuse existing session if available
  if (info.previewSessionId === sessionId) {
    console.log(`Reusing existing preview session: ${sessionId}`)
    return
  }

  // Create new session
  info.previewSessionId = sessionId
  await sandbox.process.createSession(sessionId)
  await sandbox.process.executeSessionCommand(sessionId, {
    command: config.command,
    async: true
  }, 0)
}

优势:

  • 先执行“全部预览”,再预览单个 Agent 时会复用服务器
  • 可以多次点击单个 Agent 的预览,而无需重启服务器
  • 重复预览时可减少服务器启动延迟
服务器健康检查

轮询服务器是否就绪,而不是盲目等待:

async waitForServerReady(
  agentId: string,
  port: number,
  maxAttempts: number = 30,
  intervalMs: number = 1000
): Promise<boolean> {
  for (let i = 0; i < maxAttempts; i++) {
    try {
      const response = await sandbox.process.executeCommand(
        `curl -f -s -o /dev/null -w "%{http_code}" http://localhost:${port} || echo "000"`,
        '/workspace',
        undefined,
        5
      )

      const httpCode = response.result.trim()

      // Any HTTP response (200, 404, etc.) indicates server is ready
      if (httpCode !== "000" && httpCode !== "") {
        console.log(`Server ready on port ${port} (HTTP ${httpCode})`)
        return true
      }
    } catch {}

    await new Promise(r => setTimeout(r, intervalMs))
  }

  console.warn(`Server not ready after ${maxAttempts} attempts`)
  return false
}

用法:

await startPreviewServer(agentId, config)
const isReady = await waitForServerReady(agentId, config.port, 30, 1000)
if (!isReady) {
  throw new Error(`Server failed to start on port ${config.port}`)
}
const url = await getPreviewUrl(agentId, config.port)

CLI 安装模式

采用验证、回退和重试逻辑,实现稳健的 CLI 安装:

async installClaudeCLI(agentId: string, maxRetries: number = 3): Promise<void> {
  const installCmd = 'bash -c "set -o pipefail; curl -fsSL https://claude.ai/install.sh | bash"'
  let lastError: Error | null = null

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      // Try curl install first
      const response = await sandbox.process.executeCommand(installCmd)
      if (response.exitCode !== 0) {
        throw new Error(`Install failed: ${response.result}`)
      }

      // Verify installation
      const verifyResponse = await sandbox.process.executeCommand(
        'claude --version', undefined, undefined, 30
      )
      if (verifyResponse.exitCode !== 0) {
        throw new Error('Claude CLI not found after installation')
      }
      return // Success

    } catch (err) {
      // Fallback: try npm install
      try {
        const response = await sandbox.process.executeCommand(
          'npm install -g @anthropic-ai/claude-code',
          undefined, undefined,
          300 // 5 min timeout for npm
        )
        if (response.exitCode !== 0) {
          throw new Error(`NPM install failed: ${response.result}`)
        }

        const verifyResponse = await sandbox.process.executeCommand(
          'claude --version', undefined, undefined, 30
        )
        if (verifyResponse.exitCode !== 0) {
          throw new Error('Claude CLI not found after npm installation')
        }
        return // Success

      } catch (npmErr) {
        lastError = npmErr instanceof Error ? npmErr : new Error(String(npmErr))
        const errorMsg = lastError.message

        // Check if retryable (network errors)
        const isRetryable =
          errorMsg.includes('ETIMEDOUT') ||
          errorMsg.includes('ECONNRESET') ||
          errorMsg.includes('ENOTFOUND') ||
          errorMsg.includes('EAI_AGAIN') ||
          errorMsg.includes('socket hang up')

        if (!isRetryable || attempt >= maxRetries) {
          throw lastError
        }

        // Exponential backoff: 5s, 10s, 20s
        const delayMs = 5000 * Math.pow(2, attempt - 1)
        await new Promise(r => setTimeout(r, delayMs))
      }
    }
  }
  throw lastError || new Error('Installation failed')
}

要点:

  • 在 bash 中使用 set -o pipefail,以传递 curl 故障
  • 安装后通过 --version 检查确认 CLI 可执行
  • 为 npm install 设置超时时间(建议 300s)——网络可能较慢
  • 遇到网络错误(ETIMEDOUT、ECONNRESET 等)时最多重试 3 次
  • 指数退避可避免给故障服务造成过大压力
  • Daytona 沙盒创建后,网络可能需要一些时间才能初始化

最佳实践

  1. 超时:为长时间运行的命令设置适当的超时时间(Claude 为 15 min)
  2. 清理:始终在 finally 块中销毁沙盒
  3. 并行创建:使用 Promise.all() 创建多个沙盒
  4. 会话流式传输:使用基于会话的执行方式获取实时输出(MVP 中未采用)
  5. 临时模式:在需要自动清理的场景中设置 ephemeral: true
  6. CLI 验证:执行安装命令后,始终确认 CLI 工具已安装
  7. 错误传递:遇到非零退出码时抛出异常,以确保正确处理错误

评论

支持 Markdown