From 49e0d257c4a51398641297f2f01029c4f3bbe149 Mon Sep 17 00:00:00 2001 From: lishuo121 Date: Tue, 3 Mar 2026 12:54:17 +0800 Subject: [PATCH] fix: update kernel, runtime, and skills --- crates/openfang-kernel/src/kernel.rs | 11 +- crates/openfang-runtime/src/tool_runner.rs | 153 +++++++++++++++++++++ crates/openfang-skills/src/clawhub.rs | 115 ++++++++++++++-- 3 files changed, 257 insertions(+), 22 deletions(-) diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 3d3e34f95..cdb9a36d4 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -22,6 +22,7 @@ use openfang_runtime::llm_driver::{CompletionRequest, DriverConfig, LlmDriver, S use openfang_runtime::python_runtime::{self, PythonConfig}; use openfang_runtime::routing::ModelRouter; use openfang_runtime::sandbox::{SandboxConfig, WasmSandbox}; +use openfang_runtime::str_utils::safe_truncate_str; use openfang_runtime::tool_runner::builtin_tool_definitions; use openfang_types::agent::*; use openfang_types::capability::Capability; @@ -414,11 +415,7 @@ fn append_daily_memory_log(workspace: &Path, response: &str) { } } // Truncate long responses for the log - let summary = if trimmed.len() > 500 { - &trimmed[..500] - } else { - trimmed - }; + let summary = safe_truncate_str(trimmed, 500); let timestamp = chrono::Utc::now().format("%H:%M:%S").to_string(); if let Ok(mut f) = std::fs::OpenOptions::new() .create(true) @@ -450,7 +447,7 @@ fn read_identity_file(workspace: &Path, filename: &str) -> Option { return None; } if content.len() > MAX_IDENTITY_FILE_BYTES { - Some(content[..MAX_IDENTITY_FILE_BYTES].to_string()) + Some(safe_truncate_str(&content, MAX_IDENTITY_FILE_BYTES).to_string()) } else { Some(content) } @@ -2196,7 +2193,7 @@ impl OpenFangKernel { .take(5) .enumerate() .map(|(i, t)| { - let truncated = if t.len() > 200 { &t[..200] } else { t }; + let truncated = safe_truncate_str(t, 200); format!("{}. {}", i + 1, truncated) }) .collect::>() diff --git a/crates/openfang-runtime/src/tool_runner.rs b/crates/openfang-runtime/src/tool_runner.rs index 50b521e16..44cd2f8e7 100644 --- a/crates/openfang-runtime/src/tool_runner.rs +++ b/crates/openfang-runtime/src/tool_runner.rs @@ -287,6 +287,7 @@ pub async fn execute_tool( // Location tool "location_get" => tool_location_get().await, + "system_time" => tool_system_time(input).await, // Cron scheduling tools "cron_create" => tool_cron_create(input, kernel, caller_agent_id).await, @@ -768,6 +769,17 @@ pub fn builtin_tool_definitions() -> Vec { "properties": {} }), }, + ToolDefinition { + name: "system_time".to_string(), + description: "Get current system time with timezone and Unix timestamp. Supports timezone='local' or 'utc', and format='rfc3339' or 'iso'.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "timezone": { "type": "string", "description": "Timezone mode: 'local' (default) or 'utc'" }, + "format": { "type": "string", "description": "Output time format: 'rfc3339' (default) or 'iso'" } + } + }), + }, // --- Browser automation tools --- ToolDefinition { name: "browser_navigate".to_string(), @@ -2433,6 +2445,68 @@ async fn tool_location_get() -> Result { serde_json::to_string_pretty(&result).map_err(|e| format!("Serialize error: {e}")) } +async fn tool_system_time(input: &serde_json::Value) -> Result { + let timezone = input["timezone"].as_str().unwrap_or("local").to_lowercase(); + let format = input["format"].as_str().unwrap_or("rfc3339").to_lowercase(); + + if format != "rfc3339" && format != "iso" { + return Err(format!( + "Unsupported format '{}'. Use 'rfc3339' or 'iso'.", + format + )); + } + + fn format_dt(dt: chrono::DateTime, fmt: &str) -> String + where + Tz::Offset: std::fmt::Display, + { + match fmt { + "iso" => dt.format("%Y-%m-%dT%H:%M:%S%.3f%:z").to_string(), + _ => dt.to_rfc3339(), + } + } + + let (local_time, utc_time, offset, timezone_label, unix_seconds) = match timezone.as_str() { + "local" => { + let local_now = chrono::Local::now(); + let utc_now = local_now.with_timezone(&chrono::Utc); + ( + format_dt(local_now, &format), + format_dt(utc_now, &format), + local_now.offset().to_string(), + "local".to_string(), + local_now.timestamp(), + ) + } + "utc" => { + let utc_now = chrono::Utc::now(); + ( + format_dt(utc_now, &format), + format_dt(utc_now, &format), + "+00:00".to_string(), + "utc".to_string(), + utc_now.timestamp(), + ) + } + other => { + return Err(format!( + "Unsupported timezone '{}'. Use 'local' or 'utc'.", + other + )); + } + }; + + let result = serde_json::json!({ + "local_time": local_time, + "utc_time": utc_time, + "timezone": timezone_label, + "offset": offset, + "unix_seconds": unix_seconds, + }); + + serde_json::to_string_pretty(&result).map_err(|e| format!("Serialize error: {e}")) +} + // --------------------------------------------------------------------------- // Media understanding tools // --------------------------------------------------------------------------- @@ -3225,6 +3299,85 @@ mod tests { assert!(result.content.contains("Unknown tool")); } + #[tokio::test] + async fn test_system_time_default() { + let result = execute_tool( + "test-id", + "system_time", + &serde_json::json!({}), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, // media_engine + None, // exec_policy + None, // tts_engine + None, // docker_config + None, // process_manager + ) + .await; + assert!(!result.is_error); + assert!(result.content.contains("unix_seconds")); + assert!(result.content.contains("local_time")); + } + + #[tokio::test] + async fn test_system_time_utc_iso() { + let result = execute_tool( + "test-id", + "system_time", + &serde_json::json!({"timezone":"utc","format":"iso"}), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, // media_engine + None, // exec_policy + None, // tts_engine + None, // docker_config + None, // process_manager + ) + .await; + assert!(!result.is_error); + assert!(result.content.contains("\"timezone\": \"utc\"")); + } + + #[tokio::test] + async fn test_system_time_invalid_timezone() { + let result = execute_tool( + "test-id", + "system_time", + &serde_json::json!({"timezone":"Asia/Shanghai"}), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, // media_engine + None, // exec_policy + None, // tts_engine + None, // docker_config + None, // process_manager + ) + .await; + assert!(result.is_error); + assert!(result.content.contains("Unsupported timezone")); + } + #[tokio::test] async fn test_agent_tools_without_kernel() { let result = execute_tool( diff --git a/crates/openfang-skills/src/clawhub.rs b/crates/openfang-skills/src/clawhub.rs index d7497bdc6..922ad7c36 100644 --- a/crates/openfang-skills/src/clawhub.rs +++ b/crates/openfang-skills/src/clawhub.rs @@ -17,7 +17,8 @@ use crate::SkillError; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::path::{Path, PathBuf}; -use tracing::info; +use std::time::Duration; +use tracing::{info, warn}; // --------------------------------------------------------------------------- // API response types (matching actual ClawHub v1 API — verified Feb 2026) @@ -427,20 +428,66 @@ impl ClawHubClient { info!(slug, "Downloading skill from ClawHub"); - let response = self - .client - .get(&url) - .header("User-Agent", "OpenFang/0.1") - .send() - .await - .map_err(|e| SkillError::Network(format!("ClawHub download failed: {e}")))?; - - if !response.status().is_success() { - return Err(SkillError::Network(format!( - "ClawHub download returned {}", - response.status() - ))); - } + const MAX_ATTEMPTS: usize = 4; + let response = { + let mut attempt = 0usize; + loop { + attempt += 1; + match self + .client + .get(&url) + .header("User-Agent", "OpenFang/0.1") + .send() + .await + { + Ok(resp) if resp.status().is_success() => break resp, + Ok(resp) => { + let status = resp.status(); + let retryable = status == reqwest::StatusCode::TOO_MANY_REQUESTS + || status.is_server_error(); + if retryable && attempt < MAX_ATTEMPTS { + let delay = retry_delay(&resp, attempt); + warn!( + slug, + %status, + attempt, + max_attempts = MAX_ATTEMPTS, + wait_secs = delay.as_secs(), + "ClawHub download throttled/server error; retrying" + ); + tokio::time::sleep(delay).await; + continue; + } + if retryable { + return Err(SkillError::Network(format!( + "ClawHub download returned {status} after {attempt} attempt(s)" + ))); + } + return Err(SkillError::Network(format!( + "ClawHub download returned {status}" + ))); + } + Err(e) => { + if attempt < MAX_ATTEMPTS { + let delay = exponential_backoff(attempt); + warn!( + slug, + attempt, + max_attempts = MAX_ATTEMPTS, + wait_secs = delay.as_secs(), + error = %e, + "ClawHub download request failed; retrying" + ); + tokio::time::sleep(delay).await; + continue; + } + return Err(SkillError::Network(format!( + "ClawHub download failed after {attempt} attempt(s): {e}" + ))); + } + } + } + }; let bytes = response .bytes() @@ -599,9 +646,28 @@ fn which_check(name: &str) -> Option { } } +fn retry_delay(response: &reqwest::Response, attempt: usize) -> Duration { + if let Some(retry_after) = response + .headers() + .get(reqwest::header::RETRY_AFTER) + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.trim().parse::().ok()) + { + return Duration::from_secs(retry_after.min(60)); + } + exponential_backoff(attempt) +} + +fn exponential_backoff(attempt: usize) -> Duration { + // attempt starts at 1 + let secs = 1u64 << attempt.saturating_sub(1).min(5); + Duration::from_secs(secs) +} + #[cfg(test)] mod tests { use super::*; + use reqwest::header::{HeaderMap, HeaderValue, RETRY_AFTER}; #[test] fn test_browse_entry_serde_real_format() { @@ -799,4 +865,23 @@ mod tests { }; assert_eq!(ClawHubClient::entry_version(&entry), "2.0.0"); } + + #[test] + fn test_exponential_backoff_caps() { + assert_eq!(exponential_backoff(1).as_secs(), 1); + assert_eq!(exponential_backoff(2).as_secs(), 2); + assert_eq!(exponential_backoff(3).as_secs(), 4); + assert_eq!(exponential_backoff(10).as_secs(), 32); + } + + #[test] + fn test_retry_after_parse_seconds() { + let mut headers = HeaderMap::new(); + headers.insert(RETRY_AFTER, HeaderValue::from_static("7")); + let parsed = headers + .get(RETRY_AFTER) + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.trim().parse::().ok()); + assert_eq!(parsed, Some(7)); + } }