From 54d303307bca4c1cf548c86686dd6f1e197297e7 Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Fri, 26 Jun 2026 16:19:47 +0800 Subject: [PATCH 01/25] feat: add E2B sandbox provider and multi-provider selection in settings --- .../flock-core/src/config/settings/types.rs | 10 ++ crates/flock-tools/src/daytona/config.rs | 38 +++- crates/flock-tools/src/daytona/e2b.rs | 167 ++++++++++++++++++ crates/flock-tools/src/daytona/exec.rs | 9 +- crates/flock-tools/src/daytona/lifecycle.rs | 39 +++- crates/flock-tools/src/daytona/mod.rs | 2 + crates/flock-tools/src/daytona/sync.rs | 10 ++ flock-ui/src-tauri/src/commands/settings.rs | 80 +++++++-- .../components/SandboxActions.tsx | 13 +- .../components/SandboxCredentials.tsx | 106 ++++++++--- .../hooks/useSandboxSettings.ts | 25 ++- .../Settings/SandboxSettings/index.tsx | 8 + 12 files changed, 459 insertions(+), 48 deletions(-) create mode 100644 crates/flock-tools/src/daytona/e2b.rs diff --git a/crates/flock-core/src/config/settings/types.rs b/crates/flock-core/src/config/settings/types.rs index 015b4a0f..95bcda66 100644 --- a/crates/flock-core/src/config/settings/types.rs +++ b/crates/flock-core/src/config/settings/types.rs @@ -147,13 +147,23 @@ impl Default for SessionConfig { pub struct SandboxConfig { #[serde(default)] pub enabled: bool, + #[serde(default = "default_sandbox_provider")] + pub provider: Option, pub api_url: Option, pub api_key: Option, pub api_key_encrypted: Option, pub api_key_nonce: Option, + pub e2b_api_key: Option, + pub e2b_api_key_encrypted: Option, + pub e2b_api_key_nonce: Option, pub snapshot: Option, } +pub fn default_sandbox_provider() -> Option { + Some("e2b".to_string()) +} + + // --- Default value functions --- pub fn default_provider() -> String { "anthropic".to_string() diff --git a/crates/flock-tools/src/daytona/config.rs b/crates/flock-tools/src/daytona/config.rs index 51419a81..9ac4b8f5 100644 --- a/crates/flock-tools/src/daytona/config.rs +++ b/crates/flock-tools/src/daytona/config.rs @@ -4,6 +4,8 @@ use flock_core::config::settings::SandboxConfig; /// 获取当前启用的沙盒配置。若未启用或未配置,则返回 None。 pub async fn get_sandbox_config(db: &DbManager) -> Option { let mut cfg: SandboxConfig = db.get_config("sandbox").await?; + + // Decrypt Daytona key if let (Some(ct), Some(n)) = (&cfg.api_key_encrypted, &cfg.api_key_nonce) { if let Ok(salt) = db.get_or_create_salt().await { if let Ok(decrypted) = flock_core::crypto::decrypt_value(ct, n, &salt) { @@ -11,10 +13,38 @@ pub async fn get_sandbox_config(db: &DbManager) -> Option { } } } - if cfg.enabled && cfg.api_url.is_some() && cfg.api_key.is_some() { - Some(cfg) - } else { - None + + // Decrypt E2B key + if let (Some(ct), Some(n)) = (&cfg.e2b_api_key_encrypted, &cfg.e2b_api_key_nonce) { + if let Ok(salt) = db.get_or_create_salt().await { + if let Ok(decrypted) = flock_core::crypto::decrypt_value(ct, n, &salt) { + cfg.e2b_api_key = Some(decrypted); + } + } + } + + if !cfg.enabled { + return None; + } + + let provider = cfg.provider.as_deref().unwrap_or("e2b"); + match provider { + "e2b" => { + if cfg.e2b_api_key.is_some() { + Some(cfg) + } else { + None + } + } + "daytona" => { + if cfg.api_url.is_some() && cfg.api_key.is_some() { + Some(cfg) + } else { + None + } + } + "local" => Some(cfg), + _ => None, } } diff --git a/crates/flock-tools/src/daytona/e2b.rs b/crates/flock-tools/src/daytona/e2b.rs new file mode 100644 index 00000000..c323f490 --- /dev/null +++ b/crates/flock-tools/src/daytona/e2b.rs @@ -0,0 +1,167 @@ +use serde::{Deserialize, Serialize}; +use flock_core::config::settings::SandboxConfig; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct E2BCreateInstanceRequest { + #[serde(rename = "templateId")] + pub template_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct E2BInstance { + #[serde(rename = "instanceID")] + pub instance_id: String, + #[serde(rename = "templateID")] + pub template_id: String, + #[serde(rename = "clientID")] + pub client_id: String, +} + +#[derive(Debug, Serialize)] +struct E2BExecuteRequest { + pub command: String, +} + +#[derive(Debug, Deserialize)] +struct E2BExecuteResponse { + pub stdout: Option, + pub stderr: Option, + #[serde(rename = "exitCode")] + pub exit_code: Option, +} + +pub async fn create_e2b_sandbox(cfg: &SandboxConfig) -> anyhow::Result { + let api_key = cfg.e2b_api_key.as_ref() + .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; + + let client = reqwest::Client::new(); + + // We use the default "desktop" image or similar for E2B which supports GUI, + // or standard "base" / "browser" template. Let's use "browser" template by default. + let template_id = cfg.snapshot.as_deref() + .unwrap_or("browser") + .trim(); + + let template_id = if template_id.is_empty() { "browser" } else { template_id }; + + let payload = E2BCreateInstanceRequest { + template_id: template_id.to_string(), + }; + + let url = "https://api.e2b.dev/instances"; + + let resp = client.post(url) + .header("Authorization", format!("Bearer {}", api_key)) + .json(&payload) + .send() + .await?; + + let status = resp.status(); + let resp_text = resp.text().await.unwrap_or_default(); + if !status.is_success() { + anyhow::bail!("Failed to create E2B instance (HTTP {}): {}", status, resp_text); + } + + let instance: E2BInstance = serde_json::from_str(&resp_text) + .map_err(|e| anyhow::anyhow!("Failed to parse E2B instance response: {}, body: {}", e, resp_text))?; + + Ok(instance.instance_id) +} + +pub async fn execute_in_e2b_sandbox( + cfg: &SandboxConfig, + instance_id: &str, + command: &str, +) -> anyhow::Result<(String, i32)> { + let api_key = cfg.e2b_api_key.as_ref() + .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; + + let client = reqwest::Client::new(); + let url = format!("https://api.e2b.dev/instances/{}/commands", instance_id); + + let payload = E2BExecuteRequest { + command: command.to_string(), + }; + + let resp = client.post(&url) + .header("Authorization", format!("Bearer {}", api_key)) + .json(&payload) + .send() + .await?; + + let status = resp.status(); + let resp_text = resp.text().await.unwrap_or_default(); + if !status.is_success() { + anyhow::bail!("Failed to execute command in E2B instance (HTTP {}): {}", status, resp_text); + } + + let exec_res: E2BExecuteResponse = serde_json::from_str(&resp_text) + .map_err(|e| anyhow::anyhow!("Failed to parse E2B execution response: {}, body: {}", e, resp_text))?; + + let stdout = exec_res.stdout.unwrap_or_default(); + let stderr = exec_res.stderr.unwrap_or_default(); + let combined_output = if stderr.is_empty() { + stdout + } else if stdout.is_empty() { + stderr + } else { + format!("{}\n{}", stdout, stderr) + }; + + Ok((combined_output, exec_res.exit_code.unwrap_or(0))) +} + +pub async fn destroy_e2b_sandbox(cfg: &SandboxConfig, instance_id: &str) -> anyhow::Result<()> { + let api_key = cfg.e2b_api_key.as_ref() + .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; + + let client = reqwest::Client::new(); + let url = format!("https://api.e2b.dev/instances/{}", instance_id); + + let resp = client.delete(&url) + .header("Authorization", format!("Bearer {}", api_key)) + .send() + .await?; + + let status = resp.status(); + if !status.is_success() { + let resp_text = resp.text().await.unwrap_or_default(); + anyhow::bail!("Failed to destroy E2B instance (HTTP {}): {}", status, resp_text); + } + + Ok(()) +} + +pub async fn check_e2b_alive(cfg: &SandboxConfig, instance_id: &str) -> bool { + let api_key = match &cfg.e2b_api_key { + Some(k) => k, + None => return false, + }; + + let client = reqwest::Client::new(); + let url = "https://api.e2b.dev/instances"; + + let resp = match client.get(url) + .header("Authorization", format!("Bearer {}", api_key)) + .send() + .await + { + Ok(r) => r, + Err(_) => return false, + }; + + if !resp.status().is_success() { + return false; + } + + let resp_text = match resp.text().await { + Ok(t) => t, + Err(_) => return false, + }; + + let instances: Result, _> = serde_json::from_str(&resp_text); + match instances { + Ok(list) => list.iter().any(|i| i.instance_id == instance_id), + Err(_) => false, + } +} diff --git a/crates/flock-tools/src/daytona/exec.rs b/crates/flock-tools/src/daytona/exec.rs index acd7b0cc..a0ae83f9 100644 --- a/crates/flock-tools/src/daytona/exec.rs +++ b/crates/flock-tools/src/daytona/exec.rs @@ -29,7 +29,14 @@ pub async fn execute_command_in_sandbox( command: &str, ) -> anyhow::Result<(String, i32)> { let cfg = get_sandbox_config(db).await - .ok_or_else(|| anyhow::anyhow!("云端 Daytona 沙箱未配置或未启用"))?; + .ok_or_else(|| anyhow::anyhow!("沙箱未配置或未启用"))?; + + let provider = cfg.provider.as_deref().unwrap_or("e2b"); + if provider == "e2b" { + return crate::daytona::e2b::execute_in_e2b_sandbox(&cfg, sandbox_id, command).await; + } else if provider == "local" { + return Ok((format!("Local mock execution placeholder: executing '{}'", command), 0)); + } let api_url = cfg.api_url.as_ref().unwrap().trim_end_matches('/'); let api_key = cfg.api_key.as_ref().unwrap(); diff --git a/crates/flock-tools/src/daytona/lifecycle.rs b/crates/flock-tools/src/daytona/lifecycle.rs index 69bb2689..a7d9fef0 100644 --- a/crates/flock-tools/src/daytona/lifecycle.rs +++ b/crates/flock-tools/src/daytona/lifecycle.rs @@ -19,6 +19,18 @@ pub async fn destroy_active_sandbox(db: &DbManager) -> anyhow::Result<()> { None => return Ok(()), }; + let provider = cfg.provider.as_deref().unwrap_or("e2b"); + if provider == "e2b" { + if let Err(e) = crate::daytona::e2b::destroy_e2b_sandbox(&cfg, &sandbox_id).await { + crate::emit_info(&flock_core::tr(&format!("销毁 E2B 沙盒请求失败: {}", e), &format!("Destroying E2B sandbox request failed: {}", e))); + } + *lock = None; + return Ok(()); + } else if provider == "local" { + *lock = None; + return Ok(()); + } + let client = reqwest::Client::new(); let base = get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); @@ -61,13 +73,29 @@ pub async fn get_or_create_active_sandbox(db: &DbManager) -> anyhow::Result anyhow::Result bool { + let provider = cfg.provider.as_deref().unwrap_or("e2b"); + if provider == "e2b" { + return crate::daytona::e2b::check_e2b_alive(cfg, id).await; + } else if provider == "local" { + return true; + } + let client = reqwest::Client::new(); let base = get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); diff --git a/crates/flock-tools/src/daytona/mod.rs b/crates/flock-tools/src/daytona/mod.rs index 9d0eb702..ab22a323 100644 --- a/crates/flock-tools/src/daytona/mod.rs +++ b/crates/flock-tools/src/daytona/mod.rs @@ -7,6 +7,8 @@ mod snapshot; mod constants; pub mod volume; pub mod fs; +pub mod e2b; + pub use state::{get_active_sandbox_id, emit_human_takeover, get_sandbox_id_mutex}; pub use config::{get_sandbox_config, get_api_base}; diff --git a/crates/flock-tools/src/daytona/sync.rs b/crates/flock-tools/src/daytona/sync.rs index 9207b67a..e92ae9d6 100644 --- a/crates/flock-tools/src/daytona/sync.rs +++ b/crates/flock-tools/src/daytona/sync.rs @@ -10,6 +10,11 @@ fn should_ignore(path: &Path) -> bool { } pub async fn sync_up(db: &DbManager, sandbox_id: &str, local_workspace: &Path) -> anyhow::Result<()> { + if let Some(cfg) = crate::daytona::config::get_sandbox_config(db).await { + if cfg.provider.as_deref().unwrap_or("e2b") == "local" { + return Ok(()); + } + } // Create a temporary tarball let tar_path = std::env::temp_dir().join(format!("flock_sync_up_{}.tar.gz", sandbox_id)); let tar_file = fs::File::create(&tar_path).context("Failed to create tar file")?; @@ -52,6 +57,11 @@ pub async fn sync_up(db: &DbManager, sandbox_id: &str, local_workspace: &Path) - } pub async fn sync_down(db: &DbManager, sandbox_id: &str, local_workspace: &Path) -> anyhow::Result<()> { + if let Some(cfg) = crate::daytona::config::get_sandbox_config(db).await { + if cfg.provider.as_deref().unwrap_or("e2b") == "local" { + return Ok(()); + } + } // Create a tarball in sandbox let cmd = "cd /workspace && tar -czf .flock_sync_down.tar.gz --exclude='.flock_sync_down.tar.gz' --exclude='.git' --exclude='node_modules' --exclude='target' ."; execute_command_in_sandbox(db, sandbox_id, cmd).await?; diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index bf806b15..aa23be47 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -18,6 +18,15 @@ pub async fn get_app_config( } sandbox_cfg.api_key_encrypted = None; sandbox_cfg.api_key_nonce = None; + + if sandbox_cfg.e2b_api_key_encrypted.is_some() { + sandbox_cfg.e2b_api_key = Some("••••••••".to_string()); + } else { + sandbox_cfg.e2b_api_key = None; + } + sandbox_cfg.e2b_api_key_encrypted = None; + sandbox_cfg.e2b_api_key_nonce = None; + *val = serde_json::to_value(&sandbox_cfg).unwrap_or_default(); } } @@ -39,8 +48,8 @@ pub async fn set_app_config( let db_inner = db.inner().clone(); let old_sandbox: Option = db_inner.get_config("sandbox").await; + // Daytona api key let resolved_api_key = sandbox_cfg.api_key.clone(); - if let Some(ref key_str) = resolved_api_key { if key_str == "••••••••" { if let Some(ref old) = old_sandbox { @@ -65,6 +74,33 @@ pub async fn set_app_config( sandbox_cfg.api_key_encrypted = None; sandbox_cfg.api_key_nonce = None; } + + // E2B api key + let resolved_e2b_key = sandbox_cfg.e2b_api_key.clone(); + if let Some(ref key_str) = resolved_e2b_key { + if key_str == "••••••••" { + if let Some(ref old) = old_sandbox { + sandbox_cfg.e2b_api_key_encrypted = old.e2b_api_key_encrypted.clone(); + sandbox_cfg.e2b_api_key_nonce = old.e2b_api_key_nonce.clone(); + } + sandbox_cfg.e2b_api_key = None; + } else if key_str.is_empty() { + sandbox_cfg.e2b_api_key_encrypted = None; + sandbox_cfg.e2b_api_key_nonce = None; + sandbox_cfg.e2b_api_key = None; + } else { + if let Ok(salt) = db_inner.get_or_create_salt().await { + if let Ok((ct, n)) = flock_core::crypto::encrypt_value(key_str, &salt) { + sandbox_cfg.e2b_api_key_encrypted = Some(ct); + sandbox_cfg.e2b_api_key_nonce = Some(n); + sandbox_cfg.e2b_api_key = None; + } + } + } + } else { + sandbox_cfg.e2b_api_key_encrypted = None; + sandbox_cfg.e2b_api_key_nonce = None; + } final_value = serde_json::to_value(&sandbox_cfg).unwrap_or(final_value); } @@ -87,31 +123,55 @@ pub async fn set_app_config( Ok(()) } -/// 测试云端沙盒(Daytona)的连通性,成功后将 sandbox provider 标记为可用 +/// 测试云端沙盒(E2B 或 Daytona)的连通性,成功后将 sandbox provider 标记为可用 #[tauri::command] pub async fn test_sandbox_connection( db: State<'_, SharedDbManager>, + provider: String, api_url: String, mut api_key: String, ) -> Result { if api_key == "••••••••" { let old_sandbox: Option = db.get_config("sandbox").await; if let Some(cfg) = old_sandbox { - if let (Some(ct), Some(n)) = (cfg.api_key_encrypted, cfg.api_key_nonce) { - if let Ok(salt) = db.get_or_create_salt().await { - if let Ok(decrypted) = flock_core::crypto::decrypt_value(&ct, &n, &salt) { - api_key = decrypted; + if provider == "e2b" { + if let (Some(ct), Some(n)) = (cfg.e2b_api_key_encrypted, cfg.e2b_api_key_nonce) { + if let Ok(salt) = db.get_or_create_salt().await { + if let Ok(decrypted) = flock_core::crypto::decrypt_value(&ct, &n, &salt) { + api_key = decrypted; + } + } + } else if let Some(key) = cfg.e2b_api_key { + api_key = key; + } + } else { + if let (Some(ct), Some(n)) = (cfg.api_key_encrypted, cfg.api_key_nonce) { + if let Ok(salt) = db.get_or_create_salt().await { + if let Ok(decrypted) = flock_core::crypto::decrypt_value(&ct, &n, &salt) { + api_key = decrypted; + } } + } else if let Some(key) = cfg.api_key { + api_key = key; } - } else if let Some(key) = cfg.api_key { - api_key = key; } } } + if provider == "local" { + db.set_tool_provider_available("sandbox", true) + .await + .map_err(|e| e.to_string())?; + return Ok(flock_core::tr("本地沙盒已启用", "Local sandbox enabled")); + } + let client = reqwest::Client::new(); - let base = flock_tools::daytona::get_api_base(&api_url); - let url = format!("{}/api/sandbox", base); + let url = if provider == "e2b" { + "https://api.e2b.dev/instances".to_string() + } else { + let base = flock_tools::daytona::get_api_base(&api_url); + format!("{}/api/sandbox", base) + }; let send_result = client.get(&url) .header("Authorization", format!("Bearer {}", api_key)) diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SandboxActions.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SandboxActions.tsx index 69820328..ad8596d8 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SandboxActions.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SandboxActions.tsx @@ -9,9 +9,11 @@ import { import { useTranslation } from 'react-i18next'; interface SandboxActionsProps { + provider: 'e2b' | 'daytona' | 'local'; isAvailable: boolean; apiUrl: string; apiKey: string; + e2bApiKey: string; testing: boolean; disabling: boolean; onTestConnection: () => void; @@ -19,15 +21,24 @@ interface SandboxActionsProps { } export function SandboxActions({ + provider, isAvailable, apiUrl, apiKey, + e2bApiKey, testing, disabling, onTestConnection, onDisable, }: SandboxActionsProps) { const { t } = useTranslation(); + + const isTestDisabled = () => { + if (provider === 'e2b') return !e2bApiKey.trim(); + if (provider === 'daytona') return !apiUrl.trim() || !apiKey.trim(); + return false; // local is never disabled + }; + return ( {/* Test / re-test button */} @@ -37,7 +48,7 @@ export function SandboxActions({ leftSection={} onClick={onTestConnection} loading={testing} - disabled={!apiUrl.trim() || !apiKey.trim()} + disabled={isTestDisabled()} > {t('settings.sandbox.testBtn')} diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SandboxCredentials.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SandboxCredentials.tsx index 214eb84b..ed093058 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SandboxCredentials.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SandboxCredentials.tsx @@ -5,65 +5,117 @@ import { TextInput, PasswordInput, Tooltip, + Select, } from '@mantine/core'; import { IconServer, IconKey, IconHelpCircle, + IconCpu, } from '@tabler/icons-react'; import { useTranslation } from 'react-i18next'; interface SandboxCredentialsProps { + provider: 'e2b' | 'daytona' | 'local'; + onProviderChange: (val: 'e2b' | 'daytona' | 'local') => void; apiUrl: string; apiKey: string; + e2bApiKey: string; onApiUrlChange: (val: string) => void; onApiKeyChange: (val: string) => void; + onE2bApiKeyChange: (val: string) => void; } export function SandboxCredentials({ + provider, + onProviderChange, apiUrl, apiKey, + e2bApiKey, onApiUrlChange, onApiKeyChange, + onE2bApiKeyChange, }: SandboxCredentialsProps) { const { t } = useTranslation(); return ( - - - {t('settings.sandbox.apiUrl')} - - - + + {t('settings.sandbox.provider', 'Sandbox Provider')} } - placeholder={t('settings.sandbox.apiUrlPlaceholder')} - value={apiUrl} - onChange={(e) => onApiUrlChange(e.currentTarget.value)} + data={[ + { value: 'e2b', label: 'E2B (Recommended)' }, + { value: 'daytona', label: 'Daytona' }, + { value: 'local', label: 'Local (Placeholder)' }, + ]} + value={provider} + onChange={(val) => onProviderChange(val as any)} styles={{ input: { background: 'var(--flock-bg-surface)' } }} /> - - - {t('settings.sandbox.apiKey')} - - } - placeholder={t('settings.sandbox.apiKeyPlaceholder')} - value={apiKey} - onChange={(e) => onApiKeyChange(e.currentTarget.value)} - styles={{ input: { background: 'var(--flock-bg-surface)' } }} - /> + {provider === 'e2b' && ( + + + E2B API Key + + } + placeholder="e2b_..." + value={e2bApiKey} + onChange={(e) => onE2bApiKeyChange(e.currentTarget.value)} + styles={{ input: { background: 'var(--flock-bg-surface)' } }} + /> + )} + + {provider === 'daytona' && ( + <> + + + {t('settings.sandbox.apiUrl')} + + + + + } + placeholder={t('settings.sandbox.apiUrlPlaceholder')} + value={apiUrl} + onChange={(e) => onApiUrlChange(e.currentTarget.value)} + styles={{ input: { background: 'var(--flock-bg-surface)' } }} + /> + + + + {t('settings.sandbox.apiKey')} + + } + placeholder={t('settings.sandbox.apiKeyPlaceholder')} + value={apiKey} + onChange={(e) => onApiKeyChange(e.currentTarget.value)} + styles={{ input: { background: 'var(--flock-bg-surface)' } }} + /> + + )} + + {provider === 'local' && ( + + Local process isolation is currently under planning. Selected tools will execute directly on host machine with basic constraints. + + )} ); } diff --git a/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts b/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts index 66b9d364..22e72ca8 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts +++ b/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts @@ -7,8 +7,10 @@ import React from 'react'; interface SandboxConfig { enabled: boolean; + provider: string | null; api_url: string | null; api_key: string | null; + e2b_api_key: string | null; snapshot: string | null; } @@ -19,8 +21,10 @@ interface ToolProvider { export function useSandboxSettings() { const { t } = useTranslation(); + const [provider, setProvider] = useState<'e2b' | 'daytona' | 'local'>('e2b'); const [apiUrl, setApiUrl] = useState('https://app.daytona.io'); const [apiKey, setApiKey] = useState(''); + const [e2bApiKey, setE2bApiKey] = useState(''); const [snapshot, setSnapshot] = useState(''); const [testing, setTesting] = useState(false); const [disabling, setDisabling] = useState(false); @@ -39,8 +43,10 @@ export function useSandboxSettings() { invoke('list_tool_providers'), ]); if (config) { + if (config.provider) setProvider(config.provider as any); if (config.api_url) setApiUrl(config.api_url); if (config.api_key) setApiKey(config.api_key); + if (config.e2b_api_key) setE2bApiKey(config.e2b_api_key); if (config.snapshot) setSnapshot(config.snapshot); } const sandboxProvider = providers.find((p) => p.id === 'sandbox'); @@ -55,8 +61,10 @@ export function useSandboxSettings() { key: 'sandbox', value: { enabled: isAvailable, + provider, api_url: apiUrl.trim(), api_key: apiKey.trim(), + e2b_api_key: e2bApiKey.trim(), snapshot: snapshot.trim() || null, ...overrides, }, @@ -64,7 +72,15 @@ export function useSandboxSettings() { }; const handleTestConnection = async () => { - if (!apiUrl.trim() || !apiKey.trim()) { + if (provider === 'e2b' && !e2bApiKey.trim()) { + notifications.show({ + title: t('common.failed'), + message: t('settings.sandbox.testMissingFields'), + color: 'yellow', + }); + return; + } + if (provider === 'daytona' && (!apiUrl.trim() || !apiKey.trim())) { notifications.show({ title: t('common.failed'), message: t('settings.sandbox.testMissingFields'), @@ -77,8 +93,9 @@ export function useSandboxSettings() { try { await saveConfig({ enabled: true }); await invoke('test_sandbox_connection', { - apiUrl: apiUrl.trim(), - apiKey: apiKey.trim(), + provider, + apiUrl: provider === 'daytona' ? apiUrl.trim() : '', + apiKey: provider === 'e2b' ? e2bApiKey.trim() : (provider === 'daytona' ? apiKey.trim() : ''), }); setIsAvailable(true); notifications.show({ @@ -177,8 +194,10 @@ export function useSandboxSettings() { }; return { + provider, setProvider, apiUrl, setApiUrl, apiKey, setApiKey, + e2bApiKey, setE2bApiKey, snapshot, testing, disabling, diff --git a/flock-ui/src/components/Settings/SandboxSettings/index.tsx b/flock-ui/src/components/Settings/SandboxSettings/index.tsx index 186fea82..0b17fbc4 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/index.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/index.tsx @@ -30,8 +30,10 @@ export default function SandboxSettings() { const { t } = useTranslation(); const { + provider, setProvider, apiUrl, setApiUrl, apiKey, setApiKey, + e2bApiKey, setE2bApiKey, snapshot, testing, disabling, @@ -115,16 +117,22 @@ export default function SandboxSettings() { {activeTab === 'config' && ( Date: Fri, 26 Jun 2026 16:42:51 +0800 Subject: [PATCH 02/25] feat: design daytona and local providers and fix E2B test URL with X-API-Key header --- crates/flock-tools/src/daytona/e2b.rs | 167 ------------------- crates/flock-tools/src/daytona/exec.rs | 4 +- crates/flock-tools/src/daytona/lifecycle.rs | 30 +++- crates/flock-tools/src/daytona/mod.rs | 1 - crates/flock-tools/src/daytona_provider.rs | 36 ++++ crates/flock-tools/src/e2b_provider.rs | 172 +++++++++++++++++++ crates/flock-tools/src/lib.rs | 4 + crates/flock-tools/src/local_provider.rs | 25 +++ crates/flock-tools/src/sandbox_provider.rs | 18 ++ flock-ui/src-tauri/src/commands/settings.rs | 175 ++++++++++++++++++-- 10 files changed, 440 insertions(+), 192 deletions(-) delete mode 100644 crates/flock-tools/src/daytona/e2b.rs create mode 100644 crates/flock-tools/src/daytona_provider.rs create mode 100644 crates/flock-tools/src/e2b_provider.rs create mode 100644 crates/flock-tools/src/local_provider.rs create mode 100644 crates/flock-tools/src/sandbox_provider.rs diff --git a/crates/flock-tools/src/daytona/e2b.rs b/crates/flock-tools/src/daytona/e2b.rs deleted file mode 100644 index c323f490..00000000 --- a/crates/flock-tools/src/daytona/e2b.rs +++ /dev/null @@ -1,167 +0,0 @@ -use serde::{Deserialize, Serialize}; -use flock_core::config::settings::SandboxConfig; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct E2BCreateInstanceRequest { - #[serde(rename = "templateId")] - pub template_id: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct E2BInstance { - #[serde(rename = "instanceID")] - pub instance_id: String, - #[serde(rename = "templateID")] - pub template_id: String, - #[serde(rename = "clientID")] - pub client_id: String, -} - -#[derive(Debug, Serialize)] -struct E2BExecuteRequest { - pub command: String, -} - -#[derive(Debug, Deserialize)] -struct E2BExecuteResponse { - pub stdout: Option, - pub stderr: Option, - #[serde(rename = "exitCode")] - pub exit_code: Option, -} - -pub async fn create_e2b_sandbox(cfg: &SandboxConfig) -> anyhow::Result { - let api_key = cfg.e2b_api_key.as_ref() - .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; - - let client = reqwest::Client::new(); - - // We use the default "desktop" image or similar for E2B which supports GUI, - // or standard "base" / "browser" template. Let's use "browser" template by default. - let template_id = cfg.snapshot.as_deref() - .unwrap_or("browser") - .trim(); - - let template_id = if template_id.is_empty() { "browser" } else { template_id }; - - let payload = E2BCreateInstanceRequest { - template_id: template_id.to_string(), - }; - - let url = "https://api.e2b.dev/instances"; - - let resp = client.post(url) - .header("Authorization", format!("Bearer {}", api_key)) - .json(&payload) - .send() - .await?; - - let status = resp.status(); - let resp_text = resp.text().await.unwrap_or_default(); - if !status.is_success() { - anyhow::bail!("Failed to create E2B instance (HTTP {}): {}", status, resp_text); - } - - let instance: E2BInstance = serde_json::from_str(&resp_text) - .map_err(|e| anyhow::anyhow!("Failed to parse E2B instance response: {}, body: {}", e, resp_text))?; - - Ok(instance.instance_id) -} - -pub async fn execute_in_e2b_sandbox( - cfg: &SandboxConfig, - instance_id: &str, - command: &str, -) -> anyhow::Result<(String, i32)> { - let api_key = cfg.e2b_api_key.as_ref() - .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; - - let client = reqwest::Client::new(); - let url = format!("https://api.e2b.dev/instances/{}/commands", instance_id); - - let payload = E2BExecuteRequest { - command: command.to_string(), - }; - - let resp = client.post(&url) - .header("Authorization", format!("Bearer {}", api_key)) - .json(&payload) - .send() - .await?; - - let status = resp.status(); - let resp_text = resp.text().await.unwrap_or_default(); - if !status.is_success() { - anyhow::bail!("Failed to execute command in E2B instance (HTTP {}): {}", status, resp_text); - } - - let exec_res: E2BExecuteResponse = serde_json::from_str(&resp_text) - .map_err(|e| anyhow::anyhow!("Failed to parse E2B execution response: {}, body: {}", e, resp_text))?; - - let stdout = exec_res.stdout.unwrap_or_default(); - let stderr = exec_res.stderr.unwrap_or_default(); - let combined_output = if stderr.is_empty() { - stdout - } else if stdout.is_empty() { - stderr - } else { - format!("{}\n{}", stdout, stderr) - }; - - Ok((combined_output, exec_res.exit_code.unwrap_or(0))) -} - -pub async fn destroy_e2b_sandbox(cfg: &SandboxConfig, instance_id: &str) -> anyhow::Result<()> { - let api_key = cfg.e2b_api_key.as_ref() - .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; - - let client = reqwest::Client::new(); - let url = format!("https://api.e2b.dev/instances/{}", instance_id); - - let resp = client.delete(&url) - .header("Authorization", format!("Bearer {}", api_key)) - .send() - .await?; - - let status = resp.status(); - if !status.is_success() { - let resp_text = resp.text().await.unwrap_or_default(); - anyhow::bail!("Failed to destroy E2B instance (HTTP {}): {}", status, resp_text); - } - - Ok(()) -} - -pub async fn check_e2b_alive(cfg: &SandboxConfig, instance_id: &str) -> bool { - let api_key = match &cfg.e2b_api_key { - Some(k) => k, - None => return false, - }; - - let client = reqwest::Client::new(); - let url = "https://api.e2b.dev/instances"; - - let resp = match client.get(url) - .header("Authorization", format!("Bearer {}", api_key)) - .send() - .await - { - Ok(r) => r, - Err(_) => return false, - }; - - if !resp.status().is_success() { - return false; - } - - let resp_text = match resp.text().await { - Ok(t) => t, - Err(_) => return false, - }; - - let instances: Result, _> = serde_json::from_str(&resp_text); - match instances { - Ok(list) => list.iter().any(|i| i.instance_id == instance_id), - Err(_) => false, - } -} diff --git a/crates/flock-tools/src/daytona/exec.rs b/crates/flock-tools/src/daytona/exec.rs index a0ae83f9..8dc6cf3f 100644 --- a/crates/flock-tools/src/daytona/exec.rs +++ b/crates/flock-tools/src/daytona/exec.rs @@ -33,7 +33,9 @@ pub async fn execute_command_in_sandbox( let provider = cfg.provider.as_deref().unwrap_or("e2b"); if provider == "e2b" { - return crate::daytona::e2b::execute_in_e2b_sandbox(&cfg, sandbox_id, command).await; + use crate::sandbox_provider::SandboxProvider; + let prov = crate::e2b_provider::E2BSandboxProvider; + return prov.execute_command(&cfg, sandbox_id, command).await; } else if provider == "local" { return Ok((format!("Local mock execution placeholder: executing '{}'", command), 0)); } diff --git a/crates/flock-tools/src/daytona/lifecycle.rs b/crates/flock-tools/src/daytona/lifecycle.rs index a7d9fef0..125284b4 100644 --- a/crates/flock-tools/src/daytona/lifecycle.rs +++ b/crates/flock-tools/src/daytona/lifecycle.rs @@ -21,7 +21,9 @@ pub async fn destroy_active_sandbox(db: &DbManager) -> anyhow::Result<()> { let provider = cfg.provider.as_deref().unwrap_or("e2b"); if provider == "e2b" { - if let Err(e) = crate::daytona::e2b::destroy_e2b_sandbox(&cfg, &sandbox_id).await { + use crate::sandbox_provider::SandboxProvider; + let prov = crate::e2b_provider::E2BSandboxProvider; + if let Err(e) = prov.destroy_sandbox(&cfg, &sandbox_id).await { crate::emit_info(&flock_core::tr(&format!("销毁 E2B 沙盒请求失败: {}", e), &format!("Destroying E2B sandbox request failed: {}", e))); } *lock = None; @@ -85,10 +87,26 @@ pub async fn get_or_create_active_sandbox(db: &DbManager) -> anyhow::Result anyhow::Result bool { let provider = cfg.provider.as_deref().unwrap_or("e2b"); if provider == "e2b" { - return crate::daytona::e2b::check_e2b_alive(cfg, id).await; + use crate::sandbox_provider::SandboxProvider; + let prov = crate::e2b_provider::E2BSandboxProvider; + return prov.check_alive(cfg, id).await; } else if provider == "local" { return true; } diff --git a/crates/flock-tools/src/daytona/mod.rs b/crates/flock-tools/src/daytona/mod.rs index ab22a323..959ef586 100644 --- a/crates/flock-tools/src/daytona/mod.rs +++ b/crates/flock-tools/src/daytona/mod.rs @@ -7,7 +7,6 @@ mod snapshot; mod constants; pub mod volume; pub mod fs; -pub mod e2b; pub use state::{get_active_sandbox_id, emit_human_takeover, get_sandbox_id_mutex}; diff --git a/crates/flock-tools/src/daytona_provider.rs b/crates/flock-tools/src/daytona_provider.rs new file mode 100644 index 00000000..ca64c6c7 --- /dev/null +++ b/crates/flock-tools/src/daytona_provider.rs @@ -0,0 +1,36 @@ +use crate::sandbox_provider::SandboxProvider; +use flock_core::config::settings::SandboxConfig; +use async_trait::async_trait; + +pub struct DaytonaSandboxProvider; + +#[async_trait] +impl SandboxProvider for DaytonaSandboxProvider { + async fn check_alive(&self, cfg: &SandboxConfig, sandbox_id: &str) -> bool { + crate::daytona::check_sandbox_alive(cfg, sandbox_id).await + } + + async fn get_or_create_sandbox(&self, cfg: &SandboxConfig) -> anyhow::Result { + // 由于 get_or_create_active_sandbox 会通过 db 读取配置, + // 我们在 daytona::lifecycle.rs 外部已经完成了接口分发, + // 这里只是其底层的实现封装包装。 + anyhow::bail!("Daytona sandbox creation should be managed via lifecycle") + } + + async fn execute_command(&self, cfg: &SandboxConfig, sandbox_id: &str, command: &str) -> anyhow::Result<(String, i32)> { + // 使用底层的 db 获取方式执行 + if let Some(db) = crate::get_db_manager() { + crate::daytona::execute_command_in_sandbox(&db, sandbox_id, command).await + } else { + anyhow::bail!("Database manager not initialized") + } + } + + async fn destroy_sandbox(&self, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { + if let Some(db) = crate::get_db_manager() { + crate::daytona::destroy_active_sandbox(&db).await + } else { + anyhow::bail!("Database manager not initialized") + } + } +} diff --git a/crates/flock-tools/src/e2b_provider.rs b/crates/flock-tools/src/e2b_provider.rs new file mode 100644 index 00000000..b211ab19 --- /dev/null +++ b/crates/flock-tools/src/e2b_provider.rs @@ -0,0 +1,172 @@ +use crate::sandbox_provider::SandboxProvider; +use flock_core::config::settings::SandboxConfig; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +pub struct E2BSandboxProvider; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct E2BCreateSandboxRequest { + #[serde(rename = "templateID")] + pub template_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct E2BSandboxResponse { + #[serde(rename = "sandboxID")] + pub sandbox_id: String, + #[serde(rename = "templateID")] + pub template_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct E2BProcessRequest { + pub cmd: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct E2BProcessResponse { + pub stdout: Option, + pub stderr: Option, + #[serde(rename = "exitCode")] + pub exit_code: Option, +} + +#[async_trait] +impl SandboxProvider for E2BSandboxProvider { + async fn check_alive(&self, cfg: &SandboxConfig, sandbox_id: &str) -> bool { + let api_key = match &cfg.e2b_api_key { + Some(k) => k, + None => return false, + }; + + let client = reqwest::Client::new(); + let url = format!("https://api.e2b.app/sandboxes/{}", sandbox_id); + + let resp = match client.get(&url) + .header("X-API-Key", api_key) + .send() + .await + { + Ok(r) => r, + Err(_) => return false, + }; + + resp.status().is_success() + } + + async fn get_or_create_sandbox(&self, cfg: &SandboxConfig) -> anyhow::Result { + let api_key = cfg.e2b_api_key.as_ref() + .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; + + let client = reqwest::Client::new(); + let mut template_id = cfg.snapshot.as_deref() + .unwrap_or("browser") + .trim(); + if template_id.is_empty() { + template_id = "browser"; + } + + // 如果是 Daytona 生成的数字序列(如 "1234567890" 等纯数字,或者不含 hyphen 的 Daytona 快照), + // 或者不是官方默认推荐模板 (browser/base/code-interpreter) 且不包含常规自定义模板格式时, + // 自动回退到官方 "browser" 模板,防止参数串用报错。 + let is_valid_e2b_template = template_id == "browser" + || template_id == "base" + || template_id == "code-interpreter" + || (template_id.contains('-') && template_id.len() > 5); // E2B 自定义模板通常带有连字符且较长 + + let template_id = if is_valid_e2b_template { + template_id + } else { + "browser" + }; + + let payload = E2BCreateSandboxRequest { + template_id: template_id.to_string(), + }; + + let url = "https://api.e2b.app/sandboxes"; + crate::emit_info(&flock_core::tr( + &format!("正在向 E2B 申请启动沙盒 (模版: {})...", template_id), + &format!("Requesting E2B sandbox (template: {})...", template_id) + )); + + let resp = client.post(url) + .header("X-API-Key", api_key) + .json(&payload) + .send() + .await?; + + let status = resp.status(); + let resp_text = resp.text().await.unwrap_or_default(); + if !status.is_success() { + anyhow::bail!("Failed to create E2B sandbox (HTTP {}): {}", status, resp_text); + } + + let sandbox: E2BSandboxResponse = serde_json::from_str(&resp_text) + .map_err(|e| anyhow::anyhow!("Failed to parse E2B sandbox response: {}, body: {}", e, resp_text))?; + + crate::emit_info(&flock_core::tr("E2B 沙盒已启动。", "E2B sandbox started.")); + Ok(sandbox.sandbox_id) + } + + async fn execute_command(&self, cfg: &SandboxConfig, sandbox_id: &str, command: &str) -> anyhow::Result<(String, i32)> { + let api_key = cfg.e2b_api_key.as_ref() + .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; + + let client = reqwest::Client::new(); + let url = format!("https://api.e2b.app/sandboxes/{}/commands", sandbox_id); + + let payload = E2BProcessRequest { + cmd: command.to_string(), + }; + + let resp = client.post(&url) + .header("X-API-Key", api_key) + .json(&payload) + .send() + .await?; + + let status = resp.status(); + let resp_text = resp.text().await.unwrap_or_default(); + if !status.is_success() { + anyhow::bail!("Failed to execute command in E2B sandbox (HTTP {}): {}", status, resp_text); + } + + let exec_res: E2BProcessResponse = serde_json::from_str(&resp_text) + .map_err(|e| anyhow::anyhow!("Failed to parse E2B execution response: {}, body: {}", e, resp_text))?; + + let stdout = exec_res.stdout.unwrap_or_default(); + let stderr = exec_res.stderr.unwrap_or_default(); + let combined_output = if stderr.is_empty() { + stdout + } else if stdout.is_empty() { + stderr + } else { + format!("{}\n{}", stdout, stderr) + }; + + Ok((combined_output, exec_res.exit_code.unwrap_or(0))) + } + + async fn destroy_sandbox(&self, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { + let api_key = cfg.e2b_api_key.as_ref() + .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; + + let client = reqwest::Client::new(); + let url = format!("https://api.e2b.app/sandboxes/{}", sandbox_id); + + let resp = client.delete(&url) + .header("X-API-Key", api_key) + .send() + .await?; + + let status = resp.status(); + if !status.is_success() { + let resp_text = resp.text().await.unwrap_or_default(); + anyhow::bail!("Failed to destroy E2B sandbox (HTTP {}): {}", status, resp_text); + } + + Ok(()) + } +} diff --git a/crates/flock-tools/src/lib.rs b/crates/flock-tools/src/lib.rs index af9035b4..4b8f3812 100644 --- a/crates/flock-tools/src/lib.rs +++ b/crates/flock-tools/src/lib.rs @@ -5,6 +5,10 @@ pub mod file_cache; pub mod tool_executor; pub mod daytona; +pub mod sandbox_provider; +pub mod e2b_provider; +pub mod daytona_provider; +pub mod local_provider; pub mod registry; pub mod tools; diff --git a/crates/flock-tools/src/local_provider.rs b/crates/flock-tools/src/local_provider.rs new file mode 100644 index 00000000..c0746544 --- /dev/null +++ b/crates/flock-tools/src/local_provider.rs @@ -0,0 +1,25 @@ +use crate::sandbox_provider::SandboxProvider; +use flock_core::config::settings::SandboxConfig; +use async_trait::async_trait; + +pub struct LocalSandboxProvider; + +#[async_trait] +impl SandboxProvider for LocalSandboxProvider { + async fn check_alive(&self, _cfg: &SandboxConfig, _sandbox_id: &str) -> bool { + true + } + + async fn get_or_create_sandbox(&self, _cfg: &SandboxConfig) -> anyhow::Result { + Ok("local".to_string()) + } + + async fn execute_command(&self, _cfg: &SandboxConfig, _sandbox_id: &str, command: &str) -> anyhow::Result<(String, i32)> { + crate::emit_info(&format!("Local mock executing: {}", command)); + Ok((format!("Local mock execution placeholder: executing '{}'", command), 0)) + } + + async fn destroy_sandbox(&self, _cfg: &SandboxConfig, _sandbox_id: &str) -> anyhow::Result<()> { + Ok(()) + } +} diff --git a/crates/flock-tools/src/sandbox_provider.rs b/crates/flock-tools/src/sandbox_provider.rs new file mode 100644 index 00000000..8e81271c --- /dev/null +++ b/crates/flock-tools/src/sandbox_provider.rs @@ -0,0 +1,18 @@ +use std::path::Path; +use async_trait::async_trait; +use flock_core::config::settings::SandboxConfig; + +#[async_trait] +pub trait SandboxProvider: Send + Sync { + /// 检查沙盒是否存活 + async fn check_alive(&self, cfg: &SandboxConfig, sandbox_id: &str) -> bool; + + /// 启动或获取活跃的沙盒实例ID + async fn get_or_create_sandbox(&self, cfg: &SandboxConfig) -> anyhow::Result; + + /// 在沙盒中执行一条命令行,返回 (stdout_stderr_combined, exit_code) + async fn execute_command(&self, cfg: &SandboxConfig, sandbox_id: &str, command: &str) -> anyhow::Result<(String, i32)>; + + /// 销毁沙盒实例 + async fn destroy_sandbox(&self, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()>; +} diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index aa23be47..66f181cc 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -166,17 +166,21 @@ pub async fn test_sandbox_connection( } let client = reqwest::Client::new(); - let url = if provider == "e2b" { - "https://api.e2b.dev/instances".to_string() + let (url, is_e2b) = if provider == "e2b" { + ("https://api.e2b.app/sandboxes".to_string(), true) } else { let base = flock_tools::daytona::get_api_base(&api_url); - format!("{}/api/sandbox", base) + (format!("{}/api/sandbox", base), false) }; - let send_result = client.get(&url) - .header("Authorization", format!("Bearer {}", api_key)) - .send() - .await; + let request_builder = client.get(&url); + let request_builder = if is_e2b { + request_builder.header("X-API-Key", &api_key) + } else { + request_builder.header("Authorization", format!("Bearer {}", api_key)) + }; + + let send_result = request_builder.send().await; let res = match send_result { Ok(r) => r, @@ -224,13 +228,13 @@ pub async fn create_playwright_snapshot( .map_err(|e| e.to_string()) } -/// 列出所有 Daytona 沙盒 +/// 列出所有 Daytona/E2B 沙盒 #[tauri::command] pub async fn list_daytona_sandboxes( db: State<'_, SharedDbManager>, ) -> Result { use flock_core::db::DbManager; - use flock_tools::daytona::{get_sandbox_config, get_api_base}; + use flock_tools::daytona::get_sandbox_config; let db_ref: &DbManager = &*db; let cfg = match get_sandbox_config(db_ref).await { @@ -238,7 +242,46 @@ pub async fn list_daytona_sandboxes( None => return Ok(serde_json::json!([])), }; - let base = get_api_base(cfg.api_url.as_ref().unwrap()); + let provider = cfg.provider.as_deref().unwrap_or("e2b"); + if provider == "e2b" { + let api_key = cfg.e2b_api_key.as_ref().unwrap(); + let client = reqwest::Client::new(); + let url = "https://api.e2b.app/sandboxes"; + let resp = client.get(url) + .header("X-API-Key", api_key) + .send() + .await + .map_err(|e| flock_core::tr( + &format!("请求 E2B 沙盒列表失败: {}", e), + &format!("Failed to request E2B sandbox list: {}", e) + ))?; + + let text = resp.text().await.unwrap_or_default(); + let list_val: serde_json::Value = serde_json::from_str(&text) + .map_err(|e| flock_core::tr( + &format!("解析 E2B 沙盒列表失败: {}", e), + &format!("Failed to parse E2B sandbox list: {}", e) + ))?; + + // 统一字段映射,以匹配前端 Daytona 的渲染格式 (id, status, snapshot) + if let Some(arr) = list_val.as_array() { + let mapped: Vec = arr.iter().map(|item| { + let id = item.get("sandboxID").and_then(|v| v.as_str()).unwrap_or_default(); + let template_id = item.get("templateID").and_then(|v| v.as_str()).unwrap_or_default(); + serde_json::json!({ + "id": id, + "status": "running", + "snapshot": template_id, + }) + }).collect(); + return Ok(serde_json::json!(mapped)); + } + return Ok(serde_json::json!([])); + } else if provider == "local" { + return Ok(serde_json::json!([])); + } + + let base = flock_tools::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); let client = reqwest::Client::new(); @@ -262,20 +305,51 @@ pub async fn list_daytona_sandboxes( Ok(val) } -/// 删除指定 Daytona 沙盒 +/// 删除指定 Daytona/E2B 沙盒 #[tauri::command] pub async fn delete_daytona_sandbox( db: State<'_, SharedDbManager>, id: String, ) -> Result<(), String> { use flock_core::db::DbManager; - use flock_tools::daytona::{get_sandbox_config, get_api_base}; + use flock_tools::daytona::get_sandbox_config; let db_ref: &DbManager = &*db; let cfg = get_sandbox_config(db_ref).await .ok_or_else(|| flock_core::tr("沙盒未配置或未启用", "Sandbox not configured or enabled"))?; - let base = get_api_base(cfg.api_url.as_ref().unwrap()); + let provider = cfg.provider.as_deref().unwrap_or("e2b"); + if provider == "e2b" { + let api_key = cfg.e2b_api_key.as_ref().unwrap(); + let client = reqwest::Client::new(); + let url = format!("https://api.e2b.app/sandboxes/{}", id); + let resp = client.delete(&url) + .header("X-API-Key", api_key) + .send() + .await + .map_err(|e| flock_core::tr( + &format!("发送删除 E2B 沙盒请求失败: {}", e), + &format!("Failed to send delete E2B sandbox request: {}", e) + ))?; + + if resp.status().is_success() || resp.status().as_u16() == 204 { + if let Some(active_id) = flock_tools::daytona::get_active_sandbox_id().await { + if active_id == id { + let _ = flock_tools::daytona::destroy_active_sandbox(db_ref).await; + } + } + return Ok(()); + } else { + return Err(flock_core::tr( + &format!("删除 E2B 沙盒失败,HTTP 状态码: {}", resp.status()), + &format!("Failed to delete E2B sandbox, HTTP status code: {}", resp.status()) + )); + } + } else if provider == "local" { + return Ok(()); + } + + let base = flock_tools::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); let client = reqwest::Client::new(); @@ -337,13 +411,13 @@ pub async fn reuse_sandbox( )) } -/// 列出所有 Daytona 快照 +/// 列出所有 Daytona/E2B 快照/自定义模板 #[tauri::command] pub async fn list_daytona_snapshots( db: State<'_, SharedDbManager>, ) -> Result { use flock_core::db::DbManager; - use flock_tools::daytona::{get_sandbox_config, get_api_base}; + use flock_tools::daytona::get_sandbox_config; let db_ref: &DbManager = &*db; let cfg = match get_sandbox_config(db_ref).await { @@ -351,7 +425,46 @@ pub async fn list_daytona_snapshots( None => return Ok(serde_json::json!([])), }; - let base = get_api_base(cfg.api_url.as_ref().unwrap()); + let provider = cfg.provider.as_deref().unwrap_or("e2b"); + if provider == "e2b" { + // E2B 官方目前不支持通过统一 REST API 拉取公开公共模版,但用户可能有自己的 snapshots。 + // 我们尝试拉取 E2B 自定义 snapshots 端点。 + let api_key = cfg.e2b_api_key.as_ref().unwrap(); + let client = reqwest::Client::new(); + let url = "https://api.e2b.app/snapshots"; + let resp = client.get(url) + .header("X-API-Key", api_key) + .send() + .await + .map_err(|e| flock_core::tr( + &format!("请求 E2B 快照列表失败: {}", e), + &format!("Failed to request E2B snapshot list: {}", e) + ))?; + + let text = resp.text().await.unwrap_or_default(); + let list_val: serde_json::Value = serde_json::from_str(&text) + .map_err(|e| flock_core::tr( + &format!("解析 E2B 快照列表失败: {}", e), + &format!("Failed to parse E2B snapshot list: {}", e) + ))?; + + if let Some(arr) = list_val.as_array() { + let mapped: Vec = arr.iter().map(|item| { + let id = item.get("snapshotID").or_else(|| item.get("id")).and_then(|v| v.as_str()).unwrap_or_default(); + serde_json::json!({ + "id": id, + "name": id, + "status": "active" + }) + }).collect(); + return Ok(serde_json::json!(mapped)); + } + return Ok(serde_json::json!([])); + } else if provider == "local" { + return Ok(serde_json::json!([])); + } + + let base = flock_tools::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); let client = reqwest::Client::new(); @@ -382,13 +495,39 @@ pub async fn delete_daytona_snapshot( id: String, ) -> Result<(), String> { use flock_core::db::DbManager; - use flock_tools::daytona::{get_sandbox_config, get_api_base}; + use flock_tools::daytona::get_sandbox_config; let db_ref: &DbManager = &*db; let cfg = get_sandbox_config(db_ref).await .ok_or_else(|| flock_core::tr("沙盒未配置或未启用", "Sandbox not configured or enabled"))?; - let base = get_api_base(cfg.api_url.as_ref().unwrap()); + let provider = cfg.provider.as_deref().unwrap_or("e2b"); + if provider == "e2b" { + let api_key = cfg.e2b_api_key.as_ref().unwrap(); + let client = reqwest::Client::new(); + let url = format!("https://api.e2b.app/snapshots/{}", id); + let resp = client.delete(&url) + .header("X-API-Key", api_key) + .send() + .await + .map_err(|e| flock_core::tr( + &format!("发送删除 E2B 快照请求失败: {}", e), + &format!("Failed to send delete E2B snapshot request: {}", e) + ))?; + + if resp.status().is_success() || resp.status().as_u16() == 204 { + return Ok(()); + } else { + return Err(flock_core::tr( + &format!("删除 E2B 快照失败,HTTP 状态码: {}", resp.status()), + &format!("Failed to delete E2B snapshot, HTTP status code: {}", resp.status()) + )); + } + } else if provider == "local" { + return Ok(()); + } + + let base = flock_tools::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); let client = reqwest::Client::new(); From e58d8d1b6a2455205aa5267c8230c86cbecee872 Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Fri, 26 Jun 2026 17:10:43 +0800 Subject: [PATCH 03/25] fix: resolve E2B sandbox command execution 404 and template creation fake success --- crates/flock-tools/src/e2b_provider.rs | 104 +++++++++++++++--- flock-ui/src-tauri/src/commands/settings.rs | 20 ++-- .../components/SnapshotListSection.tsx | 24 +++- .../hooks/useSandboxSettings.ts | 31 ++++-- .../Settings/SandboxSettings/index.tsx | 1 + flock-ui/src/i18n/locales/en/settings.json | 14 +-- flock-ui/src/i18n/locales/zh/settings.json | 14 +-- 7 files changed, 155 insertions(+), 53 deletions(-) diff --git a/crates/flock-tools/src/e2b_provider.rs b/crates/flock-tools/src/e2b_provider.rs index b211ab19..4069f60a 100644 --- a/crates/flock-tools/src/e2b_provider.rs +++ b/crates/flock-tools/src/e2b_provider.rs @@ -115,38 +115,110 @@ impl SandboxProvider for E2BSandboxProvider { .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; let client = reqwest::Client::new(); - let url = format!("https://api.e2b.app/sandboxes/{}/commands", sandbox_id); + let url = format!("https://49983-{}.e2b.app/process.Process/Start", sandbox_id); + + #[derive(Debug, serde::Serialize)] + struct ProcessConfig { + cmd: String, + args: Vec, + envs: std::collections::HashMap, + cwd: String, + } + + #[derive(Debug, serde::Serialize)] + struct StartRequest { + process: ProcessConfig, + } - let payload = E2BProcessRequest { - cmd: command.to_string(), + let payload = StartRequest { + process: ProcessConfig { + cmd: "/bin/bash".to_string(), + args: vec!["-c".to_string(), command.to_string()], + envs: std::collections::HashMap::new(), + cwd: "/workspace".to_string(), + }, }; + let body_str = serde_json::to_string(&payload)?; + let resp = client.post(&url) .header("X-API-Key", api_key) - .json(&payload) + .header("Connect-Protocol-Version", "1") + .header("Content-Type", "application/connect+json") + .header(reqwest::header::USER_AGENT, "flock-agent") + .body(body_str) .send() .await?; let status = resp.status(); - let resp_text = resp.text().await.unwrap_or_default(); if !status.is_success() { - anyhow::bail!("Failed to execute command in E2B sandbox (HTTP {}): {}", status, resp_text); + let resp_text = resp.text().await.unwrap_or_default(); + anyhow::bail!("Failed to start command in E2B sandbox (HTTP {}): {}", status, resp_text); } - let exec_res: E2BProcessResponse = serde_json::from_str(&resp_text) - .map_err(|e| anyhow::anyhow!("Failed to parse E2B execution response: {}, body: {}", e, resp_text))?; + let mut stdout_accum = String::new(); + let mut stderr_accum = String::new(); + let mut exit_code = 0; + + use futures::StreamExt; + let mut body = resp.bytes_stream(); + let mut buffer = Vec::new(); + + while let Some(chunk_res) = body.next().await { + let chunk = chunk_res?; + buffer.extend_from_slice(&chunk); + + while buffer.len() >= 5 { + let flags = buffer[0]; + let length = u32::from_be_bytes([buffer[1], buffer[2], buffer[3], buffer[4]]) as usize; + + if buffer.len() >= 5 + length { + let payload_bytes = &buffer[5..5 + length]; + + if flags == 0x00 { + if let Ok(val) = serde_json::from_slice::(payload_bytes) { + if let Some(event) = val.get("event") { + if let Some(data) = event.get("data") { + if let Some(stdout_b64) = data.get("stdout").and_then(|v| v.as_str()) { + if let Ok(decoded) = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, stdout_b64) { + if let Ok(s) = String::from_utf8(decoded) { + stdout_accum.push_str(&s); + } + } + } + if let Some(stderr_b64) = data.get("stderr").and_then(|v| v.as_str()) { + if let Ok(decoded) = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, stderr_b64) { + if let Ok(s) = String::from_utf8(decoded) { + stderr_accum.push_str(&s); + } + } + } + } + if let Some(end) = event.get("end") { + if let Some(code) = end.get("exitCode").or_else(|| end.get("exit_code")).and_then(|v| v.as_i64()) { + exit_code = code as i32; + } + } + } + } + } + + buffer.drain(0..5 + length); + } else { + break; + } + } + } - let stdout = exec_res.stdout.unwrap_or_default(); - let stderr = exec_res.stderr.unwrap_or_default(); - let combined_output = if stderr.is_empty() { - stdout - } else if stdout.is_empty() { - stderr + let combined_output = if stderr_accum.is_empty() { + stdout_accum + } else if stdout_accum.is_empty() { + stderr_accum } else { - format!("{}\n{}", stdout, stderr) + format!("{}\n{}", stdout_accum, stderr_accum) }; - Ok((combined_output, exec_res.exit_code.unwrap_or(0))) + Ok((combined_output, exit_code)) } async fn destroy_sandbox(&self, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index 66f181cc..ce48ee68 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -112,12 +112,18 @@ pub async fn set_app_config( &format!("Failed to save config '{}': {}", key, e) ))?; - // 当 sandbox 被禁用时,将其 provider 标记为不可用 + // 当 sandbox 被禁用时,将其 provider 标记为不可用。同时,清空当前活跃沙盒的内存缓存。 if key == "sandbox" { let enabled = final_value.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false); if !enabled { let _ = db.set_tool_provider_available("sandbox", false).await; } + + // 关键修复:清除旧的活跃沙盒缓存,防止状态穿透 + let mutex = flock_tools::daytona::get_sandbox_id_mutex(); + if let Ok(mut lock) = mutex.try_lock() { + *lock = None; + } } Ok(()) @@ -427,25 +433,23 @@ pub async fn list_daytona_snapshots( let provider = cfg.provider.as_deref().unwrap_or("e2b"); if provider == "e2b" { - // E2B 官方目前不支持通过统一 REST API 拉取公开公共模版,但用户可能有自己的 snapshots。 - // 我们尝试拉取 E2B 自定义 snapshots 端点。 let api_key = cfg.e2b_api_key.as_ref().unwrap(); let client = reqwest::Client::new(); - let url = "https://api.e2b.app/snapshots"; + let url = "https://api.e2b.app/templates"; let resp = client.get(url) .header("X-API-Key", api_key) .send() .await .map_err(|e| flock_core::tr( - &format!("请求 E2B 快照列表失败: {}", e), - &format!("Failed to request E2B snapshot list: {}", e) + &format!("请求 E2B 模板列表失败: {}", e), + &format!("Failed to request E2B template list: {}", e) ))?; let text = resp.text().await.unwrap_or_default(); let list_val: serde_json::Value = serde_json::from_str(&text) .map_err(|e| flock_core::tr( - &format!("解析 E2B 快照列表失败: {}", e), - &format!("Failed to parse E2B snapshot list: {}", e) + &format!("解析 E2B 模板列表失败: {}", e), + &format!("Failed to parse E2B template list: {}", e) ))?; if let Some(arr) = list_val.as_array() { diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx index 3cab5466..4cb1e82d 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx @@ -38,6 +38,7 @@ interface SnapshotItem { } interface SnapshotListSectionProps { + provider: 'e2b' | 'daytona' | 'local'; currentDefaultSnapshot: string; onSetDefaultSnapshot: (name: string) => void; onCreateSnapshot: (name: string) => Promise; @@ -45,6 +46,7 @@ interface SnapshotListSectionProps { } export function SnapshotListSection({ + provider, currentDefaultSnapshot, onSetDefaultSnapshot, onCreateSnapshot, @@ -56,7 +58,7 @@ export function SnapshotListSection({ const [deletingId, setDeletingId] = useState(null); const [newSnapshotName, setNewSnapshotName] = useState(''); - const defaultSnapshotName = 'flock-playwright'; + const defaultSnapshotName = provider === 'e2b' ? 'browser' : 'flock-playwright'; const fetchSnapshots = async () => { setLoading(true); @@ -71,7 +73,9 @@ export function SnapshotListSection({ list = data.data; } // Filter out system-managed templates - const userSnapshots = list.filter((snap: any) => !snap.general && !snap.system && !snap.isSystem); + const userSnapshots = provider === 'e2b' + ? list + : list.filter((snap: any) => !snap.general && !snap.system && !snap.isSystem); setSnapshots(userSnapshots); } catch (e) { console.error('获取快照列表失败:', e); @@ -167,7 +171,7 @@ export function SnapshotListSection({ - {t('settings.sandbox.createSnapshotTitle')} + {provider === 'e2b' ? 'E2B Custom Sandboxes / Templates' : t('settings.sandbox.createSnapshotTitle')} @@ -186,12 +190,20 @@ export function SnapshotListSection({ color={isExistingSnapshot ? 'teal' : 'blue'} onClick={handleCreate} loading={creatingSnapshot} - leftSection={isExistingSnapshot ? : } + leftSection={ + isExistingSnapshot + ? + : (provider === 'e2b' || provider === 'local') + ? + : + } > {isExistingSnapshot ? t('settings.sandbox.useExistingSnapshot') : creatingSnapshot ? t('settings.sandbox.snapshotCreating') + : (provider === 'e2b' || provider === 'local') + ? t('settings.sandbox.setAsDefault') : t('settings.sandbox.snapshotCreateBtn')} @@ -204,7 +216,9 @@ export function SnapshotListSection({ mb="lg" style={{ fontSize: 12 }} > - {t('settings.sandbox.snapshotHint')} + {provider === 'e2b' + ? 'E2B provides a pre-bundled "browser" template with all Playwright dependencies pre-installed. There is no need to manually compile browser dependencies. You can enter any of your custom E2B Template IDs above, click Star to set it as default and use it instantly.' + : t('settings.sandbox.snapshotHint')} diff --git a/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts b/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts index 22e72ca8..f8999fd1 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts +++ b/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts @@ -151,16 +151,27 @@ export function useSandboxSettings() { } try { - await invoke('create_playwright_snapshot', { snapshotName: snapName }); - setSnapshot(snapName); - await saveConfig({ snapshot: snapName }); - notifications.show({ - title: t('settings.sandbox.snapshotDone'), - message: t('settings.sandbox.snapshotDoneMsg', { name: snapName }), - color: 'teal', - icon: React.createElement(IconCheck, { size: 18 }), - autoClose: 8000, - }); + if (provider === 'e2b' || provider === 'local') { + setSnapshot(snapName); + await saveConfig({ snapshot: snapName }); + notifications.show({ + title: t('common.success'), + message: t('settings.sandbox.saveDefaultSuccess'), + color: 'teal', + icon: React.createElement(IconCheck, { size: 18 }), + }); + } else { + await invoke('create_playwright_snapshot', { snapshotName: snapName }); + setSnapshot(snapName); + await saveConfig({ snapshot: snapName }); + notifications.show({ + title: t('settings.sandbox.snapshotDone'), + message: t('settings.sandbox.snapshotDoneMsg', { name: snapName }), + color: 'teal', + icon: React.createElement(IconCheck, { size: 18 }), + autoClose: 8000, + }); + } } catch (e) { notifications.show({ title: t('settings.sandbox.snapshotFailed'), diff --git a/flock-ui/src/components/Settings/SandboxSettings/index.tsx b/flock-ui/src/components/Settings/SandboxSettings/index.tsx index 0b17fbc4..9999a4b0 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/index.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/index.tsx @@ -150,6 +150,7 @@ export default function SandboxSettings() { {activeTab === 'snapshots' && isAvailable && ( Date: Fri, 26 Jun 2026 18:03:56 +0800 Subject: [PATCH 04/25] feat: pre-populate official E2B templates in list_daytona_snapshots command --- crates/flock-tools/src/daytona/vnc.rs | 11 +- crates/flock-tools/src/e2b_provider.rs | 18 +-- .../src/tools/sandbox/browser/interactive.rs | 23 +-- .../src/tools/sandbox/computer_use.rs | 28 +++- .../tools/sandbox/request_human_assistance.rs | 11 +- crates/flock-tools/tests/diagnose_sandbox.rs | 49 +++++++ .../src-tauri/src/commands/common/sandbox.rs | 16 ++- flock-ui/src-tauri/src/commands/settings.rs | 135 ++++++++++++++---- .../components/SnapshotListSection.tsx | 2 +- .../hooks/useSandboxSettings.ts | 27 ++++ .../Settings/SandboxSettings/index.tsx | 60 ++++---- 11 files changed, 293 insertions(+), 87 deletions(-) diff --git a/crates/flock-tools/src/daytona/vnc.rs b/crates/flock-tools/src/daytona/vnc.rs index 2d15f617..4fdaa492 100644 --- a/crates/flock-tools/src/daytona/vnc.rs +++ b/crates/flock-tools/src/daytona/vnc.rs @@ -173,7 +173,8 @@ pub async fn ensure_vnc_running_in_sandbox(db: &DbManager, sandbox_id: &str) -> crate::emit_info(&flock_core::tr("检测到 VNC 服务未运行,手动拉起 Xvfb, VNC, noVNC...", "Detected VNC service not running, manually starting Xvfb, VNC, noVNC...")); let launch_cmd = format!("sh -c '\ if command -v start-vnc >/dev/null 2>&1; then \ - start-vnc; \ + setsid nohup start-vnc >/tmp/vnc_start.log 2>&1 & \ + sleep 2; \ else \ export DISPLAY={display} && \ rm -f /tmp/.X0-lock && \ @@ -214,6 +215,14 @@ pub async fn get_sandbox_vnc_url( let cfg = get_sandbox_config(db).await .ok_or_else(|| anyhow::anyhow!(flock_core::tr("云端 Daytona 沙箱未配置或未启用", "Cloud Daytona sandbox not configured or enabled")))?; + let provider = cfg.provider.as_deref().unwrap_or("e2b"); + if provider == "e2b" { + return Ok(format!( + "https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", + sandbox_id + )); + } + let client = reqwest::Client::new(); let base = get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); diff --git a/crates/flock-tools/src/e2b_provider.rs b/crates/flock-tools/src/e2b_provider.rs index 4069f60a..aff47b6c 100644 --- a/crates/flock-tools/src/e2b_provider.rs +++ b/crates/flock-tools/src/e2b_provider.rs @@ -61,25 +61,11 @@ impl SandboxProvider for E2BSandboxProvider { let client = reqwest::Client::new(); let mut template_id = cfg.snapshot.as_deref() - .unwrap_or("browser") + .unwrap_or("base") .trim(); if template_id.is_empty() { - template_id = "browser"; + template_id = "base"; } - - // 如果是 Daytona 生成的数字序列(如 "1234567890" 等纯数字,或者不含 hyphen 的 Daytona 快照), - // 或者不是官方默认推荐模板 (browser/base/code-interpreter) 且不包含常规自定义模板格式时, - // 自动回退到官方 "browser" 模板,防止参数串用报错。 - let is_valid_e2b_template = template_id == "browser" - || template_id == "base" - || template_id == "code-interpreter" - || (template_id.contains('-') && template_id.len() > 5); // E2B 自定义模板通常带有连字符且较长 - - let template_id = if is_valid_e2b_template { - template_id - } else { - "browser" - }; let payload = E2BCreateSandboxRequest { template_id: template_id.to_string(), diff --git a/crates/flock-tools/src/tools/sandbox/browser/interactive.rs b/crates/flock-tools/src/tools/sandbox/browser/interactive.rs index 10ba3753..92204ba3 100644 --- a/crates/flock-tools/src/tools/sandbox/browser/interactive.rs +++ b/crates/flock-tools/src/tools/sandbox/browser/interactive.rs @@ -23,15 +23,20 @@ pub async fn handle_interactive( ) -> Result { let proxy_url = match crate::daytona::get_sandbox_vnc_url(db, sandbox_id).await { Ok(u) => u, - Err(e) => { - crate::emit_info(&flock_core::tr( - &format!("获取动态 VNC URL 失败: {}。使用静态备用 URL...", e), - &format!("Failed to retrieve dynamic VNC URL: {}. Falling back to static VNC URL...", e), - )); - format!( - "https://{}-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", - WEBSOCKIFY_PORT, sandbox_id - ) + Err(_) => { + let is_e2b = if let Some(cfg) = crate::daytona::get_sandbox_config(db).await { + cfg.provider.as_deref().unwrap_or("e2b") == "e2b" + } else { + true + }; + if is_e2b { + format!("https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", sandbox_id) + } else { + format!( + "https://{}-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", + WEBSOCKIFY_PORT, sandbox_id + ) + } } }; diff --git a/crates/flock-tools/src/tools/sandbox/computer_use.rs b/crates/flock-tools/src/tools/sandbox/computer_use.rs index 762a53cb..406f5425 100644 --- a/crates/flock-tools/src/tools/sandbox/computer_use.rs +++ b/crates/flock-tools/src/tools/sandbox/computer_use.rs @@ -211,8 +211,21 @@ pub async fn computer_use( match act.as_str() { "interactive" => { - let proxy_url = crate::daytona::get_sandbox_vnc_url(&db, &sandbox_id).await - .unwrap_or_else(|_| format!("https://{}-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", crate::daytona::WEBSOCKIFY_PORT, sandbox_id)); + let proxy_url = match crate::daytona::get_sandbox_vnc_url(&db, &sandbox_id).await { + Ok(url) => url, + Err(_) => { + let is_e2b = if let Some(cfg) = crate::daytona::get_sandbox_config(&db).await { + cfg.provider.as_deref().unwrap_or("e2b") == "e2b" + } else { + true + }; + if is_e2b { + format!("https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", sandbox_id) + } else { + format!("https://{}-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", crate::daytona::WEBSOCKIFY_PORT, sandbox_id) + } + } + }; if let (Some(cid), Some(mid), Some(app_mgr)) = (call_id.clone(), msg_id, crate::get_global_approval_manager()) { crate::emit_info(&flock_core::tr( @@ -305,7 +318,16 @@ pub async fn computer_use( Ok(u) => u, Err(e) => { crate::emit_info(&flock_core::tr(&format!("获取动态 VNC URL 失败: {}。使用静态备用 URL...", e), &format!("Failed to retrieve dynamic VNC URL: {}. Falling back to static VNC URL...", e))); - format!("https://{}-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", crate::daytona::WEBSOCKIFY_PORT, sandbox_id) + let is_e2b = if let Some(cfg) = crate::daytona::get_sandbox_config(&db).await { + cfg.provider.as_deref().unwrap_or("e2b") == "e2b" + } else { + true + }; + if is_e2b { + format!("https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", sandbox_id) + } else { + format!("https://{}-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", crate::daytona::WEBSOCKIFY_PORT, sandbox_id) + } } }; diff --git a/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs b/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs index fdb69deb..b91c9580 100644 --- a/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs +++ b/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs @@ -43,7 +43,16 @@ pub async fn request_human_assistance( let proxy_url = match get_sandbox_vnc_url(&db, &sandbox_id).await { Ok(u) => u, Err(_) => { - format!("https://6080-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", sandbox_id) + let is_e2b = if let Some(cfg) = crate::daytona::get_sandbox_config(&db).await { + cfg.provider.as_deref().unwrap_or("e2b") == "e2b" + } else { + true + }; + if is_e2b { + format!("https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", sandbox_id) + } else { + format!("https://6080-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", sandbox_id) + } } }; diff --git a/crates/flock-tools/tests/diagnose_sandbox.rs b/crates/flock-tools/tests/diagnose_sandbox.rs index 4a7476a6..71fc27d2 100644 --- a/crates/flock-tools/tests/diagnose_sandbox.rs +++ b/crates/flock-tools/tests/diagnose_sandbox.rs @@ -127,3 +127,52 @@ async fn test_diagnose_sandbox() { } } } + +#[tokio::test] +async fn test_diagnose_e2b_templates() { + let db = match DbManager::init().await { + Ok(d) => d, + Err(e) => { + println!("Failed to open DB: {}", e); + return; + } + }; + + // Load sandbox config (enabled or not) + let mut cfg: flock_core::config::settings::SandboxConfig = db.get_config("sandbox").await.unwrap(); + if let (Some(ct), Some(n)) = (&cfg.e2b_api_key_encrypted, &cfg.e2b_api_key_nonce) { + if let Ok(salt) = db.get_or_create_salt().await { + if let Ok(decrypted) = flock_core::crypto::decrypt_value(ct, n, &salt) { + cfg.e2b_api_key = Some(decrypted); + } + } + } + + let key = cfg.e2b_api_key.unwrap_or_default(); + println!("Retrieved E2B Key length: {}", key.len()); + if key.is_empty() { + println!("E2B API Key is empty in database!"); + return; + } + + println!("E2B Key starts with: {}", &key[..std::cmp::min(10, key.len())]); + + let client = reqwest::Client::new(); + let url = "https://api.e2b.app/templates"; + let resp = match client.get(url) + .header("X-API-Key", &key) + .send() + .await + { + Ok(r) => r, + Err(e) => { + println!("HTTP Request failed: {}", e); + return; + } + }; + + let status = resp.status(); + let text = resp.text().await.unwrap_or_default(); + println!("HTTP Response Status: {}", status); + println!("HTTP Response Body: {}", text); +} diff --git a/flock-ui/src-tauri/src/commands/common/sandbox.rs b/flock-ui/src-tauri/src/commands/common/sandbox.rs index d1da0172..80093b5f 100644 --- a/flock-ui/src-tauri/src/commands/common/sandbox.rs +++ b/flock-ui/src-tauri/src/commands/common/sandbox.rs @@ -96,12 +96,16 @@ pub async fn get_active_sandbox_vnc_url( if let Some(sandbox_id) = flock_tools::daytona::get_active_sandbox_id().await { match flock_tools::daytona::get_sandbox_vnc_url(&*db, &sandbox_id).await { Ok(url) => Ok(Some(url)), - Err(e) => { - log::warn!("{}", flock_core::tr( - &format!("获取动态 VNC URL 失败: {}。使用静态备用 URL...", e), - &format!("Failed to retrieve dynamic VNC URL: {}. Using static fallback URL...", e) - )); - Ok(Some(format!("https://6080-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", sandbox_id))) + Err(_) => { + let fallback_url = match flock_tools::daytona::get_sandbox_config(&*db).await { + Some(cfg) if cfg.provider.as_deref().unwrap_or("e2b") == "e2b" => { + format!("https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", sandbox_id) + } + _ => { + format!("https://6080-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", sandbox_id) + } + }; + Ok(Some(fallback_url)) } } } else { diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index ce48ee68..4952c19b 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -240,10 +240,9 @@ pub async fn list_daytona_sandboxes( db: State<'_, SharedDbManager>, ) -> Result { use flock_core::db::DbManager; - use flock_tools::daytona::get_sandbox_config; let db_ref: &DbManager = &*db; - let cfg = match get_sandbox_config(db_ref).await { + let cfg = match get_sandbox_config_regardless(db_ref).await { Some(c) => c, None => return Ok(serde_json::json!([])), }; @@ -421,23 +420,36 @@ pub async fn reuse_sandbox( #[tauri::command] pub async fn list_daytona_snapshots( db: State<'_, SharedDbManager>, + provider: Option, + api_key: Option, ) -> Result { use flock_core::db::DbManager; - use flock_tools::daytona::get_sandbox_config; - let db_ref: &DbManager = &*db; - let cfg = match get_sandbox_config(db_ref).await { - Some(c) => c, - None => return Ok(serde_json::json!([])), - }; + let active_provider = provider.unwrap_or_else(|| "e2b".to_string()); + if active_provider == "e2b" { + let key = if let Some(ref k) = api_key { + if k.is_empty() { + let db_ref: &DbManager = &*db; + let cfg = get_sandbox_config_regardless(db_ref).await.unwrap_or_default(); + cfg.e2b_api_key.unwrap_or_default() + } else { + k.clone() + } + } else { + let db_ref: &DbManager = &*db; + let cfg = get_sandbox_config_regardless(db_ref).await.unwrap_or_default(); + cfg.e2b_api_key.unwrap_or_default() + }; + + log::info!("list_daytona_snapshots: active_provider = E2B, key len = {}, key starts with: {}", key.len(), &key[..std::cmp::min(5, key.len())]); + if key.is_empty() { + return Ok(serde_json::json!([])); + } - let provider = cfg.provider.as_deref().unwrap_or("e2b"); - if provider == "e2b" { - let api_key = cfg.e2b_api_key.as_ref().unwrap(); let client = reqwest::Client::new(); let url = "https://api.e2b.app/templates"; let resp = client.get(url) - .header("X-API-Key", api_key) + .header("X-API-Key", key) .send() .await .map_err(|e| flock_core::tr( @@ -445,7 +457,39 @@ pub async fn list_daytona_snapshots( &format!("Failed to request E2B template list: {}", e) ))?; + let status = resp.status(); let text = resp.text().await.unwrap_or_default(); + log::info!("list_daytona_snapshots: E2B templates status = {}, text = {}", status, text); + if !status.is_success() { + return Err(flock_core::tr( + &format!("E2B API 错误 ({}): {}", status, text), + &format!("E2B API error ({}): {}", status, text) + )); + } + + let mut mapped: Vec = vec![ + serde_json::json!({ + "id": "desktop", + "name": "desktop (GUI Desktop / VNC)", + "status": "active" + }), + serde_json::json!({ + "id": "base", + "name": "base (Standard Python/Bash)", + "status": "active" + }), + serde_json::json!({ + "id": "code-interpreter", + "name": "code-interpreter", + "status": "active" + }), + serde_json::json!({ + "id": "browser", + "name": "browser", + "status": "active" + }), + ]; + let list_val: serde_json::Value = serde_json::from_str(&text) .map_err(|e| flock_core::tr( &format!("解析 E2B 模板列表失败: {}", e), @@ -453,21 +497,37 @@ pub async fn list_daytona_snapshots( ))?; if let Some(arr) = list_val.as_array() { - let mapped: Vec = arr.iter().map(|item| { - let id = item.get("snapshotID").or_else(|| item.get("id")).and_then(|v| v.as_str()).unwrap_or_default(); - serde_json::json!({ - "id": id, - "name": id, - "status": "active" - }) - }).collect(); - return Ok(serde_json::json!(mapped)); + for item in arr { + let id = item.get("templateID") + .or_else(|| item.get("snapshotID")) + .or_else(|| item.get("id")) + .and_then(|v| v.as_str()) + .unwrap_or_default(); + let name = item.get("aliases") + .and_then(|a| a.as_array()) + .and_then(|arr| arr.first()) + .and_then(|v| v.as_str()) + .unwrap_or(id); + if !id.is_empty() && !mapped.iter().any(|m| m.get("id").and_then(|v| v.as_str()) == Some(id)) { + mapped.push(serde_json::json!({ + "id": id, + "name": name, + "status": "active" + })); + } + } } - return Ok(serde_json::json!([])); - } else if provider == "local" { + return Ok(serde_json::json!(mapped)); + } else if active_provider == "local" { return Ok(serde_json::json!([])); } + let db_ref: &DbManager = &*db; + let cfg = match get_sandbox_config_regardless(db_ref).await { + Some(c) => c, + None => return Ok(serde_json::json!([])), + }; + let base = flock_tools::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); @@ -499,11 +559,10 @@ pub async fn delete_daytona_snapshot( id: String, ) -> Result<(), String> { use flock_core::db::DbManager; - use flock_tools::daytona::get_sandbox_config; let db_ref: &DbManager = &*db; - let cfg = get_sandbox_config(db_ref).await - .ok_or_else(|| flock_core::tr("沙盒未配置或未启用", "Sandbox not configured or enabled"))?; + let cfg = get_sandbox_config_regardless(db_ref).await + .ok_or_else(|| flock_core::tr("沙盒未配置", "Sandbox not configured"))?; let provider = cfg.provider.as_deref().unwrap_or("e2b"); if provider == "e2b" { @@ -560,3 +619,27 @@ pub fn set_locale(locale: String) { flock_core::set_locale(&locale); } +async fn get_sandbox_config_regardless(db: &flock_core::db::DbManager) -> Option { + let mut cfg: flock_core::config::settings::SandboxConfig = db.get_config("sandbox").await?; + + // Decrypt Daytona key + if let (Some(ct), Some(n)) = (&cfg.api_key_encrypted, &cfg.api_key_nonce) { + if let Ok(salt) = db.get_or_create_salt().await { + if let Ok(decrypted) = flock_core::crypto::decrypt_value(ct, n, &salt) { + cfg.api_key = Some(decrypted); + } + } + } + + // Decrypt E2B key + if let (Some(ct), Some(n)) = (&cfg.e2b_api_key_encrypted, &cfg.e2b_api_key_nonce) { + if let Ok(salt) = db.get_or_create_salt().await { + if let Ok(decrypted) = flock_core::crypto::decrypt_value(ct, n, &salt) { + cfg.e2b_api_key = Some(decrypted); + } + } + } + + Some(cfg) +} + diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx index 4cb1e82d..6ee475d8 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx @@ -91,7 +91,7 @@ export function SnapshotListSection({ useEffect(() => { fetchSnapshots(); - }, []); + }, [provider]); const handleCopy = (text: string) => { navigator.clipboard.writeText(text); diff --git a/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts b/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts index f8999fd1..db4f11b0 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts +++ b/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts @@ -31,11 +31,37 @@ export function useSandboxSettings() { const [creatingSnapshot, setCreatingSnapshot] = useState(false); const [isAvailable, setIsAvailable] = useState(false); const [activeTab, setActiveTab] = useState('config'); + const [snapshotsList, setSnapshotsList] = useState<{ id: string; name: string }[]>([]); + + const fetchSnapshotsList = async () => { + try { + const data = await invoke('list_daytona_snapshots', { + provider, + apiKey: provider === 'e2b' ? e2bApiKey : apiKey, + }); + let list: any[] = []; + if (Array.isArray(data)) { + list = data; + } else if (data && Array.isArray(data.items)) { + list = data.items; + } else if (data && Array.isArray(data.data)) { + list = data.data; + } + setSnapshotsList(list.map((item: any) => ({ id: item.id || item.name, name: item.name || item.id }))); + } catch (e) { + console.error('Failed to fetch snapshots:', e); + setSnapshotsList([]); + } + }; useEffect(() => { loadAll(); }, []); + useEffect(() => { + fetchSnapshotsList(); + }, [provider, e2bApiKey]); + const loadAll = async () => { try { const [config, providers] = await Promise.all([ @@ -215,6 +241,7 @@ export function useSandboxSettings() { creatingSnapshot, isAvailable, activeTab, setActiveTab, + snapshotsList, handleTestConnection, handleDisable, handleCreateSnapshot, diff --git a/flock-ui/src/components/Settings/SandboxSettings/index.tsx b/flock-ui/src/components/Settings/SandboxSettings/index.tsx index 9999a4b0..a5c6454b 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/index.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/index.tsx @@ -40,6 +40,7 @@ export default function SandboxSettings() { creatingSnapshot, isAvailable, activeTab, setActiveTab, + snapshotsList, handleTestConnection, handleDisable, handleCreateSnapshot, @@ -91,28 +92,39 @@ export default function SandboxSettings() { )} - setActiveTab(val as any)} - data={[ - { value: 'config', label: t('settings.sandbox.tabConfig') }, - { value: 'instances', label: t('settings.sandbox.tabInstances'), disabled: !isAvailable }, - { value: 'snapshots', label: t('settings.sandbox.tabSnapshots'), disabled: !isAvailable }, - ]} - size="xs" - mb="lg" - styles={{ - root: { - background: 'var(--flock-bg-surface)', - border: '1px solid var(--flock-border-dim)', - padding: 2, - borderRadius: 8, - }, - control: { - minWidth: 100, - } - }} - /> + {(() => { + const tabs = [ + { value: 'config', label: t('settings.sandbox.tabConfig') } + ]; + if (provider === 'daytona') { + tabs.push({ value: 'instances', label: t('settings.sandbox.tabInstances'), disabled: !isAvailable }); + } + if (provider === 'daytona' || provider === 'e2b') { + tabs.push({ value: 'snapshots', label: t('settings.sandbox.tabSnapshots'), disabled: !isAvailable }); + } + if (tabs.length <= 1) return null; + + return ( + setActiveTab(val as any)} + data={tabs} + size="xs" + mb="lg" + styles={{ + root: { + background: 'var(--flock-bg-surface)', + border: '1px solid var(--flock-border-dim)', + padding: 2, + borderRadius: 8, + }, + control: { + minWidth: 100, + } + }} + /> + ); + })()} {activeTab === 'config' && ( @@ -141,13 +153,13 @@ export default function SandboxSettings() { )} - {activeTab === 'instances' && isAvailable && ( + {provider === 'daytona' && activeTab === 'instances' && isAvailable && ( )} - {activeTab === 'snapshots' && isAvailable && ( + {(provider === 'daytona' || provider === 'e2b') && activeTab === 'snapshots' && isAvailable && ( Date: Sat, 27 Jun 2026 08:41:01 +0800 Subject: [PATCH 05/25] refactor(sandbox): extract sandbox manager and implement unified dispatch for daytona and e2b --- crates/flock-tools/src/daytona/exec.rs | 22 +- crates/flock-tools/src/daytona/fs.rs | 3 +- crates/flock-tools/src/daytona/lifecycle.rs | 195 +++++------------- crates/flock-tools/src/daytona/mod.rs | 7 +- crates/flock-tools/src/daytona/state.rs | 15 -- crates/flock-tools/src/daytona/vnc.rs | 8 - crates/flock-tools/src/e2b/exec.rs | 127 ++++++++++++ crates/flock-tools/src/e2b/lifecycle.rs | 100 +++++++++ crates/flock-tools/src/e2b/mod.rs | 5 + crates/flock-tools/src/e2b_provider.rs | 3 + crates/flock-tools/src/lib.rs | 5 +- crates/flock-tools/src/sandbox_manager.rs | 171 +++++++++++++++ .../src/tools/sandbox/browser/interactive.rs | 25 +-- .../src/tools/sandbox/browser/mod.rs | 2 +- .../src/tools/sandbox/code_execution.rs | 2 +- .../src/tools/sandbox/computer_use.rs | 39 ++-- .../tools/sandbox/request_human_assistance.rs | 19 +- .../src/tools/sandbox/sandbox_exec.rs | 2 +- crates/flock-tools/tests/diagnose_sandbox.rs | 71 +++++++ .../src-tauri/src/commands/common/sandbox.rs | 8 +- flock-ui/src-tauri/src/commands/file.rs | 3 +- flock-ui/src-tauri/src/commands/settings.rs | 95 ++------- flock-ui/src-tauri/src/lib.rs | 8 +- .../components/SandboxListSection.tsx | 4 +- .../components/SnapshotListSection.tsx | 4 +- .../hooks/useSandboxSettings.ts | 2 +- 26 files changed, 604 insertions(+), 341 deletions(-) create mode 100644 crates/flock-tools/src/e2b/exec.rs create mode 100644 crates/flock-tools/src/e2b/lifecycle.rs create mode 100644 crates/flock-tools/src/e2b/mod.rs create mode 100644 crates/flock-tools/src/sandbox_manager.rs diff --git a/crates/flock-tools/src/daytona/exec.rs b/crates/flock-tools/src/daytona/exec.rs index 8dc6cf3f..b86639bf 100644 --- a/crates/flock-tools/src/daytona/exec.rs +++ b/crates/flock-tools/src/daytona/exec.rs @@ -22,35 +22,24 @@ struct ExecuteResponse { pub exit_code: Option, } -/// 在沙盒中执行指令,带有端点兼容重试机制 +/// 在 Daytona 沙盒中执行指令(通过 Toolbox API),带端点兼容重试机制 pub async fn execute_command_in_sandbox( db: &DbManager, sandbox_id: &str, command: &str, ) -> anyhow::Result<(String, i32)> { let cfg = get_sandbox_config(db).await - .ok_or_else(|| anyhow::anyhow!("沙箱未配置或未启用"))?; - - let provider = cfg.provider.as_deref().unwrap_or("e2b"); - if provider == "e2b" { - use crate::sandbox_provider::SandboxProvider; - let prov = crate::e2b_provider::E2BSandboxProvider; - return prov.execute_command(&cfg, sandbox_id, command).await; - } else if provider == "local" { - return Ok((format!("Local mock execution placeholder: executing '{}'", command), 0)); - } + .ok_or_else(|| anyhow::anyhow!("Daytona 沙箱未配置或未启用"))?; let api_url = cfg.api_url.as_ref().unwrap().trim_end_matches('/'); let api_key = cfg.api_key.as_ref().unwrap(); - - // 生成 Toolbox 执行请求 of URL list + let urls = if api_url.contains("app.daytona.io") { vec!( format!("https://proxy.app.daytona.io/toolbox/{}/toolbox/process/execute", sandbox_id), format!("https://proxy.app.daytona.io/toolbox/{}/process/execute", sandbox_id), ) } else { - // 自建模式 let base = api_url.trim_end_matches("/api").trim_end_matches("/"); vec!( format!("{}/toolbox/{}/toolbox/process/execute", base, sandbox_id), @@ -89,12 +78,10 @@ pub async fn execute_command_in_sandbox( return Err(anyhow::anyhow!("解析执行响应失败。原始响应体: {}", resp_text)); } } else if status == reqwest::StatusCode::NOT_FOUND { - // 如果 404,我们尝试下一个候选 endpoint last_error = Some(anyhow::anyhow!("Toolbox API 返回 404: {}", url)); continue; } else if status.is_server_error() || status.as_u16() == 502 || status.as_u16() == 503 { last_error = Some(anyhow::anyhow!("Toolbox API 响应服务端错误 ({}): {}", url, status)); - // 可能是沙盒内的 agent 还没完全起来,继续重试 } else { let err_body = resp.text().await.unwrap_or_default(); return Err(anyhow::anyhow!("Toolbox API 响应失败 ({}): {}", url, err_body)); @@ -105,8 +92,7 @@ pub async fn execute_command_in_sandbox( } } } - - // 如果不是最后一次尝试,等待一小段时间再重试 + if attempt < max_retries { tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; } diff --git a/crates/flock-tools/src/daytona/fs.rs b/crates/flock-tools/src/daytona/fs.rs index 6bf3fd8e..6b2afc6f 100644 --- a/crates/flock-tools/src/daytona/fs.rs +++ b/crates/flock-tools/src/daytona/fs.rs @@ -1,5 +1,6 @@ use flock_core::db::DbManager; -use crate::daytona::{execute_command_in_sandbox, get_or_create_active_sandbox}; +use crate::daytona::execute_command_in_sandbox; +use crate::sandbox_manager::get_or_create_active_sandbox; use serde::{Deserialize, Serialize}; use std::path::{Component, Path}; diff --git a/crates/flock-tools/src/daytona/lifecycle.rs b/crates/flock-tools/src/daytona/lifecycle.rs index 125284b4..3dcb50e7 100644 --- a/crates/flock-tools/src/daytona/lifecycle.rs +++ b/crates/flock-tools/src/daytona/lifecycle.rs @@ -1,131 +1,21 @@ use flock_core::db::DbManager; use flock_core::config::settings::SandboxConfig; -use crate::daytona::state::get_sandbox_id_mutex; use crate::daytona::config::{get_sandbox_config, get_api_base}; use crate::daytona::volume::get_or_create_volume; -/// 销毁当前活跃的沙盒 -pub async fn destroy_active_sandbox(db: &DbManager) -> anyhow::Result<()> { - let cfg = match get_sandbox_config(db).await { - Some(c) => c, - None => return Ok(()), - }; - - let mutex = get_sandbox_id_mutex(); - let mut lock = mutex.lock().await; - - let sandbox_id = match lock.as_ref() { - Some(id) => id.clone(), - None => return Ok(()), - }; - - let provider = cfg.provider.as_deref().unwrap_or("e2b"); - if provider == "e2b" { - use crate::sandbox_provider::SandboxProvider; - let prov = crate::e2b_provider::E2BSandboxProvider; - if let Err(e) = prov.destroy_sandbox(&cfg, &sandbox_id).await { - crate::emit_info(&flock_core::tr(&format!("销毁 E2B 沙盒请求失败: {}", e), &format!("Destroying E2B sandbox request failed: {}", e))); - } - *lock = None; - return Ok(()); - } else if provider == "local" { - *lock = None; - return Ok(()); - } +/// 创建 Daytona 沙盒,等待 started 状态后返回 sandbox_id +pub async fn create_sandbox(db: &DbManager, cfg: &SandboxConfig) -> anyhow::Result { + crate::emit_info(&flock_core::tr("正在向云端申请启动 Daytona 沙盒...", "Requesting to start Daytona sandbox from the cloud...")); let client = reqwest::Client::new(); let base = get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); - if let Some(ws_path) = crate::get_workspace_dir() { - if let Err(e) = crate::daytona::sync::sync_down(db, &sandbox_id, &ws_path).await { - crate::emit_info(&format!("Sync Down failed: {}", e)); - } - } - - let del_url = format!("{}/api/sandbox/{}", base, sandbox_id); - match client.delete(&del_url) - .header("Authorization", format!("Bearer {}", api_key)) - .send() - .await - { - Ok(resp) => { - if !resp.status().is_success() { - let status = resp.status(); - let body = resp.text().await.unwrap_or_default(); - crate::emit_info(&flock_core::tr(&format!("销毁沙盒返回非成功状态 (HTTP {}): {}", status, body), &format!("Destroying sandbox returned non-success status (HTTP {}): {}", status, body))); - } - } - Err(e) => { - crate::emit_info(&flock_core::tr(&format!("销毁沙盒请求失败: {}", e), &format!("Destroying sandbox request failed: {}", e))); - } - } - - *lock = None; - Ok(()) -} - -/// 获取或创建活跃的沙盒 ID -pub async fn get_or_create_active_sandbox(db: &DbManager) -> anyhow::Result { - let cfg = get_sandbox_config(db).await - .ok_or_else(|| anyhow::anyhow!(flock_core::tr("云端 Daytona 沙箱未启用或未配置。请在系统设置中配置有效的 API 地址和密钥。", "Cloud Daytona sandbox not enabled or configured. Please configure a valid API URL and Key in system settings.")))?; - - let mutex = get_sandbox_id_mutex(); - let mut lock = mutex.lock().await; - - if let Some(id) = lock.as_ref() { - if check_sandbox_alive(&cfg, id).await { - let provider = cfg.provider.as_deref().unwrap_or("e2b"); - if provider == "daytona" { - let _ = set_sandbox_public(&cfg, id, true).await; - } - return Ok(id.clone()); - } - crate::emit_info(&flock_core::tr(&format!("沙盒 {} 已失效,准备重新创建...", id), &format!("Sandbox {} has expired, preparing to recreate...", id))); - *lock = None; - } - - let provider = cfg.provider.as_deref().unwrap_or("e2b"); - if provider == "e2b" { - use crate::sandbox_provider::SandboxProvider; - let prov = crate::e2b_provider::E2BSandboxProvider; - let instance_id = prov.get_or_create_sandbox(&cfg).await?; - - // Prepare /workspace in E2B (E2B standard workspace is usually /home/user or similar, - // but we enforce /workspace to maintain paths compatibility) - let ensure_workspace_cmd = "sudo mkdir -p /workspace && sudo chown -R user:user /workspace && mkdir -p /workspace"; - if let Err(e) = prov.execute_command(&cfg, &instance_id, ensure_workspace_cmd).await { - crate::emit_info(&format!("E2B /workspace prep failed: {}", e)); - } - - *lock = Some(instance_id.clone()); - - // Sync local files up - if let Some(ws_path) = crate::get_workspace_dir() { - if let Err(e) = crate::daytona::sync::sync_up(db, &instance_id, &ws_path).await { - crate::emit_info(&format!("Sync Up failed: {}", e)); - } - } - - return Ok(instance_id); - } else if provider == "local" { - crate::emit_info(&flock_core::tr("正在启动本地 Mock 沙盒 (占位)...", "Starting local mock sandbox (placeholder)...")); - *lock = Some("local".to_string()); - return Ok("local".to_string()); - } - - crate::emit_info(&flock_core::tr("正在向云端申请启动沙盒...", "Requesting to start sandbox from the cloud...")); - - let client = reqwest::Client::new(); - let base = get_api_base(cfg.api_url.as_ref().unwrap()); - let api_key = cfg.api_key.as_ref().unwrap(); - let workspace_id = crate::get_workspace_dir() .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string())) .unwrap_or_else(|| "default".to_string()); - // Create volume - let volume_id = match get_or_create_volume(&cfg, &workspace_id).await { + let volume_id = match get_or_create_volume(cfg, &workspace_id).await { Ok(id) => Some(id), Err(e) => { crate::emit_info(&format!("未能创建或获取云端 Volume: {} (将使用临时沙盒存储)", e)); @@ -134,7 +24,7 @@ pub async fn get_or_create_active_sandbox(db: &DbManager) -> anyhow::Result anyhow::Result anyhow::Result anyhow::Result v, Err(e) => anyhow::bail!( @@ -197,7 +84,7 @@ pub async fn get_or_create_active_sandbox(db: &DbManager) -> anyhow::Result anyhow::Result anyhow::Result anyhow::Result anyhow::Result { @@ -300,8 +186,7 @@ pub async fn get_or_create_active_sandbox(db: &DbManager) -> anyhow::Result anyhow::Result bool { - let provider = cfg.provider.as_deref().unwrap_or("e2b"); - if provider == "e2b" { - use crate::sandbox_provider::SandboxProvider; - let prov = crate::e2b_provider::E2BSandboxProvider; - return prov.check_alive(cfg, id).await; - } else if provider == "local" { - return true; +/// 销毁指定 Daytona 沙盒(通过 REST API) +pub async fn destroy_daytona_sandbox(cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { + let client = reqwest::Client::new(); + let base = get_api_base(cfg.api_url.as_ref().unwrap()); + let api_key = cfg.api_key.as_ref().unwrap(); + + let del_url = format!("{}/api/sandbox/{}", base, sandbox_id); + match client.delete(&del_url) + .header("Authorization", format!("Bearer {}", api_key)) + .send() + .await + { + Ok(resp) => { + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + anyhow::bail!(flock_core::tr( + &format!("销毁 Daytona 沙盒返回非成功状态 (HTTP {}): {}", status, body), + &format!("Destroying Daytona sandbox returned non-success status (HTTP {}): {}", status, body) + )); + } + } + Err(e) => { + anyhow::bail!(flock_core::tr( + &format!("销毁 Daytona 沙盒请求失败: {}", e), + &format!("Destroying Daytona sandbox request failed: {}", e) + )); + } } + Ok(()) +} +/// 检查 Daytona 沙盒是否存活 +pub async fn check_sandbox_alive(cfg: &SandboxConfig, id: &str) -> bool { let client = reqwest::Client::new(); let base = get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); @@ -359,7 +268,7 @@ pub async fn set_sandbox_public( let api_key = cfg.api_key.as_ref().unwrap(); let url = format!("{}/api/sandbox/{}/public/{}", base, sandbox_id, is_public); - + let res = client.post(&url) .header("Authorization", format!("Bearer {}", api_key)) .send() @@ -367,10 +276,6 @@ pub async fn set_sandbox_public( let status = res.status(); if status.is_success() { - // crate::emit_info(&flock_core::tr( - // &format!("Daytona 沙盒 {} 的 public 属性设置成功。", sandbox_id), - // &format!("Daytona sandbox {} public attribute set successfully.", sandbox_id) - // )); Ok(()) } else { let err_body = res.text().await.unwrap_or_default(); diff --git a/crates/flock-tools/src/daytona/mod.rs b/crates/flock-tools/src/daytona/mod.rs index 959ef586..a68ec03c 100644 --- a/crates/flock-tools/src/daytona/mod.rs +++ b/crates/flock-tools/src/daytona/mod.rs @@ -8,10 +8,11 @@ mod constants; pub mod volume; pub mod fs; - -pub use state::{get_active_sandbox_id, emit_human_takeover, get_sandbox_id_mutex}; +pub use state::emit_human_takeover; pub use config::{get_sandbox_config, get_api_base}; -pub use lifecycle::{destroy_active_sandbox, get_or_create_active_sandbox, set_sandbox_public, check_sandbox_alive}; +pub use lifecycle::{ + create_sandbox, destroy_daytona_sandbox, check_sandbox_alive, set_sandbox_public, +}; pub use exec::{execute_command_in_sandbox, DaytonaSandboxResponse}; pub use vnc::{ start_computer_use_in_sandbox, check_computer_use_status, diff --git a/crates/flock-tools/src/daytona/state.rs b/crates/flock-tools/src/daytona/state.rs index e6a3dc91..b44072a7 100644 --- a/crates/flock-tools/src/daytona/state.rs +++ b/crates/flock-tools/src/daytona/state.rs @@ -1,18 +1,3 @@ -use std::sync::OnceLock; -use tokio::sync::Mutex; - -static ACTIVE_SANDBOX_ID: OnceLock>> = OnceLock::new(); - -pub fn get_sandbox_id_mutex() -> &'static Mutex> { - ACTIVE_SANDBOX_ID.get_or_init(|| Mutex::new(None)) -} - -pub async fn get_active_sandbox_id() -> Option { - let mutex = get_sandbox_id_mutex(); - let lock = mutex.lock().await; - lock.clone() -} - /// 向前端发送"需要人工接管"事件 pub fn emit_human_takeover(call_id: &str, msg_id: &str, message: &str, remote_url: Option) { if let Some(emitter) = crate::get_global_emitter() { diff --git a/crates/flock-tools/src/daytona/vnc.rs b/crates/flock-tools/src/daytona/vnc.rs index 4fdaa492..9fcda40c 100644 --- a/crates/flock-tools/src/daytona/vnc.rs +++ b/crates/flock-tools/src/daytona/vnc.rs @@ -215,14 +215,6 @@ pub async fn get_sandbox_vnc_url( let cfg = get_sandbox_config(db).await .ok_or_else(|| anyhow::anyhow!(flock_core::tr("云端 Daytona 沙箱未配置或未启用", "Cloud Daytona sandbox not configured or enabled")))?; - let provider = cfg.provider.as_deref().unwrap_or("e2b"); - if provider == "e2b" { - return Ok(format!( - "https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", - sandbox_id - )); - } - let client = reqwest::Client::new(); let base = get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); diff --git a/crates/flock-tools/src/e2b/exec.rs b/crates/flock-tools/src/e2b/exec.rs new file mode 100644 index 00000000..4006e17f --- /dev/null +++ b/crates/flock-tools/src/e2b/exec.rs @@ -0,0 +1,127 @@ +use flock_core::config::settings::SandboxConfig; + +/// 在 E2B 沙盒中执行命令(通过 Connect Protocol) +/// 返回 (combined_output, exit_code) +pub async fn execute_command( + cfg: &SandboxConfig, + sandbox_id: &str, + command: &str, +) -> anyhow::Result<(String, i32)> { + let api_key = cfg.e2b_api_key.as_ref() + .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; + + let client = reqwest::Client::new(); + let url = format!("https://49983-{}.e2b.app/process.Process/Start", sandbox_id); + + #[derive(Debug, serde::Serialize)] + struct ProcessConfig { + cmd: String, + args: Vec, + envs: std::collections::HashMap, + cwd: String, + } + + #[derive(Debug, serde::Serialize)] + struct StartRequest { + process: ProcessConfig, + } + + let payload = StartRequest { + process: ProcessConfig { + cmd: "/bin/bash".to_string(), + args: vec!["-c".to_string(), command.to_string()], + envs: std::collections::HashMap::new(), + cwd: "/home/user".to_string(), + }, + }; + + let body_str = serde_json::to_string(&payload)?; + + let resp = client.post(&url) + .header("X-API-Key", api_key) + .header("Connect-Protocol-Version", "1") + .header("Content-Type", "application/connect+json") + .header(reqwest::header::USER_AGENT, "flock-agent") + .body(body_str) + .send() + .await?; + + let status = resp.status(); + if !status.is_success() { + let resp_text = resp.text().await.unwrap_or_default(); + anyhow::bail!("Failed to start command in E2B sandbox (HTTP {}): {}", status, resp_text); + } + + let mut stdout_accum = String::new(); + let mut stderr_accum = String::new(); + let mut exit_code = 0; + + use futures::StreamExt; + let mut body = resp.bytes_stream(); + let mut buffer = Vec::new(); + + while let Some(chunk_res) = body.next().await { + let chunk = chunk_res?; + buffer.extend_from_slice(&chunk); + + while buffer.len() >= 5 { + let flags = buffer[0]; + let length = u32::from_be_bytes([buffer[1], buffer[2], buffer[3], buffer[4]]) as usize; + + if buffer.len() >= 5 + length { + let payload_bytes = &buffer[5..5 + length]; + + if flags == 0x00 { + if let Ok(val) = serde_json::from_slice::(payload_bytes) { + if let Some(event) = val.get("event") { + if let Some(data) = event.get("data") { + if let Some(stdout_b64) = data.get("stdout").and_then(|v| v.as_str()) { + if let Ok(decoded) = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, stdout_b64) { + if let Ok(s) = String::from_utf8(decoded) { + stdout_accum.push_str(&s); + } + } + } + if let Some(stderr_b64) = data.get("stderr").and_then(|v| v.as_str()) { + if let Ok(decoded) = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, stderr_b64) { + if let Ok(s) = String::from_utf8(decoded) { + stderr_accum.push_str(&s); + } + } + } + } + if let Some(end) = event.get("end") { + if let Some(code) = end.get("exitCode").or_else(|| end.get("exit_code")).and_then(|v| v.as_i64()) { + exit_code = code as i32; + } + } + } + } + } + + buffer.drain(0..5 + length); + } else { + break; + } + } + } + + let combined_output = if stderr_accum.is_empty() { + stdout_accum + } else if stdout_accum.is_empty() { + stderr_accum + } else { + format!("{}\n{}", stdout_accum, stderr_accum) + }; + + Ok((combined_output, exit_code)) +} + +/// 获取 E2B 沙盒的 VNC URL(desktop 模板内置 noVNC on port 6080) +/// 直接返回固定格式 URL,不检查端口,不拉起任何进程。 +pub fn get_vnc_url(sandbox_id: &str) -> String { + format!( + "https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", + sandbox_id + ) +} diff --git a/crates/flock-tools/src/e2b/lifecycle.rs b/crates/flock-tools/src/e2b/lifecycle.rs new file mode 100644 index 00000000..9586c370 --- /dev/null +++ b/crates/flock-tools/src/e2b/lifecycle.rs @@ -0,0 +1,100 @@ +use flock_core::config::settings::SandboxConfig; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct E2BCreateSandboxRequest { + #[serde(rename = "templateID")] + pub template_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct E2BSandboxResponse { + #[serde(rename = "sandboxID")] + pub sandbox_id: String, + #[serde(rename = "templateID")] + pub template_id: String, +} + +/// 检查 E2B 沙盒是否存活 +pub async fn check_alive(cfg: &SandboxConfig, sandbox_id: &str) -> bool { + let api_key = match &cfg.e2b_api_key { + Some(k) => k, + None => return false, + }; + + let client = reqwest::Client::new(); + let url = format!("https://api.e2b.app/sandboxes/{}", sandbox_id); + + match client.get(&url) + .header("X-API-Key", api_key) + .send() + .await + { + Ok(r) => r.status().is_success(), + Err(_) => false, + } +} + +/// 创建 E2B 沙盒,返回 sandbox_id +pub async fn create_sandbox(cfg: &SandboxConfig) -> anyhow::Result { + let api_key = cfg.e2b_api_key.as_ref() + .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; + + let template_id = cfg.snapshot.as_deref() + .unwrap_or("") + .trim(); + if template_id.is_empty() { + anyhow::bail!("E2B template ID is not configured. Please set a default template in Sandbox Settings."); + } + + let client = reqwest::Client::new(); + let payload = E2BCreateSandboxRequest { + template_id: template_id.to_string(), + }; + + crate::emit_info(&flock_core::tr( + &format!("正在向 E2B 申请启动沙盒 (模版: {})...", template_id), + &format!("Requesting E2B sandbox (template: {})...", template_id) + )); + + let url = "https://api.e2b.app/sandboxes"; + let resp = client.post(url) + .header("X-API-Key", api_key) + .json(&payload) + .send() + .await?; + + let status = resp.status(); + let resp_text = resp.text().await.unwrap_or_default(); + if !status.is_success() { + anyhow::bail!("Failed to create E2B sandbox (HTTP {}): {}", status, resp_text); + } + + let sandbox: E2BSandboxResponse = serde_json::from_str(&resp_text) + .map_err(|e| anyhow::anyhow!("Failed to parse E2B sandbox response: {}, body: {}", e, resp_text))?; + + crate::emit_info(&flock_core::tr("E2B 沙盒已启动。", "E2B sandbox started.")); + Ok(sandbox.sandbox_id) +} + +/// 销毁 E2B 沙盒 +pub async fn destroy_sandbox(cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { + let api_key = cfg.e2b_api_key.as_ref() + .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; + + let client = reqwest::Client::new(); + let url = format!("https://api.e2b.app/sandboxes/{}", sandbox_id); + + let resp = client.delete(&url) + .header("X-API-Key", api_key) + .send() + .await?; + + let status = resp.status(); + if !status.is_success() && status.as_u16() != 204 { + let resp_text = resp.text().await.unwrap_or_default(); + anyhow::bail!("Failed to destroy E2B sandbox (HTTP {}): {}", status, resp_text); + } + + Ok(()) +} diff --git a/crates/flock-tools/src/e2b/mod.rs b/crates/flock-tools/src/e2b/mod.rs new file mode 100644 index 00000000..326583cc --- /dev/null +++ b/crates/flock-tools/src/e2b/mod.rs @@ -0,0 +1,5 @@ +pub mod lifecycle; +pub mod exec; + +pub use lifecycle::{check_alive, create_sandbox, destroy_sandbox}; +pub use exec::execute_command; diff --git a/crates/flock-tools/src/e2b_provider.rs b/crates/flock-tools/src/e2b_provider.rs index aff47b6c..a7b6620c 100644 --- a/crates/flock-tools/src/e2b_provider.rs +++ b/crates/flock-tools/src/e2b_provider.rs @@ -161,8 +161,10 @@ impl SandboxProvider for E2BSandboxProvider { if buffer.len() >= 5 + length { let payload_bytes = &buffer[5..5 + length]; + println!("[DEBUG E2B] flags: {}, length: {}, payload_raw: {:?}", flags, length, String::from_utf8_lossy(payload_bytes)); if flags == 0x00 { if let Ok(val) = serde_json::from_slice::(payload_bytes) { + println!("[DEBUG E2B] Parsed JSON: {:?}", val); if let Some(event) = val.get("event") { if let Some(data) = event.get("data") { if let Some(stdout_b64) = data.get("stdout").and_then(|v| v.as_str()) { @@ -204,6 +206,7 @@ impl SandboxProvider for E2BSandboxProvider { format!("{}\n{}", stdout_accum, stderr_accum) }; + println!("[DEBUG E2B] Result: exit_code={}, combined_output={:?}", exit_code, combined_output); Ok((combined_output, exit_code)) } diff --git a/crates/flock-tools/src/lib.rs b/crates/flock-tools/src/lib.rs index 4b8f3812..832bb098 100644 --- a/crates/flock-tools/src/lib.rs +++ b/crates/flock-tools/src/lib.rs @@ -5,13 +5,14 @@ pub mod file_cache; pub mod tool_executor; pub mod daytona; +pub mod e2b; +pub mod sandbox_manager; pub mod sandbox_provider; -pub mod e2b_provider; -pub mod daytona_provider; pub mod local_provider; pub mod registry; pub mod tools; + pub use tools::{baidu, builtin, google, math, mcp, openweather, sandbox, serper}; /// Snapshot of all registered tools and their provider metadata. diff --git a/crates/flock-tools/src/sandbox_manager.rs b/crates/flock-tools/src/sandbox_manager.rs new file mode 100644 index 00000000..ffafaf17 --- /dev/null +++ b/crates/flock-tools/src/sandbox_manager.rs @@ -0,0 +1,171 @@ +/// sandbox_manager.rs — 沙盒管理中间层 +/// +/// 职责: +/// - 维护全局活跃沙盒 ID 的状态(ACTIVE_SANDBOX_ID) +/// - 根据 provider 字段分发到具体 provider 实现 +/// - 暴露统一接口:get_or_create、destroy、check_alive、get_vnc_url、execute_command + +use std::sync::OnceLock; +use tokio::sync::Mutex; +use flock_core::db::DbManager; +use crate::daytona::get_sandbox_config; + +static ACTIVE_SANDBOX_ID: OnceLock>> = OnceLock::new(); + +pub fn get_sandbox_id_mutex() -> &'static Mutex> { + ACTIVE_SANDBOX_ID.get_or_init(|| Mutex::new(None)) +} + +pub async fn get_active_sandbox_id() -> Option { + let mutex = get_sandbox_id_mutex(); + let lock = mutex.lock().await; + lock.clone() +} + +/// 清空活跃沙盒缓存(配置变更时调用) +pub async fn clear_active_sandbox_id() { + let mutex = get_sandbox_id_mutex(); + let mut lock = mutex.lock().await; + *lock = None; +} + +/// 检查沙盒是否存活(provider dispatch) +pub async fn check_sandbox_alive(cfg: &flock_core::config::settings::SandboxConfig, id: &str) -> bool { + let provider = cfg.provider.as_deref().unwrap_or("e2b"); + match provider { + "e2b" => crate::e2b::check_alive(cfg, id).await, + "local" => true, + _ => crate::daytona::check_sandbox_alive(cfg, id).await, + } +} + +/// 获取或创建活跃沙盒,返回 sandbox_id +pub async fn get_or_create_active_sandbox(db: &DbManager) -> anyhow::Result { + let cfg = get_sandbox_config(db).await + .ok_or_else(|| anyhow::anyhow!(flock_core::tr( + "沙盒未启用或未配置。请在系统设置中配置有效的 API Key。", + "Sandbox not enabled or configured. Please configure a valid API Key in system settings." + )))?; + + let mutex = get_sandbox_id_mutex(); + let mut lock = mutex.lock().await; + + if let Some(id) = lock.as_ref() { + if check_sandbox_alive(&cfg, id).await { + // Daytona 需要额外 set_public + if cfg.provider.as_deref().unwrap_or("e2b") == "daytona" { + let _ = crate::daytona::set_sandbox_public(&cfg, id, true).await; + } + return Ok(id.clone()); + } + crate::emit_info(&flock_core::tr( + &format!("沙盒 {} 已失效,准备重新创建...", id), + &format!("Sandbox {} has expired, preparing to recreate...", id) + )); + *lock = None; + } + + let provider = cfg.provider.as_deref().unwrap_or("e2b"); + let sandbox_id = match provider { + "e2b" => { + let id = crate::e2b::create_sandbox(&cfg).await?; + // 同步本地 workspace 到 E2B + if let Some(ws_path) = crate::get_workspace_dir() { + if let Err(e) = crate::daytona::sync::sync_up(db, &id, &ws_path).await { + crate::emit_info(&format!("E2B Sync Up failed: {}", e)); + } + } + id + } + "local" => { + crate::emit_info(&flock_core::tr("正在启动本地 Mock 沙盒 (占位)...", "Starting local mock sandbox (placeholder)...")); + "local".to_string() + } + _ => { + // Daytona + crate::daytona::create_sandbox(db, &cfg).await? + } + }; + + *lock = Some(sandbox_id.clone()); + Ok(sandbox_id) +} + +/// 销毁当前活跃沙盒 +pub async fn destroy_active_sandbox(db: &DbManager) -> anyhow::Result<()> { + let cfg = match get_sandbox_config(db).await { + Some(c) => c, + None => return Ok(()), + }; + + let mutex = get_sandbox_id_mutex(); + let mut lock = mutex.lock().await; + + let sandbox_id = match lock.as_ref() { + Some(id) => id.clone(), + None => return Ok(()), + }; + + let provider = cfg.provider.as_deref().unwrap_or("e2b"); + match provider { + "e2b" => { + if let Err(e) = crate::e2b::destroy_sandbox(&cfg, &sandbox_id).await { + crate::emit_info(&flock_core::tr( + &format!("销毁 E2B 沙盒请求失败: {}", e), + &format!("Destroying E2B sandbox request failed: {}", e) + )); + } + } + "local" => {} + _ => { + // Daytona: sync down 然后销毁 + if let Some(ws_path) = crate::get_workspace_dir() { + if let Err(e) = crate::daytona::sync::sync_down(db, &sandbox_id, &ws_path).await { + crate::emit_info(&format!("Sync Down failed: {}", e)); + } + } + if let Err(e) = crate::daytona::destroy_daytona_sandbox(&cfg, &sandbox_id).await { + crate::emit_info(&flock_core::tr( + &format!("销毁 Daytona 沙盒请求失败: {}", e), + &format!("Destroying Daytona sandbox request failed: {}", e) + )); + } + } + } + + *lock = None; + Ok(()) +} + +/// 在沙盒中执行命令(provider dispatch) +pub async fn execute_command_in_sandbox( + db: &DbManager, + sandbox_id: &str, + command: &str, +) -> anyhow::Result<(String, i32)> { + let cfg = get_sandbox_config(db).await + .ok_or_else(|| anyhow::anyhow!("沙箱未配置或未启用"))?; + + let provider = cfg.provider.as_deref().unwrap_or("e2b"); + match provider { + "e2b" => crate::e2b::execute_command(&cfg, sandbox_id, command).await, + "local" => Ok((format!("Local mock execution placeholder: executing '{}'", command), 0)), + _ => crate::daytona::execute_command_in_sandbox(db, sandbox_id, command).await, + } +} + +/// 获取沙盒 VNC URL(provider dispatch) +pub async fn get_sandbox_vnc_url(db: &DbManager, sandbox_id: &str) -> anyhow::Result { + let cfg = get_sandbox_config(db).await + .ok_or_else(|| anyhow::anyhow!(flock_core::tr( + "沙盒未配置或未启用", + "Sandbox not configured or enabled" + )))?; + + let provider = cfg.provider.as_deref().unwrap_or("e2b"); + match provider { + "e2b" => Ok(crate::e2b::exec::get_vnc_url(sandbox_id)), + "local" => anyhow::bail!("Local sandbox does not support VNC"), + _ => crate::daytona::get_sandbox_vnc_url(db, sandbox_id).await, + } +} diff --git a/crates/flock-tools/src/tools/sandbox/browser/interactive.rs b/crates/flock-tools/src/tools/sandbox/browser/interactive.rs index 92204ba3..027eb1cf 100644 --- a/crates/flock-tools/src/tools/sandbox/browser/interactive.rs +++ b/crates/flock-tools/src/tools/sandbox/browser/interactive.rs @@ -2,8 +2,9 @@ use crate::daytona::{ execute_command_in_sandbox, start_computer_use_in_sandbox, ensure_vnc_running_in_sandbox, - DISPLAY_ID, WEBSOCKIFY_PORT, + DISPLAY_ID, }; +use crate::sandbox_manager::get_sandbox_vnc_url; use flock_core::ipc_interface::events::ToolCategory; use flock_core::db::DbManager; use base64::{Engine as _, engine::general_purpose}; @@ -21,22 +22,14 @@ pub async fn handle_interactive( call_id: Option, msg_id: Option, ) -> Result { - let proxy_url = match crate::daytona::get_sandbox_vnc_url(db, sandbox_id).await { + let proxy_url = match get_sandbox_vnc_url(db, sandbox_id).await { Ok(u) => u, - Err(_) => { - let is_e2b = if let Some(cfg) = crate::daytona::get_sandbox_config(db).await { - cfg.provider.as_deref().unwrap_or("e2b") == "e2b" - } else { - true - }; - if is_e2b { - format!("https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", sandbox_id) - } else { - format!( - "https://{}-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", - WEBSOCKIFY_PORT, sandbox_id - ) - } + Err(e) => { + crate::emit_info(&flock_core::tr( + &format!("获取 VNC URL 失败: {}", e), + &format!("Failed to get VNC URL: {}", e) + )); + return Err(format!("无法获取沙盒 VNC 连接地址: {}", e)); } }; diff --git a/crates/flock-tools/src/tools/sandbox/browser/mod.rs b/crates/flock-tools/src/tools/sandbox/browser/mod.rs index 7c7f0fb0..6c6fad8c 100644 --- a/crates/flock-tools/src/tools/sandbox/browser/mod.rs +++ b/crates/flock-tools/src/tools/sandbox/browser/mod.rs @@ -3,8 +3,8 @@ mod interactive; use crate::adapter::LangGraphToolAdapter; use crate::Tool; +use crate::sandbox_manager::{get_or_create_active_sandbox, execute_command_in_sandbox}; use crate::daytona::{ - get_or_create_active_sandbox, execute_command_in_sandbox, ensure_vnc_running_in_sandbox, DISPLAY_ID, }; diff --git a/crates/flock-tools/src/tools/sandbox/code_execution.rs b/crates/flock-tools/src/tools/sandbox/code_execution.rs index 98bf9bc5..c23b59f8 100644 --- a/crates/flock-tools/src/tools/sandbox/code_execution.rs +++ b/crates/flock-tools/src/tools/sandbox/code_execution.rs @@ -1,6 +1,6 @@ use crate::adapter::LangGraphToolAdapter; use crate::Tool; -use crate::daytona::{get_or_create_active_sandbox, execute_command_in_sandbox}; +use crate::sandbox_manager::{get_or_create_active_sandbox, execute_command_in_sandbox}; use flock_core::ipc_interface::events::ToolCategory; use langgraph::tool; use std::path::Path; diff --git a/crates/flock-tools/src/tools/sandbox/computer_use.rs b/crates/flock-tools/src/tools/sandbox/computer_use.rs index 406f5425..2acf040f 100644 --- a/crates/flock-tools/src/tools/sandbox/computer_use.rs +++ b/crates/flock-tools/src/tools/sandbox/computer_use.rs @@ -1,7 +1,7 @@ use crate::adapter::LangGraphToolAdapter; use crate::Tool; +use crate::sandbox_manager::{get_or_create_active_sandbox, execute_command_in_sandbox, get_sandbox_vnc_url}; use crate::daytona::{ - get_or_create_active_sandbox, execute_command_in_sandbox, start_computer_use_in_sandbox, check_computer_use_status, ensure_vnc_running_in_sandbox, DISPLAY_ID, }; @@ -211,19 +211,14 @@ pub async fn computer_use( match act.as_str() { "interactive" => { - let proxy_url = match crate::daytona::get_sandbox_vnc_url(&db, &sandbox_id).await { + let proxy_url = match get_sandbox_vnc_url(&db, &sandbox_id).await { Ok(url) => url, - Err(_) => { - let is_e2b = if let Some(cfg) = crate::daytona::get_sandbox_config(&db).await { - cfg.provider.as_deref().unwrap_or("e2b") == "e2b" - } else { - true - }; - if is_e2b { - format!("https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", sandbox_id) - } else { - format!("https://{}-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", crate::daytona::WEBSOCKIFY_PORT, sandbox_id) - } + Err(e) => { + crate::emit_info(&flock_core::tr( + &format!("获取 VNC URL 失败: {}", e), + &format!("Failed to get VNC URL: {}", e) + )); + return Err(format!("无法获取沙盒 VNC 连接地址: {}", e)); } }; @@ -314,20 +309,14 @@ pub async fn computer_use( let (image_md, _screenshot_saved) = capture_desktop_screenshot(&db, &sandbox_id, &session_id, &name_id).await; - let proxy_url = match crate::daytona::get_sandbox_vnc_url(&db, &sandbox_id).await { + let proxy_url = match get_sandbox_vnc_url(&db, &sandbox_id).await { Ok(u) => u, Err(e) => { - crate::emit_info(&flock_core::tr(&format!("获取动态 VNC URL 失败: {}。使用静态备用 URL...", e), &format!("Failed to retrieve dynamic VNC URL: {}. Falling back to static VNC URL...", e))); - let is_e2b = if let Some(cfg) = crate::daytona::get_sandbox_config(&db).await { - cfg.provider.as_deref().unwrap_or("e2b") == "e2b" - } else { - true - }; - if is_e2b { - format!("https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", sandbox_id) - } else { - format!("https://{}-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", crate::daytona::WEBSOCKIFY_PORT, sandbox_id) - } + crate::emit_info(&flock_core::tr( + &format!("获取动态 VNC URL 失败: {}。", e), + &format!("Failed to retrieve dynamic VNC URL: {}.", e) + )); + return Err(format!("无法获取沙盒 VNC 连接地址: {}", e)); } }; diff --git a/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs b/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs index b91c9580..c6d0cbdc 100644 --- a/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs +++ b/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs @@ -1,7 +1,7 @@ use crate::adapter::LangGraphToolAdapter; use crate::Tool; +use crate::sandbox_manager::{get_or_create_active_sandbox, get_sandbox_vnc_url}; use crate::daytona::{ - get_or_create_active_sandbox, get_sandbox_vnc_url, start_computer_use_in_sandbox, ensure_vnc_running_in_sandbox }; use flock_core::ipc_interface::events::ToolCategory; @@ -42,17 +42,12 @@ pub async fn request_human_assistance( // 3. 拿到当前的 VNC 控制台 URL 链接 let proxy_url = match get_sandbox_vnc_url(&db, &sandbox_id).await { Ok(u) => u, - Err(_) => { - let is_e2b = if let Some(cfg) = crate::daytona::get_sandbox_config(&db).await { - cfg.provider.as_deref().unwrap_or("e2b") == "e2b" - } else { - true - }; - if is_e2b { - format!("https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", sandbox_id) - } else { - format!("https://6080-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", sandbox_id) - } + Err(e) => { + crate::emit_info(&flock_core::tr( + &format!("获取 VNC URL 失败: {}", e), + &format!("Failed to get VNC URL: {}", e) + )); + return Err(format!("无法获取沙盒 VNC 连接地址: {}", e)); } }; diff --git a/crates/flock-tools/src/tools/sandbox/sandbox_exec.rs b/crates/flock-tools/src/tools/sandbox/sandbox_exec.rs index 3892b1b1..24025245 100644 --- a/crates/flock-tools/src/tools/sandbox/sandbox_exec.rs +++ b/crates/flock-tools/src/tools/sandbox/sandbox_exec.rs @@ -1,6 +1,6 @@ use crate::adapter::LangGraphToolAdapter; use crate::Tool; -use crate::daytona::{get_or_create_active_sandbox, execute_command_in_sandbox}; +use crate::sandbox_manager::{get_or_create_active_sandbox, execute_command_in_sandbox}; use flock_core::ipc_interface::events::ToolCategory; use langgraph::tool; diff --git a/crates/flock-tools/tests/diagnose_sandbox.rs b/crates/flock-tools/tests/diagnose_sandbox.rs index 71fc27d2..d8cc835f 100644 --- a/crates/flock-tools/tests/diagnose_sandbox.rs +++ b/crates/flock-tools/tests/diagnose_sandbox.rs @@ -176,3 +176,74 @@ async fn test_diagnose_e2b_templates() { println!("HTTP Response Status: {}", status); println!("HTTP Response Body: {}", text); } + +#[tokio::test] +async fn test_diagnose_e2b_sandbox() { + let db = match DbManager::init().await { + Ok(d) => d, + Err(e) => { + println!("Failed to open DB: {}", e); + return; + } + }; + + let mut cfg: flock_core::config::settings::SandboxConfig = db.get_config("sandbox").await.unwrap(); + if let (Some(ct), Some(n)) = (&cfg.e2b_api_key_encrypted, &cfg.e2b_api_key_nonce) { + if let Ok(salt) = db.get_or_create_salt().await { + if let Ok(decrypted) = flock_core::crypto::decrypt_value(ct, n, &salt) { + cfg.e2b_api_key = Some(decrypted); + } + } + } + + // Set template to the desktop template ID + cfg.snapshot = Some("k0wmnzir0zuzye6dndlw".to_string()); + + use flock_tools::sandbox_provider::SandboxProvider; + let prov = flock_tools::e2b_provider::E2BSandboxProvider; + + println!("Starting E2B sandbox with template k0wmnzir0zuzye6dndlw..."); + let sandbox_id = match prov.get_or_create_sandbox(&cfg).await { + Ok(id) => id, + Err(e) => { + println!("Failed to create sandbox: {}", e); + return; + } + }; + println!("Sandbox started successfully: {}", sandbox_id); + + // Ensure VNC running + println!("Ensuring VNC is running..."); + if let Err(e) = flock_tools::daytona::ensure_vnc_running_in_sandbox(&db, &sandbox_id).await { + println!("ensure_vnc_running_in_sandbox error: {}", e); + } + + // 1. Check listening ports and env + println!("\n--- Env and Path ---"); + let env_out = match prov.execute_command(&cfg, &sandbox_id, "env && echo 'PATH IS:' && echo $PATH").await { + Ok((out, _)) => out, + Err(e) => format!("Error executing env command: {}", e), + }; + println!("{}", env_out); + + // 2. Check installed debian packages + println!("\n--- Debian Packages Check ---"); + let dpkg_out = match prov.execute_command(&cfg, &sandbox_id, "dpkg -l | grep -iE 'vnc|xvfb|fluxbox|websockify|novnc'").await { + Ok((out, _)) => out, + Err(e) => format!("Error executing dpkg command: {}", e), + }; + println!("{}", dpkg_out); + + // 3. Check logs of vnc + println!("\n--- VNC logs ---"); + let log_out = match prov.execute_command(&cfg, &sandbox_id, "cat /tmp/vnc_start.log /tmp/websockify.log /tmp/x11vnc.log 2>/dev/null").await { + Ok((out, _)) => out, + Err(e) => format!("Error executing cat command: {}", e), + }; + println!("{}", log_out); + + // Cleanup + println!("\nDestroying sandbox..."); + let _ = prov.destroy_sandbox(&cfg, &sandbox_id).await; + println!("Sandbox destroyed."); +} diff --git a/flock-ui/src-tauri/src/commands/common/sandbox.rs b/flock-ui/src-tauri/src/commands/common/sandbox.rs index 80093b5f..b4e069ea 100644 --- a/flock-ui/src-tauri/src/commands/common/sandbox.rs +++ b/flock-ui/src-tauri/src/commands/common/sandbox.rs @@ -6,7 +6,7 @@ use crate::commands::assistant::SharedAgentState; pub async fn destroy_sandbox( db: State<'_, crate::SharedDbManager>, ) -> Result<(), String> { - flock_tools::daytona::destroy_active_sandbox(&*db) + flock_tools::sandbox_manager::destroy_active_sandbox(&*db) .await .map_err(|e| e.to_string()) } @@ -79,7 +79,7 @@ pub async fn cleanup_all_sandboxes( } // 清除本地缓存 - let _ = flock_tools::daytona::destroy_active_sandbox(db_ref).await; + let _ = flock_tools::sandbox_manager::destroy_active_sandbox(db_ref).await; Ok(flock_core::tr( &format!("清理完成:已销毁 {} 个沙盒,失败 {} 个。", deleted, failed), @@ -93,8 +93,8 @@ pub async fn get_active_sandbox_vnc_url( _state: State<'_, SharedAgentState>, db: State<'_, crate::SharedDbManager>, ) -> Result, String> { - if let Some(sandbox_id) = flock_tools::daytona::get_active_sandbox_id().await { - match flock_tools::daytona::get_sandbox_vnc_url(&*db, &sandbox_id).await { + if let Some(sandbox_id) = flock_tools::sandbox_manager::get_active_sandbox_id().await { + match flock_tools::sandbox_manager::get_sandbox_vnc_url(&*db, &sandbox_id).await { Ok(url) => Ok(Some(url)), Err(_) => { let fallback_url = match flock_tools::daytona::get_sandbox_config(&*db).await { diff --git a/flock-ui/src-tauri/src/commands/file.rs b/flock-ui/src-tauri/src/commands/file.rs index a31fa4d4..b84d1c63 100644 --- a/flock-ui/src-tauri/src/commands/file.rs +++ b/flock-ui/src-tauri/src/commands/file.rs @@ -1,5 +1,6 @@ use crate::workspace; -use flock_tools::daytona::{get_active_sandbox_id, fs::DaytonaFs}; +use flock_tools::sandbox_manager::get_active_sandbox_id; +use flock_tools::daytona::fs::DaytonaFs; use std::path::PathBuf; async fn is_sandbox_active(_db: &crate::SharedDbManager) -> bool { diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index 4952c19b..a0cd867a 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -120,10 +120,7 @@ pub async fn set_app_config( } // 关键修复:清除旧的活跃沙盒缓存,防止状态穿透 - let mutex = flock_tools::daytona::get_sandbox_id_mutex(); - if let Ok(mut lock) = mutex.try_lock() { - *lock = None; - } + let _ = flock_tools::sandbox_manager::clear_active_sandbox_id().await; } Ok(()) @@ -234,9 +231,9 @@ pub async fn create_playwright_snapshot( .map_err(|e| e.to_string()) } -/// 列出所有 Daytona/E2B 沙盒 +/// 列出 Daytona 沙盒实例(E2B 不显示实例列表) #[tauri::command] -pub async fn list_daytona_sandboxes( +pub async fn list_sandboxes( db: State<'_, SharedDbManager>, ) -> Result { use flock_core::db::DbManager; @@ -248,41 +245,8 @@ pub async fn list_daytona_sandboxes( }; let provider = cfg.provider.as_deref().unwrap_or("e2b"); - if provider == "e2b" { - let api_key = cfg.e2b_api_key.as_ref().unwrap(); - let client = reqwest::Client::new(); - let url = "https://api.e2b.app/sandboxes"; - let resp = client.get(url) - .header("X-API-Key", api_key) - .send() - .await - .map_err(|e| flock_core::tr( - &format!("请求 E2B 沙盒列表失败: {}", e), - &format!("Failed to request E2B sandbox list: {}", e) - ))?; - - let text = resp.text().await.unwrap_or_default(); - let list_val: serde_json::Value = serde_json::from_str(&text) - .map_err(|e| flock_core::tr( - &format!("解析 E2B 沙盒列表失败: {}", e), - &format!("Failed to parse E2B sandbox list: {}", e) - ))?; - - // 统一字段映射,以匹配前端 Daytona 的渲染格式 (id, status, snapshot) - if let Some(arr) = list_val.as_array() { - let mapped: Vec = arr.iter().map(|item| { - let id = item.get("sandboxID").and_then(|v| v.as_str()).unwrap_or_default(); - let template_id = item.get("templateID").and_then(|v| v.as_str()).unwrap_or_default(); - serde_json::json!({ - "id": id, - "status": "running", - "snapshot": template_id, - }) - }).collect(); - return Ok(serde_json::json!(mapped)); - } - return Ok(serde_json::json!([])); - } else if provider == "local" { + // E2B 不显示 instances,Daytona 会有残留需要手动销毁 + if provider == "e2b" || provider == "local" { return Ok(serde_json::json!([])); } @@ -310,9 +274,9 @@ pub async fn list_daytona_sandboxes( Ok(val) } -/// 删除指定 Daytona/E2B 沙盒 +/// 删除指定 Daytona 沙盒(E2B 不需要此操作) #[tauri::command] -pub async fn delete_daytona_sandbox( +pub async fn delete_sandbox( db: State<'_, SharedDbManager>, id: String, ) -> Result<(), String> { @@ -324,33 +288,7 @@ pub async fn delete_daytona_sandbox( .ok_or_else(|| flock_core::tr("沙盒未配置或未启用", "Sandbox not configured or enabled"))?; let provider = cfg.provider.as_deref().unwrap_or("e2b"); - if provider == "e2b" { - let api_key = cfg.e2b_api_key.as_ref().unwrap(); - let client = reqwest::Client::new(); - let url = format!("https://api.e2b.app/sandboxes/{}", id); - let resp = client.delete(&url) - .header("X-API-Key", api_key) - .send() - .await - .map_err(|e| flock_core::tr( - &format!("发送删除 E2B 沙盒请求失败: {}", e), - &format!("Failed to send delete E2B sandbox request: {}", e) - ))?; - - if resp.status().is_success() || resp.status().as_u16() == 204 { - if let Some(active_id) = flock_tools::daytona::get_active_sandbox_id().await { - if active_id == id { - let _ = flock_tools::daytona::destroy_active_sandbox(db_ref).await; - } - } - return Ok(()); - } else { - return Err(flock_core::tr( - &format!("删除 E2B 沙盒失败,HTTP 状态码: {}", resp.status()), - &format!("Failed to delete E2B sandbox, HTTP status code: {}", resp.status()) - )); - } - } else if provider == "local" { + if provider == "e2b" || provider == "local" { return Ok(()); } @@ -370,9 +308,9 @@ pub async fn delete_daytona_sandbox( if resp.status().is_success() { // 如果删除的是当前活跃的沙盒,清理本地缓存 - if let Some(active_id) = flock_tools::daytona::get_active_sandbox_id().await { + if let Some(active_id) = flock_tools::sandbox_manager::get_active_sandbox_id().await { if active_id == id { - let _ = flock_tools::daytona::destroy_active_sandbox(db_ref).await; + flock_tools::sandbox_manager::clear_active_sandbox_id().await; } } Ok(()) @@ -391,14 +329,13 @@ pub async fn reuse_sandbox( sandbox_id: String, ) -> Result { use flock_core::db::DbManager; - use flock_tools::daytona::{get_sandbox_config, check_sandbox_alive}; let db_ref: &DbManager = &*db; - let cfg = get_sandbox_config(db_ref).await + let cfg = flock_tools::daytona::get_sandbox_config(db_ref).await .ok_or_else(|| flock_core::tr("沙盒未配置或未启用", "Sandbox not configured or enabled"))?; // 验证沙盒是否存活 - if !check_sandbox_alive(&cfg, &sandbox_id).await { + if !flock_tools::sandbox_manager::check_sandbox_alive(&cfg, &sandbox_id).await { return Err(flock_core::tr( &format!("沙盒 {} 不存在或已停止", sandbox_id), &format!("Sandbox {} does not exist or has stopped", sandbox_id) @@ -406,7 +343,7 @@ pub async fn reuse_sandbox( } // 设置为活跃沙盒 - let mutex = flock_tools::daytona::get_sandbox_id_mutex(); + let mutex = flock_tools::sandbox_manager::get_sandbox_id_mutex(); let mut lock = mutex.lock().await; *lock = Some(sandbox_id.clone()); @@ -418,7 +355,7 @@ pub async fn reuse_sandbox( /// 列出所有 Daytona/E2B 快照/自定义模板 #[tauri::command] -pub async fn list_daytona_snapshots( +pub async fn list_sandbox_templates( db: State<'_, SharedDbManager>, provider: Option, api_key: Option, @@ -552,9 +489,9 @@ pub async fn list_daytona_snapshots( Ok(val) } -/// 删除指定 Daytona 快照 +/// 删除指定 Daytona/E2B 快照/模板 #[tauri::command] -pub async fn delete_daytona_snapshot( +pub async fn delete_sandbox_template( db: State<'_, SharedDbManager>, id: String, ) -> Result<(), String> { diff --git a/flock-ui/src-tauri/src/lib.rs b/flock-ui/src-tauri/src/lib.rs index 2dd7b4b1..7330249a 100644 --- a/flock-ui/src-tauri/src/lib.rs +++ b/flock-ui/src-tauri/src/lib.rs @@ -135,11 +135,11 @@ pub fn run() { commands::set_locale, commands::test_sandbox_connection, commands::create_playwright_snapshot, - commands::list_daytona_sandboxes, - commands::delete_daytona_sandbox, + commands::list_sandboxes, + commands::delete_sandbox, commands::reuse_sandbox, - commands::list_daytona_snapshots, - commands::delete_daytona_snapshot, + commands::list_sandbox_templates, + commands::delete_sandbox_template, // 工具提供商 commands::list_tool_providers, commands::list_tools, diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SandboxListSection.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SandboxListSection.tsx index e29337e3..a7fbdc35 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SandboxListSection.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SandboxListSection.tsx @@ -41,7 +41,7 @@ export function SandboxListSection() { const fetchSandboxes = async () => { setLoading(true); try { - const data = await invoke('list_daytona_sandboxes'); + const data = await invoke('list_sandboxes'); // 解析 sandboxes 数组 let list: SandboxItem[] = []; if (Array.isArray(data)) { @@ -103,7 +103,7 @@ export function SandboxListSection() { const handleDelete = async (id: string) => { setDeletingId(id); try { - await invoke('delete_daytona_sandbox', { id }); + await invoke('delete_sandbox', { id }); notifications.show({ title: t('common.success'), message: t('settings.sandbox.deleteSuccessMsg'), diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx index 6ee475d8..0b44e8a9 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx @@ -63,7 +63,7 @@ export function SnapshotListSection({ const fetchSnapshots = async () => { setLoading(true); try { - const data = await invoke('list_daytona_snapshots'); + const data = await invoke('list_sandbox_templates'); let list: SnapshotItem[] = []; if (Array.isArray(data)) { list = data; @@ -105,7 +105,7 @@ export function SnapshotListSection({ const handleDelete = async (id: string) => { setDeletingId(id); try { - await invoke('delete_daytona_snapshot', { id }); + await invoke('delete_sandbox_template', { id }); notifications.show({ title: t('common.success'), message: t('settings.sandbox.deleteSnapshotSuccess'), diff --git a/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts b/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts index db4f11b0..4292d28e 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts +++ b/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts @@ -35,7 +35,7 @@ export function useSandboxSettings() { const fetchSnapshotsList = async () => { try { - const data = await invoke('list_daytona_snapshots', { + const data = await invoke('list_sandbox_templates', { provider, apiKey: provider === 'e2b' ? e2bApiKey : apiKey, }); From 4ba59826b570c9834652ef307a7121b757ca029d Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Sat, 27 Jun 2026 08:57:29 +0800 Subject: [PATCH 06/25] refactor(sandbox): implement strategy pattern for SandboxProvider and fix E2B daylight routing --- crates/flock-tools/src/daytona/mod.rs | 1 + crates/flock-tools/src/daytona/provider.rs | 35 +++ crates/flock-tools/src/daytona_provider.rs | 36 --- crates/flock-tools/src/e2b/mod.rs | 1 + crates/flock-tools/src/e2b/provider.rs | 35 +++ crates/flock-tools/src/e2b_provider.rs | 233 ------------------ crates/flock-tools/src/local_provider.rs | 24 +- crates/flock-tools/src/sandbox_manager.rs | 121 ++++----- crates/flock-tools/src/sandbox_provider.rs | 17 +- .../src/tools/sandbox/browser/interactive.rs | 8 +- .../src/tools/sandbox/browser/mod.rs | 7 +- .../src/tools/sandbox/computer_use.rs | 4 +- .../components/SnapshotListSection.tsx | 12 +- 13 files changed, 164 insertions(+), 370 deletions(-) create mode 100644 crates/flock-tools/src/daytona/provider.rs delete mode 100644 crates/flock-tools/src/daytona_provider.rs create mode 100644 crates/flock-tools/src/e2b/provider.rs delete mode 100644 crates/flock-tools/src/e2b_provider.rs diff --git a/crates/flock-tools/src/daytona/mod.rs b/crates/flock-tools/src/daytona/mod.rs index a68ec03c..f7d05ab9 100644 --- a/crates/flock-tools/src/daytona/mod.rs +++ b/crates/flock-tools/src/daytona/mod.rs @@ -7,6 +7,7 @@ mod snapshot; mod constants; pub mod volume; pub mod fs; +pub mod provider; pub use state::emit_human_takeover; pub use config::{get_sandbox_config, get_api_base}; diff --git a/crates/flock-tools/src/daytona/provider.rs b/crates/flock-tools/src/daytona/provider.rs new file mode 100644 index 00000000..d5dde025 --- /dev/null +++ b/crates/flock-tools/src/daytona/provider.rs @@ -0,0 +1,35 @@ +use crate::sandbox_provider::SandboxProvider; +use flock_core::config::settings::SandboxConfig; +use flock_core::db::DbManager; +use async_trait::async_trait; +use std::path::Path; + +pub struct DaytonaProvider; + +#[async_trait] +impl SandboxProvider for DaytonaProvider { + async fn check_alive(&self, cfg: &SandboxConfig, sandbox_id: &str) -> bool { + crate::daytona::check_sandbox_alive(cfg, sandbox_id).await + } + async fn create_sandbox(&self, db: &DbManager, cfg: &SandboxConfig) -> anyhow::Result { + crate::daytona::create_sandbox(db, cfg).await + } + async fn destroy_sandbox(&self, _db: &DbManager, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { + crate::daytona::destroy_daytona_sandbox(cfg, sandbox_id).await + } + async fn execute_command(&self, db: &DbManager, _cfg: &SandboxConfig, sandbox_id: &str, command: &str) -> anyhow::Result<(String, i32)> { + crate::daytona::execute_command_in_sandbox(db, sandbox_id, command).await + } + async fn get_vnc_url(&self, db: &DbManager, _cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result { + crate::daytona::get_sandbox_vnc_url(db, sandbox_id).await + } + async fn ensure_vnc_running(&self, db: &DbManager, _cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { + crate::daytona::ensure_vnc_running_in_sandbox(db, sandbox_id).await + } + async fn sync_up(&self, db: &DbManager, sandbox_id: &str, ws_path: &Path) -> anyhow::Result<()> { + crate::daytona::sync::sync_up(db, sandbox_id, ws_path).await + } + async fn sync_down(&self, db: &DbManager, sandbox_id: &str, ws_path: &Path) -> anyhow::Result<()> { + crate::daytona::sync::sync_down(db, sandbox_id, ws_path).await + } +} diff --git a/crates/flock-tools/src/daytona_provider.rs b/crates/flock-tools/src/daytona_provider.rs deleted file mode 100644 index ca64c6c7..00000000 --- a/crates/flock-tools/src/daytona_provider.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::sandbox_provider::SandboxProvider; -use flock_core::config::settings::SandboxConfig; -use async_trait::async_trait; - -pub struct DaytonaSandboxProvider; - -#[async_trait] -impl SandboxProvider for DaytonaSandboxProvider { - async fn check_alive(&self, cfg: &SandboxConfig, sandbox_id: &str) -> bool { - crate::daytona::check_sandbox_alive(cfg, sandbox_id).await - } - - async fn get_or_create_sandbox(&self, cfg: &SandboxConfig) -> anyhow::Result { - // 由于 get_or_create_active_sandbox 会通过 db 读取配置, - // 我们在 daytona::lifecycle.rs 外部已经完成了接口分发, - // 这里只是其底层的实现封装包装。 - anyhow::bail!("Daytona sandbox creation should be managed via lifecycle") - } - - async fn execute_command(&self, cfg: &SandboxConfig, sandbox_id: &str, command: &str) -> anyhow::Result<(String, i32)> { - // 使用底层的 db 获取方式执行 - if let Some(db) = crate::get_db_manager() { - crate::daytona::execute_command_in_sandbox(&db, sandbox_id, command).await - } else { - anyhow::bail!("Database manager not initialized") - } - } - - async fn destroy_sandbox(&self, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { - if let Some(db) = crate::get_db_manager() { - crate::daytona::destroy_active_sandbox(&db).await - } else { - anyhow::bail!("Database manager not initialized") - } - } -} diff --git a/crates/flock-tools/src/e2b/mod.rs b/crates/flock-tools/src/e2b/mod.rs index 326583cc..af774681 100644 --- a/crates/flock-tools/src/e2b/mod.rs +++ b/crates/flock-tools/src/e2b/mod.rs @@ -1,5 +1,6 @@ pub mod lifecycle; pub mod exec; +pub mod provider; pub use lifecycle::{check_alive, create_sandbox, destroy_sandbox}; pub use exec::execute_command; diff --git a/crates/flock-tools/src/e2b/provider.rs b/crates/flock-tools/src/e2b/provider.rs new file mode 100644 index 00000000..a58f9631 --- /dev/null +++ b/crates/flock-tools/src/e2b/provider.rs @@ -0,0 +1,35 @@ +use crate::sandbox_provider::SandboxProvider; +use flock_core::config::settings::SandboxConfig; +use flock_core::db::DbManager; +use async_trait::async_trait; +use std::path::Path; + +pub struct E2bProvider; + +#[async_trait] +impl SandboxProvider for E2bProvider { + async fn check_alive(&self, cfg: &SandboxConfig, sandbox_id: &str) -> bool { + crate::e2b::check_alive(cfg, sandbox_id).await + } + async fn create_sandbox(&self, _db: &DbManager, cfg: &SandboxConfig) -> anyhow::Result { + crate::e2b::create_sandbox(cfg).await + } + async fn destroy_sandbox(&self, _db: &DbManager, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { + crate::e2b::destroy_sandbox(cfg, sandbox_id).await + } + async fn execute_command(&self, _db: &DbManager, cfg: &SandboxConfig, sandbox_id: &str, command: &str) -> anyhow::Result<(String, i32)> { + crate::e2b::execute_command(cfg, sandbox_id, command).await + } + async fn get_vnc_url(&self, _db: &DbManager, _cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result { + Ok(crate::e2b::exec::get_vnc_url(sandbox_id)) + } + async fn ensure_vnc_running(&self, _db: &DbManager, _cfg: &SandboxConfig, _sandbox_id: &str) -> anyhow::Result<()> { + Ok(()) + } + async fn sync_up(&self, _db: &DbManager, _sandbox_id: &str, _ws_path: &Path) -> anyhow::Result<()> { + Ok(()) + } + async fn sync_down(&self, _db: &DbManager, _sandbox_id: &str, _ws_path: &Path) -> anyhow::Result<()> { + Ok(()) + } +} diff --git a/crates/flock-tools/src/e2b_provider.rs b/crates/flock-tools/src/e2b_provider.rs deleted file mode 100644 index a7b6620c..00000000 --- a/crates/flock-tools/src/e2b_provider.rs +++ /dev/null @@ -1,233 +0,0 @@ -use crate::sandbox_provider::SandboxProvider; -use flock_core::config::settings::SandboxConfig; -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; - -pub struct E2BSandboxProvider; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct E2BCreateSandboxRequest { - #[serde(rename = "templateID")] - pub template_id: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct E2BSandboxResponse { - #[serde(rename = "sandboxID")] - pub sandbox_id: String, - #[serde(rename = "templateID")] - pub template_id: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct E2BProcessRequest { - pub cmd: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct E2BProcessResponse { - pub stdout: Option, - pub stderr: Option, - #[serde(rename = "exitCode")] - pub exit_code: Option, -} - -#[async_trait] -impl SandboxProvider for E2BSandboxProvider { - async fn check_alive(&self, cfg: &SandboxConfig, sandbox_id: &str) -> bool { - let api_key = match &cfg.e2b_api_key { - Some(k) => k, - None => return false, - }; - - let client = reqwest::Client::new(); - let url = format!("https://api.e2b.app/sandboxes/{}", sandbox_id); - - let resp = match client.get(&url) - .header("X-API-Key", api_key) - .send() - .await - { - Ok(r) => r, - Err(_) => return false, - }; - - resp.status().is_success() - } - - async fn get_or_create_sandbox(&self, cfg: &SandboxConfig) -> anyhow::Result { - let api_key = cfg.e2b_api_key.as_ref() - .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; - - let client = reqwest::Client::new(); - let mut template_id = cfg.snapshot.as_deref() - .unwrap_or("base") - .trim(); - if template_id.is_empty() { - template_id = "base"; - } - - let payload = E2BCreateSandboxRequest { - template_id: template_id.to_string(), - }; - - let url = "https://api.e2b.app/sandboxes"; - crate::emit_info(&flock_core::tr( - &format!("正在向 E2B 申请启动沙盒 (模版: {})...", template_id), - &format!("Requesting E2B sandbox (template: {})...", template_id) - )); - - let resp = client.post(url) - .header("X-API-Key", api_key) - .json(&payload) - .send() - .await?; - - let status = resp.status(); - let resp_text = resp.text().await.unwrap_or_default(); - if !status.is_success() { - anyhow::bail!("Failed to create E2B sandbox (HTTP {}): {}", status, resp_text); - } - - let sandbox: E2BSandboxResponse = serde_json::from_str(&resp_text) - .map_err(|e| anyhow::anyhow!("Failed to parse E2B sandbox response: {}, body: {}", e, resp_text))?; - - crate::emit_info(&flock_core::tr("E2B 沙盒已启动。", "E2B sandbox started.")); - Ok(sandbox.sandbox_id) - } - - async fn execute_command(&self, cfg: &SandboxConfig, sandbox_id: &str, command: &str) -> anyhow::Result<(String, i32)> { - let api_key = cfg.e2b_api_key.as_ref() - .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; - - let client = reqwest::Client::new(); - let url = format!("https://49983-{}.e2b.app/process.Process/Start", sandbox_id); - - #[derive(Debug, serde::Serialize)] - struct ProcessConfig { - cmd: String, - args: Vec, - envs: std::collections::HashMap, - cwd: String, - } - - #[derive(Debug, serde::Serialize)] - struct StartRequest { - process: ProcessConfig, - } - - let payload = StartRequest { - process: ProcessConfig { - cmd: "/bin/bash".to_string(), - args: vec!["-c".to_string(), command.to_string()], - envs: std::collections::HashMap::new(), - cwd: "/workspace".to_string(), - }, - }; - - let body_str = serde_json::to_string(&payload)?; - - let resp = client.post(&url) - .header("X-API-Key", api_key) - .header("Connect-Protocol-Version", "1") - .header("Content-Type", "application/connect+json") - .header(reqwest::header::USER_AGENT, "flock-agent") - .body(body_str) - .send() - .await?; - - let status = resp.status(); - if !status.is_success() { - let resp_text = resp.text().await.unwrap_or_default(); - anyhow::bail!("Failed to start command in E2B sandbox (HTTP {}): {}", status, resp_text); - } - - let mut stdout_accum = String::new(); - let mut stderr_accum = String::new(); - let mut exit_code = 0; - - use futures::StreamExt; - let mut body = resp.bytes_stream(); - let mut buffer = Vec::new(); - - while let Some(chunk_res) = body.next().await { - let chunk = chunk_res?; - buffer.extend_from_slice(&chunk); - - while buffer.len() >= 5 { - let flags = buffer[0]; - let length = u32::from_be_bytes([buffer[1], buffer[2], buffer[3], buffer[4]]) as usize; - - if buffer.len() >= 5 + length { - let payload_bytes = &buffer[5..5 + length]; - - println!("[DEBUG E2B] flags: {}, length: {}, payload_raw: {:?}", flags, length, String::from_utf8_lossy(payload_bytes)); - if flags == 0x00 { - if let Ok(val) = serde_json::from_slice::(payload_bytes) { - println!("[DEBUG E2B] Parsed JSON: {:?}", val); - if let Some(event) = val.get("event") { - if let Some(data) = event.get("data") { - if let Some(stdout_b64) = data.get("stdout").and_then(|v| v.as_str()) { - if let Ok(decoded) = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, stdout_b64) { - if let Ok(s) = String::from_utf8(decoded) { - stdout_accum.push_str(&s); - } - } - } - if let Some(stderr_b64) = data.get("stderr").and_then(|v| v.as_str()) { - if let Ok(decoded) = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, stderr_b64) { - if let Ok(s) = String::from_utf8(decoded) { - stderr_accum.push_str(&s); - } - } - } - } - if let Some(end) = event.get("end") { - if let Some(code) = end.get("exitCode").or_else(|| end.get("exit_code")).and_then(|v| v.as_i64()) { - exit_code = code as i32; - } - } - } - } - } - - buffer.drain(0..5 + length); - } else { - break; - } - } - } - - let combined_output = if stderr_accum.is_empty() { - stdout_accum - } else if stdout_accum.is_empty() { - stderr_accum - } else { - format!("{}\n{}", stdout_accum, stderr_accum) - }; - - println!("[DEBUG E2B] Result: exit_code={}, combined_output={:?}", exit_code, combined_output); - Ok((combined_output, exit_code)) - } - - async fn destroy_sandbox(&self, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { - let api_key = cfg.e2b_api_key.as_ref() - .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; - - let client = reqwest::Client::new(); - let url = format!("https://api.e2b.app/sandboxes/{}", sandbox_id); - - let resp = client.delete(&url) - .header("X-API-Key", api_key) - .send() - .await?; - - let status = resp.status(); - if !status.is_success() { - let resp_text = resp.text().await.unwrap_or_default(); - anyhow::bail!("Failed to destroy E2B sandbox (HTTP {}): {}", status, resp_text); - } - - Ok(()) - } -} diff --git a/crates/flock-tools/src/local_provider.rs b/crates/flock-tools/src/local_provider.rs index c0746544..0b8d7820 100644 --- a/crates/flock-tools/src/local_provider.rs +++ b/crates/flock-tools/src/local_provider.rs @@ -1,6 +1,8 @@ use crate::sandbox_provider::SandboxProvider; use flock_core::config::settings::SandboxConfig; +use flock_core::db::DbManager; use async_trait::async_trait; +use std::path::Path; pub struct LocalSandboxProvider; @@ -9,17 +11,27 @@ impl SandboxProvider for LocalSandboxProvider { async fn check_alive(&self, _cfg: &SandboxConfig, _sandbox_id: &str) -> bool { true } - - async fn get_or_create_sandbox(&self, _cfg: &SandboxConfig) -> anyhow::Result { + async fn create_sandbox(&self, _db: &DbManager, _cfg: &SandboxConfig) -> anyhow::Result { + crate::emit_info(&flock_core::tr("正在启动本地 Mock 沙盒 (占位)...", "Starting local mock sandbox (placeholder)...")); Ok("local".to_string()) } - - async fn execute_command(&self, _cfg: &SandboxConfig, _sandbox_id: &str, command: &str) -> anyhow::Result<(String, i32)> { + async fn destroy_sandbox(&self, _db: &DbManager, _cfg: &SandboxConfig, _sandbox_id: &str) -> anyhow::Result<()> { + Ok(()) + } + async fn execute_command(&self, _db: &DbManager, _cfg: &SandboxConfig, _sandbox_id: &str, command: &str) -> anyhow::Result<(String, i32)> { crate::emit_info(&format!("Local mock executing: {}", command)); Ok((format!("Local mock execution placeholder: executing '{}'", command), 0)) } - - async fn destroy_sandbox(&self, _cfg: &SandboxConfig, _sandbox_id: &str) -> anyhow::Result<()> { + async fn get_vnc_url(&self, _db: &DbManager, _cfg: &SandboxConfig, _sandbox_id: &str) -> anyhow::Result { + anyhow::bail!("Local sandbox does not support VNC") + } + async fn ensure_vnc_running(&self, _db: &DbManager, _cfg: &SandboxConfig, _sandbox_id: &str) -> anyhow::Result<()> { + Ok(()) + } + async fn sync_up(&self, _db: &DbManager, _sandbox_id: &str, _ws_path: &Path) -> anyhow::Result<()> { + Ok(()) + } + async fn sync_down(&self, _db: &DbManager, _sandbox_id: &str, _ws_path: &Path) -> anyhow::Result<()> { Ok(()) } } diff --git a/crates/flock-tools/src/sandbox_manager.rs b/crates/flock-tools/src/sandbox_manager.rs index ffafaf17..ada2752d 100644 --- a/crates/flock-tools/src/sandbox_manager.rs +++ b/crates/flock-tools/src/sandbox_manager.rs @@ -9,6 +9,7 @@ use std::sync::OnceLock; use tokio::sync::Mutex; use flock_core::db::DbManager; use crate::daytona::get_sandbox_config; +use crate::sandbox_provider::SandboxProvider; static ACTIVE_SANDBOX_ID: OnceLock>> = OnceLock::new(); @@ -22,24 +23,27 @@ pub async fn get_active_sandbox_id() -> Option { lock.clone() } -/// 清空活跃沙盒缓存(配置变更时调用) pub async fn clear_active_sandbox_id() { let mutex = get_sandbox_id_mutex(); let mut lock = mutex.lock().await; *lock = None; } -/// 检查沙盒是否存活(provider dispatch) -pub async fn check_sandbox_alive(cfg: &flock_core::config::settings::SandboxConfig, id: &str) -> bool { - let provider = cfg.provider.as_deref().unwrap_or("e2b"); - match provider { - "e2b" => crate::e2b::check_alive(cfg, id).await, - "local" => true, - _ => crate::daytona::check_sandbox_alive(cfg, id).await, +/// 获取当前配置对应的 SandboxProvider 实现 +pub fn get_provider(provider_name: &str) -> Box { + match provider_name { + "e2b" => Box::new(crate::e2b::provider::E2bProvider), + "local" => Box::new(crate::local_provider::LocalSandboxProvider), + _ => Box::new(crate::daytona::provider::DaytonaProvider), } } -/// 获取或创建活跃沙盒,返回 sandbox_id +pub async fn check_sandbox_alive(cfg: &flock_core::config::settings::SandboxConfig, id: &str) -> bool { + let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); + let provider = get_provider(provider_name); + provider.check_alive(cfg, id).await +} + pub async fn get_or_create_active_sandbox(db: &DbManager) -> anyhow::Result { let cfg = get_sandbox_config(db).await .ok_or_else(|| anyhow::anyhow!(flock_core::tr( @@ -52,7 +56,8 @@ pub async fn get_or_create_active_sandbox(db: &DbManager) -> anyhow::Result anyhow::Result { - let id = crate::e2b::create_sandbox(&cfg).await?; - // 同步本地 workspace 到 E2B - if let Some(ws_path) = crate::get_workspace_dir() { - if let Err(e) = crate::daytona::sync::sync_up(db, &id, &ws_path).await { - crate::emit_info(&format!("E2B Sync Up failed: {}", e)); - } - } - id - } - "local" => { - crate::emit_info(&flock_core::tr("正在启动本地 Mock 沙盒 (占位)...", "Starting local mock sandbox (placeholder)...")); - "local".to_string() + let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); + let provider = get_provider(provider_name); + + let sandbox_id = provider.create_sandbox(db, &cfg).await?; + + if let Some(ws_path) = crate::get_workspace_dir() { + if let Err(e) = provider.sync_up(db, &sandbox_id, &ws_path).await { + crate::emit_info(&format!("Sync Up failed: {}", e)); } - _ => { - // Daytona - crate::daytona::create_sandbox(db, &cfg).await? - } - }; + } *lock = Some(sandbox_id.clone()); Ok(sandbox_id) } -/// 销毁当前活跃沙盒 pub async fn destroy_active_sandbox(db: &DbManager) -> anyhow::Result<()> { let cfg = match get_sandbox_config(db).await { Some(c) => c, @@ -106,38 +99,23 @@ pub async fn destroy_active_sandbox(db: &DbManager) -> anyhow::Result<()> { None => return Ok(()), }; - let provider = cfg.provider.as_deref().unwrap_or("e2b"); - match provider { - "e2b" => { - if let Err(e) = crate::e2b::destroy_sandbox(&cfg, &sandbox_id).await { - crate::emit_info(&flock_core::tr( - &format!("销毁 E2B 沙盒请求失败: {}", e), - &format!("Destroying E2B sandbox request failed: {}", e) - )); - } - } - "local" => {} - _ => { - // Daytona: sync down 然后销毁 - if let Some(ws_path) = crate::get_workspace_dir() { - if let Err(e) = crate::daytona::sync::sync_down(db, &sandbox_id, &ws_path).await { - crate::emit_info(&format!("Sync Down failed: {}", e)); - } - } - if let Err(e) = crate::daytona::destroy_daytona_sandbox(&cfg, &sandbox_id).await { - crate::emit_info(&flock_core::tr( - &format!("销毁 Daytona 沙盒请求失败: {}", e), - &format!("Destroying Daytona sandbox request failed: {}", e) - )); - } + let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); + let provider = get_provider(provider_name); + + if let Some(ws_path) = crate::get_workspace_dir() { + if let Err(e) = provider.sync_down(db, &sandbox_id, &ws_path).await { + crate::emit_info(&format!("Sync Down failed: {}", e)); } } + if let Err(e) = provider.destroy_sandbox(db, &cfg, &sandbox_id).await { + crate::emit_info(&format!("Destroying sandbox request failed: {}", e)); + } + *lock = None; Ok(()) } -/// 在沙盒中执行命令(provider dispatch) pub async fn execute_command_in_sandbox( db: &DbManager, sandbox_id: &str, @@ -146,15 +124,12 @@ pub async fn execute_command_in_sandbox( let cfg = get_sandbox_config(db).await .ok_or_else(|| anyhow::anyhow!("沙箱未配置或未启用"))?; - let provider = cfg.provider.as_deref().unwrap_or("e2b"); - match provider { - "e2b" => crate::e2b::execute_command(&cfg, sandbox_id, command).await, - "local" => Ok((format!("Local mock execution placeholder: executing '{}'", command), 0)), - _ => crate::daytona::execute_command_in_sandbox(db, sandbox_id, command).await, - } + let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); + let provider = get_provider(provider_name); + + provider.execute_command(db, &cfg, sandbox_id, command).await } -/// 获取沙盒 VNC URL(provider dispatch) pub async fn get_sandbox_vnc_url(db: &DbManager, sandbox_id: &str) -> anyhow::Result { let cfg = get_sandbox_config(db).await .ok_or_else(|| anyhow::anyhow!(flock_core::tr( @@ -162,10 +137,18 @@ pub async fn get_sandbox_vnc_url(db: &DbManager, sandbox_id: &str) -> anyhow::Re "Sandbox not configured or enabled" )))?; - let provider = cfg.provider.as_deref().unwrap_or("e2b"); - match provider { - "e2b" => Ok(crate::e2b::exec::get_vnc_url(sandbox_id)), - "local" => anyhow::bail!("Local sandbox does not support VNC"), - _ => crate::daytona::get_sandbox_vnc_url(db, sandbox_id).await, - } + let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); + let provider = get_provider(provider_name); + + provider.get_vnc_url(db, &cfg, sandbox_id).await +} + +pub async fn ensure_vnc_running_in_sandbox(db: &DbManager, sandbox_id: &str) -> anyhow::Result<()> { + let cfg = get_sandbox_config(db).await + .ok_or_else(|| anyhow::anyhow!("沙盒未配置"))?; + + let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); + let provider = get_provider(provider_name); + + provider.ensure_vnc_running(db, &cfg, sandbox_id).await } diff --git a/crates/flock-tools/src/sandbox_provider.rs b/crates/flock-tools/src/sandbox_provider.rs index 8e81271c..c137492f 100644 --- a/crates/flock-tools/src/sandbox_provider.rs +++ b/crates/flock-tools/src/sandbox_provider.rs @@ -4,15 +4,12 @@ use flock_core::config::settings::SandboxConfig; #[async_trait] pub trait SandboxProvider: Send + Sync { - /// 检查沙盒是否存活 async fn check_alive(&self, cfg: &SandboxConfig, sandbox_id: &str) -> bool; - - /// 启动或获取活跃的沙盒实例ID - async fn get_or_create_sandbox(&self, cfg: &SandboxConfig) -> anyhow::Result; - - /// 在沙盒中执行一条命令行,返回 (stdout_stderr_combined, exit_code) - async fn execute_command(&self, cfg: &SandboxConfig, sandbox_id: &str, command: &str) -> anyhow::Result<(String, i32)>; - - /// 销毁沙盒实例 - async fn destroy_sandbox(&self, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()>; + async fn create_sandbox(&self, db: &flock_core::db::DbManager, cfg: &SandboxConfig) -> anyhow::Result; + async fn destroy_sandbox(&self, db: &flock_core::db::DbManager, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()>; + async fn execute_command(&self, db: &flock_core::db::DbManager, cfg: &SandboxConfig, sandbox_id: &str, command: &str) -> anyhow::Result<(String, i32)>; + async fn get_vnc_url(&self, db: &flock_core::db::DbManager, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result; + async fn ensure_vnc_running(&self, db: &flock_core::db::DbManager, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()>; + async fn sync_up(&self, db: &flock_core::db::DbManager, sandbox_id: &str, ws_path: &Path) -> anyhow::Result<()>; + async fn sync_down(&self, db: &flock_core::db::DbManager, sandbox_id: &str, ws_path: &Path) -> anyhow::Result<()>; } diff --git a/crates/flock-tools/src/tools/sandbox/browser/interactive.rs b/crates/flock-tools/src/tools/sandbox/browser/interactive.rs index 027eb1cf..b0fac837 100644 --- a/crates/flock-tools/src/tools/sandbox/browser/interactive.rs +++ b/crates/flock-tools/src/tools/sandbox/browser/interactive.rs @@ -1,10 +1,12 @@ use crate::daytona::{ - execute_command_in_sandbox, start_computer_use_in_sandbox, - ensure_vnc_running_in_sandbox, DISPLAY_ID, }; -use crate::sandbox_manager::get_sandbox_vnc_url; +use crate::sandbox_manager::{ + execute_command_in_sandbox, + get_sandbox_vnc_url, + ensure_vnc_running_in_sandbox, +}; use flock_core::ipc_interface::events::ToolCategory; use flock_core::db::DbManager; use base64::{Engine as _, engine::general_purpose}; diff --git a/crates/flock-tools/src/tools/sandbox/browser/mod.rs b/crates/flock-tools/src/tools/sandbox/browser/mod.rs index 6c6fad8c..dc57b6a9 100644 --- a/crates/flock-tools/src/tools/sandbox/browser/mod.rs +++ b/crates/flock-tools/src/tools/sandbox/browser/mod.rs @@ -3,11 +3,8 @@ mod interactive; use crate::adapter::LangGraphToolAdapter; use crate::Tool; -use crate::sandbox_manager::{get_or_create_active_sandbox, execute_command_in_sandbox}; -use crate::daytona::{ - ensure_vnc_running_in_sandbox, - DISPLAY_ID, -}; +use crate::sandbox_manager::{get_or_create_active_sandbox, execute_command_in_sandbox, ensure_vnc_running_in_sandbox}; +use crate::daytona::DISPLAY_ID; use flock_core::ipc_interface::events::ToolCategory; use langgraph::tool; use base64::{Engine as _, engine::general_purpose}; diff --git a/crates/flock-tools/src/tools/sandbox/computer_use.rs b/crates/flock-tools/src/tools/sandbox/computer_use.rs index 2acf040f..8db38ea9 100644 --- a/crates/flock-tools/src/tools/sandbox/computer_use.rs +++ b/crates/flock-tools/src/tools/sandbox/computer_use.rs @@ -1,8 +1,8 @@ use crate::adapter::LangGraphToolAdapter; use crate::Tool; -use crate::sandbox_manager::{get_or_create_active_sandbox, execute_command_in_sandbox, get_sandbox_vnc_url}; +use crate::sandbox_manager::{get_or_create_active_sandbox, execute_command_in_sandbox, ensure_vnc_running_in_sandbox, get_sandbox_vnc_url}; use crate::daytona::{ - start_computer_use_in_sandbox, check_computer_use_status, ensure_vnc_running_in_sandbox, + start_computer_use_in_sandbox, check_computer_use_status, DISPLAY_ID, }; use flock_core::db::DbManager; diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx index 0b44e8a9..00298c52 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx @@ -126,14 +126,14 @@ export function SnapshotListSection({ }; const handleCreate = async () => { - const name = newSnapshotName.trim() || defaultSnapshotName; - const existing = snapshots.find((s) => s.name === name); + const nameOrId = newSnapshotName.trim() || defaultSnapshotName; + const existing = snapshots.find((s) => s.name === nameOrId || s.id === nameOrId); if (existing) { - await onSetDefaultSnapshot(name); + await onSetDefaultSnapshot(existing.id); setNewSnapshotName(''); return; } - await onCreateSnapshot(name); + await onCreateSnapshot(nameOrId); setNewSnapshotName(''); fetchSnapshots(); }; @@ -270,7 +270,7 @@ export function SnapshotListSection({ {snapshots.map((snap) => { - const isDefault = currentDefaultSnapshot === snap.name; + const isDefault = currentDefaultSnapshot === snap.name || currentDefaultSnapshot === snap.id; return ( @@ -301,7 +301,7 @@ export function SnapshotListSection({ variant="subtle" color={isDefault ? 'yellow' : 'gray'} size="sm" - onClick={() => onSetDefaultSnapshot(snap.name)} + onClick={() => onSetDefaultSnapshot(snap.id)} > {isDefault ? : } From 961738136e751751d97a6a21fcb2f9dfde35371d Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Sat, 27 Jun 2026 09:13:32 +0800 Subject: [PATCH 07/25] feat(sandbox): implement auto-provisioning for E2B enhanced custom templates --- crates/flock-tools/src/e2b/builder.rs | 116 ++++++++++++++++++ crates/flock-tools/src/e2b/mod.rs | 1 + flock-ui/src-tauri/src/commands/settings.rs | 25 ++++ flock-ui/src-tauri/src/lib.rs | 1 + .../components/SnapshotListSection.tsx | 50 ++++++-- .../hooks/useSandboxSettings.ts | 43 +++++++ .../Settings/SandboxSettings/index.tsx | 22 ++-- 7 files changed, 240 insertions(+), 18 deletions(-) create mode 100644 crates/flock-tools/src/e2b/builder.rs diff --git a/crates/flock-tools/src/e2b/builder.rs b/crates/flock-tools/src/e2b/builder.rs new file mode 100644 index 00000000..72c92c33 --- /dev/null +++ b/crates/flock-tools/src/e2b/builder.rs @@ -0,0 +1,116 @@ +use anyhow::{Context, Result}; +use flock_core::config::settings::SandboxConfig; +use std::path::PathBuf; +use std::process::Stdio; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::Command; + +/// 自动构建 E2B 的专属桌面镜像(自带 VNC 和 Playwright) +/// 构建日志将通过传入的回调函数 (on_log) 实时推送 +pub async fn build_enhanced_template(api_key: &str, on_log: F) -> Result +where + F: Fn(String) + Send + 'static, +{ + let builder_dir = std::env::temp_dir().join("flock_e2b_builder"); + + if !builder_dir.exists() { + std::fs::create_dir_all(&builder_dir) + .context("Failed to create e2b_builder directory")?; + } + + let dockerfile_path = builder_dir.join("Dockerfile"); + let toml_path = builder_dir.join("e2b.toml"); + + // 1. 写入 Dockerfile + let dockerfile_content = r#"FROM e2bdev/desktop:latest +# 安装 python3-pip 和相关依赖 +RUN sudo apt-get update && sudo apt-get install -y python3-pip + +# 安装 Playwright 及浏览器 +RUN pip install playwright && \ + playwright install chromium && \ + playwright install-deps chromium +"#; + std::fs::write(&dockerfile_path, dockerfile_content) + .context("Failed to write Dockerfile")?; + + // 2. 写入 e2b.toml + let toml_content = r#"template_name = "flock-enhanced-desktop" +dockerfile = "Dockerfile" +"#; + std::fs::write(&toml_path, toml_content) + .context("Failed to write e2b.toml")?; + + on_log("环境准备完毕,开始调用 E2B CLI 构建镜像 (可能需要 1-3 分钟)...\n".to_string()); + + // 3. 执行 npx @e2b/cli template build + // 在 Windows 下需要调用 npx.cmd + let npx_cmd = if cfg!(windows) { "npx.cmd" } else { "npx" }; + + let mut child = Command::new(npx_cmd) + .arg("-y") + .arg("@e2b/cli@latest") + .arg("template") + .arg("build") + .arg("-c") + .arg("e2b.toml") + .env("E2B_API_KEY", api_key) + .current_dir(&builder_dir) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("Failed to spawn npx @e2b/cli")?; + + let stdout = child.stdout.take().expect("Failed to open stdout"); + let stderr = child.stderr.take().expect("Failed to open stderr"); + + let mut stdout_reader = BufReader::new(stdout).lines(); + let mut stderr_reader = BufReader::new(stderr).lines(); + + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let tx1 = tx.clone(); + + tokio::spawn(async move { + while let Ok(Some(line)) = stdout_reader.next_line().await { + let _ = tx1.send(line); + } + }); + + let tx2 = tx.clone(); + tokio::spawn(async move { + while let Ok(Some(line)) = stderr_reader.next_line().await { + let _ = tx2.send(line); + } + }); + + drop(tx); // Close the main sender so the receiver will end when tasks complete + + let mut full_output = String::new(); + while let Some(line) = rx.recv().await { + on_log(format!("{}\n", line)); + full_output.push_str(&line); + full_output.push('\n'); + } + + let status = child.wait().await?; + if !status.success() { + anyhow::bail!("E2B template build failed with exit code: {}", status); + } + + on_log("构建成功!正在解析 Template ID...\n".to_string()); + + // 解析出 template ID (通常在构建输出中,或者在构建后的 e2b.toml 中) + // E2B CLI 构建后,会将 template_id 写入 e2b.toml + let updated_toml = std::fs::read_to_string(&toml_path)?; + for line in updated_toml.lines() { + if line.starts_with("template_id") { + let parts: Vec<&str> = line.split('=').collect(); + if parts.len() == 2 { + let id = parts[1].trim().trim_matches('"').to_string(); + return Ok(id); + } + } + } + + anyhow::bail!("构建成功,但未能在 e2b.toml 中找到生成的 template_id") +} diff --git a/crates/flock-tools/src/e2b/mod.rs b/crates/flock-tools/src/e2b/mod.rs index af774681..75ce9c65 100644 --- a/crates/flock-tools/src/e2b/mod.rs +++ b/crates/flock-tools/src/e2b/mod.rs @@ -1,6 +1,7 @@ pub mod lifecycle; pub mod exec; pub mod provider; +pub mod builder; pub use lifecycle::{check_alive, create_sandbox, destroy_sandbox}; pub use exec::execute_command; diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index a0cd867a..db9d9060 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -126,6 +126,31 @@ pub async fn set_app_config( Ok(()) } +/// 自动构建 E2B 专属模板 +#[tauri::command] +pub async fn build_e2b_template( + app: tauri::AppHandle, + db: State<'_, SharedDbManager>, +) -> Result { + use flock_tools::daytona::get_sandbox_config; + + let cfg = get_sandbox_config(&db).await + .ok_or_else(|| "沙盒未配置".to_string())?; + + let api_key = cfg.e2b_api_key.as_deref().or(cfg.api_key.as_deref()) + .ok_or_else(|| "E2B API Key 未配置".to_string())?; + + let template_id = flock_tools::e2b::builder::build_enhanced_template( + api_key, + move |log| { + use tauri::Emitter; + let _ = app.emit("e2b-build-log", log); + } + ).await.map_err(|e| e.to_string())?; + + Ok(template_id) +} + /// 测试云端沙盒(E2B 或 Daytona)的连通性,成功后将 sandbox provider 标记为可用 #[tauri::command] pub async fn test_sandbox_connection( diff --git a/flock-ui/src-tauri/src/lib.rs b/flock-ui/src-tauri/src/lib.rs index 7330249a..f9457f82 100644 --- a/flock-ui/src-tauri/src/lib.rs +++ b/flock-ui/src-tauri/src/lib.rs @@ -132,6 +132,7 @@ pub fn run() { // 系统配置 commands::get_app_config, commands::set_app_config, + commands::build_e2b_template, commands::set_locale, commands::test_sandbox_connection, commands::create_playwright_snapshot, diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx index 00298c52..ac40923a 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx @@ -43,6 +43,9 @@ interface SnapshotListSectionProps { onSetDefaultSnapshot: (name: string) => void; onCreateSnapshot: (name: string) => Promise; creatingSnapshot: boolean; + buildingE2b?: boolean; + e2bBuildLogs?: string[]; + onBuildE2bTemplate?: () => void; } export function SnapshotListSection({ @@ -51,6 +54,9 @@ export function SnapshotListSection({ onSetDefaultSnapshot, onCreateSnapshot, creatingSnapshot, + buildingE2b, + e2bBuildLogs, + onBuildE2bTemplate, }: SnapshotListSectionProps) { const { t } = useTranslation(); const [snapshots, setSnapshots] = useState([]); @@ -226,22 +232,44 @@ export function SnapshotListSection({ {/* 快照列表 */} - + {t('settings.sandbox.snapshots')} - + + {provider === 'e2b' && ( + + )} + + + {buildingE2b && ( + + Building Custom Template... (this might take a few minutes) + {e2bBuildLogs?.map((log, i) => ( +
{log}
+ ))} +
+ )} + {loading && snapshots.length === 0 ? ( diff --git a/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts b/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts index 4292d28e..a544ec38 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts +++ b/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import { invoke } from '@tauri-apps/api/core'; +import { listen } from '@tauri-apps/api/event'; import { notifications } from '@mantine/notifications'; import { useTranslation } from 'react-i18next'; import { IconCheck, IconAlertCircle, IconPlugConnectedX } from '@tabler/icons-react'; @@ -32,6 +33,8 @@ export function useSandboxSettings() { const [isAvailable, setIsAvailable] = useState(false); const [activeTab, setActiveTab] = useState('config'); const [snapshotsList, setSnapshotsList] = useState<{ id: string; name: string }[]>([]); + const [buildingE2b, setBuildingE2b] = useState(false); + const [e2bBuildLogs, setE2bBuildLogs] = useState([]); const fetchSnapshotsList = async () => { try { @@ -230,6 +233,43 @@ export function useSandboxSettings() { } }; + const handleBuildE2bTemplate = async () => { + setBuildingE2b(true); + setE2bBuildLogs([]); + let unlisten: (() => void) | undefined; + try { + await saveConfig(); // Save current API key + + unlisten = await listen('e2b-build-log', (event) => { + setE2bBuildLogs((prev) => [...prev, event.payload]); + }); + + const templateId = await invoke('build_e2b_template'); + + notifications.show({ + title: t('common.success'), + message: `E2B template ${templateId} built successfully!`, + color: 'teal', + icon: React.createElement(IconCheck, { size: 18 }), + }); + + setSnapshot(templateId); + await saveConfig({ snapshot: templateId }); + fetchSnapshotsList(); + } catch (e) { + notifications.show({ + title: t('common.failed'), + message: String(e), + color: 'red', + icon: React.createElement(IconAlertCircle, { size: 18 }), + autoClose: 10000, + }); + } finally { + if (unlisten) unlisten(); + setBuildingE2b(false); + } + }; + return { provider, setProvider, apiUrl, setApiUrl, @@ -242,9 +282,12 @@ export function useSandboxSettings() { isAvailable, activeTab, setActiveTab, snapshotsList, + buildingE2b, + e2bBuildLogs, handleTestConnection, handleDisable, handleCreateSnapshot, handleSetDefaultSnapshot, + handleBuildE2bTemplate, }; } diff --git a/flock-ui/src/components/Settings/SandboxSettings/index.tsx b/flock-ui/src/components/Settings/SandboxSettings/index.tsx index a5c6454b..796b8e71 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/index.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/index.tsx @@ -41,10 +41,13 @@ export default function SandboxSettings() { isAvailable, activeTab, setActiveTab, snapshotsList, + buildingE2b, + e2bBuildLogs, handleTestConnection, handleDisable, handleCreateSnapshot, handleSetDefaultSnapshot, + handleBuildE2bTemplate, } = useSandboxSettings(); return ( @@ -161,13 +164,18 @@ export default function SandboxSettings() { {(provider === 'daytona' || provider === 'e2b') && activeTab === 'snapshots' && isAvailable && ( - + {activeTab === 'snapshots' && ( + + )} )} From 568c3b1121c250d93c47be687005161cf39608c5 Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Sat, 27 Jun 2026 09:23:47 +0800 Subject: [PATCH 08/25] chore: move local_provider into sandbox directory --- crates/flock-agent/src/agent_setup_helpers.rs | 2 +- crates/flock-tools/src/lib.rs | 6 +--- .../src/{ => sandbox_core}/daytona/config.rs | 0 .../{ => sandbox_core}/daytona/constants.rs | 0 .../src/{ => sandbox_core}/daytona/exec.rs | 2 +- .../src/{ => sandbox_core}/daytona/fs.rs | 4 +-- .../{ => sandbox_core}/daytona/lifecycle.rs | 8 ++--- .../src/{ => sandbox_core}/daytona/mod.rs | 0 .../{ => sandbox_core}/daytona/provider.rs | 18 +++++------ .../{ => sandbox_core}/daytona/snapshot.rs | 4 +-- .../src/{ => sandbox_core}/daytona/state.rs | 0 .../src/{ => sandbox_core}/daytona/sync.rs | 6 ++-- .../src/{ => sandbox_core}/daytona/vnc.rs | 6 ++-- .../src/{ => sandbox_core}/daytona/volume.rs | 2 +- .../src/{ => sandbox_core}/e2b/builder.rs | 0 .../src/{ => sandbox_core}/e2b/exec.rs | 0 .../src/{ => sandbox_core}/e2b/lifecycle.rs | 0 .../src/{ => sandbox_core}/e2b/mod.rs | 0 .../src/{ => sandbox_core}/e2b/provider.rs | 12 ++++---- .../src/{ => sandbox_core}/local_provider.rs | 2 +- .../manager.rs} | 12 ++++---- crates/flock-tools/src/sandbox_core/mod.rs | 5 ++++ .../provider.rs} | 0 .../src/tools/sandbox/browser/interactive.rs | 6 ++-- .../src/tools/sandbox/browser/mod.rs | 4 +-- .../src/tools/sandbox/code_execution.rs | 4 +-- .../src/tools/sandbox/computer_use.rs | 6 ++-- .../flock-tools/src/tools/sandbox/fs_tools.rs | 2 +- .../tools/sandbox/request_human_assistance.rs | 6 ++-- .../src/tools/sandbox/sandbox_exec.rs | 4 +-- crates/flock-tools/tests/diagnose_sandbox.rs | 8 ++--- .../src-tauri/src/commands/common/sandbox.rs | 12 ++++---- flock-ui/src-tauri/src/commands/file.rs | 6 ++-- flock-ui/src-tauri/src/commands/settings.rs | 30 +++++++++---------- 34 files changed, 89 insertions(+), 88 deletions(-) rename crates/flock-tools/src/{ => sandbox_core}/daytona/config.rs (100%) rename crates/flock-tools/src/{ => sandbox_core}/daytona/constants.rs (100%) rename crates/flock-tools/src/{ => sandbox_core}/daytona/exec.rs (98%) rename crates/flock-tools/src/{ => sandbox_core}/daytona/fs.rs (98%) rename crates/flock-tools/src/{ => sandbox_core}/daytona/lifecycle.rs (96%) rename crates/flock-tools/src/{ => sandbox_core}/daytona/mod.rs (100%) rename crates/flock-tools/src/{ => sandbox_core}/daytona/provider.rs (61%) rename crates/flock-tools/src/{ => sandbox_core}/daytona/snapshot.rs (97%) rename crates/flock-tools/src/{ => sandbox_core}/daytona/state.rs (100%) rename crates/flock-tools/src/{ => sandbox_core}/daytona/sync.rs (95%) rename crates/flock-tools/src/{ => sandbox_core}/daytona/vnc.rs (98%) rename crates/flock-tools/src/{ => sandbox_core}/daytona/volume.rs (97%) rename crates/flock-tools/src/{ => sandbox_core}/e2b/builder.rs (100%) rename crates/flock-tools/src/{ => sandbox_core}/e2b/exec.rs (100%) rename crates/flock-tools/src/{ => sandbox_core}/e2b/lifecycle.rs (100%) rename crates/flock-tools/src/{ => sandbox_core}/e2b/mod.rs (100%) rename crates/flock-tools/src/{ => sandbox_core}/e2b/provider.rs (74%) rename crates/flock-tools/src/{ => sandbox_core}/local_provider.rs (96%) rename crates/flock-tools/src/{sandbox_manager.rs => sandbox_core/manager.rs} (92%) create mode 100644 crates/flock-tools/src/sandbox_core/mod.rs rename crates/flock-tools/src/{sandbox_provider.rs => sandbox_core/provider.rs} (100%) diff --git a/crates/flock-agent/src/agent_setup_helpers.rs b/crates/flock-agent/src/agent_setup_helpers.rs index a7e11d71..7940bf61 100644 --- a/crates/flock-agent/src/agent_setup_helpers.rs +++ b/crates/flock-agent/src/agent_setup_helpers.rs @@ -21,7 +21,7 @@ use super::sinks::OutputSink; /// when the global sandbox is configured. pub async fn filter_sandbox_tools(config: &Config, registry: &mut ToolRegistry) { let is_sandbox_configured = if let Some(db) = &config.db_manager { - flock_tools::daytona::get_sandbox_config(db).await.is_some() + flock_tools::sandbox_core::daytona::get_sandbox_config(db).await.is_some() } else { false }; diff --git a/crates/flock-tools/src/lib.rs b/crates/flock-tools/src/lib.rs index 832bb098..adae2954 100644 --- a/crates/flock-tools/src/lib.rs +++ b/crates/flock-tools/src/lib.rs @@ -4,11 +4,7 @@ pub mod adapter; pub mod file_cache; pub mod tool_executor; -pub mod daytona; -pub mod e2b; -pub mod sandbox_manager; -pub mod sandbox_provider; -pub mod local_provider; +pub mod sandbox_core; pub mod registry; pub mod tools; diff --git a/crates/flock-tools/src/daytona/config.rs b/crates/flock-tools/src/sandbox_core/daytona/config.rs similarity index 100% rename from crates/flock-tools/src/daytona/config.rs rename to crates/flock-tools/src/sandbox_core/daytona/config.rs diff --git a/crates/flock-tools/src/daytona/constants.rs b/crates/flock-tools/src/sandbox_core/daytona/constants.rs similarity index 100% rename from crates/flock-tools/src/daytona/constants.rs rename to crates/flock-tools/src/sandbox_core/daytona/constants.rs diff --git a/crates/flock-tools/src/daytona/exec.rs b/crates/flock-tools/src/sandbox_core/daytona/exec.rs similarity index 98% rename from crates/flock-tools/src/daytona/exec.rs rename to crates/flock-tools/src/sandbox_core/daytona/exec.rs index b86639bf..334f0b82 100644 --- a/crates/flock-tools/src/daytona/exec.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/exec.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use flock_core::db::DbManager; -use crate::daytona::config::get_sandbox_config; +use crate::sandbox_core::daytona::config::get_sandbox_config; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DaytonaSandboxResponse { diff --git a/crates/flock-tools/src/daytona/fs.rs b/crates/flock-tools/src/sandbox_core/daytona/fs.rs similarity index 98% rename from crates/flock-tools/src/daytona/fs.rs rename to crates/flock-tools/src/sandbox_core/daytona/fs.rs index 6b2afc6f..ef44c80f 100644 --- a/crates/flock-tools/src/daytona/fs.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/fs.rs @@ -1,6 +1,6 @@ use flock_core::db::DbManager; -use crate::daytona::execute_command_in_sandbox; -use crate::sandbox_manager::get_or_create_active_sandbox; +use crate::sandbox_core::daytona::execute_command_in_sandbox; +use crate::sandbox_core::manager::get_or_create_active_sandbox; use serde::{Deserialize, Serialize}; use std::path::{Component, Path}; diff --git a/crates/flock-tools/src/daytona/lifecycle.rs b/crates/flock-tools/src/sandbox_core/daytona/lifecycle.rs similarity index 96% rename from crates/flock-tools/src/daytona/lifecycle.rs rename to crates/flock-tools/src/sandbox_core/daytona/lifecycle.rs index 3dcb50e7..6d76062e 100644 --- a/crates/flock-tools/src/daytona/lifecycle.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/lifecycle.rs @@ -1,7 +1,7 @@ use flock_core::db::DbManager; use flock_core::config::settings::SandboxConfig; -use crate::daytona::config::{get_sandbox_config, get_api_base}; -use crate::daytona::volume::get_or_create_volume; +use crate::sandbox_core::daytona::config::{get_sandbox_config, get_api_base}; +use crate::sandbox_core::daytona::volume::get_or_create_volume; /// 创建 Daytona 沙盒,等待 started 状态后返回 sandbox_id pub async fn create_sandbox(db: &DbManager, cfg: &SandboxConfig) -> anyhow::Result { @@ -169,7 +169,7 @@ pub async fn create_sandbox(db: &DbManager, cfg: &SandboxConfig) -> anyhow::Resu // 确保 /workspace 目录存在 let ensure_workspace_cmd = "mkdir -p /workspace && ls -la /workspace"; - match crate::daytona::execute_command_in_sandbox(db, &sandbox_id, ensure_workspace_cmd).await { + match crate::sandbox_core::daytona::execute_command_in_sandbox(db, &sandbox_id, ensure_workspace_cmd).await { Ok((out, code)) => { if code != 0 { crate::emit_info(&flock_core::tr( @@ -188,7 +188,7 @@ pub async fn create_sandbox(db: &DbManager, cfg: &SandboxConfig) -> anyhow::Resu // Sync up local workspace if let Some(ws_path) = crate::get_workspace_dir() { - if let Err(e) = crate::daytona::sync::sync_up(db, &sandbox_id, &ws_path).await { + if let Err(e) = crate::sandbox_core::daytona::sync::sync_up(db, &sandbox_id, &ws_path).await { crate::emit_info(&format!("Sync Up failed: {}", e)); } } diff --git a/crates/flock-tools/src/daytona/mod.rs b/crates/flock-tools/src/sandbox_core/daytona/mod.rs similarity index 100% rename from crates/flock-tools/src/daytona/mod.rs rename to crates/flock-tools/src/sandbox_core/daytona/mod.rs diff --git a/crates/flock-tools/src/daytona/provider.rs b/crates/flock-tools/src/sandbox_core/daytona/provider.rs similarity index 61% rename from crates/flock-tools/src/daytona/provider.rs rename to crates/flock-tools/src/sandbox_core/daytona/provider.rs index d5dde025..6ff35496 100644 --- a/crates/flock-tools/src/daytona/provider.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/provider.rs @@ -1,4 +1,4 @@ -use crate::sandbox_provider::SandboxProvider; +use crate::sandbox_core::provider::SandboxProvider; use flock_core::config::settings::SandboxConfig; use flock_core::db::DbManager; use async_trait::async_trait; @@ -9,27 +9,27 @@ pub struct DaytonaProvider; #[async_trait] impl SandboxProvider for DaytonaProvider { async fn check_alive(&self, cfg: &SandboxConfig, sandbox_id: &str) -> bool { - crate::daytona::check_sandbox_alive(cfg, sandbox_id).await + crate::sandbox_core::daytona::check_sandbox_alive(cfg, sandbox_id).await } async fn create_sandbox(&self, db: &DbManager, cfg: &SandboxConfig) -> anyhow::Result { - crate::daytona::create_sandbox(db, cfg).await + crate::sandbox_core::daytona::create_sandbox(db, cfg).await } async fn destroy_sandbox(&self, _db: &DbManager, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { - crate::daytona::destroy_daytona_sandbox(cfg, sandbox_id).await + crate::sandbox_core::daytona::destroy_daytona_sandbox(cfg, sandbox_id).await } async fn execute_command(&self, db: &DbManager, _cfg: &SandboxConfig, sandbox_id: &str, command: &str) -> anyhow::Result<(String, i32)> { - crate::daytona::execute_command_in_sandbox(db, sandbox_id, command).await + crate::sandbox_core::daytona::execute_command_in_sandbox(db, sandbox_id, command).await } async fn get_vnc_url(&self, db: &DbManager, _cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result { - crate::daytona::get_sandbox_vnc_url(db, sandbox_id).await + crate::sandbox_core::daytona::get_sandbox_vnc_url(db, sandbox_id).await } async fn ensure_vnc_running(&self, db: &DbManager, _cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { - crate::daytona::ensure_vnc_running_in_sandbox(db, sandbox_id).await + crate::sandbox_core::daytona::ensure_vnc_running_in_sandbox(db, sandbox_id).await } async fn sync_up(&self, db: &DbManager, sandbox_id: &str, ws_path: &Path) -> anyhow::Result<()> { - crate::daytona::sync::sync_up(db, sandbox_id, ws_path).await + crate::sandbox_core::daytona::sync::sync_up(db, sandbox_id, ws_path).await } async fn sync_down(&self, db: &DbManager, sandbox_id: &str, ws_path: &Path) -> anyhow::Result<()> { - crate::daytona::sync::sync_down(db, sandbox_id, ws_path).await + crate::sandbox_core::daytona::sync::sync_down(db, sandbox_id, ws_path).await } } diff --git a/crates/flock-tools/src/daytona/snapshot.rs b/crates/flock-tools/src/sandbox_core/daytona/snapshot.rs similarity index 97% rename from crates/flock-tools/src/daytona/snapshot.rs rename to crates/flock-tools/src/sandbox_core/daytona/snapshot.rs index c06e2c1f..1cd87f96 100644 --- a/crates/flock-tools/src/daytona/snapshot.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/snapshot.rs @@ -1,6 +1,6 @@ use flock_core::db::DbManager; -use crate::daytona::config::{get_sandbox_config, get_api_base}; -use crate::daytona::{DISPLAY_ID, SCREEN_RESOLUTION, X11VNC_PORT, WEBSOCKIFY_PORT}; +use crate::sandbox_core::daytona::config::{get_sandbox_config, get_api_base}; +use crate::sandbox_core::daytona::{DISPLAY_ID, SCREEN_RESOLUTION, X11VNC_PORT, WEBSOCKIFY_PORT}; /// 创建一个预装 Playwright 的 Daytona Snapshot pub async fn create_playwright_snapshot( diff --git a/crates/flock-tools/src/daytona/state.rs b/crates/flock-tools/src/sandbox_core/daytona/state.rs similarity index 100% rename from crates/flock-tools/src/daytona/state.rs rename to crates/flock-tools/src/sandbox_core/daytona/state.rs diff --git a/crates/flock-tools/src/daytona/sync.rs b/crates/flock-tools/src/sandbox_core/daytona/sync.rs similarity index 95% rename from crates/flock-tools/src/daytona/sync.rs rename to crates/flock-tools/src/sandbox_core/daytona/sync.rs index e92ae9d6..bad28cb3 100644 --- a/crates/flock-tools/src/daytona/sync.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/sync.rs @@ -1,5 +1,5 @@ use flock_core::db::DbManager; -use crate::daytona::execute_command_in_sandbox; +use crate::sandbox_core::daytona::execute_command_in_sandbox; use std::path::{Path, PathBuf}; use std::fs; use anyhow::Context; @@ -10,7 +10,7 @@ fn should_ignore(path: &Path) -> bool { } pub async fn sync_up(db: &DbManager, sandbox_id: &str, local_workspace: &Path) -> anyhow::Result<()> { - if let Some(cfg) = crate::daytona::config::get_sandbox_config(db).await { + if let Some(cfg) = crate::sandbox_core::daytona::config::get_sandbox_config(db).await { if cfg.provider.as_deref().unwrap_or("e2b") == "local" { return Ok(()); } @@ -57,7 +57,7 @@ pub async fn sync_up(db: &DbManager, sandbox_id: &str, local_workspace: &Path) - } pub async fn sync_down(db: &DbManager, sandbox_id: &str, local_workspace: &Path) -> anyhow::Result<()> { - if let Some(cfg) = crate::daytona::config::get_sandbox_config(db).await { + if let Some(cfg) = crate::sandbox_core::daytona::config::get_sandbox_config(db).await { if cfg.provider.as_deref().unwrap_or("e2b") == "local" { return Ok(()); } diff --git a/crates/flock-tools/src/daytona/vnc.rs b/crates/flock-tools/src/sandbox_core/daytona/vnc.rs similarity index 98% rename from crates/flock-tools/src/daytona/vnc.rs rename to crates/flock-tools/src/sandbox_core/daytona/vnc.rs index 9fcda40c..9b226851 100644 --- a/crates/flock-tools/src/daytona/vnc.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/vnc.rs @@ -1,7 +1,7 @@ use flock_core::db::DbManager; -use crate::daytona::config::{get_sandbox_config, get_api_base}; -use crate::daytona::exec::execute_command_in_sandbox; -use crate::daytona::{DISPLAY_ID, SCREEN_RESOLUTION, X11VNC_PORT, WEBSOCKIFY_PORT}; +use crate::sandbox_core::daytona::config::{get_sandbox_config, get_api_base}; +use crate::sandbox_core::daytona::exec::execute_command_in_sandbox; +use crate::sandbox_core::daytona::{DISPLAY_ID, SCREEN_RESOLUTION, X11VNC_PORT, WEBSOCKIFY_PORT}; /// 启动沙盒中的 Computer Use(VNC桌面) pub async fn start_computer_use_in_sandbox( diff --git a/crates/flock-tools/src/daytona/volume.rs b/crates/flock-tools/src/sandbox_core/daytona/volume.rs similarity index 97% rename from crates/flock-tools/src/daytona/volume.rs rename to crates/flock-tools/src/sandbox_core/daytona/volume.rs index 31b128ee..a85c061b 100644 --- a/crates/flock-tools/src/daytona/volume.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/volume.rs @@ -1,5 +1,5 @@ use flock_core::config::settings::SandboxConfig; -use crate::daytona::config::get_api_base; +use crate::sandbox_core::daytona::config::get_api_base; use serde_json::Value; pub async fn get_or_create_volume(cfg: &SandboxConfig, workspace_id: &str) -> anyhow::Result { diff --git a/crates/flock-tools/src/e2b/builder.rs b/crates/flock-tools/src/sandbox_core/e2b/builder.rs similarity index 100% rename from crates/flock-tools/src/e2b/builder.rs rename to crates/flock-tools/src/sandbox_core/e2b/builder.rs diff --git a/crates/flock-tools/src/e2b/exec.rs b/crates/flock-tools/src/sandbox_core/e2b/exec.rs similarity index 100% rename from crates/flock-tools/src/e2b/exec.rs rename to crates/flock-tools/src/sandbox_core/e2b/exec.rs diff --git a/crates/flock-tools/src/e2b/lifecycle.rs b/crates/flock-tools/src/sandbox_core/e2b/lifecycle.rs similarity index 100% rename from crates/flock-tools/src/e2b/lifecycle.rs rename to crates/flock-tools/src/sandbox_core/e2b/lifecycle.rs diff --git a/crates/flock-tools/src/e2b/mod.rs b/crates/flock-tools/src/sandbox_core/e2b/mod.rs similarity index 100% rename from crates/flock-tools/src/e2b/mod.rs rename to crates/flock-tools/src/sandbox_core/e2b/mod.rs diff --git a/crates/flock-tools/src/e2b/provider.rs b/crates/flock-tools/src/sandbox_core/e2b/provider.rs similarity index 74% rename from crates/flock-tools/src/e2b/provider.rs rename to crates/flock-tools/src/sandbox_core/e2b/provider.rs index a58f9631..3bbb3df9 100644 --- a/crates/flock-tools/src/e2b/provider.rs +++ b/crates/flock-tools/src/sandbox_core/e2b/provider.rs @@ -1,4 +1,4 @@ -use crate::sandbox_provider::SandboxProvider; +use crate::sandbox_core::provider::SandboxProvider; use flock_core::config::settings::SandboxConfig; use flock_core::db::DbManager; use async_trait::async_trait; @@ -9,19 +9,19 @@ pub struct E2bProvider; #[async_trait] impl SandboxProvider for E2bProvider { async fn check_alive(&self, cfg: &SandboxConfig, sandbox_id: &str) -> bool { - crate::e2b::check_alive(cfg, sandbox_id).await + crate::sandbox_core::e2b::check_alive(cfg, sandbox_id).await } async fn create_sandbox(&self, _db: &DbManager, cfg: &SandboxConfig) -> anyhow::Result { - crate::e2b::create_sandbox(cfg).await + crate::sandbox_core::e2b::create_sandbox(cfg).await } async fn destroy_sandbox(&self, _db: &DbManager, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { - crate::e2b::destroy_sandbox(cfg, sandbox_id).await + crate::sandbox_core::e2b::destroy_sandbox(cfg, sandbox_id).await } async fn execute_command(&self, _db: &DbManager, cfg: &SandboxConfig, sandbox_id: &str, command: &str) -> anyhow::Result<(String, i32)> { - crate::e2b::execute_command(cfg, sandbox_id, command).await + crate::sandbox_core::e2b::execute_command(cfg, sandbox_id, command).await } async fn get_vnc_url(&self, _db: &DbManager, _cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result { - Ok(crate::e2b::exec::get_vnc_url(sandbox_id)) + Ok(crate::sandbox_core::e2b::exec::get_vnc_url(sandbox_id)) } async fn ensure_vnc_running(&self, _db: &DbManager, _cfg: &SandboxConfig, _sandbox_id: &str) -> anyhow::Result<()> { Ok(()) diff --git a/crates/flock-tools/src/local_provider.rs b/crates/flock-tools/src/sandbox_core/local_provider.rs similarity index 96% rename from crates/flock-tools/src/local_provider.rs rename to crates/flock-tools/src/sandbox_core/local_provider.rs index 0b8d7820..04d32594 100644 --- a/crates/flock-tools/src/local_provider.rs +++ b/crates/flock-tools/src/sandbox_core/local_provider.rs @@ -1,4 +1,4 @@ -use crate::sandbox_provider::SandboxProvider; +use crate::sandbox_core::provider::SandboxProvider; use flock_core::config::settings::SandboxConfig; use flock_core::db::DbManager; use async_trait::async_trait; diff --git a/crates/flock-tools/src/sandbox_manager.rs b/crates/flock-tools/src/sandbox_core/manager.rs similarity index 92% rename from crates/flock-tools/src/sandbox_manager.rs rename to crates/flock-tools/src/sandbox_core/manager.rs index ada2752d..ad446541 100644 --- a/crates/flock-tools/src/sandbox_manager.rs +++ b/crates/flock-tools/src/sandbox_core/manager.rs @@ -8,8 +8,8 @@ use std::sync::OnceLock; use tokio::sync::Mutex; use flock_core::db::DbManager; -use crate::daytona::get_sandbox_config; -use crate::sandbox_provider::SandboxProvider; +use crate::sandbox_core::daytona::get_sandbox_config; +use crate::sandbox_core::provider::SandboxProvider; static ACTIVE_SANDBOX_ID: OnceLock>> = OnceLock::new(); @@ -32,9 +32,9 @@ pub async fn clear_active_sandbox_id() { /// 获取当前配置对应的 SandboxProvider 实现 pub fn get_provider(provider_name: &str) -> Box { match provider_name { - "e2b" => Box::new(crate::e2b::provider::E2bProvider), - "local" => Box::new(crate::local_provider::LocalSandboxProvider), - _ => Box::new(crate::daytona::provider::DaytonaProvider), + "e2b" => Box::new(crate::sandbox_core::e2b::provider::E2bProvider), + "local" => Box::new(crate::sandbox_core::local_provider::LocalSandboxProvider), + _ => Box::new(crate::sandbox_core::daytona::provider::DaytonaProvider), } } @@ -59,7 +59,7 @@ pub async fn get_or_create_active_sandbox(db: &DbManager) -> anyhow::Result Result { let db_clone = db.clone(); let sandbox_id_clone = sandbox_id.clone(); tokio::spawn(async move { - if let Err(e) = crate::daytona::sync::sync_down(&db_clone, &sandbox_id_clone, &ws_path).await { + if let Err(e) = crate::sandbox_core::daytona::sync::sync_down(&db_clone, &sandbox_id_clone, &ws_path).await { log::warn!("自动 Sync Down 失败: {}", e); } }); diff --git a/crates/flock-tools/src/tools/sandbox/computer_use.rs b/crates/flock-tools/src/tools/sandbox/computer_use.rs index 8db38ea9..a7ab5659 100644 --- a/crates/flock-tools/src/tools/sandbox/computer_use.rs +++ b/crates/flock-tools/src/tools/sandbox/computer_use.rs @@ -1,7 +1,7 @@ use crate::adapter::LangGraphToolAdapter; use crate::Tool; -use crate::sandbox_manager::{get_or_create_active_sandbox, execute_command_in_sandbox, ensure_vnc_running_in_sandbox, get_sandbox_vnc_url}; -use crate::daytona::{ +use crate::sandbox_core::manager::{get_or_create_active_sandbox, execute_command_in_sandbox, ensure_vnc_running_in_sandbox, get_sandbox_vnc_url}; +use crate::sandbox_core::daytona::{ start_computer_use_in_sandbox, check_computer_use_status, DISPLAY_ID, }; @@ -227,7 +227,7 @@ pub async fn computer_use( &format!("检测到敏感桌面操作(验证码/人工介入),正在通知前端拉起人工接管横幅 (Call ID: {})...", cid), &format!("Sensitive desktop action detected (captcha/human verification), notifying client to display takeover banner (Call ID: {})...", cid) )); - crate::daytona::emit_human_takeover( + crate::sandbox_core::daytona::emit_human_takeover( &cid, &mid, "人机协同远程桌面已拉起!检测到当前桌面应用需要人工介入(如滑动拼图验证、扫码登录等),大模型自动执行已暂停。您可以在右侧预览面板中直接操作页面。完成后请点击横幅上的【我已完成操作】按钮以恢复大模型的自动运行。", diff --git a/crates/flock-tools/src/tools/sandbox/fs_tools.rs b/crates/flock-tools/src/tools/sandbox/fs_tools.rs index 41ab00a2..e718c09c 100644 --- a/crates/flock-tools/src/tools/sandbox/fs_tools.rs +++ b/crates/flock-tools/src/tools/sandbox/fs_tools.rs @@ -1,6 +1,6 @@ use crate::adapter::LangGraphToolAdapter; use crate::Tool; -use crate::daytona::fs::DaytonaFs; +use crate::sandbox_core::daytona::fs::DaytonaFs; use flock_core::ipc_interface::events::ToolCategory; use langgraph::tool; diff --git a/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs b/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs index c6d0cbdc..adb2aefe 100644 --- a/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs +++ b/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs @@ -1,7 +1,7 @@ use crate::adapter::LangGraphToolAdapter; use crate::Tool; -use crate::sandbox_manager::{get_or_create_active_sandbox, get_sandbox_vnc_url}; -use crate::daytona::{ +use crate::sandbox_core::manager::{get_or_create_active_sandbox, get_sandbox_vnc_url}; +use crate::sandbox_core::daytona::{ start_computer_use_in_sandbox, ensure_vnc_running_in_sandbox }; use flock_core::ipc_interface::events::ToolCategory; @@ -63,7 +63,7 @@ pub async fn request_human_assistance( reason ); - crate::daytona::emit_human_takeover( + crate::sandbox_core::daytona::emit_human_takeover( &cid, &mid, &display_message, diff --git a/crates/flock-tools/src/tools/sandbox/sandbox_exec.rs b/crates/flock-tools/src/tools/sandbox/sandbox_exec.rs index 24025245..5685fc7e 100644 --- a/crates/flock-tools/src/tools/sandbox/sandbox_exec.rs +++ b/crates/flock-tools/src/tools/sandbox/sandbox_exec.rs @@ -1,6 +1,6 @@ use crate::adapter::LangGraphToolAdapter; use crate::Tool; -use crate::sandbox_manager::{get_or_create_active_sandbox, execute_command_in_sandbox}; +use crate::sandbox_core::manager::{get_or_create_active_sandbox, execute_command_in_sandbox}; use flock_core::ipc_interface::events::ToolCategory; use langgraph::tool; @@ -65,7 +65,7 @@ pub async fn sandbox_exec( let db_clone = db.clone(); let sandbox_id_clone = sandbox_id.clone(); tokio::spawn(async move { - if let Err(e) = crate::daytona::sync::sync_down(&db_clone, &sandbox_id_clone, &ws_path).await { + if let Err(e) = crate::sandbox_core::daytona::sync::sync_down(&db_clone, &sandbox_id_clone, &ws_path).await { log::warn!("自动 Sync Down 失败: {}", e); } }); diff --git a/crates/flock-tools/tests/diagnose_sandbox.rs b/crates/flock-tools/tests/diagnose_sandbox.rs index d8cc835f..168d085a 100644 --- a/crates/flock-tools/tests/diagnose_sandbox.rs +++ b/crates/flock-tools/tests/diagnose_sandbox.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; use flock_core::db::DbManager; -use flock_tools::daytona::{get_sandbox_config, get_api_base, execute_command_in_sandbox}; +use flock_tools::sandbox_core::daytona::{get_sandbox_config, get_api_base, execute_command_in_sandbox}; #[tokio::test] async fn test_diagnose_sandbox() { @@ -199,8 +199,8 @@ async fn test_diagnose_e2b_sandbox() { // Set template to the desktop template ID cfg.snapshot = Some("k0wmnzir0zuzye6dndlw".to_string()); - use flock_tools::sandbox_provider::SandboxProvider; - let prov = flock_tools::e2b_provider::E2BSandboxProvider; + use flock_tools::sandbox_core::provider::SandboxProvider; + let prov = flock_tools::sandbox_core::e2b_provider::E2BSandboxProvider; println!("Starting E2B sandbox with template k0wmnzir0zuzye6dndlw..."); let sandbox_id = match prov.get_or_create_sandbox(&cfg).await { @@ -214,7 +214,7 @@ async fn test_diagnose_e2b_sandbox() { // Ensure VNC running println!("Ensuring VNC is running..."); - if let Err(e) = flock_tools::daytona::ensure_vnc_running_in_sandbox(&db, &sandbox_id).await { + if let Err(e) = flock_tools::sandbox_core::daytona::ensure_vnc_running_in_sandbox(&db, &sandbox_id).await { println!("ensure_vnc_running_in_sandbox error: {}", e); } diff --git a/flock-ui/src-tauri/src/commands/common/sandbox.rs b/flock-ui/src-tauri/src/commands/common/sandbox.rs index b4e069ea..684e2294 100644 --- a/flock-ui/src-tauri/src/commands/common/sandbox.rs +++ b/flock-ui/src-tauri/src/commands/common/sandbox.rs @@ -6,7 +6,7 @@ use crate::commands::assistant::SharedAgentState; pub async fn destroy_sandbox( db: State<'_, crate::SharedDbManager>, ) -> Result<(), String> { - flock_tools::sandbox_manager::destroy_active_sandbox(&*db) + flock_tools::sandbox_core::manager::destroy_active_sandbox(&*db) .await .map_err(|e| e.to_string()) } @@ -17,7 +17,7 @@ pub async fn cleanup_all_sandboxes( db: State<'_, crate::SharedDbManager>, ) -> Result { use flock_core::db::DbManager; - use flock_tools::daytona::{get_sandbox_config, get_api_base}; + use flock_tools::sandbox_core::daytona::{get_sandbox_config, get_api_base}; let db_ref: &DbManager = &*db; let cfg = get_sandbox_config(db_ref).await @@ -79,7 +79,7 @@ pub async fn cleanup_all_sandboxes( } // 清除本地缓存 - let _ = flock_tools::sandbox_manager::destroy_active_sandbox(db_ref).await; + let _ = flock_tools::sandbox_core::manager::destroy_active_sandbox(db_ref).await; Ok(flock_core::tr( &format!("清理完成:已销毁 {} 个沙盒,失败 {} 个。", deleted, failed), @@ -93,11 +93,11 @@ pub async fn get_active_sandbox_vnc_url( _state: State<'_, SharedAgentState>, db: State<'_, crate::SharedDbManager>, ) -> Result, String> { - if let Some(sandbox_id) = flock_tools::sandbox_manager::get_active_sandbox_id().await { - match flock_tools::sandbox_manager::get_sandbox_vnc_url(&*db, &sandbox_id).await { + if let Some(sandbox_id) = flock_tools::sandbox_core::manager::get_active_sandbox_id().await { + match flock_tools::sandbox_core::manager::get_sandbox_vnc_url(&*db, &sandbox_id).await { Ok(url) => Ok(Some(url)), Err(_) => { - let fallback_url = match flock_tools::daytona::get_sandbox_config(&*db).await { + let fallback_url = match flock_tools::sandbox_core::daytona::get_sandbox_config(&*db).await { Some(cfg) if cfg.provider.as_deref().unwrap_or("e2b") == "e2b" => { format!("https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", sandbox_id) } diff --git a/flock-ui/src-tauri/src/commands/file.rs b/flock-ui/src-tauri/src/commands/file.rs index b84d1c63..62f424b1 100644 --- a/flock-ui/src-tauri/src/commands/file.rs +++ b/flock-ui/src-tauri/src/commands/file.rs @@ -1,6 +1,6 @@ use crate::workspace; -use flock_tools::sandbox_manager::get_active_sandbox_id; -use flock_tools::daytona::fs::DaytonaFs; +use flock_tools::sandbox_core::manager::get_active_sandbox_id; +use flock_tools::sandbox_core::daytona::fs::DaytonaFs; use std::path::PathBuf; async fn is_sandbox_active(_db: &crate::SharedDbManager) -> bool { @@ -64,7 +64,7 @@ pub async fn list_workspace_files( } } -fn map_daytona_entry(entry: flock_tools::daytona::fs::DaytonaFileEntry) -> workspace::FileEntry { +fn map_daytona_entry(entry: flock_tools::sandbox_core::daytona::fs::DaytonaFileEntry) -> workspace::FileEntry { workspace::FileEntry { name: entry.name, path: entry.path, diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index db9d9060..d36794d3 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -120,7 +120,7 @@ pub async fn set_app_config( } // 关键修复:清除旧的活跃沙盒缓存,防止状态穿透 - let _ = flock_tools::sandbox_manager::clear_active_sandbox_id().await; + let _ = flock_tools::sandbox_core::manager::clear_active_sandbox_id().await; } Ok(()) @@ -132,7 +132,7 @@ pub async fn build_e2b_template( app: tauri::AppHandle, db: State<'_, SharedDbManager>, ) -> Result { - use flock_tools::daytona::get_sandbox_config; + use flock_tools::sandbox_core::daytona::get_sandbox_config; let cfg = get_sandbox_config(&db).await .ok_or_else(|| "沙盒未配置".to_string())?; @@ -140,7 +140,7 @@ pub async fn build_e2b_template( let api_key = cfg.e2b_api_key.as_deref().or(cfg.api_key.as_deref()) .ok_or_else(|| "E2B API Key 未配置".to_string())?; - let template_id = flock_tools::e2b::builder::build_enhanced_template( + let template_id = flock_tools::sandbox_core::e2b::builder::build_enhanced_template( api_key, move |log| { use tauri::Emitter; @@ -197,7 +197,7 @@ pub async fn test_sandbox_connection( let (url, is_e2b) = if provider == "e2b" { ("https://api.e2b.app/sandboxes".to_string(), true) } else { - let base = flock_tools::daytona::get_api_base(&api_url); + let base = flock_tools::sandbox_core::daytona::get_api_base(&api_url); (format!("{}/api/sandbox", base), false) }; @@ -248,7 +248,7 @@ pub async fn create_playwright_snapshot( db: State<'_, SharedDbManager>, snapshot_name: String, ) -> Result { - flock_tools::daytona::create_playwright_snapshot( + flock_tools::sandbox_core::daytona::create_playwright_snapshot( &*db, &snapshot_name, ) @@ -275,7 +275,7 @@ pub async fn list_sandboxes( return Ok(serde_json::json!([])); } - let base = flock_tools::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); + let base = flock_tools::sandbox_core::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); let client = reqwest::Client::new(); @@ -306,7 +306,7 @@ pub async fn delete_sandbox( id: String, ) -> Result<(), String> { use flock_core::db::DbManager; - use flock_tools::daytona::get_sandbox_config; + use flock_tools::sandbox_core::daytona::get_sandbox_config; let db_ref: &DbManager = &*db; let cfg = get_sandbox_config(db_ref).await @@ -317,7 +317,7 @@ pub async fn delete_sandbox( return Ok(()); } - let base = flock_tools::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); + let base = flock_tools::sandbox_core::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); let client = reqwest::Client::new(); @@ -333,9 +333,9 @@ pub async fn delete_sandbox( if resp.status().is_success() { // 如果删除的是当前活跃的沙盒,清理本地缓存 - if let Some(active_id) = flock_tools::sandbox_manager::get_active_sandbox_id().await { + if let Some(active_id) = flock_tools::sandbox_core::manager::get_active_sandbox_id().await { if active_id == id { - flock_tools::sandbox_manager::clear_active_sandbox_id().await; + flock_tools::sandbox_core::manager::clear_active_sandbox_id().await; } } Ok(()) @@ -356,11 +356,11 @@ pub async fn reuse_sandbox( use flock_core::db::DbManager; let db_ref: &DbManager = &*db; - let cfg = flock_tools::daytona::get_sandbox_config(db_ref).await + let cfg = flock_tools::sandbox_core::daytona::get_sandbox_config(db_ref).await .ok_or_else(|| flock_core::tr("沙盒未配置或未启用", "Sandbox not configured or enabled"))?; // 验证沙盒是否存活 - if !flock_tools::sandbox_manager::check_sandbox_alive(&cfg, &sandbox_id).await { + if !flock_tools::sandbox_core::manager::check_sandbox_alive(&cfg, &sandbox_id).await { return Err(flock_core::tr( &format!("沙盒 {} 不存在或已停止", sandbox_id), &format!("Sandbox {} does not exist or has stopped", sandbox_id) @@ -368,7 +368,7 @@ pub async fn reuse_sandbox( } // 设置为活跃沙盒 - let mutex = flock_tools::sandbox_manager::get_sandbox_id_mutex(); + let mutex = flock_tools::sandbox_core::manager::get_sandbox_id_mutex(); let mut lock = mutex.lock().await; *lock = Some(sandbox_id.clone()); @@ -490,7 +490,7 @@ pub async fn list_sandbox_templates( None => return Ok(serde_json::json!([])), }; - let base = flock_tools::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); + let base = flock_tools::sandbox_core::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); let client = reqwest::Client::new(); @@ -552,7 +552,7 @@ pub async fn delete_sandbox_template( return Ok(()); } - let base = flock_tools::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); + let base = flock_tools::sandbox_core::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); let client = reqwest::Client::new(); From eed693f55651352bf54216e19089d94f7b6ec3e1 Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Sat, 27 Jun 2026 09:26:29 +0800 Subject: [PATCH 09/25] chore: move sandbox config out of daytona module into sandbox_core --- crates/flock-agent/src/agent_setup_helpers.rs | 2 +- .../src/sandbox_core/{daytona => }/config.rs | 0 .../flock-tools/src/sandbox_core/daytona/exec.rs | 2 +- .../src/sandbox_core/daytona/lifecycle.rs | 2 +- .../flock-tools/src/sandbox_core/daytona/mod.rs | 2 -- .../src/sandbox_core/daytona/snapshot.rs | 2 +- .../flock-tools/src/sandbox_core/daytona/sync.rs | 4 ++-- .../flock-tools/src/sandbox_core/daytona/vnc.rs | 2 +- .../src/sandbox_core/daytona/volume.rs | 2 +- crates/flock-tools/src/sandbox_core/manager.rs | 2 +- crates/flock-tools/src/sandbox_core/mod.rs | 1 + .../src-tauri/src/commands/common/sandbox.rs | 4 ++-- flock-ui/src-tauri/src/commands/settings.rs | 16 ++++++++-------- 13 files changed, 20 insertions(+), 21 deletions(-) rename crates/flock-tools/src/sandbox_core/{daytona => }/config.rs (100%) diff --git a/crates/flock-agent/src/agent_setup_helpers.rs b/crates/flock-agent/src/agent_setup_helpers.rs index 7940bf61..3af1b521 100644 --- a/crates/flock-agent/src/agent_setup_helpers.rs +++ b/crates/flock-agent/src/agent_setup_helpers.rs @@ -21,7 +21,7 @@ use super::sinks::OutputSink; /// when the global sandbox is configured. pub async fn filter_sandbox_tools(config: &Config, registry: &mut ToolRegistry) { let is_sandbox_configured = if let Some(db) = &config.db_manager { - flock_tools::sandbox_core::daytona::get_sandbox_config(db).await.is_some() + flock_tools::sandbox_core::config::get_sandbox_config(db).await.is_some() } else { false }; diff --git a/crates/flock-tools/src/sandbox_core/daytona/config.rs b/crates/flock-tools/src/sandbox_core/config.rs similarity index 100% rename from crates/flock-tools/src/sandbox_core/daytona/config.rs rename to crates/flock-tools/src/sandbox_core/config.rs diff --git a/crates/flock-tools/src/sandbox_core/daytona/exec.rs b/crates/flock-tools/src/sandbox_core/daytona/exec.rs index 334f0b82..b34f4bab 100644 --- a/crates/flock-tools/src/sandbox_core/daytona/exec.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/exec.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use flock_core::db::DbManager; -use crate::sandbox_core::daytona::config::get_sandbox_config; +use crate::sandbox_core::config::get_sandbox_config; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DaytonaSandboxResponse { diff --git a/crates/flock-tools/src/sandbox_core/daytona/lifecycle.rs b/crates/flock-tools/src/sandbox_core/daytona/lifecycle.rs index 6d76062e..05c88b2e 100644 --- a/crates/flock-tools/src/sandbox_core/daytona/lifecycle.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/lifecycle.rs @@ -1,6 +1,6 @@ use flock_core::db::DbManager; use flock_core::config::settings::SandboxConfig; -use crate::sandbox_core::daytona::config::{get_sandbox_config, get_api_base}; +use crate::sandbox_core::config::{get_sandbox_config, get_api_base}; use crate::sandbox_core::daytona::volume::get_or_create_volume; /// 创建 Daytona 沙盒,等待 started 状态后返回 sandbox_id diff --git a/crates/flock-tools/src/sandbox_core/daytona/mod.rs b/crates/flock-tools/src/sandbox_core/daytona/mod.rs index f7d05ab9..03b21502 100644 --- a/crates/flock-tools/src/sandbox_core/daytona/mod.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/mod.rs @@ -1,5 +1,4 @@ mod state; -mod config; mod lifecycle; mod exec; mod vnc; @@ -10,7 +9,6 @@ pub mod fs; pub mod provider; pub use state::emit_human_takeover; -pub use config::{get_sandbox_config, get_api_base}; pub use lifecycle::{ create_sandbox, destroy_daytona_sandbox, check_sandbox_alive, set_sandbox_public, }; diff --git a/crates/flock-tools/src/sandbox_core/daytona/snapshot.rs b/crates/flock-tools/src/sandbox_core/daytona/snapshot.rs index 1cd87f96..2dd20666 100644 --- a/crates/flock-tools/src/sandbox_core/daytona/snapshot.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/snapshot.rs @@ -1,5 +1,5 @@ use flock_core::db::DbManager; -use crate::sandbox_core::daytona::config::{get_sandbox_config, get_api_base}; +use crate::sandbox_core::config::{get_sandbox_config, get_api_base}; use crate::sandbox_core::daytona::{DISPLAY_ID, SCREEN_RESOLUTION, X11VNC_PORT, WEBSOCKIFY_PORT}; /// 创建一个预装 Playwright 的 Daytona Snapshot diff --git a/crates/flock-tools/src/sandbox_core/daytona/sync.rs b/crates/flock-tools/src/sandbox_core/daytona/sync.rs index bad28cb3..330ef3b1 100644 --- a/crates/flock-tools/src/sandbox_core/daytona/sync.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/sync.rs @@ -10,7 +10,7 @@ fn should_ignore(path: &Path) -> bool { } pub async fn sync_up(db: &DbManager, sandbox_id: &str, local_workspace: &Path) -> anyhow::Result<()> { - if let Some(cfg) = crate::sandbox_core::daytona::config::get_sandbox_config(db).await { + if let Some(cfg) = crate::sandbox_core::config::get_sandbox_config(db).await { if cfg.provider.as_deref().unwrap_or("e2b") == "local" { return Ok(()); } @@ -57,7 +57,7 @@ pub async fn sync_up(db: &DbManager, sandbox_id: &str, local_workspace: &Path) - } pub async fn sync_down(db: &DbManager, sandbox_id: &str, local_workspace: &Path) -> anyhow::Result<()> { - if let Some(cfg) = crate::sandbox_core::daytona::config::get_sandbox_config(db).await { + if let Some(cfg) = crate::sandbox_core::config::get_sandbox_config(db).await { if cfg.provider.as_deref().unwrap_or("e2b") == "local" { return Ok(()); } diff --git a/crates/flock-tools/src/sandbox_core/daytona/vnc.rs b/crates/flock-tools/src/sandbox_core/daytona/vnc.rs index 9b226851..915b6dbe 100644 --- a/crates/flock-tools/src/sandbox_core/daytona/vnc.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/vnc.rs @@ -1,5 +1,5 @@ use flock_core::db::DbManager; -use crate::sandbox_core::daytona::config::{get_sandbox_config, get_api_base}; +use crate::sandbox_core::config::{get_sandbox_config, get_api_base}; use crate::sandbox_core::daytona::exec::execute_command_in_sandbox; use crate::sandbox_core::daytona::{DISPLAY_ID, SCREEN_RESOLUTION, X11VNC_PORT, WEBSOCKIFY_PORT}; diff --git a/crates/flock-tools/src/sandbox_core/daytona/volume.rs b/crates/flock-tools/src/sandbox_core/daytona/volume.rs index a85c061b..ceec1bf1 100644 --- a/crates/flock-tools/src/sandbox_core/daytona/volume.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/volume.rs @@ -1,5 +1,5 @@ use flock_core::config::settings::SandboxConfig; -use crate::sandbox_core::daytona::config::get_api_base; +use crate::sandbox_core::config::get_api_base; use serde_json::Value; pub async fn get_or_create_volume(cfg: &SandboxConfig, workspace_id: &str) -> anyhow::Result { diff --git a/crates/flock-tools/src/sandbox_core/manager.rs b/crates/flock-tools/src/sandbox_core/manager.rs index ad446541..a46fe469 100644 --- a/crates/flock-tools/src/sandbox_core/manager.rs +++ b/crates/flock-tools/src/sandbox_core/manager.rs @@ -8,7 +8,7 @@ use std::sync::OnceLock; use tokio::sync::Mutex; use flock_core::db::DbManager; -use crate::sandbox_core::daytona::get_sandbox_config; +use crate::sandbox_core::config::get_sandbox_config; use crate::sandbox_core::provider::SandboxProvider; static ACTIVE_SANDBOX_ID: OnceLock>> = OnceLock::new(); diff --git a/crates/flock-tools/src/sandbox_core/mod.rs b/crates/flock-tools/src/sandbox_core/mod.rs index d80677d2..f4659d2e 100644 --- a/crates/flock-tools/src/sandbox_core/mod.rs +++ b/crates/flock-tools/src/sandbox_core/mod.rs @@ -1,3 +1,4 @@ +pub mod config; pub mod daytona; pub mod e2b; pub mod manager; diff --git a/flock-ui/src-tauri/src/commands/common/sandbox.rs b/flock-ui/src-tauri/src/commands/common/sandbox.rs index 684e2294..320191d7 100644 --- a/flock-ui/src-tauri/src/commands/common/sandbox.rs +++ b/flock-ui/src-tauri/src/commands/common/sandbox.rs @@ -17,7 +17,7 @@ pub async fn cleanup_all_sandboxes( db: State<'_, crate::SharedDbManager>, ) -> Result { use flock_core::db::DbManager; - use flock_tools::sandbox_core::daytona::{get_sandbox_config, get_api_base}; + use flock_tools::sandbox_core::config::{get_sandbox_config, get_api_base}; let db_ref: &DbManager = &*db; let cfg = get_sandbox_config(db_ref).await @@ -97,7 +97,7 @@ pub async fn get_active_sandbox_vnc_url( match flock_tools::sandbox_core::manager::get_sandbox_vnc_url(&*db, &sandbox_id).await { Ok(url) => Ok(Some(url)), Err(_) => { - let fallback_url = match flock_tools::sandbox_core::daytona::get_sandbox_config(&*db).await { + let fallback_url = match flock_tools::sandbox_core::config::get_sandbox_config(&*db).await { Some(cfg) if cfg.provider.as_deref().unwrap_or("e2b") == "e2b" => { format!("https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", sandbox_id) } diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index d36794d3..bcc604a0 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -132,7 +132,7 @@ pub async fn build_e2b_template( app: tauri::AppHandle, db: State<'_, SharedDbManager>, ) -> Result { - use flock_tools::sandbox_core::daytona::get_sandbox_config; + use flock_tools::sandbox_core::config::get_sandbox_config; let cfg = get_sandbox_config(&db).await .ok_or_else(|| "沙盒未配置".to_string())?; @@ -197,7 +197,7 @@ pub async fn test_sandbox_connection( let (url, is_e2b) = if provider == "e2b" { ("https://api.e2b.app/sandboxes".to_string(), true) } else { - let base = flock_tools::sandbox_core::daytona::get_api_base(&api_url); + let base = flock_tools::sandbox_core::config::get_api_base(&api_url); (format!("{}/api/sandbox", base), false) }; @@ -275,7 +275,7 @@ pub async fn list_sandboxes( return Ok(serde_json::json!([])); } - let base = flock_tools::sandbox_core::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); + let base = flock_tools::sandbox_core::config::get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); let client = reqwest::Client::new(); @@ -306,7 +306,7 @@ pub async fn delete_sandbox( id: String, ) -> Result<(), String> { use flock_core::db::DbManager; - use flock_tools::sandbox_core::daytona::get_sandbox_config; + use flock_tools::sandbox_core::config::get_sandbox_config; let db_ref: &DbManager = &*db; let cfg = get_sandbox_config(db_ref).await @@ -317,7 +317,7 @@ pub async fn delete_sandbox( return Ok(()); } - let base = flock_tools::sandbox_core::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); + let base = flock_tools::sandbox_core::config::get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); let client = reqwest::Client::new(); @@ -356,7 +356,7 @@ pub async fn reuse_sandbox( use flock_core::db::DbManager; let db_ref: &DbManager = &*db; - let cfg = flock_tools::sandbox_core::daytona::get_sandbox_config(db_ref).await + let cfg = flock_tools::sandbox_core::config::get_sandbox_config(db_ref).await .ok_or_else(|| flock_core::tr("沙盒未配置或未启用", "Sandbox not configured or enabled"))?; // 验证沙盒是否存活 @@ -490,7 +490,7 @@ pub async fn list_sandbox_templates( None => return Ok(serde_json::json!([])), }; - let base = flock_tools::sandbox_core::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); + let base = flock_tools::sandbox_core::config::get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); let client = reqwest::Client::new(); @@ -552,7 +552,7 @@ pub async fn delete_sandbox_template( return Ok(()); } - let base = flock_tools::sandbox_core::daytona::get_api_base(cfg.api_url.as_ref().unwrap()); + let base = flock_tools::sandbox_core::config::get_api_base(cfg.api_url.as_ref().unwrap()); let api_key = cfg.api_key.as_ref().unwrap(); let client = reqwest::Client::new(); From bb0ea783912ed778a857c742912979d3c6e8c8bc Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Sat, 27 Jun 2026 09:47:29 +0800 Subject: [PATCH 10/25] feat: add E2B API endpoint URL config and fix sandbox snapshot settings bugs --- .../flock-core/src/config/settings/types.rs | 1 + .../src/sandbox_core/e2b/lifecycle.rs | 9 ++-- crates/flock-tools/tests/diagnose_sandbox.rs | 15 ++++--- flock-ui/src-tauri/src/commands/settings.rs | 45 ++++++------------- .../components/SandboxActions.tsx | 4 +- .../components/SandboxCredentials.tsx | 43 +++++++++++++----- .../components/SnapshotListSection.tsx | 23 +++++----- .../hooks/useSandboxSettings.ts | 9 +++- .../Settings/SandboxSettings/index.tsx | 8 ++-- 9 files changed, 85 insertions(+), 72 deletions(-) diff --git a/crates/flock-core/src/config/settings/types.rs b/crates/flock-core/src/config/settings/types.rs index 95bcda66..74c6c742 100644 --- a/crates/flock-core/src/config/settings/types.rs +++ b/crates/flock-core/src/config/settings/types.rs @@ -156,6 +156,7 @@ pub struct SandboxConfig { pub e2b_api_key: Option, pub e2b_api_key_encrypted: Option, pub e2b_api_key_nonce: Option, + pub e2b_api_url: Option, pub snapshot: Option, } diff --git a/crates/flock-tools/src/sandbox_core/e2b/lifecycle.rs b/crates/flock-tools/src/sandbox_core/e2b/lifecycle.rs index 9586c370..b0bcdbf3 100644 --- a/crates/flock-tools/src/sandbox_core/e2b/lifecycle.rs +++ b/crates/flock-tools/src/sandbox_core/e2b/lifecycle.rs @@ -21,9 +21,10 @@ pub async fn check_alive(cfg: &SandboxConfig, sandbox_id: &str) -> bool { Some(k) => k, None => return false, }; + let base_url = cfg.e2b_api_url.as_deref().unwrap_or("https://api.e2b.app").trim_end_matches('/'); let client = reqwest::Client::new(); - let url = format!("https://api.e2b.app/sandboxes/{}", sandbox_id); + let url = format!("{}/sandboxes/{}", base_url, sandbox_id); match client.get(&url) .header("X-API-Key", api_key) @@ -39,6 +40,7 @@ pub async fn check_alive(cfg: &SandboxConfig, sandbox_id: &str) -> bool { pub async fn create_sandbox(cfg: &SandboxConfig) -> anyhow::Result { let api_key = cfg.e2b_api_key.as_ref() .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; + let base_url = cfg.e2b_api_url.as_deref().unwrap_or("https://api.e2b.app").trim_end_matches('/'); let template_id = cfg.snapshot.as_deref() .unwrap_or("") @@ -57,7 +59,7 @@ pub async fn create_sandbox(cfg: &SandboxConfig) -> anyhow::Result { &format!("Requesting E2B sandbox (template: {})...", template_id) )); - let url = "https://api.e2b.app/sandboxes"; + let url = format!("{}/sandboxes", base_url); let resp = client.post(url) .header("X-API-Key", api_key) .json(&payload) @@ -81,9 +83,10 @@ pub async fn create_sandbox(cfg: &SandboxConfig) -> anyhow::Result { pub async fn destroy_sandbox(cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { let api_key = cfg.e2b_api_key.as_ref() .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; + let base_url = cfg.e2b_api_url.as_deref().unwrap_or("https://api.e2b.app").trim_end_matches('/'); let client = reqwest::Client::new(); - let url = format!("https://api.e2b.app/sandboxes/{}", sandbox_id); + let url = format!("{}/sandboxes/{}", base_url, sandbox_id); let resp = client.delete(&url) .header("X-API-Key", api_key) diff --git a/crates/flock-tools/tests/diagnose_sandbox.rs b/crates/flock-tools/tests/diagnose_sandbox.rs index 168d085a..cda54801 100644 --- a/crates/flock-tools/tests/diagnose_sandbox.rs +++ b/crates/flock-tools/tests/diagnose_sandbox.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use flock_core::db::DbManager; -use flock_tools::sandbox_core::daytona::{get_sandbox_config, get_api_base, execute_command_in_sandbox}; +use flock_tools::sandbox_core::config::{get_sandbox_config, get_api_base}; +use flock_tools::sandbox_core::daytona::execute_command_in_sandbox; #[tokio::test] async fn test_diagnose_sandbox() { @@ -200,10 +201,10 @@ async fn test_diagnose_e2b_sandbox() { cfg.snapshot = Some("k0wmnzir0zuzye6dndlw".to_string()); use flock_tools::sandbox_core::provider::SandboxProvider; - let prov = flock_tools::sandbox_core::e2b_provider::E2BSandboxProvider; + let prov = flock_tools::sandbox_core::e2b::provider::E2bProvider; println!("Starting E2B sandbox with template k0wmnzir0zuzye6dndlw..."); - let sandbox_id = match prov.get_or_create_sandbox(&cfg).await { + let sandbox_id = match prov.create_sandbox(&db, &cfg).await { Ok(id) => id, Err(e) => { println!("Failed to create sandbox: {}", e); @@ -220,7 +221,7 @@ async fn test_diagnose_e2b_sandbox() { // 1. Check listening ports and env println!("\n--- Env and Path ---"); - let env_out = match prov.execute_command(&cfg, &sandbox_id, "env && echo 'PATH IS:' && echo $PATH").await { + let env_out = match prov.execute_command(&db, &cfg, &sandbox_id, "env && echo 'PATH IS:' && echo $PATH").await { Ok((out, _)) => out, Err(e) => format!("Error executing env command: {}", e), }; @@ -228,7 +229,7 @@ async fn test_diagnose_e2b_sandbox() { // 2. Check installed debian packages println!("\n--- Debian Packages Check ---"); - let dpkg_out = match prov.execute_command(&cfg, &sandbox_id, "dpkg -l | grep -iE 'vnc|xvfb|fluxbox|websockify|novnc'").await { + let dpkg_out = match prov.execute_command(&db, &cfg, &sandbox_id, "dpkg -l | grep -iE 'vnc|xvfb|fluxbox|websockify|novnc'").await { Ok((out, _)) => out, Err(e) => format!("Error executing dpkg command: {}", e), }; @@ -236,7 +237,7 @@ async fn test_diagnose_e2b_sandbox() { // 3. Check logs of vnc println!("\n--- VNC logs ---"); - let log_out = match prov.execute_command(&cfg, &sandbox_id, "cat /tmp/vnc_start.log /tmp/websockify.log /tmp/x11vnc.log 2>/dev/null").await { + let log_out = match prov.execute_command(&db, &cfg, &sandbox_id, "cat /tmp/vnc_start.log /tmp/websockify.log /tmp/x11vnc.log 2>/dev/null").await { Ok((out, _)) => out, Err(e) => format!("Error executing cat command: {}", e), }; @@ -244,6 +245,6 @@ async fn test_diagnose_e2b_sandbox() { // Cleanup println!("\nDestroying sandbox..."); - let _ = prov.destroy_sandbox(&cfg, &sandbox_id).await; + let _ = prov.destroy_sandbox(&db, &cfg, &sandbox_id).await; println!("Sandbox destroyed."); } diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index bcc604a0..40615e6c 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -195,7 +195,8 @@ pub async fn test_sandbox_connection( let client = reqwest::Client::new(); let (url, is_e2b) = if provider == "e2b" { - ("https://api.e2b.app/sandboxes".to_string(), true) + let base_url = api_url.trim_end_matches('/'); + (format!("{}/sandboxes", base_url), true) } else { let base = flock_tools::sandbox_core::config::get_api_base(&api_url); (format!("{}/api/sandbox", base), false) @@ -389,18 +390,16 @@ pub async fn list_sandbox_templates( let active_provider = provider.unwrap_or_else(|| "e2b".to_string()); if active_provider == "e2b" { + let db_ref: &DbManager = &*db; + let cfg = get_sandbox_config_regardless(db_ref).await.unwrap_or_default(); let key = if let Some(ref k) = api_key { if k.is_empty() { - let db_ref: &DbManager = &*db; - let cfg = get_sandbox_config_regardless(db_ref).await.unwrap_or_default(); - cfg.e2b_api_key.unwrap_or_default() + cfg.e2b_api_key.clone().unwrap_or_default() } else { k.clone() } } else { - let db_ref: &DbManager = &*db; - let cfg = get_sandbox_config_regardless(db_ref).await.unwrap_or_default(); - cfg.e2b_api_key.unwrap_or_default() + cfg.e2b_api_key.clone().unwrap_or_default() }; log::info!("list_daytona_snapshots: active_provider = E2B, key len = {}, key starts with: {}", key.len(), &key[..std::cmp::min(5, key.len())]); @@ -408,9 +407,11 @@ pub async fn list_sandbox_templates( return Ok(serde_json::json!([])); } + let base_url = cfg.e2b_api_url.as_deref().unwrap_or("https://api.e2b.app").trim_end_matches('/'); + let client = reqwest::Client::new(); - let url = "https://api.e2b.app/templates"; - let resp = client.get(url) + let url = format!("{}/templates", base_url); + let resp = client.get(&url) .header("X-API-Key", key) .send() .await @@ -429,28 +430,7 @@ pub async fn list_sandbox_templates( )); } - let mut mapped: Vec = vec![ - serde_json::json!({ - "id": "desktop", - "name": "desktop (GUI Desktop / VNC)", - "status": "active" - }), - serde_json::json!({ - "id": "base", - "name": "base (Standard Python/Bash)", - "status": "active" - }), - serde_json::json!({ - "id": "code-interpreter", - "name": "code-interpreter", - "status": "active" - }), - serde_json::json!({ - "id": "browser", - "name": "browser", - "status": "active" - }), - ]; + let mut mapped: Vec = vec![]; let list_val: serde_json::Value = serde_json::from_str(&text) .map_err(|e| flock_core::tr( @@ -529,8 +509,9 @@ pub async fn delete_sandbox_template( let provider = cfg.provider.as_deref().unwrap_or("e2b"); if provider == "e2b" { let api_key = cfg.e2b_api_key.as_ref().unwrap(); + let base_url = cfg.e2b_api_url.as_deref().unwrap_or("https://api.e2b.app").trim_end_matches('/'); let client = reqwest::Client::new(); - let url = format!("https://api.e2b.app/snapshots/{}", id); + let url = format!("{}/snapshots/{}", base_url, id); let resp = client.delete(&url) .header("X-API-Key", api_key) .send() diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SandboxActions.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SandboxActions.tsx index ad8596d8..360d43e5 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SandboxActions.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SandboxActions.tsx @@ -14,6 +14,7 @@ interface SandboxActionsProps { apiUrl: string; apiKey: string; e2bApiKey: string; + e2bApiUrl: string; testing: boolean; disabling: boolean; onTestConnection: () => void; @@ -26,6 +27,7 @@ export function SandboxActions({ apiUrl, apiKey, e2bApiKey, + e2bApiUrl, testing, disabling, onTestConnection, @@ -34,7 +36,7 @@ export function SandboxActions({ const { t } = useTranslation(); const isTestDisabled = () => { - if (provider === 'e2b') return !e2bApiKey.trim(); + if (provider === 'e2b') return !e2bApiKey.trim() || !e2bApiUrl.trim(); if (provider === 'daytona') return !apiUrl.trim() || !apiKey.trim(); return false; // local is never disabled }; diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SandboxCredentials.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SandboxCredentials.tsx index ed093058..154cc906 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SandboxCredentials.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SandboxCredentials.tsx @@ -21,9 +21,11 @@ interface SandboxCredentialsProps { apiUrl: string; apiKey: string; e2bApiKey: string; + e2bApiUrl: string; onApiUrlChange: (val: string) => void; onApiKeyChange: (val: string) => void; onE2bApiKeyChange: (val: string) => void; + onE2bApiUrlChange: (val: string) => void; } export function SandboxCredentials({ @@ -32,9 +34,11 @@ export function SandboxCredentials({ apiUrl, apiKey, e2bApiKey, + e2bApiUrl, onApiUrlChange, onApiKeyChange, onE2bApiKeyChange, + onE2bApiUrlChange, }: SandboxCredentialsProps) { const { t } = useTranslation(); @@ -58,18 +62,33 @@ export function SandboxCredentials({ /> {provider === 'e2b' && ( - - - E2B API Key - - } - placeholder="e2b_..." - value={e2bApiKey} - onChange={(e) => onE2bApiKeyChange(e.currentTarget.value)} - styles={{ input: { background: 'var(--flock-bg-surface)' } }} - /> + <> + + + {t('settings.sandbox.apiUrl')} +
+ } + placeholder="https://api.e2b.app" + value={e2bApiUrl} + onChange={(e) => onE2bApiUrlChange(e.currentTarget.value)} + styles={{ input: { background: 'var(--flock-bg-surface)' } }} + /> + + + + E2B API Key + + } + placeholder="e2b_..." + value={e2bApiKey} + onChange={(e) => onE2bApiKeyChange(e.currentTarget.value)} + styles={{ input: { background: 'var(--flock-bg-surface)' } }} + /> + )} {provider === 'daytona' && ( diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx index ac40923a..6528ff7c 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx @@ -69,7 +69,7 @@ export function SnapshotListSection({ const fetchSnapshots = async () => { setLoading(true); try { - const data = await invoke('list_sandbox_templates'); + const data = await invoke('list_sandbox_templates', { provider }); let list: SnapshotItem[] = []; if (Array.isArray(data)) { list = data; @@ -191,6 +191,16 @@ export function SnapshotListSection({ style={{ flex: 1 }} styles={{ input: { background: 'var(--flock-bg-surface)' } }} /> + {provider === 'e2b' && ( + + )} - )} - )} @@ -233,7 +233,7 @@ export function SnapshotListSection({ style={{ fontSize: 12 }} > {provider === 'e2b' - ? 'E2B provides a pre-bundled "browser" template with all Playwright dependencies pre-installed. There is no need to manually compile browser dependencies. You can enter any of your custom E2B Template IDs above, click Star to set it as default and use it instantly.' + ? 'You can enter a template ID above to build a custom snapshot template, or choose an existing snapshot template from the list below (click the star icon to set as default and reuse it instantly).' : t('settings.sandbox.snapshotHint')}
From 6b73707b07806edc4978b70293d380abbf2ffdfa Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Sat, 27 Jun 2026 10:06:59 +0800 Subject: [PATCH 13/25] feat: migrate E2B builder to template create API for cloud builds without local Docker --- .../src/sandbox_core/e2b/builder.rs | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/crates/flock-tools/src/sandbox_core/e2b/builder.rs b/crates/flock-tools/src/sandbox_core/e2b/builder.rs index 50d53fba..85e7b78e 100644 --- a/crates/flock-tools/src/sandbox_core/e2b/builder.rs +++ b/crates/flock-tools/src/sandbox_core/e2b/builder.rs @@ -41,9 +41,9 @@ dockerfile = "Dockerfile" std::fs::write(&toml_path, toml_content) .context("Failed to write e2b.toml")?; - on_log("环境准备完毕,开始调用 E2B CLI 构建镜像 (可能需要 1-3 分钟)...\n".to_string()); + on_log("环境准备完毕,开始调用 E2B CLI 构建镜像 (E2B 2.0 在云端构建,不需要本地安装 Docker)...\n".to_string()); - // 3. 执行 npx @e2b/cli template build + // 3. 执行 npx @e2b/cli template create // 在 Windows 下需要调用 npx.cmd let npx_cmd = if cfg!(windows) { "npx.cmd" } else { "npx" }; @@ -51,9 +51,10 @@ dockerfile = "Dockerfile" cmd.arg("-y") .arg("@e2b/cli@latest") .arg("template") - .arg("build") - .arg("-c") - .arg("e2b.toml") + .arg("create") + .arg("-d") + .arg("Dockerfile") + .arg("flock-enhanced-desktop") .env("E2B_API_KEY", api_key) .current_dir(&builder_dir) .stdout(Stdio::piped()) @@ -104,18 +105,21 @@ dockerfile = "Dockerfile" on_log("构建成功!正在解析 Template ID...\n".to_string()); - // 解析出 template ID (通常在构建输出中,或者在构建后的 e2b.toml 中) - // E2B CLI 构建后,会将 template_id 写入 e2b.toml - let updated_toml = std::fs::read_to_string(&toml_path)?; - for line in updated_toml.lines() { - if line.starts_with("template_id") { - let parts: Vec<&str> = line.split('=').collect(); - if parts.len() == 2 { - let id = parts[1].trim().trim_matches('"').to_string(); - return Ok(id); - } + // 解析出 template ID + let re = regex::Regex::new(r"(?i)\b(?:id|template_id)[:\s]+([a-z0-9]{20})\b").unwrap(); + if let Some(cap) = re.captures(&full_output) { + let id = cap.get(1).unwrap().as_str().to_string(); + return Ok(id); + } + + // 回退解析:寻找任何非 "dockerfile" 的 20 位字母数字组合作为 ID + let re_fallback = regex::Regex::new(r"\b([a-z0-9]{20})\b").unwrap(); + for cap in re_fallback.captures_iter(&full_output) { + let matched = cap.get(1).unwrap().as_str(); + if matched != "dockerfile" && matched != "template" { + return Ok(matched.to_string()); } } - anyhow::bail!("构建成功,但未能在 e2b.toml 中找到生成的 template_id") + anyhow::bail!("构建成功,但未能从 E2B CLI 输出日志中提取出生成的 template_id") } From d2556651ff9f6ed88d72ad8a547eff63e9671e1f Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Sat, 27 Jun 2026 10:10:15 +0800 Subject: [PATCH 14/25] style: unify snapshot creation button styles and update E2B default template name --- .../SandboxSettings/components/SnapshotListSection.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx index 5a154758..3afa17f1 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx @@ -66,7 +66,7 @@ export function SnapshotListSection({ const [deletingId, setDeletingId] = useState(null); const [newSnapshotName, setNewSnapshotName] = useState(''); - const defaultSnapshotName = provider === 'e2b' ? 'browser' : 'flock-playwright'; + const defaultSnapshotName = provider === 'e2b' ? 'flock-e2b-desktop' : 'flock-daytona-desktop'; const fetchSnapshots = async () => { setLoading(true); @@ -207,8 +207,6 @@ export function SnapshotListSection({ leftSection={ isExistingSnapshot ? - : provider === 'e2b' - ? : provider === 'local' ? : @@ -216,8 +214,6 @@ export function SnapshotListSection({ > {isExistingSnapshot ? t('settings.sandbox.useExistingSnapshot') - : provider === 'e2b' - ? '初始化增强版云端沙盒 (自带VNC和浏览器)' : provider === 'local' ? t('settings.sandbox.setAsDefault') : t('settings.sandbox.snapshotCreateBtn')} From 5488b5d52a06c86f86789b24e116b51083a7fadf Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Sat, 27 Jun 2026 10:13:27 +0800 Subject: [PATCH 15/25] fix: fix E2B template status rendering and pass active provider during template deletion --- flock-ui/src-tauri/src/commands/settings.rs | 7 ++++--- .../SandboxSettings/components/SnapshotListSection.tsx | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index 307e2ef1..1aa817c8 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -500,6 +500,7 @@ pub async fn list_sandbox_templates( pub async fn delete_sandbox_template( db: State<'_, SharedDbManager>, id: String, + provider: Option, ) -> Result<(), String> { use flock_core::db::DbManager; @@ -507,8 +508,8 @@ pub async fn delete_sandbox_template( let cfg = get_sandbox_config_regardless(db_ref).await .ok_or_else(|| flock_core::tr("沙盒未配置", "Sandbox not configured"))?; - let provider = cfg.provider.as_deref().unwrap_or("e2b"); - if provider == "e2b" { + let active_provider = provider.as_deref().unwrap_or(cfg.provider.as_deref().unwrap_or("e2b")); + if active_provider == "e2b" { let api_key = cfg.e2b_api_key.as_ref().unwrap(); let base_url = cfg.e2b_api_url.as_deref().unwrap_or("https://api.e2b.app").trim_end_matches('/'); let client = reqwest::Client::new(); @@ -530,7 +531,7 @@ pub async fn delete_sandbox_template( &format!("Failed to delete E2B snapshot, HTTP status code: {}", resp.status()) )); } - } else if provider == "local" { + } else if active_provider == "local" { return Ok(()); } diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx index 3afa17f1..9d16fc38 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx @@ -113,7 +113,7 @@ export function SnapshotListSection({ const handleDelete = async (id: string) => { setDeletingId(id); try { - await invoke('delete_sandbox_template', { id }); + await invoke('delete_sandbox_template', { id, provider }); notifications.show({ title: t('common.success'), message: t('settings.sandbox.deleteSnapshotSuccess'), @@ -147,7 +147,7 @@ export function SnapshotListSection({ }; const renderState = (snap: SnapshotItem) => { - const stateStr = (snap.state || snap.snapshotState || 'unknown').toLowerCase(); + const stateStr = (snap.state || snap.snapshotState || snap.status || 'unknown').toLowerCase(); let color = 'gray'; if (stateStr === 'ready' || stateStr === 'active') color = 'teal'; if (stateStr === 'building' || stateStr === 'creating') color = 'blue'; From c19180b73377d28a974442a8aff8b91edcd7990b Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Sat, 27 Jun 2026 10:26:31 +0800 Subject: [PATCH 16/25] fix: switch to USER root in E2B Dockerfile to prevent sudo privilege issues during build --- .../src/sandbox_core/e2b/builder.rs | 27 +++++++++++-------- flock-ui/src-tauri/src/commands/settings.rs | 4 ++- .../components/SnapshotListSection.tsx | 4 +-- .../hooks/useSandboxSettings.ts | 4 +-- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/crates/flock-tools/src/sandbox_core/e2b/builder.rs b/crates/flock-tools/src/sandbox_core/e2b/builder.rs index 85e7b78e..e199525e 100644 --- a/crates/flock-tools/src/sandbox_core/e2b/builder.rs +++ b/crates/flock-tools/src/sandbox_core/e2b/builder.rs @@ -7,7 +7,7 @@ use tokio::process::Command; /// 自动构建 E2B 的专属桌面镜像(自带 VNC 和 Playwright) /// 构建日志将通过传入的回调函数 (on_log) 实时推送 -pub async fn build_enhanced_template(api_key: &str, api_url: Option<&str>, on_log: F) -> Result +pub async fn build_enhanced_template(api_key: &str, api_url: Option<&str>, name: &str, on_log: F) -> Result where F: Fn(String) + Send + 'static, { @@ -21,26 +21,27 @@ where let dockerfile_path = builder_dir.join("Dockerfile"); let toml_path = builder_dir.join("e2b.toml"); + // 删除残留的 e2b.toml,避免干扰新的构建 + if toml_path.exists() { + let _ = std::fs::remove_file(&toml_path); + } + // 1. 写入 Dockerfile let dockerfile_content = r#"FROM e2bdev/desktop:latest +USER root # 安装 python3-pip 和相关依赖 -RUN sudo apt-get update && sudo apt-get install -y python3-pip +RUN apt-get update && apt-get install -y python3-pip # 安装 Playwright 及浏览器 RUN pip install playwright && \ playwright install chromium && \ playwright install-deps chromium + +USER user "#; std::fs::write(&dockerfile_path, dockerfile_content) .context("Failed to write Dockerfile")?; - // 2. 写入 e2b.toml - let toml_content = r#"template_name = "flock-enhanced-desktop" -dockerfile = "Dockerfile" -"#; - std::fs::write(&toml_path, toml_content) - .context("Failed to write e2b.toml")?; - on_log("环境准备完毕,开始调用 E2B CLI 构建镜像 (E2B 2.0 在云端构建,不需要本地安装 Docker)...\n".to_string()); // 3. 执行 npx @e2b/cli template create @@ -54,7 +55,11 @@ dockerfile = "Dockerfile" .arg("create") .arg("-d") .arg("Dockerfile") - .arg("flock-enhanced-desktop") + .arg("-c") + .arg("/start_command.sh") + .arg("--ready-cmd") + .arg("python3 -c \"import socket; s = socket.socket(); s.connect(('127.0.0.1', 6080))\"") + .arg(name) .env("E2B_API_KEY", api_key) .current_dir(&builder_dir) .stdout(Stdio::piped()) @@ -100,7 +105,7 @@ dockerfile = "Dockerfile" let status = child.wait().await?; if !status.success() { - anyhow::bail!("E2B template build failed with exit code: {}", status); + anyhow::bail!("E2B template build failed with exit code: {}.\nError details:\n{}", status, full_output); } on_log("构建成功!正在解析 Template ID...\n".to_string()); diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index 1aa817c8..5fe94014 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -131,6 +131,7 @@ pub async fn set_app_config( pub async fn build_e2b_template( app: tauri::AppHandle, db: State<'_, SharedDbManager>, + name: String, ) -> Result { use flock_tools::sandbox_core::config::get_sandbox_config; @@ -143,6 +144,7 @@ pub async fn build_e2b_template( let template_id = flock_tools::sandbox_core::e2b::builder::build_enhanced_template( api_key, cfg.e2b_api_url.as_deref(), + &name, move |log| { use tauri::Emitter; let _ = app.emit("e2b-build-log", log); @@ -513,7 +515,7 @@ pub async fn delete_sandbox_template( let api_key = cfg.e2b_api_key.as_ref().unwrap(); let base_url = cfg.e2b_api_url.as_deref().unwrap_or("https://api.e2b.app").trim_end_matches('/'); let client = reqwest::Client::new(); - let url = format!("{}/snapshots/{}", base_url, id); + let url = format!("{}/templates/{}", base_url, id); let resp = client.delete(&url) .header("X-API-Key", api_key) .send() diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx index 9d16fc38..67223891 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx @@ -47,7 +47,7 @@ interface SnapshotListSectionProps { creatingSnapshot: boolean; buildingE2b?: boolean; e2bBuildLogs?: string[]; - onBuildE2bTemplate?: () => void; + onBuildE2bTemplate?: (name: string) => void; } export function SnapshotListSection({ @@ -200,7 +200,7 @@ export function SnapshotListSection({ isExistingSnapshot ? handleCreate : provider === 'e2b' - ? onBuildE2bTemplate + ? () => onBuildE2bTemplate?.(newSnapshotName.trim() || defaultSnapshotName) : handleCreate } loading={isExistingSnapshot ? creatingSnapshot : provider === 'e2b' ? buildingE2b : creatingSnapshot} diff --git a/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts b/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts index 2df9d01d..63caed7a 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts +++ b/flock-ui/src/components/Settings/SandboxSettings/hooks/useSandboxSettings.ts @@ -237,7 +237,7 @@ export function useSandboxSettings() { } }; - const handleBuildE2bTemplate = async () => { + const handleBuildE2bTemplate = async (name: string) => { setBuildingE2b(true); setE2bBuildLogs([]); let unlisten: (() => void) | undefined; @@ -248,7 +248,7 @@ export function useSandboxSettings() { setE2bBuildLogs((prev) => [...prev, event.payload]); }); - const templateId = await invoke('build_e2b_template'); + const templateId = await invoke('build_e2b_template', { name }); notifications.show({ title: t('common.success'), From b2e6e4b5954c313e69aa3adda4fa18f3d96f8b4a Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Sat, 27 Jun 2026 10:43:10 +0800 Subject: [PATCH 17/25] fix: generate and copy start_command.sh into E2B template during build to start VNC desktop --- .../src/sandbox_core/e2b/builder.rs | 24 ++++++++++++++++--- flock-ui/src-tauri/src/commands/settings.rs | 3 +++ .../components/SnapshotListSection.tsx | 8 ++++--- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/crates/flock-tools/src/sandbox_core/e2b/builder.rs b/crates/flock-tools/src/sandbox_core/e2b/builder.rs index e199525e..75dc8514 100644 --- a/crates/flock-tools/src/sandbox_core/e2b/builder.rs +++ b/crates/flock-tools/src/sandbox_core/e2b/builder.rs @@ -19,6 +19,7 @@ where } let dockerfile_path = builder_dir.join("Dockerfile"); + let start_script_path = builder_dir.join("start_command.sh"); let toml_path = builder_dir.join("e2b.toml"); // 删除残留的 e2b.toml,避免干扰新的构建 @@ -26,13 +27,30 @@ where let _ = std::fs::remove_file(&toml_path); } + // 写入 start_command.sh 启动脚本 + let start_script_content = r#"#!/bin/bash +export DISPLAY=:0 +rm -f /tmp/.X0-lock +setsid Xvfb :0 -screen 0 1280x800x24 >/tmp/xvfb.log 2>&1 & +sleep 1 +setsid fluxbox >/tmp/fluxbox.log 2>&1 & +sleep 1 +setsid x11vnc -display :0 -forever -shared -nopw -rfbport 5900 >/tmp/x11vnc.log 2>&1 & +sleep 1 +setsid websockify --web /usr/share/novnc 0.0.0.0:6080 localhost:5900 >/tmp/websockify.log 2>&1 & +sleep 1 +"#; + std::fs::write(&start_script_path, start_script_content) + .context("Failed to write start_command.sh")?; + // 1. 写入 Dockerfile let dockerfile_content = r#"FROM e2bdev/desktop:latest USER root -# 安装 python3-pip 和相关依赖 -RUN apt-get update && apt-get install -y python3-pip +# 复制启动脚本并赋予执行权限 +COPY start_command.sh /start_command.sh +RUN chmod +x /start_command.sh -# 安装 Playwright 及浏览器 +# 直接使用已有的 python3 和 pip 安装 Playwright 及其浏览器依赖 RUN pip install playwright && \ playwright install chromium && \ playwright install-deps chromium diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index 5fe94014..c40176b7 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -146,6 +146,9 @@ pub async fn build_e2b_template( cfg.e2b_api_url.as_deref(), &name, move |log| { + print!("{}", log); + use std::io::Write; + let _ = std::io::stdout().flush(); use tauri::Emitter; let _ = app.emit("e2b-build-log", log); } diff --git a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx index 67223891..f6525e1c 100644 --- a/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx +++ b/flock-ui/src/components/Settings/SandboxSettings/components/SnapshotListSection.tsx @@ -256,9 +256,11 @@ export function SnapshotListSection({ - {buildingE2b && ( - - Building Custom Template... (this might take a few minutes) + {e2bBuildLogs && e2bBuildLogs.length > 0 && ( + + + {buildingE2b ? 'Building Custom Template... (this might take a few minutes)' : 'Build Logs / Status:'} + {e2bBuildLogs?.map((log, i) => (
{log}
))} From f893d76cf17068b5b7fcf94cee749b871c886534 Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Sat, 27 Jun 2026 10:53:22 +0800 Subject: [PATCH 18/25] fix: install missing fluxbox and websockify packages in E2B template build to enable VNC desktop --- crates/flock-tools/src/sandbox_core/e2b/builder.rs | 13 ++++++++++++- flock-ui/src-tauri/src/commands/settings.rs | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/flock-tools/src/sandbox_core/e2b/builder.rs b/crates/flock-tools/src/sandbox_core/e2b/builder.rs index 75dc8514..03685d3d 100644 --- a/crates/flock-tools/src/sandbox_core/e2b/builder.rs +++ b/crates/flock-tools/src/sandbox_core/e2b/builder.rs @@ -29,6 +29,13 @@ where // 写入 start_command.sh 启动脚本 let start_script_content = r#"#!/bin/bash +echo "=== Debug: Checking installed commands ===" +which Xvfb || echo "Xvfb missing" +which fluxbox || echo "fluxbox missing" +which x11vnc || echo "x11vnc missing" +which websockify || echo "websockify missing" +echo "==========================================" + export DISPLAY=:0 rm -f /tmp/.X0-lock setsid Xvfb :0 -screen 0 1280x800x24 >/tmp/xvfb.log 2>&1 & @@ -40,7 +47,8 @@ sleep 1 setsid websockify --web /usr/share/novnc 0.0.0.0:6080 localhost:5900 >/tmp/websockify.log 2>&1 & sleep 1 "#; - std::fs::write(&start_script_path, start_script_content) + let lf_content = start_script_content.replace("\r\n", "\n"); + std::fs::write(&start_script_path, lf_content) .context("Failed to write start_command.sh")?; // 1. 写入 Dockerfile @@ -50,6 +58,9 @@ USER root COPY start_command.sh /start_command.sh RUN chmod +x /start_command.sh +# 安装缺少的 VNC 窗口管理器和 noVNC 代理组件 +RUN apt-get update && apt-get install -y fluxbox websockify novnc + # 直接使用已有的 python3 和 pip 安装 Playwright 及其浏览器依赖 RUN pip install playwright && \ playwright install chromium && \ diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index c40176b7..df90e07f 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -408,7 +408,7 @@ pub async fn list_sandbox_templates( cfg.e2b_api_key.clone().unwrap_or_default() }; - log::info!("list_daytona_snapshots: active_provider = E2B, key len = {}, key starts with: {}", key.len(), key.chars().take(5).collect::()); + log::info!("list_sandbox_templates: active_provider = E2B, key len = {}, key starts with: {}", key.len(), key.chars().take(5).collect::()); if key.is_empty() { return Ok(serde_json::json!([])); } @@ -428,7 +428,7 @@ pub async fn list_sandbox_templates( let status = resp.status(); let text = resp.text().await.unwrap_or_default(); - log::info!("list_daytona_snapshots: E2B templates status = {}, text = {}", status, text); + log::info!("list_sandbox_templates: E2B templates status = {}, text = {}", status, text); if !status.is_success() { return Err(flock_core::tr( &format!("E2B API 错误 ({}): {}", status, text), From 21f95a0e1cf9f75c999f4ac474986d82d61d76f8 Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Sat, 27 Jun 2026 10:57:16 +0800 Subject: [PATCH 19/25] fix: resolve E2B list templates 401 unauthorized caused by masked API key --- flock-ui/src-tauri/src/commands/settings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index df90e07f..ea7b2e77 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -399,7 +399,7 @@ pub async fn list_sandbox_templates( let db_ref: &DbManager = &*db; let cfg = get_sandbox_config_regardless(db_ref).await.unwrap_or_default(); let key = if let Some(ref k) = api_key { - if k.is_empty() { + if k.is_empty() || k.contains('•') { cfg.e2b_api_key.clone().unwrap_or_default() } else { k.clone() From f85f75ca2195d9768578f41c800837acc1b52801 Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Sat, 27 Jun 2026 10:59:18 +0800 Subject: [PATCH 20/25] feat: hardcode public E2B template ID jj17rl441iqdc6ea5mkb as default fallback --- crates/flock-tools/src/sandbox_core/e2b/lifecycle.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/flock-tools/src/sandbox_core/e2b/lifecycle.rs b/crates/flock-tools/src/sandbox_core/e2b/lifecycle.rs index b0bcdbf3..4a9ba854 100644 --- a/crates/flock-tools/src/sandbox_core/e2b/lifecycle.rs +++ b/crates/flock-tools/src/sandbox_core/e2b/lifecycle.rs @@ -42,11 +42,11 @@ pub async fn create_sandbox(cfg: &SandboxConfig) -> anyhow::Result { .ok_or_else(|| anyhow::anyhow!("E2B API key is missing"))?; let base_url = cfg.e2b_api_url.as_deref().unwrap_or("https://api.e2b.app").trim_end_matches('/'); - let template_id = cfg.snapshot.as_deref() + let mut template_id = cfg.snapshot.as_deref() .unwrap_or("") .trim(); if template_id.is_empty() { - anyhow::bail!("E2B template ID is not configured. Please set a default template in Sandbox Settings."); + template_id = "jj17rl441iqdc6ea5mkb"; // 使用您发布的公共模版 ID 作为默认兜底 } let client = reqwest::Client::new(); From f948018dc142f45873534ca517ce64d7132376f7 Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Sat, 27 Jun 2026 11:32:09 +0800 Subject: [PATCH 21/25] fix: add --ignore-origin to websockify to bypass VNC connection 502 Bad Gateway --- .../src/sandbox_core/e2b/builder.rs | 2 +- crates/flock-tools/tests/diagnose_e2b.rs | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 crates/flock-tools/tests/diagnose_e2b.rs diff --git a/crates/flock-tools/src/sandbox_core/e2b/builder.rs b/crates/flock-tools/src/sandbox_core/e2b/builder.rs index 03685d3d..aeb0c813 100644 --- a/crates/flock-tools/src/sandbox_core/e2b/builder.rs +++ b/crates/flock-tools/src/sandbox_core/e2b/builder.rs @@ -44,7 +44,7 @@ setsid fluxbox >/tmp/fluxbox.log 2>&1 & sleep 1 setsid x11vnc -display :0 -forever -shared -nopw -rfbport 5900 >/tmp/x11vnc.log 2>&1 & sleep 1 -setsid websockify --web /usr/share/novnc 0.0.0.0:6080 localhost:5900 >/tmp/websockify.log 2>&1 & +setsid websockify --ignore-origin --web /usr/share/novnc 0.0.0.0:6080 127.0.0.1:5900 >/tmp/websockify.log 2>&1 & sleep 1 "#; let lf_content = start_script_content.replace("\r\n", "\n"); diff --git a/crates/flock-tools/tests/diagnose_e2b.rs b/crates/flock-tools/tests/diagnose_e2b.rs new file mode 100644 index 00000000..4ed70287 --- /dev/null +++ b/crates/flock-tools/tests/diagnose_e2b.rs @@ -0,0 +1,37 @@ +use flock_core::db::DbManager; +use flock_core::config::settings::SandboxConfig; + +#[tokio::test] +async fn test_diagnose_e2b() { + let db = DbManager::init().await.expect("Failed to init DB"); + // Get E2B config + let cfg = flock_tools::sandbox_core::config::get_sandbox_config(&db).await + .expect("No active sandbox config"); + + let api_key = cfg.e2b_api_key.as_ref().expect("E2B API key is missing"); + let base_url = cfg.e2b_api_url.as_deref().unwrap_or("https://api.e2b.app").trim_end_matches('/'); + + let client = reqwest::Client::new(); + let url = format!("{}/sandboxes", base_url); + let resp = client.get(&url) + .header("X-API-Key", api_key) + .send() + .await + .expect("Failed to get E2B sandboxes"); + + let text = resp.text().await.unwrap_or_default(); + println!("Running E2B Sandboxes: {}", text); + + let val: serde_json::Value = serde_json::from_str(&text).expect("Failed to parse JSON"); + if let Some(arr) = val.as_array() { + for sb in arr { + if let Some(sandbox_id) = sb.get("sandboxID").or_else(|| sb.get("sandbox_id")).and_then(|v| v.as_str()) { + println!("Diagnosing E2B Sandbox: {}", sandbox_id); + let diag_cmd = "echo '--- websockify.log ---'; cat /tmp/websockify.log; echo '--- x11vnc.log ---'; cat /tmp/x11vnc.log"; + if let Ok((stdout, _exit_code)) = flock_tools::sandbox_core::e2b::exec::execute_command(&cfg, sandbox_id, diag_cmd).await { + println!("Diagnostics output:\n{}", stdout); + } + } + } + } +} From aedd5cd4fb125f1ce3d54a577965b02f5551c7bd Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Wed, 1 Jul 2026 14:04:45 +0800 Subject: [PATCH 22/25] refactor: decouple daytona and e2b sandboxes and genericize VNC and sync logic --- .../src/sandbox_core/daytona/mod.rs | 4 - .../src/sandbox_core/daytona/provider.rs | 100 ++++++++++- .../src/sandbox_core/daytona/vnc.rs | 43 +---- .../src/sandbox_core/e2b/builder.rs | 2 +- .../src/sandbox_core/e2b/provider.rs | 80 ++++++++- .../src/sandbox_core/local_provider.rs | 12 ++ .../flock-tools/src/sandbox_core/manager.rs | 27 +++ crates/flock-tools/src/sandbox_core/mod.rs | 3 + .../flock-tools/src/sandbox_core/provider.rs | 5 + .../src/sandbox_core/{daytona => }/state.rs | 0 .../src/sandbox_core/{daytona => }/sync.rs | 36 +++- .../src/sandbox_core/vnc_helper.rs | 60 +++++++ .../src/tools/sandbox/browser/interactive.rs | 2 +- .../src/tools/sandbox/computer_use.rs | 2 +- .../tools/sandbox/request_human_assistance.rs | 2 +- .../src-tauri/src/commands/common/sandbox.rs | 86 ++------- flock-ui/src-tauri/src/commands/settings.rs | 167 +++--------------- 17 files changed, 350 insertions(+), 281 deletions(-) rename crates/flock-tools/src/sandbox_core/{daytona => }/state.rs (100%) rename crates/flock-tools/src/sandbox_core/{daytona => }/sync.rs (79%) create mode 100644 crates/flock-tools/src/sandbox_core/vnc_helper.rs diff --git a/crates/flock-tools/src/sandbox_core/daytona/mod.rs b/crates/flock-tools/src/sandbox_core/daytona/mod.rs index 03b21502..94faeefb 100644 --- a/crates/flock-tools/src/sandbox_core/daytona/mod.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/mod.rs @@ -1,4 +1,3 @@ -mod state; mod lifecycle; mod exec; mod vnc; @@ -8,7 +7,6 @@ pub mod volume; pub mod fs; pub mod provider; -pub use state::emit_human_takeover; pub use lifecycle::{ create_sandbox, destroy_daytona_sandbox, check_sandbox_alive, set_sandbox_public, }; @@ -19,5 +17,3 @@ pub use vnc::{ }; pub use snapshot::create_playwright_snapshot; pub use constants::*; - -pub mod sync; diff --git a/crates/flock-tools/src/sandbox_core/daytona/provider.rs b/crates/flock-tools/src/sandbox_core/daytona/provider.rs index 6ff35496..62e95c3c 100644 --- a/crates/flock-tools/src/sandbox_core/daytona/provider.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/provider.rs @@ -27,9 +27,105 @@ impl SandboxProvider for DaytonaProvider { crate::sandbox_core::daytona::ensure_vnc_running_in_sandbox(db, sandbox_id).await } async fn sync_up(&self, db: &DbManager, sandbox_id: &str, ws_path: &Path) -> anyhow::Result<()> { - crate::sandbox_core::daytona::sync::sync_up(db, sandbox_id, ws_path).await + crate::sandbox_core::sync::sync_up(db, sandbox_id, ws_path).await } async fn sync_down(&self, db: &DbManager, sandbox_id: &str, ws_path: &Path) -> anyhow::Result<()> { - crate::sandbox_core::daytona::sync::sync_down(db, sandbox_id, ws_path).await + crate::sandbox_core::sync::sync_down(db, sandbox_id, ws_path).await + } + fn get_workspace_dir(&self) -> &str { + "/workspace" + } + async fn list_templates(&self, _db: &DbManager, cfg: &SandboxConfig) -> anyhow::Result { + let base = crate::sandbox_core::config::get_api_base(cfg.api_url.as_ref().unwrap()); + let api_key = cfg.api_key.as_ref().unwrap(); + + let client = reqwest::Client::new(); + let url = format!("{}/api/snapshots", base); + let resp = client.get(&url) + .header("Authorization", format!("Bearer {}", api_key)) + .send() + .await?; + + let status = resp.status(); + let text = resp.text().await.unwrap_or_default(); + if !status.is_success() { + anyhow::bail!("Daytona API error ({}): {}", status, text); + } + + let val: serde_json::Value = serde_json::from_str(&text)?; + Ok(val) + } + async fn delete_template(&self, _db: &DbManager, cfg: &SandboxConfig, id: &str) -> anyhow::Result<()> { + let base = crate::sandbox_core::config::get_api_base(cfg.api_url.as_ref().unwrap()); + let api_key = cfg.api_key.as_ref().unwrap(); + + let client = reqwest::Client::new(); + let url = format!("{}/api/snapshots/{}", base, id); + let resp = client.delete(&url) + .header("Authorization", format!("Bearer {}", api_key)) + .send() + .await?; + + if !resp.status().is_success() { + let err_body = resp.text().await.unwrap_or_default(); + anyhow::bail!("Failed to delete Daytona snapshot: {} - {}", resp.status(), err_body); + } + Ok(()) + } + async fn cleanup_all_instances(&self, _db: &DbManager, cfg: &SandboxConfig) -> anyhow::Result { + let base = crate::sandbox_core::config::get_api_base(cfg.api_url.as_ref().unwrap()); + let api_key = cfg.api_key.as_ref().unwrap(); + + let client = reqwest::Client::new(); + let list_url = format!("{}/api/sandbox", base); + let resp = client.get(&list_url) + .header("Authorization", format!("Bearer {}", api_key)) + .send() + .await?; + + let status = resp.status(); + let text = resp.text().await.unwrap_or_default(); + if !status.is_success() { + anyhow::bail!("Failed to list Daytona sandboxes (HTTP {}): {}", status, text); + } + + let val: serde_json::Value = serde_json::from_str(&text)?; + let sandboxes = val.as_array() + .cloned() + .unwrap_or_else(|| { + val.get("items").or_else(|| val.get("data")) + .and_then(|v| v.as_array()) + .cloned() + .unwrap_or_default() + }); + + let mut deleted = 0usize; + let mut failed = 0usize; + + for sb in &sandboxes { + let id = sb.get("id").and_then(|v| v.as_str()).unwrap_or_default(); + if id.is_empty() { continue; } + + let state_str = sb.get("state").or_else(|| sb.get("status")) + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if state_str == "deleted" || state_str == "archived" { continue; } + + let del_url = format!("{}/api/sandbox/{}", base, id); + match client.delete(&del_url) + .header("Authorization", format!("Bearer {}", api_key)) + .send() + .await + { + Ok(r) if r.status().is_success() => deleted += 1, + _ => failed += 1, + } + } + + Ok(flock_core::tr( + &format!("清理完成:已销毁 {} 个沙盒,失败 {} 个。", deleted, failed), + &format!("Cleanup complete: destroyed {} sandboxes, failed {}.", deleted, failed) + )) } } diff --git a/crates/flock-tools/src/sandbox_core/daytona/vnc.rs b/crates/flock-tools/src/sandbox_core/daytona/vnc.rs index 915b6dbe..de167605 100644 --- a/crates/flock-tools/src/sandbox_core/daytona/vnc.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/vnc.rs @@ -162,48 +162,7 @@ pub async fn check_computer_use_status( /// 确保沙盒中 VNC 桌面相关进程正在后台运行,具有自愈拉起和 setsid/nohup 防进程清理机制。 pub async fn ensure_vnc_running_in_sandbox(db: &DbManager, sandbox_id: &str) -> anyhow::Result<()> { - // 检查 websockify 是否已经在运行且在指定端口监听 - let check_cmd = format!("python3 -c \"import socket; s = socket.socket(); s.connect(('127.0.0.1', {}))\"", WEBSOCKIFY_PORT); - let (_, exit_code) = execute_command_in_sandbox(db, sandbox_id, &check_cmd).await.unwrap_or(("-1".to_string(), -1)); - if exit_code == 0 { - crate::emit_info(&flock_core::tr("检测到 VNC 桌面服务已经在运行。", "Detected VNC desktop service is already running.")); - return Ok(()); - } - - crate::emit_info(&flock_core::tr("检测到 VNC 服务未运行,手动拉起 Xvfb, VNC, noVNC...", "Detected VNC service not running, manually starting Xvfb, VNC, noVNC...")); - let launch_cmd = format!("sh -c '\ - if command -v start-vnc >/dev/null 2>&1; then \ - setsid nohup start-vnc >/tmp/vnc_start.log 2>&1 & \ - sleep 2; \ - else \ - export DISPLAY={display} && \ - rm -f /tmp/.X0-lock && \ - setsid nohup Xvfb {display} -screen 0 {res} >/tmp/xvfb.log 2>&1 & \ - sleep 1 && \ - setsid nohup fluxbox >/tmp/fluxbox.log 2>&1 & \ - sleep 1 && \ - setsid nohup x11vnc -display {display} -forever -shared -nopw -rfbport {vnc_port} >/tmp/x11vnc.log 2>&1 & \ - sleep 1 && \ - setsid nohup websockify --web /usr/share/novnc 0.0.0.0:{web_port} localhost:{vnc_port} >/tmp/websockify.log 2>&1 & \ - sleep 1; \ - fi'", - display = DISPLAY_ID, - res = SCREEN_RESOLUTION, - vnc_port = X11VNC_PORT, - web_port = WEBSOCKIFY_PORT - ); - - let (out, code) = execute_command_in_sandbox(db, sandbox_id, &launch_cmd).await?; - if code != 0 { - crate::emit_info(&flock_core::tr( - &format!("手动拉起桌面服务进程失败 (退出码 {}): {}", code, out), - &format!("Failed to manually start desktop service process (exit code {}): {}", code, out) - )); - } else { - crate::emit_info(&flock_core::tr("手动拉起桌面服务进程指令已发送。", "Manual start desktop service process command sent.")); - } - - Ok(()) + crate::sandbox_core::vnc_helper::ensure_vnc_running_in_sandbox(db, sandbox_id).await } /// 获取沙盒 6080 端口 of Preview URL,并自动转化为 noVNC 控制台 URL。 diff --git a/crates/flock-tools/src/sandbox_core/e2b/builder.rs b/crates/flock-tools/src/sandbox_core/e2b/builder.rs index aeb0c813..9f66a2aa 100644 --- a/crates/flock-tools/src/sandbox_core/e2b/builder.rs +++ b/crates/flock-tools/src/sandbox_core/e2b/builder.rs @@ -44,7 +44,7 @@ setsid fluxbox >/tmp/fluxbox.log 2>&1 & sleep 1 setsid x11vnc -display :0 -forever -shared -nopw -rfbport 5900 >/tmp/x11vnc.log 2>&1 & sleep 1 -setsid websockify --ignore-origin --web /usr/share/novnc 0.0.0.0:6080 127.0.0.1:5900 >/tmp/websockify.log 2>&1 & +setsid websockify --web /usr/share/novnc 0.0.0.0:6080 127.0.0.1:5900 >/tmp/websockify.log 2>&1 & sleep 1 "#; let lf_content = start_script_content.replace("\r\n", "\n"); diff --git a/crates/flock-tools/src/sandbox_core/e2b/provider.rs b/crates/flock-tools/src/sandbox_core/e2b/provider.rs index 3bbb3df9..92935c96 100644 --- a/crates/flock-tools/src/sandbox_core/e2b/provider.rs +++ b/crates/flock-tools/src/sandbox_core/e2b/provider.rs @@ -23,13 +23,83 @@ impl SandboxProvider for E2bProvider { async fn get_vnc_url(&self, _db: &DbManager, _cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result { Ok(crate::sandbox_core::e2b::exec::get_vnc_url(sandbox_id)) } - async fn ensure_vnc_running(&self, _db: &DbManager, _cfg: &SandboxConfig, _sandbox_id: &str) -> anyhow::Result<()> { - Ok(()) + async fn ensure_vnc_running(&self, db: &DbManager, _cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()> { + crate::sandbox_core::vnc_helper::ensure_vnc_running_in_sandbox(db, sandbox_id).await } - async fn sync_up(&self, _db: &DbManager, _sandbox_id: &str, _ws_path: &Path) -> anyhow::Result<()> { - Ok(()) + async fn sync_up(&self, db: &DbManager, sandbox_id: &str, ws_path: &Path) -> anyhow::Result<()> { + crate::sandbox_core::sync::sync_up(db, sandbox_id, ws_path).await + } + async fn sync_down(&self, db: &DbManager, sandbox_id: &str, ws_path: &Path) -> anyhow::Result<()> { + crate::sandbox_core::sync::sync_down(db, sandbox_id, ws_path).await + } + fn get_workspace_dir(&self) -> &str { + "/home/user" + } + async fn list_templates(&self, _db: &DbManager, cfg: &SandboxConfig) -> anyhow::Result { + let key = cfg.e2b_api_key.clone().unwrap_or_default(); + if key.is_empty() { + return Ok(serde_json::json!([])); + } + let base_url = cfg.e2b_api_url.as_deref().unwrap_or("https://api.e2b.app").trim_end_matches('/'); + let client = reqwest::Client::new(); + let url = format!("{}/templates", base_url); + let resp = client.get(&url) + .header("X-API-Key", key) + .send() + .await?; + + let status = resp.status(); + let text = resp.text().await.unwrap_or_default(); + if !status.is_success() { + anyhow::bail!("E2B API error ({}): {}", status, text); + } + + let mut mapped: Vec = vec![]; + let list_val: serde_json::Value = serde_json::from_str(&text)?; + + if let Some(arr) = list_val.as_array() { + for item in arr { + let id = item.get("templateID") + .or_else(|| item.get("snapshotID")) + .or_else(|| item.get("id")) + .and_then(|v| v.as_str()) + .unwrap_or_default(); + let name = item.get("aliases") + .and_then(|a| a.as_array()) + .and_then(|arr| arr.first()) + .and_then(|v| v.as_str()) + .unwrap_or(id); + if !id.is_empty() && !mapped.iter().any(|m| m.get("id").and_then(|v| v.as_str()) == Some(id)) { + mapped.push(serde_json::json!({ + "id": id, + "name": name, + "status": "active" + })); + } + } + } + Ok(serde_json::json!(mapped)) } - async fn sync_down(&self, _db: &DbManager, _sandbox_id: &str, _ws_path: &Path) -> anyhow::Result<()> { + async fn delete_template(&self, _db: &DbManager, cfg: &SandboxConfig, id: &str) -> anyhow::Result<()> { + let api_key = cfg.e2b_api_key.as_ref().ok_or_else(|| anyhow::anyhow!("E2B API key missing"))?; + let base_url = cfg.e2b_api_url.as_deref().unwrap_or("https://api.e2b.app").trim_end_matches('/'); + let client = reqwest::Client::new(); + let url = format!("{}/templates/{}", base_url, id); + let resp = client.delete(&url) + .header("X-API-Key", api_key) + .send() + .await?; + + if !resp.status().is_success() { + let err_body = resp.text().await.unwrap_or_default(); + anyhow::bail!("Failed to delete E2B template: {} - {}", resp.status(), err_body); + } Ok(()) } + async fn cleanup_all_instances(&self, _db: &DbManager, _cfg: &SandboxConfig) -> anyhow::Result { + Ok(flock_core::tr( + "E2B 实例由平台自动进行生命周期托管与清理,无需手动干预。", + "E2B instances are self-cleaning and managed automatically by the platform." + )) + } } diff --git a/crates/flock-tools/src/sandbox_core/local_provider.rs b/crates/flock-tools/src/sandbox_core/local_provider.rs index 04d32594..a235e245 100644 --- a/crates/flock-tools/src/sandbox_core/local_provider.rs +++ b/crates/flock-tools/src/sandbox_core/local_provider.rs @@ -34,4 +34,16 @@ impl SandboxProvider for LocalSandboxProvider { async fn sync_down(&self, _db: &DbManager, _sandbox_id: &str, _ws_path: &Path) -> anyhow::Result<()> { Ok(()) } + fn get_workspace_dir(&self) -> &str { + "" + } + async fn list_templates(&self, _db: &DbManager, _cfg: &SandboxConfig) -> anyhow::Result { + Ok(serde_json::json!([])) + } + async fn delete_template(&self, _db: &DbManager, _cfg: &SandboxConfig, _id: &str) -> anyhow::Result<()> { + Ok(()) + } + async fn cleanup_all_instances(&self, _db: &DbManager, _cfg: &SandboxConfig) -> anyhow::Result { + Ok("Local provider does not support cleanup".to_string()) + } } diff --git a/crates/flock-tools/src/sandbox_core/manager.rs b/crates/flock-tools/src/sandbox_core/manager.rs index a46fe469..4368b5b1 100644 --- a/crates/flock-tools/src/sandbox_core/manager.rs +++ b/crates/flock-tools/src/sandbox_core/manager.rs @@ -152,3 +152,30 @@ pub async fn ensure_vnc_running_in_sandbox(db: &DbManager, sandbox_id: &str) -> provider.ensure_vnc_running(db, &cfg, sandbox_id).await } + +pub async fn list_sandbox_templates(db: &DbManager) -> anyhow::Result { + let cfg = get_sandbox_config(db).await + .ok_or_else(|| anyhow::anyhow!("沙盒未配置"))?; + + let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); + let provider = get_provider(provider_name); + provider.list_templates(db, &cfg).await +} + +pub async fn delete_sandbox_template(db: &DbManager, id: &str) -> anyhow::Result<()> { + let cfg = get_sandbox_config(db).await + .ok_or_else(|| anyhow::anyhow!("沙盒未配置"))?; + + let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); + let provider = get_provider(provider_name); + provider.delete_template(db, &cfg, id).await +} + +pub async fn cleanup_all_sandbox_instances(db: &DbManager) -> anyhow::Result { + let cfg = get_sandbox_config(db).await + .ok_or_else(|| anyhow::anyhow!("沙盒未配置"))?; + + let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); + let provider = get_provider(provider_name); + provider.cleanup_all_instances(db, &cfg).await +} diff --git a/crates/flock-tools/src/sandbox_core/mod.rs b/crates/flock-tools/src/sandbox_core/mod.rs index f4659d2e..c0d3ab35 100644 --- a/crates/flock-tools/src/sandbox_core/mod.rs +++ b/crates/flock-tools/src/sandbox_core/mod.rs @@ -4,3 +4,6 @@ pub mod e2b; pub mod manager; pub mod provider; pub mod local_provider; +pub mod state; +pub mod sync; +pub mod vnc_helper; diff --git a/crates/flock-tools/src/sandbox_core/provider.rs b/crates/flock-tools/src/sandbox_core/provider.rs index c137492f..c77c6230 100644 --- a/crates/flock-tools/src/sandbox_core/provider.rs +++ b/crates/flock-tools/src/sandbox_core/provider.rs @@ -1,6 +1,7 @@ use std::path::Path; use async_trait::async_trait; use flock_core::config::settings::SandboxConfig; +use serde_json::Value; #[async_trait] pub trait SandboxProvider: Send + Sync { @@ -12,4 +13,8 @@ pub trait SandboxProvider: Send + Sync { async fn ensure_vnc_running(&self, db: &flock_core::db::DbManager, cfg: &SandboxConfig, sandbox_id: &str) -> anyhow::Result<()>; async fn sync_up(&self, db: &flock_core::db::DbManager, sandbox_id: &str, ws_path: &Path) -> anyhow::Result<()>; async fn sync_down(&self, db: &flock_core::db::DbManager, sandbox_id: &str, ws_path: &Path) -> anyhow::Result<()>; + fn get_workspace_dir(&self) -> &str; + async fn list_templates(&self, db: &flock_core::db::DbManager, cfg: &SandboxConfig) -> anyhow::Result; + async fn delete_template(&self, db: &flock_core::db::DbManager, cfg: &SandboxConfig, id: &str) -> anyhow::Result<()>; + async fn cleanup_all_instances(&self, db: &flock_core::db::DbManager, cfg: &SandboxConfig) -> anyhow::Result; } diff --git a/crates/flock-tools/src/sandbox_core/daytona/state.rs b/crates/flock-tools/src/sandbox_core/state.rs similarity index 100% rename from crates/flock-tools/src/sandbox_core/daytona/state.rs rename to crates/flock-tools/src/sandbox_core/state.rs diff --git a/crates/flock-tools/src/sandbox_core/daytona/sync.rs b/crates/flock-tools/src/sandbox_core/sync.rs similarity index 79% rename from crates/flock-tools/src/sandbox_core/daytona/sync.rs rename to crates/flock-tools/src/sandbox_core/sync.rs index 330ef3b1..90ea5949 100644 --- a/crates/flock-tools/src/sandbox_core/daytona/sync.rs +++ b/crates/flock-tools/src/sandbox_core/sync.rs @@ -1,5 +1,5 @@ use flock_core::db::DbManager; -use crate::sandbox_core::daytona::execute_command_in_sandbox; +use crate::sandbox_core::manager::execute_command_in_sandbox; use std::path::{Path, PathBuf}; use std::fs; use anyhow::Context; @@ -10,11 +10,20 @@ fn should_ignore(path: &Path) -> bool { } pub async fn sync_up(db: &DbManager, sandbox_id: &str, local_workspace: &Path) -> anyhow::Result<()> { + let mut ws_dir = "/workspace"; if let Some(cfg) = crate::sandbox_core::config::get_sandbox_config(db).await { if cfg.provider.as_deref().unwrap_or("e2b") == "local" { return Ok(()); } + let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); + let provider = crate::sandbox_core::manager::get_provider(provider_name); + ws_dir = provider.get_workspace_dir(); } + + if ws_dir.is_empty() { + return Ok(()); + } + // Create a temporary tarball let tar_path = std::env::temp_dir().join(format!("flock_sync_up_{}.tar.gz", sandbox_id)); let tar_file = fs::File::create(&tar_path).context("Failed to create tar file")?; @@ -45,29 +54,38 @@ pub async fn sync_up(db: &DbManager, sandbox_id: &str, local_workspace: &Path) - // Upload tarball let tar_data = fs::read(&tar_path)?; let b64_content = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &tar_data); - let cmd = format!("echo '{}' | base64 -d > /workspace/.flock_sync.tar.gz", b64_content); + let cmd = format!("echo '{}' | base64 -d > {}/.flock_sync.tar.gz", b64_content, ws_dir); execute_command_in_sandbox(db, sandbox_id, &cmd).await?; // Extract tarball - let cmd = "cd /workspace && tar -xzf .flock_sync.tar.gz && rm .flock_sync.tar.gz"; - execute_command_in_sandbox(db, sandbox_id, cmd).await?; + let cmd = format!("cd {} && tar -xzf .flock_sync.tar.gz && rm .flock_sync.tar.gz", ws_dir); + execute_command_in_sandbox(db, sandbox_id, &cmd).await?; let _ = fs::remove_file(tar_path); Ok(()) } pub async fn sync_down(db: &DbManager, sandbox_id: &str, local_workspace: &Path) -> anyhow::Result<()> { + let mut ws_dir = "/workspace"; if let Some(cfg) = crate::sandbox_core::config::get_sandbox_config(db).await { if cfg.provider.as_deref().unwrap_or("e2b") == "local" { return Ok(()); } + let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); + let provider = crate::sandbox_core::manager::get_provider(provider_name); + ws_dir = provider.get_workspace_dir(); } + + if ws_dir.is_empty() { + return Ok(()); + } + // Create a tarball in sandbox - let cmd = "cd /workspace && tar -czf .flock_sync_down.tar.gz --exclude='.flock_sync_down.tar.gz' --exclude='.git' --exclude='node_modules' --exclude='target' ."; - execute_command_in_sandbox(db, sandbox_id, cmd).await?; + let cmd = format!("cd {} && tar -czf .flock_sync_down.tar.gz --exclude='.flock_sync_down.tar.gz' --exclude='.git' --exclude='node_modules' --exclude='target' .", ws_dir); + execute_command_in_sandbox(db, sandbox_id, &cmd).await?; // Download tarball - let cmd = "base64 -w 0 /workspace/.flock_sync_down.tar.gz"; + let cmd = format!("base64 -w 0 {}/.flock_sync_down.tar.gz", ws_dir); let (b64, code) = execute_command_in_sandbox(db, sandbox_id, &cmd).await?; if code != 0 { anyhow::bail!("Failed to download tarball: {}", b64); @@ -131,8 +149,8 @@ pub async fn sync_down(db: &DbManager, sandbox_id: &str, local_workspace: &Path) let _ = fs::remove_file(tar_path); // Cleanup sandbox - let cmd = "rm -f /workspace/.flock_sync_down.tar.gz"; - let _ = execute_command_in_sandbox(db, sandbox_id, cmd).await; + let cmd = format!("rm -f {}/.flock_sync_down.tar.gz", ws_dir); + let _ = execute_command_in_sandbox(db, sandbox_id, &cmd).await; Ok(()) } diff --git a/crates/flock-tools/src/sandbox_core/vnc_helper.rs b/crates/flock-tools/src/sandbox_core/vnc_helper.rs new file mode 100644 index 00000000..3625df7c --- /dev/null +++ b/crates/flock-tools/src/sandbox_core/vnc_helper.rs @@ -0,0 +1,60 @@ +use flock_core::db::DbManager; +use crate::sandbox_core::manager::execute_command_in_sandbox; + +/// VNC 桌面服务端监听端口 (默认 6080,基于 websockify 提供 Web VNC 服务) +pub const WEBSOCKIFY_PORT: u16 = 6080; + +/// 原始 VNC 服务监听端口 (默认 5900) +pub const X11VNC_PORT: u16 = 5900; + +/// 默认的 X11 Display 标识 +pub const DISPLAY_ID: &str = ":0"; + +/// 桌面分辨率 +pub const SCREEN_RESOLUTION: &str = "1280x1024x24"; + +/// 确保沙盒中 VNC 桌面相关进程正在后台运行,具有自愈拉起和 setsid/nohup 防进程清理机制。 +pub async fn ensure_vnc_running_in_sandbox(db: &DbManager, sandbox_id: &str) -> anyhow::Result<()> { + // 检查 websockify 是否已经在运行且在指定端口监听 + let check_cmd = format!("python3 -c \"import socket; s = socket.socket(); s.connect(('127.0.0.1', {}))\"", WEBSOCKIFY_PORT); + let (_, exit_code) = execute_command_in_sandbox(db, sandbox_id, &check_cmd).await.unwrap_or(("-1".to_string(), -1)); + if exit_code == 0 { + crate::emit_info(&flock_core::tr("检测到 VNC 桌面服务已经在运行。", "Detected VNC desktop service is already running.")); + return Ok(()); + } + + crate::emit_info(&flock_core::tr("检测到 VNC 服务未运行,手动拉起 Xvfb, VNC, noVNC...", "Detected VNC service not running, manually starting Xvfb, VNC, noVNC...")); + let launch_cmd = format!("sh -c '\ + if command -v start-vnc >/dev/null 2>&1; then \ + setsid nohup start-vnc >/tmp/vnc_start.log 2>&1 & \ + sleep 2; \ + else \ + export DISPLAY={display} && \ + rm -f /tmp/.X0-lock && \ + setsid nohup Xvfb {display} -screen 0 {res} >/tmp/xvfb.log 2>&1 & \ + sleep 1 && \ + setsid nohup fluxbox >/tmp/fluxbox.log 2>&1 & \ + sleep 1 && \ + setsid nohup x11vnc -display {display} -forever -shared -nopw -rfbport {vnc_port} >/tmp/x11vnc.log 2>&1 & \ + sleep 1 && \ + setsid nohup websockify --web /usr/share/novnc 0.0.0.0:{web_port} localhost:{vnc_port} >/tmp/websockify.log 2>&1 & \ + sleep 1; \ + fi'", + display = DISPLAY_ID, + res = SCREEN_RESOLUTION, + vnc_port = X11VNC_PORT, + web_port = WEBSOCKIFY_PORT + ); + + let (out, code) = execute_command_in_sandbox(db, sandbox_id, &launch_cmd).await?; + if code != 0 { + crate::emit_info(&flock_core::tr( + &format!("手动拉起桌面服务进程失败 (退出码 {}): {}", code, out), + &format!("Failed to manually start desktop service process (exit code {}): {}", code, out) + )); + } else { + crate::emit_info(&flock_core::tr("手动拉起桌面服务进程指令已发送。", "Manual start desktop service process command sent.")); + } + + Ok(()) +} diff --git a/crates/flock-tools/src/tools/sandbox/browser/interactive.rs b/crates/flock-tools/src/tools/sandbox/browser/interactive.rs index 2518ad35..e0ed66e5 100644 --- a/crates/flock-tools/src/tools/sandbox/browser/interactive.rs +++ b/crates/flock-tools/src/tools/sandbox/browser/interactive.rs @@ -105,7 +105,7 @@ pub async fn handle_interactive( &format!("检测到敏感网页元素(密码输入框/验证码),正在通知前端拉起人工接管横幅 (Call ID: {})...", cid), &format!("Sensitive page element detected (password input/captcha), notifying client to display takeover banner (Call ID: {})...", cid), )); - crate::sandbox_core::daytona::emit_human_takeover( + crate::sandbox_core::state::emit_human_takeover( &cid, &mid, "人机协同远程桌面已拉起!检测到当前操作需要人工介入(如输入密码、手动验证码、安全登录等),大模型自动执行已暂停。您可以在右侧预览面板中直接操作页面。完成后请点击横幅上的【我已完成操作】按钮以恢复大模型的自动运行。", diff --git a/crates/flock-tools/src/tools/sandbox/computer_use.rs b/crates/flock-tools/src/tools/sandbox/computer_use.rs index a7ab5659..8f19a0a1 100644 --- a/crates/flock-tools/src/tools/sandbox/computer_use.rs +++ b/crates/flock-tools/src/tools/sandbox/computer_use.rs @@ -227,7 +227,7 @@ pub async fn computer_use( &format!("检测到敏感桌面操作(验证码/人工介入),正在通知前端拉起人工接管横幅 (Call ID: {})...", cid), &format!("Sensitive desktop action detected (captcha/human verification), notifying client to display takeover banner (Call ID: {})...", cid) )); - crate::sandbox_core::daytona::emit_human_takeover( + crate::sandbox_core::state::emit_human_takeover( &cid, &mid, "人机协同远程桌面已拉起!检测到当前桌面应用需要人工介入(如滑动拼图验证、扫码登录等),大模型自动执行已暂停。您可以在右侧预览面板中直接操作页面。完成后请点击横幅上的【我已完成操作】按钮以恢复大模型的自动运行。", diff --git a/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs b/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs index adb2aefe..3d4b00f7 100644 --- a/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs +++ b/crates/flock-tools/src/tools/sandbox/request_human_assistance.rs @@ -63,7 +63,7 @@ pub async fn request_human_assistance( reason ); - crate::sandbox_core::daytona::emit_human_takeover( + crate::sandbox_core::state::emit_human_takeover( &cid, &mid, &display_message, diff --git a/flock-ui/src-tauri/src/commands/common/sandbox.rs b/flock-ui/src-tauri/src/commands/common/sandbox.rs index 320191d7..50e20862 100644 --- a/flock-ui/src-tauri/src/commands/common/sandbox.rs +++ b/flock-ui/src-tauri/src/commands/common/sandbox.rs @@ -1,7 +1,7 @@ use tauri::State; use crate::commands::assistant::SharedAgentState; -/// 手动销毁当前活跃的 Daytona 沙盒(并清除内存缓存) +/// 手动销毁当前活跃的沙盒(并清除内存缓存) #[tauri::command] pub async fn destroy_sandbox( db: State<'_, crate::SharedDbManager>, @@ -11,80 +11,20 @@ pub async fn destroy_sandbox( .map_err(|e| e.to_string()) } -/// 列出并销毁 Daytona 上所有运行中的沙盒(清理历史遗留的僵尸沙盒) +/// 列出并销毁所有运行中的沙盒(清理历史遗留的僵尸沙盒) #[tauri::command] pub async fn cleanup_all_sandboxes( db: State<'_, crate::SharedDbManager>, ) -> Result { - use flock_core::db::DbManager; - use flock_tools::sandbox_core::config::{get_sandbox_config, get_api_base}; - - let db_ref: &DbManager = &*db; - let cfg = get_sandbox_config(db_ref).await - .ok_or_else(|| flock_core::tr("沙盒未配置或未启用", "Sandbox not configured or enabled"))?; - - let base = get_api_base(cfg.api_url.as_ref().unwrap()); - let api_key = cfg.api_key.as_ref().unwrap(); - - let client = reqwest::Client::new(); - let list_url = format!("{}/api/sandbox", base); - let resp = client.get(&list_url) - .header("Authorization", format!("Bearer {}", api_key)) - .send() + let db_ref = &*db; + let res = flock_tools::sandbox_core::manager::cleanup_all_sandbox_instances(db_ref) .await - .map_err(|e| flock_core::tr( - &format!("获取沙盒列表失败: {}", e), - &format!("Failed to retrieve sandbox list: {}", e) - ))?; - - let text = resp.text().await.unwrap_or_default(); - let val: serde_json::Value = serde_json::from_str(&text) - .map_err(|e| flock_core::tr( - &format!("解析沙盒列表失败: {}", e), - &format!("Failed to parse sandbox list: {}", e) - ))?; - - let sandboxes = val.as_array() - .cloned() - .unwrap_or_else(|| { - val.get("items").or_else(|| val.get("data")) - .and_then(|v| v.as_array()) - .cloned() - .unwrap_or_default() - }); - - let mut deleted = 0usize; - let mut failed = 0usize; - - for sb in &sandboxes { - let id = sb.get("id").and_then(|v| v.as_str()).unwrap_or_default(); - if id.is_empty() { continue; } - - let state_str = sb.get("state").or_else(|| sb.get("status")) - .and_then(|v| v.as_str()) - .unwrap_or(""); - - // 只销毁 started / running / stopped 状态(跳过已删除的) - if state_str == "deleted" || state_str == "archived" { continue; } - - let del_url = format!("{}/api/sandbox/{}", base, id); - match client.delete(&del_url) - .header("Authorization", format!("Bearer {}", api_key)) - .send() - .await - { - Ok(r) if r.status().is_success() => deleted += 1, - _ => failed += 1, - } - } + .map_err(|e| e.to_string())?; // 清除本地缓存 let _ = flock_tools::sandbox_core::manager::destroy_active_sandbox(db_ref).await; - Ok(flock_core::tr( - &format!("清理完成:已销毁 {} 个沙盒,失败 {} 个。", deleted, failed), - &format!("Cleanup complete: destroyed {} sandboxes, failed {}.", deleted, failed) - )) + Ok(res) } /// 获取当前活动沙盒的 VNC 代理链接 @@ -98,10 +38,18 @@ pub async fn get_active_sandbox_vnc_url( Ok(url) => Ok(Some(url)), Err(_) => { let fallback_url = match flock_tools::sandbox_core::config::get_sandbox_config(&*db).await { - Some(cfg) if cfg.provider.as_deref().unwrap_or("e2b") == "e2b" => { - format!("https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", sandbox_id) + Some(cfg) => { + let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); + let provider = flock_tools::sandbox_core::manager::get_provider(provider_name); + provider.get_vnc_url(&*db, &cfg, &sandbox_id).await.unwrap_or_else(|_| { + if provider_name == "e2b" { + format!("https://6080-{}.e2b.app/vnc.html?autoconnect=true&resize=scale&skip-preview-warning=true&skip_preview_warning=true", sandbox_id) + } else { + format!("https://6080-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", sandbox_id) + } + }) } - _ => { + None => { format!("https://6080-{}.proxy.app.daytona.io/vnc.html?autoconnect=true&resize=scale", sandbox_id) } }; diff --git a/flock-ui/src-tauri/src/commands/settings.rs b/flock-ui/src-tauri/src/commands/settings.rs index ea7b2e77..bf0c2388 100644 --- a/flock-ui/src-tauri/src/commands/settings.rs +++ b/flock-ui/src-tauri/src/commands/settings.rs @@ -394,110 +394,26 @@ pub async fn list_sandbox_templates( ) -> Result { use flock_core::db::DbManager; - let active_provider = provider.unwrap_or_else(|| "e2b".to_string()); - if active_provider == "e2b" { - let db_ref: &DbManager = &*db; - let cfg = get_sandbox_config_regardless(db_ref).await.unwrap_or_default(); - let key = if let Some(ref k) = api_key { - if k.is_empty() || k.contains('•') { - cfg.e2b_api_key.clone().unwrap_or_default() + let db_ref: &DbManager = &*db; + let mut cfg = get_sandbox_config_regardless(db_ref).await.unwrap_or_default(); + if let Some(p) = provider { + cfg.provider = Some(p); + } + if let Some(k) = api_key { + if !k.is_empty() && !k.contains('•') { + if cfg.provider.as_deref().unwrap_or("e2b") == "e2b" { + cfg.e2b_api_key = Some(k); } else { - k.clone() - } - } else { - cfg.e2b_api_key.clone().unwrap_or_default() - }; - - log::info!("list_sandbox_templates: active_provider = E2B, key len = {}, key starts with: {}", key.len(), key.chars().take(5).collect::()); - if key.is_empty() { - return Ok(serde_json::json!([])); - } - - let base_url = cfg.e2b_api_url.as_deref().unwrap_or("https://api.e2b.app").trim_end_matches('/'); - - let client = reqwest::Client::new(); - let url = format!("{}/templates", base_url); - let resp = client.get(&url) - .header("X-API-Key", key) - .send() - .await - .map_err(|e| flock_core::tr( - &format!("请求 E2B 模板列表失败: {}", e), - &format!("Failed to request E2B template list: {}", e) - ))?; - - let status = resp.status(); - let text = resp.text().await.unwrap_or_default(); - log::info!("list_sandbox_templates: E2B templates status = {}, text = {}", status, text); - if !status.is_success() { - return Err(flock_core::tr( - &format!("E2B API 错误 ({}): {}", status, text), - &format!("E2B API error ({}): {}", status, text) - )); - } - - let mut mapped: Vec = vec![]; - - let list_val: serde_json::Value = serde_json::from_str(&text) - .map_err(|e| flock_core::tr( - &format!("解析 E2B 模板列表失败: {}", e), - &format!("Failed to parse E2B template list: {}", e) - ))?; - - if let Some(arr) = list_val.as_array() { - for item in arr { - let id = item.get("templateID") - .or_else(|| item.get("snapshotID")) - .or_else(|| item.get("id")) - .and_then(|v| v.as_str()) - .unwrap_or_default(); - let name = item.get("aliases") - .and_then(|a| a.as_array()) - .and_then(|arr| arr.first()) - .and_then(|v| v.as_str()) - .unwrap_or(id); - if !id.is_empty() && !mapped.iter().any(|m| m.get("id").and_then(|v| v.as_str()) == Some(id)) { - mapped.push(serde_json::json!({ - "id": id, - "name": name, - "status": "active" - })); - } + cfg.api_key = Some(k); } } - return Ok(serde_json::json!(mapped)); - } else if active_provider == "local" { - return Ok(serde_json::json!([])); } - let db_ref: &DbManager = &*db; - let cfg = match get_sandbox_config_regardless(db_ref).await { - Some(c) => c, - None => return Ok(serde_json::json!([])), - }; - - let base = flock_tools::sandbox_core::config::get_api_base(cfg.api_url.as_ref().unwrap()); - let api_key = cfg.api_key.as_ref().unwrap(); - - let client = reqwest::Client::new(); - let url = format!("{}/api/snapshots", base); - let resp = client.get(&url) - .header("Authorization", format!("Bearer {}", api_key)) - .send() + let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); + let provider_impl = flock_tools::sandbox_core::manager::get_provider(provider_name); + provider_impl.list_templates(db_ref, &cfg) .await - .map_err(|e| flock_core::tr( - &format!("请求快照列表失败: {}", e), - &format!("Failed to request snapshot list: {}", e) - ))?; - - let text = resp.text().await.unwrap_or_default(); - let val: serde_json::Value = serde_json::from_str(&text) - .map_err(|e| flock_core::tr( - &format!("解析快照列表失败: {}", e), - &format!("Failed to parse snapshot list: {}", e) - ))?; - - Ok(val) + .map_err(|e| e.to_string()) } /// 删除指定 Daytona/E2B 快照/模板 @@ -510,58 +426,17 @@ pub async fn delete_sandbox_template( use flock_core::db::DbManager; let db_ref: &DbManager = &*db; - let cfg = get_sandbox_config_regardless(db_ref).await + let mut cfg = get_sandbox_config_regardless(db_ref).await .ok_or_else(|| flock_core::tr("沙盒未配置", "Sandbox not configured"))?; - - let active_provider = provider.as_deref().unwrap_or(cfg.provider.as_deref().unwrap_or("e2b")); - if active_provider == "e2b" { - let api_key = cfg.e2b_api_key.as_ref().unwrap(); - let base_url = cfg.e2b_api_url.as_deref().unwrap_or("https://api.e2b.app").trim_end_matches('/'); - let client = reqwest::Client::new(); - let url = format!("{}/templates/{}", base_url, id); - let resp = client.delete(&url) - .header("X-API-Key", api_key) - .send() - .await - .map_err(|e| flock_core::tr( - &format!("发送删除 E2B 快照请求失败: {}", e), - &format!("Failed to send delete E2B snapshot request: {}", e) - ))?; - - if resp.status().is_success() || resp.status().as_u16() == 204 { - return Ok(()); - } else { - return Err(flock_core::tr( - &format!("删除 E2B 快照失败,HTTP 状态码: {}", resp.status()), - &format!("Failed to delete E2B snapshot, HTTP status code: {}", resp.status()) - )); - } - } else if active_provider == "local" { - return Ok(()); + if let Some(p) = provider { + cfg.provider = Some(p); } - let base = flock_tools::sandbox_core::config::get_api_base(cfg.api_url.as_ref().unwrap()); - let api_key = cfg.api_key.as_ref().unwrap(); - - let client = reqwest::Client::new(); - let url = format!("{}/api/snapshots/{}", base, id); - let resp = client.delete(&url) - .header("Authorization", format!("Bearer {}", api_key)) - .send() + let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); + let provider_impl = flock_tools::sandbox_core::manager::get_provider(provider_name); + provider_impl.delete_template(db_ref, &cfg, &id) .await - .map_err(|e| flock_core::tr( - &format!("发送删除快照请求失败: {}", e), - &format!("Failed to send delete snapshot request: {}", e) - ))?; - - if resp.status().is_success() { - Ok(()) - } else { - Err(flock_core::tr( - &format!("删除快照失败,HTTP 状态码: {}", resp.status()), - &format!("Failed to delete snapshot, HTTP status code: {}", resp.status()) - )) - } + .map_err(|e| e.to_string()) } #[tauri::command] From 1e2208bd61294e470c5dac3e3d63e9efed430d00 Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Wed, 1 Jul 2026 14:29:09 +0800 Subject: [PATCH 23/25] fix: resolve compilation errors from sandbox decoupling refactor --- crates/flock-tools/src/sandbox_core/daytona/lifecycle.rs | 3 +-- crates/flock-tools/src/sandbox_core/daytona/provider.rs | 5 +++-- crates/flock-tools/src/sandbox_core/e2b/provider.rs | 5 +++-- crates/flock-tools/src/sandbox_core/sync.rs | 8 ++++---- crates/flock-tools/src/tools/sandbox/code_execution.rs | 2 +- crates/flock-tools/src/tools/sandbox/sandbox_exec.rs | 2 +- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/crates/flock-tools/src/sandbox_core/daytona/lifecycle.rs b/crates/flock-tools/src/sandbox_core/daytona/lifecycle.rs index 05c88b2e..8d2ef980 100644 --- a/crates/flock-tools/src/sandbox_core/daytona/lifecycle.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/lifecycle.rs @@ -186,9 +186,8 @@ pub async fn create_sandbox(db: &DbManager, cfg: &SandboxConfig) -> anyhow::Resu } } - // Sync up local workspace if let Some(ws_path) = crate::get_workspace_dir() { - if let Err(e) = crate::sandbox_core::daytona::sync::sync_up(db, &sandbox_id, &ws_path).await { + if let Err(e) = crate::sandbox_core::sync::sync_up(db, &sandbox_id, &ws_path).await { crate::emit_info(&format!("Sync Up failed: {}", e)); } } diff --git a/crates/flock-tools/src/sandbox_core/daytona/provider.rs b/crates/flock-tools/src/sandbox_core/daytona/provider.rs index 62e95c3c..985266f0 100644 --- a/crates/flock-tools/src/sandbox_core/daytona/provider.rs +++ b/crates/flock-tools/src/sandbox_core/daytona/provider.rs @@ -66,9 +66,10 @@ impl SandboxProvider for DaytonaProvider { .send() .await?; - if !resp.status().is_success() { + let status = resp.status(); + if !status.is_success() { let err_body = resp.text().await.unwrap_or_default(); - anyhow::bail!("Failed to delete Daytona snapshot: {} - {}", resp.status(), err_body); + anyhow::bail!("Failed to delete Daytona snapshot: {} - {}", status, err_body); } Ok(()) } diff --git a/crates/flock-tools/src/sandbox_core/e2b/provider.rs b/crates/flock-tools/src/sandbox_core/e2b/provider.rs index 92935c96..1e584fb3 100644 --- a/crates/flock-tools/src/sandbox_core/e2b/provider.rs +++ b/crates/flock-tools/src/sandbox_core/e2b/provider.rs @@ -90,9 +90,10 @@ impl SandboxProvider for E2bProvider { .send() .await?; - if !resp.status().is_success() { + let status = resp.status(); + if !status.is_success() { let err_body = resp.text().await.unwrap_or_default(); - anyhow::bail!("Failed to delete E2B template: {} - {}", resp.status(), err_body); + anyhow::bail!("Failed to delete E2B template: {} - {}", status, err_body); } Ok(()) } diff --git a/crates/flock-tools/src/sandbox_core/sync.rs b/crates/flock-tools/src/sandbox_core/sync.rs index 90ea5949..2720185b 100644 --- a/crates/flock-tools/src/sandbox_core/sync.rs +++ b/crates/flock-tools/src/sandbox_core/sync.rs @@ -10,14 +10,14 @@ fn should_ignore(path: &Path) -> bool { } pub async fn sync_up(db: &DbManager, sandbox_id: &str, local_workspace: &Path) -> anyhow::Result<()> { - let mut ws_dir = "/workspace"; + let mut ws_dir = "/workspace".to_string(); if let Some(cfg) = crate::sandbox_core::config::get_sandbox_config(db).await { if cfg.provider.as_deref().unwrap_or("e2b") == "local" { return Ok(()); } let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); let provider = crate::sandbox_core::manager::get_provider(provider_name); - ws_dir = provider.get_workspace_dir(); + ws_dir = provider.get_workspace_dir().to_string(); } if ws_dir.is_empty() { @@ -66,14 +66,14 @@ pub async fn sync_up(db: &DbManager, sandbox_id: &str, local_workspace: &Path) - } pub async fn sync_down(db: &DbManager, sandbox_id: &str, local_workspace: &Path) -> anyhow::Result<()> { - let mut ws_dir = "/workspace"; + let mut ws_dir = "/workspace".to_string(); if let Some(cfg) = crate::sandbox_core::config::get_sandbox_config(db).await { if cfg.provider.as_deref().unwrap_or("e2b") == "local" { return Ok(()); } let provider_name = cfg.provider.as_deref().unwrap_or("e2b"); let provider = crate::sandbox_core::manager::get_provider(provider_name); - ws_dir = provider.get_workspace_dir(); + ws_dir = provider.get_workspace_dir().to_string(); } if ws_dir.is_empty() { diff --git a/crates/flock-tools/src/tools/sandbox/code_execution.rs b/crates/flock-tools/src/tools/sandbox/code_execution.rs index 162e47ad..5c24347b 100644 --- a/crates/flock-tools/src/tools/sandbox/code_execution.rs +++ b/crates/flock-tools/src/tools/sandbox/code_execution.rs @@ -45,7 +45,7 @@ pub async fn code_execution(code: String) -> Result { let db_clone = db.clone(); let sandbox_id_clone = sandbox_id.clone(); tokio::spawn(async move { - if let Err(e) = crate::sandbox_core::daytona::sync::sync_down(&db_clone, &sandbox_id_clone, &ws_path).await { + if let Err(e) = crate::sandbox_core::sync::sync_down(&db_clone, &sandbox_id_clone, &ws_path).await { log::warn!("自动 Sync Down 失败: {}", e); } }); diff --git a/crates/flock-tools/src/tools/sandbox/sandbox_exec.rs b/crates/flock-tools/src/tools/sandbox/sandbox_exec.rs index 5685fc7e..9fb4ed60 100644 --- a/crates/flock-tools/src/tools/sandbox/sandbox_exec.rs +++ b/crates/flock-tools/src/tools/sandbox/sandbox_exec.rs @@ -65,7 +65,7 @@ pub async fn sandbox_exec( let db_clone = db.clone(); let sandbox_id_clone = sandbox_id.clone(); tokio::spawn(async move { - if let Err(e) = crate::sandbox_core::daytona::sync::sync_down(&db_clone, &sandbox_id_clone, &ws_path).await { + if let Err(e) = crate::sandbox_core::sync::sync_down(&db_clone, &sandbox_id_clone, &ws_path).await { log::warn!("自动 Sync Down 失败: {}", e); } }); From 8d901b50f44dedf2f2de17f8bc74d789a3898854 Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Wed, 1 Jul 2026 17:41:41 +0800 Subject: [PATCH 24/25] chore:bump langgraph-rust version to 0.2.5 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e6828553..97b01a59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2562,9 +2562,9 @@ dependencies = [ [[package]] name = "langgraph" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b925fdc6a07842f54e4426e33185e8f7d16c80574df819ceb4d6f8bee8111615" +checksum = "1ad5fb1669d00eccb11533ac6f89ba17d1f9e256a2f4f49fe016d7367e281e33" dependencies = [ "async-trait", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index 4aa79908..5684b9fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ flock-agent = { path = "crates/flock-agent" } flock-workflow = { path = "crates/flock-workflow" } # LangGraph -langgraph = { version = "0.2.4", features = ["prebuilt", "providers", "sqlite"] } +langgraph = { version = "0.2.5", features = ["prebuilt", "providers", "sqlite"] } tokio = { version = "1", features = ["full"] } tokio-stream = "0.1" From 6cc9a842501f8edace7bb12841e6a08c36ffa772 Mon Sep 17 00:00:00 2001 From: Onelevenvy Date: Wed, 1 Jul 2026 17:50:00 +0800 Subject: [PATCH 25/25] chore:bump langgraph-rust version to 0.2.5 --- Cargo.lock | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97b01a59..c50fc8dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -909,7 +909,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1117,7 +1117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2409,7 +2409,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2583,9 +2583,9 @@ dependencies = [ [[package]] name = "langgraph-checkpoint-rs" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c5f0b99933e702eaf866532674ded639c48c74f827fa4fe13f36f62aac69af" +checksum = "d4c17b5fd19b488b2e2cf8910a63673fdf31f0e7efee1c295918b4d361c2f8a6" dependencies = [ "async-trait", "chrono", @@ -2602,9 +2602,9 @@ dependencies = [ [[package]] name = "langgraph-checkpoint-sqlite-rs" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f42b44e047d9a154f3468388f26bc8e24a82fee5754b6b3c95f6f61ac58aceb7" +checksum = "496be498690be3a0e500d28236c44a0c2d113f540b39ce47d75dfea48f7424ac" dependencies = [ "async-trait", "langgraph-checkpoint-rs", @@ -2618,9 +2618,9 @@ dependencies = [ [[package]] name = "langgraph-core-rs" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b093199fc4fcbfe719a8e9520904a7605c612873a11bbef142acd6da816bf67e" +checksum = "b7896b19fc4eb63a649450538220c40a01b61cfad2c858f5e7f480853774847d" dependencies = [ "async-trait", "chrono", @@ -2637,9 +2637,9 @@ dependencies = [ [[package]] name = "langgraph-derive" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "905351183a865abd688e2eb2082718d85d9c68ef6ddc6072470e686d11c34932" +checksum = "addb7aef2048aa86994b28a1e4f21577377ba4057abb47192de1e413faa42d19" dependencies = [ "proc-macro2", "quote", @@ -2648,9 +2648,9 @@ dependencies = [ [[package]] name = "langgraph-prebuilt" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b81cc1770264db55f009ff284f5184822c40da1bd608e0dba29ef3628d11df" +checksum = "d56329bf94ca66dae598f0076b609663131505800cd0bf5e4c8a106b57237190" dependencies = [ "async-stream", "async-trait", @@ -2666,9 +2666,9 @@ dependencies = [ [[package]] name = "langgraph-providers" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "717f701cb7e3c13d1f0daa1d1729d7cd555760ca059874910c0a28d63130e082" +checksum = "64a09678ab6b4abc5bc4cadc299a8fcc0cc2436f458b7452c700bd61cbe44c99" dependencies = [ "async-stream", "async-trait", @@ -3409,7 +3409,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.45.0", ] [[package]] @@ -3827,7 +3827,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -4289,7 +4289,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5623,7 +5623,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6574,7 +6574,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]]