mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 06:32:17 +00:00
batch fixes
This commit is contained in:
Generated
+14
-14
@@ -3875,7 +3875,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-api"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3912,7 +3912,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-channels"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3944,7 +3944,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-cli"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
@@ -3971,7 +3971,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-desktop"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"open",
|
||||
@@ -3997,7 +3997,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-extensions"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
@@ -4025,7 +4025,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-hands"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dashmap",
|
||||
@@ -4042,7 +4042,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-kernel"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4078,7 +4078,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-memory"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4097,7 +4097,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-migrate"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 6.0.0",
|
||||
@@ -4116,7 +4116,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-runtime"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4148,7 +4148,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-skills"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
@@ -4171,7 +4171,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-types"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4190,7 +4190,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-wire"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -8818,7 +8818,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
|
||||
@@ -6329,10 +6329,19 @@ pub async fn set_model(
|
||||
}
|
||||
};
|
||||
match state.kernel.set_agent_model(agent_id, model) {
|
||||
Ok(()) => (
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({"status": "ok", "model": model})),
|
||||
),
|
||||
Ok(()) => {
|
||||
// Return the resolved provider so frontend can update its state
|
||||
let provider = state
|
||||
.kernel
|
||||
.registry
|
||||
.get(agent_id)
|
||||
.map(|e| e.manifest.model.provider.clone())
|
||||
.unwrap_or_default();
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({"status": "ok", "model": model, "provider": provider})),
|
||||
)
|
||||
}
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"error": format!("{e}")})),
|
||||
|
||||
@@ -420,9 +420,10 @@ function chatPage() {
|
||||
case '/model':
|
||||
if (self.currentAgent) {
|
||||
if (cmdArgs) {
|
||||
OpenFangAPI.put('/api/agents/' + self.currentAgent.id + '/model', { model: cmdArgs }).then(function() {
|
||||
OpenFangAPI.put('/api/agents/' + self.currentAgent.id + '/model', { model: cmdArgs }).then(function(resp) {
|
||||
self.currentAgent.model_name = cmdArgs;
|
||||
self.messages.push({ id: ++msgId, role: 'system', text: 'Model switched to: `' + cmdArgs + '`', meta: '', tools: [] });
|
||||
if (resp && resp.provider) { self.currentAgent.model_provider = resp.provider; }
|
||||
self.messages.push({ id: ++msgId, role: 'system', text: 'Model switched to: `' + cmdArgs + '`' + (resp && resp.provider ? ' (provider: `' + resp.provider + '`)' : ''), meta: '', tools: [] });
|
||||
self.scrollToBottom();
|
||||
}).catch(function(e) { OpenFangToast.error('Model switch failed: ' + e.message); });
|
||||
} else {
|
||||
|
||||
@@ -20,6 +20,21 @@ use tokio::sync::{mpsc, watch};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
/// SASL PLAIN authenticator for IMAP servers that reject LOGIN
|
||||
/// (e.g., Lark/Larksuite which only advertise AUTH=PLAIN).
|
||||
struct PlainAuthenticator {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl imap::Authenticator for PlainAuthenticator {
|
||||
type Response = String;
|
||||
fn process(&self, _data: &[u8]) -> Self::Response {
|
||||
// SASL PLAIN: \0<username>\0<password>
|
||||
format!("\x00{}\x00{}", self.username, self.password)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reply context for email threading (In-Reply-To / Subject continuity).
|
||||
#[derive(Debug, Clone)]
|
||||
struct ReplyCtx {
|
||||
@@ -203,9 +218,22 @@ fn fetch_unseen_emails(
|
||||
let client = imap::connect((host, port), host, &tls)
|
||||
.map_err(|e| format!("IMAP connect failed: {e}"))?;
|
||||
|
||||
let mut session = client
|
||||
.login(username, password)
|
||||
.map_err(|(e, _)| format!("IMAP login failed: {e}"))?;
|
||||
// Try LOGIN first; fall back to AUTHENTICATE PLAIN for servers like Lark
|
||||
// that reject LOGIN and only support AUTH=PLAIN (SASL).
|
||||
let mut session = match client.login(username, password) {
|
||||
Ok(s) => s,
|
||||
Err((login_err, client)) => {
|
||||
let authenticator = PlainAuthenticator {
|
||||
username: username.to_string(),
|
||||
password: password.to_string(),
|
||||
};
|
||||
client
|
||||
.authenticate("PLAIN", &authenticator)
|
||||
.map_err(|(e, _)| {
|
||||
format!("IMAP login failed: {login_err}; AUTH=PLAIN also failed: {e}")
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
|
||||
@@ -1287,9 +1287,10 @@ fn launch_desktop_app(_openfang_dir: &std::path::Path) {
|
||||
ui::blank();
|
||||
if let Some(base) = find_daemon() {
|
||||
let url = format!("{base}/");
|
||||
if !open_in_browser(&url) {
|
||||
ui::hint(&format!("Visit: {url}"));
|
||||
}
|
||||
let _ = open_in_browser(&url);
|
||||
// Always print the URL — browser launch may silently fail
|
||||
// (e.g., Chromium sandbox EPERM in containers)
|
||||
ui::hint(&format!("Dashboard: {url}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1337,7 +1338,7 @@ fn provider_list() -> Vec<(&'static str, &'static str, &'static str, &'static st
|
||||
(
|
||||
"openrouter",
|
||||
"OPENROUTER_API_KEY",
|
||||
"openrouter/auto",
|
||||
"openrouter/anthropic/claude-sonnet-4",
|
||||
"OpenRouter",
|
||||
),
|
||||
]
|
||||
@@ -2632,7 +2633,7 @@ decay_rate = 0.05
|
||||
checks.push(serde_json::json!({"check": "daemon_uptime", "status": "ok", "secs": uptime}));
|
||||
}
|
||||
if let Some(db_status) = body.get("database").and_then(|v| v.as_str()) {
|
||||
if db_status == "ok" {
|
||||
if db_status == "connected" || db_status == "ok" {
|
||||
if !json {
|
||||
ui::check_ok("Database connectivity: OK");
|
||||
}
|
||||
@@ -2954,8 +2955,14 @@ pub(crate) fn open_in_browser(url: &str) -> bool {
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Detach from parent to avoid inheriting sandbox restrictions.
|
||||
// Some Chromium-based browsers fail with EPERM when launched from
|
||||
// restricted environments (containers, snaps, flatpaks).
|
||||
std::process::Command::new("xdg-open")
|
||||
.arg(url)
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ const PROVIDERS: &[ProviderInfo] = &[
|
||||
name: "openrouter",
|
||||
display: "OpenRouter",
|
||||
env_var: "OPENROUTER_API_KEY",
|
||||
default_model: "openrouter/auto",
|
||||
default_model: "openrouter/anthropic/claude-sonnet-4",
|
||||
needs_key: true,
|
||||
hint: "",
|
||||
},
|
||||
|
||||
@@ -29,6 +29,8 @@ pub enum HandError {
|
||||
TomlParse(String),
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Config error: {0}")]
|
||||
Config(String),
|
||||
}
|
||||
|
||||
pub type HandResult<T> = Result<T, HandError>;
|
||||
|
||||
@@ -52,6 +52,51 @@ impl HandRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist active hand state to disk so it survives restarts.
|
||||
pub fn persist_state(&self, path: &std::path::Path) -> HandResult<()> {
|
||||
let entries: Vec<serde_json::Value> = self
|
||||
.instances
|
||||
.iter()
|
||||
.filter(|e| e.status == HandStatus::Active)
|
||||
.map(|e| {
|
||||
serde_json::json!({
|
||||
"hand_id": e.hand_id,
|
||||
"config": e.config,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let json = serde_json::to_string_pretty(&entries)
|
||||
.map_err(|e| HandError::Config(format!("serialize hand state: {e}")))?;
|
||||
std::fs::write(path, json)
|
||||
.map_err(|e| HandError::Config(format!("write hand state: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load persisted hand state and re-activate hands.
|
||||
/// Returns list of (hand_id, config) that should be activated.
|
||||
pub fn load_state(path: &std::path::Path) -> Vec<(String, HashMap<String, serde_json::Value>)> {
|
||||
let data = match std::fs::read_to_string(path) {
|
||||
Ok(d) => d,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
let entries: Vec<serde_json::Value> = match serde_json::from_str(&data) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
warn!("Failed to parse hand state file: {e}");
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
entries
|
||||
.into_iter()
|
||||
.filter_map(|e| {
|
||||
let hand_id = e["hand_id"].as_str()?.to_string();
|
||||
let config: HashMap<String, serde_json::Value> =
|
||||
serde_json::from_value(e["config"].clone()).unwrap_or_default();
|
||||
Some((hand_id, config))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Load all bundled hand definitions. Returns count of definitions loaded.
|
||||
pub fn load_bundled(&self) -> usize {
|
||||
let bundled = bundled::bundled_hands();
|
||||
|
||||
@@ -3036,6 +3036,9 @@ impl OpenFangKernel {
|
||||
"Hand activated with agent"
|
||||
);
|
||||
|
||||
// Persist hand state so it survives restarts
|
||||
self.persist_hand_state();
|
||||
|
||||
// Return instance with agent set
|
||||
Ok(self
|
||||
.hand_registry
|
||||
@@ -3067,9 +3070,19 @@ impl OpenFangKernel {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Persist hand state so it survives restarts
|
||||
self.persist_hand_state();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Persist active hand state to disk.
|
||||
fn persist_hand_state(&self) {
|
||||
let state_path = self.config.home_dir.join("hand_state.json");
|
||||
if let Err(e) = self.hand_registry.persist_state(&state_path) {
|
||||
warn!(error = %e, "Failed to persist hand state");
|
||||
}
|
||||
}
|
||||
|
||||
/// Pause a hand (marks it paused; agent stays alive but won't receive new work).
|
||||
pub fn pause_hand(&self, instance_id: uuid::Uuid) -> KernelResult<()> {
|
||||
self.hand_registry
|
||||
@@ -3346,6 +3359,19 @@ impl OpenFangKernel {
|
||||
/// Iterates the agent registry and starts background tasks for agents with
|
||||
/// `Continuous`, `Periodic`, or `Proactive` schedules.
|
||||
pub fn start_background_agents(self: &Arc<Self>) {
|
||||
// Restore previously active hands from persisted state
|
||||
let state_path = self.config.home_dir.join("hand_state.json");
|
||||
let saved_hands = openfang_hands::registry::HandRegistry::load_state(&state_path);
|
||||
if !saved_hands.is_empty() {
|
||||
info!("Restoring {} persisted hand(s)", saved_hands.len());
|
||||
for (hand_id, config) in saved_hands {
|
||||
match self.activate_hand(&hand_id, config) {
|
||||
Ok(inst) => info!(hand = %hand_id, instance = %inst.instance_id, "Hand restored"),
|
||||
Err(e) => warn!(hand = %hand_id, error = %e, "Failed to restore hand"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let agents = self.registry.list();
|
||||
let mut bg_agents: Vec<(openfang_types::agent::AgentId, String, ScheduleMode)> =
|
||||
Vec::new();
|
||||
|
||||
@@ -92,6 +92,9 @@ struct GeminiInlineData {
|
||||
struct GeminiFunctionCallData {
|
||||
name: String,
|
||||
args: serde_json::Value,
|
||||
/// Gemini 2.5+ thinking models return this on functionCall parts.
|
||||
#[serde(rename = "thoughtSignature", default, skip_serializing_if = "Option::is_none")]
|
||||
thought_signature: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
@@ -202,6 +205,7 @@ fn convert_messages(
|
||||
function_call: GeminiFunctionCallData {
|
||||
name: name.clone(),
|
||||
args: input.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ fn provider_defaults(provider: &str) -> Option<ProviderDefaults> {
|
||||
api_key_env: "MOONSHOT_API_KEY",
|
||||
key_required: true,
|
||||
}),
|
||||
"qwen" | "dashscope" => Some(ProviderDefaults {
|
||||
"qwen" | "dashscope" | "model_studio" => Some(ProviderDefaults {
|
||||
base_url: QWEN_BASE_URL,
|
||||
api_key_env: "DASHSCOPE_API_KEY",
|
||||
key_required: true,
|
||||
@@ -374,7 +374,7 @@ pub fn detect_available_provider() -> Option<(&'static str, &'static str, &'stat
|
||||
("gemini", "gemini-2.5-flash", "GEMINI_API_KEY"),
|
||||
("groq", "llama-3.3-70b-versatile", "GROQ_API_KEY"),
|
||||
("deepseek", "deepseek-chat", "DEEPSEEK_API_KEY"),
|
||||
("openrouter", "openrouter/auto", "OPENROUTER_API_KEY"),
|
||||
("openrouter", "openrouter/anthropic/claude-sonnet-4", "OPENROUTER_API_KEY"),
|
||||
("mistral", "mistral-large-latest", "MISTRAL_API_KEY"),
|
||||
("together", "meta-llama/Llama-3-70b-chat-hf", "TOGETHER_API_KEY"),
|
||||
("fireworks", "accounts/fireworks/models/llama-v3p1-70b-instruct", "FIREWORKS_API_KEY"),
|
||||
|
||||
@@ -222,7 +222,9 @@ impl LlmDriver for OpenAIDriver {
|
||||
has_tool_results = true;
|
||||
oai_messages.push(OaiMessage {
|
||||
role: "tool".to_string(),
|
||||
content: Some(OaiMessageContent::Text(content.clone())),
|
||||
content: Some(OaiMessageContent::Text(
|
||||
if content.is_empty() { "(empty)".to_string() } else { content.clone() }
|
||||
)),
|
||||
tool_calls: None,
|
||||
tool_call_id: Some(tool_use_id.clone()),
|
||||
});
|
||||
@@ -580,7 +582,9 @@ impl LlmDriver for OpenAIDriver {
|
||||
{
|
||||
oai_messages.push(OaiMessage {
|
||||
role: "tool".to_string(),
|
||||
content: Some(OaiMessageContent::Text(content.clone())),
|
||||
content: Some(OaiMessageContent::Text(
|
||||
if content.is_empty() { "(empty)".to_string() } else { content.clone() }
|
||||
)),
|
||||
tool_calls: None,
|
||||
tool_call_id: Some(tool_use_id.clone()),
|
||||
});
|
||||
|
||||
@@ -1480,47 +1480,19 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
// OpenRouter (11)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
ModelCatalogEntry {
|
||||
id: "openrouter/auto".into(),
|
||||
display_name: "OpenRouter Auto".into(),
|
||||
id: "openrouter/openai/gpt-4o".into(),
|
||||
display_name: "GPT-4o (OpenRouter)".into(),
|
||||
provider: "openrouter".into(),
|
||||
tier: ModelTier::Smart,
|
||||
context_window: 200_000,
|
||||
max_output_tokens: 32_000,
|
||||
input_cost_per_m: 1.0,
|
||||
output_cost_per_m: 3.0,
|
||||
context_window: 128_000,
|
||||
max_output_tokens: 16_384,
|
||||
input_cost_per_m: 2.5,
|
||||
output_cost_per_m: 10.0,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "openrouter/optimus".into(),
|
||||
display_name: "OpenRouter Optimus".into(),
|
||||
provider: "openrouter".into(),
|
||||
tier: ModelTier::Balanced,
|
||||
context_window: 200_000,
|
||||
max_output_tokens: 32_000,
|
||||
input_cost_per_m: 0.50,
|
||||
output_cost_per_m: 1.50,
|
||||
supports_tools: true,
|
||||
supports_vision: false,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "openrouter/nitro".into(),
|
||||
display_name: "OpenRouter Nitro".into(),
|
||||
provider: "openrouter".into(),
|
||||
tier: ModelTier::Fast,
|
||||
context_window: 128_000,
|
||||
max_output_tokens: 16_000,
|
||||
input_cost_per_m: 0.20,
|
||||
output_cost_per_m: 0.60,
|
||||
supports_tools: true,
|
||||
supports_vision: false,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "openrouter/anthropic/claude-sonnet-4".into(),
|
||||
display_name: "Claude Sonnet 4 (OpenRouter)".into(),
|
||||
|
||||
Reference in New Issue
Block a user