关于 daytona-integration
面向使用 Daytona SDK 管理远程沙箱的开发者的 TypeScript 集成指南,涵盖沙箱生命周期、进程执行、文件传输和预览服务器模式,并提供配置与错误处理建议。
主要功能
- 01创建和管理沙箱
- 02配置运行时、资源与环境变量
- 03运行命令、代码和交互会话
- 04通过沙箱文件系统传输文件
- 05处理重试与预览服务器
适用场景
- 01在沙箱中运行开发命令
- 02为并行 Agent 创建独立沙箱
- 03上传项目文件并取回输出
- 04启动预览服务器并获取 URL
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ seanchiuai/ multishot --skill daytona-integration安装后在 Claude Code 中输入/daytona-integration确认已加载。
详细介绍
Daytona 集成
安装
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 接口
| 属性 | 类型 | 必填 | 描述 |
|---|---|---|---|
apiKey | string | 否* | API 密钥身份验证 |
apiUrl | string | 否 | API 端点(默认:https://app.daytona.io/api) |
jwtToken | string | 否* | JWT 身份验证(需要 organizationId) |
organizationId | string | 否 | 使用 JWT 时必需 |
target | string | 否 | 沙盒位置偏好 |
*必须提供 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' | 运行时语言 |
envVars | Record<string, string> | {} | 环境变量 |
autoStopInterval | number | 15 | 空闲多少分钟后停止 |
autoArchiveInterval | number | 10080 | 多少分钟后归档 |
autoDeleteInterval | number | - | 多少分钟后删除 |
labels | Record<string, string> | - | 元数据标签 |
ephemeral | boolean | false | 停止时自动清理 |
volumes | VolumeMount[] | - | 卷挂载 |
image | string | Image | - | 自定义 Docker 镜像 |
resources | Resources | - | CPU/内存/磁盘分配 |
snapshot | string | - | 从快照 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 类
属性
| 属性 | 类型 | 描述 |
|---|---|---|
id | string | 唯一的沙盒标识符 |
name | string | 沙盒名称 |
state | SandboxState | 'started' | 'stopped' | ... |
cpu | number | CPU 数量 |
memory | number | 内存大小(GiB) |
disk | number | 磁盘空间(GiB) |
gpu | number | GPU 数量 |
env | Record<string, string> | 环境变量 |
labels | Record<string, string> | 元数据 |
process | Process | 进程执行接口 |
fs | FileSystem | 文件系统接口 |
git | Git | Git 操作接口 |
方法
// 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 接口
| 属性 | 类型 | 描述 |
|---|---|---|
exitCode | number | 进程退出状态 |
result | string | stdout 内容 |
artifacts | ExecutionArtifacts | 附加数据(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 沙盒创建后,网络可能需要一些时间才能初始化
最佳实践
- 超时:为长时间运行的命令设置适当的超时时间(Claude 为 15 min)
- 清理:始终在 finally 块中销毁沙盒
- 并行创建:使用 Promise.all() 创建多个沙盒
- 会话流式传输:使用基于会话的执行方式获取实时输出(MVP 中未采用)
- 临时模式:在需要自动清理的场景中设置
ephemeral: true - CLI 验证:执行安装命令后,始终确认 CLI 工具已安装
- 错误传递:遇到非零退出码时抛出异常,以确保正确处理错误