mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-29 22:22:35 +00:00
community fixes
This commit is contained in:
@@ -42,6 +42,11 @@ jobs:
|
||||
args: "--target x86_64-pc-windows-msvc"
|
||||
rust_target: x86_64-pc-windows-msvc
|
||||
|
||||
- name: Windows ARM64
|
||||
os: windows-latest
|
||||
args: "--target aarch64-pc-windows-msvc"
|
||||
rust_target: aarch64-pc-windows-msvc
|
||||
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -152,6 +157,9 @@ jobs:
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
archive: zip
|
||||
- target: aarch64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
archive: zip
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -785,6 +785,15 @@ impl ChannelBridgeHandle for KernelBridgeAdapter {
|
||||
)
|
||||
};
|
||||
self.kernel.delivery_tracker.record(agent_id, receipt);
|
||||
|
||||
// Persist last channel for cron CronDelivery::LastChannel
|
||||
if success {
|
||||
let kv_val = serde_json::json!({"channel": channel, "recipient": recipient});
|
||||
let _ = self
|
||||
.kernel
|
||||
.memory
|
||||
.structured_set(agent_id, "delivery.last_channel", kv_val);
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_auto_reply(&self, agent_id: AgentId, message: &str) -> Option<String> {
|
||||
|
||||
@@ -1098,19 +1098,25 @@ mark.search-highlight {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Theme toggle */
|
||||
.theme-toggle {
|
||||
cursor: pointer;
|
||||
padding: 6px 8px;
|
||||
/* Theme switcher — 3-mode pill (Light / System / Dark) */
|
||||
.theme-switcher {
|
||||
display: inline-flex;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
font-size: 16px;
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.theme-toggle:hover { color: var(--accent); border-color: var(--border); }
|
||||
.theme-opt {
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
font-size: 14px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
transition: all 0.2s;
|
||||
line-height: 1;
|
||||
}
|
||||
.theme-opt:hover { color: var(--text-primary); background: var(--bg-hover); }
|
||||
.theme-opt.active { color: var(--accent); background: var(--accent-glow); }
|
||||
|
||||
/* Utility */
|
||||
.flex { display: flex; }
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="theme-toggle" @click="toggleTheme()" :title="theme === 'dark' ? 'Switch to light' : 'Switch to dark'" x-text="theme === 'dark' ? '\u2600' : '\u263E'"></button>
|
||||
<div class="theme-switcher">
|
||||
<button class="theme-opt" :class="{ active: themeMode === 'light' }" @click="setTheme('light')" title="Light">☼</button>
|
||||
<button class="theme-opt" :class="{ active: themeMode === 'system' }" @click="setTheme('system')" title="System">○</button>
|
||||
<button class="theme-opt" :class="{ active: themeMode === 'dark' }" @click="setTheme('dark')" title="Dark">☾</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-status" :class="{ offline: !connected && !$store.app.booting }">
|
||||
|
||||
@@ -154,7 +154,12 @@ document.addEventListener('alpine:init', function() {
|
||||
function app() {
|
||||
return {
|
||||
page: 'agents',
|
||||
theme: localStorage.getItem('openfang-theme') || 'light',
|
||||
themeMode: localStorage.getItem('openfang-theme-mode') || 'system',
|
||||
theme: (() => {
|
||||
var mode = localStorage.getItem('openfang-theme-mode') || 'system';
|
||||
if (mode === 'system') return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
return mode;
|
||||
})(),
|
||||
sidebarCollapsed: localStorage.getItem('openfang-sidebar') === 'collapsed',
|
||||
mobileMenuOpen: false,
|
||||
connected: false,
|
||||
@@ -167,6 +172,13 @@ function app() {
|
||||
init() {
|
||||
var self = this;
|
||||
|
||||
// Listen for OS theme changes (only matters when mode is 'system')
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
||||
if (self.themeMode === 'system') {
|
||||
self.theme = e.matches ? 'dark' : 'light';
|
||||
}
|
||||
});
|
||||
|
||||
// Hash routing
|
||||
var validPages = ['overview','agents','sessions','approvals','workflows','scheduler','channels','skills','hands','analytics','logs','settings','wizard'];
|
||||
var pageRedirects = {
|
||||
@@ -234,9 +246,20 @@ function app() {
|
||||
this.mobileMenuOpen = false;
|
||||
},
|
||||
|
||||
setTheme(mode) {
|
||||
this.themeMode = mode;
|
||||
localStorage.setItem('openfang-theme-mode', mode);
|
||||
if (mode === 'system') {
|
||||
this.theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
} else {
|
||||
this.theme = mode;
|
||||
}
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
this.theme = this.theme === 'dark' ? 'light' : 'dark';
|
||||
localStorage.setItem('openfang-theme', this.theme);
|
||||
var modes = ['light', 'system', 'dark'];
|
||||
var next = modes[(modes.indexOf(this.themeMode) + 1) % modes.length];
|
||||
this.setTheme(next);
|
||||
},
|
||||
|
||||
toggleSidebar() {
|
||||
|
||||
@@ -1083,12 +1083,22 @@ impl OpenFangKernel {
|
||||
}
|
||||
|
||||
/// Send a message to an agent and get a response.
|
||||
///
|
||||
/// Automatically upgrades the kernel handle from `self_handle` so that
|
||||
/// agent turns triggered by cron, channels, events, or inter-agent calls
|
||||
/// have full access to kernel tools (cron_create, agent_send, etc.).
|
||||
pub async fn send_message(
|
||||
&self,
|
||||
agent_id: AgentId,
|
||||
message: &str,
|
||||
) -> KernelResult<AgentLoopResult> {
|
||||
self.send_message_with_handle(agent_id, message, None).await
|
||||
let handle: Option<Arc<dyn KernelHandle>> = self
|
||||
.self_handle
|
||||
.get()
|
||||
.and_then(|w| w.upgrade())
|
||||
.map(|arc| arc as Arc<dyn KernelHandle>);
|
||||
self.send_message_with_handle(agent_id, message, handle)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a message with an optional kernel handle for inter-agent tools.
|
||||
@@ -3105,15 +3115,24 @@ impl OpenFangKernel {
|
||||
tracing::debug!(job = %job_name, agent = %agent_id, "Cron: firing agent turn");
|
||||
let timeout_s = timeout_secs.unwrap_or(120);
|
||||
let timeout = std::time::Duration::from_secs(timeout_s);
|
||||
let delivery = job.delivery.clone();
|
||||
match tokio::time::timeout(
|
||||
timeout,
|
||||
kernel.send_message(agent_id, message),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_result)) => {
|
||||
Ok(Ok(result)) => {
|
||||
tracing::info!(job = %job_name, "Cron job completed successfully");
|
||||
kernel.cron_scheduler.record_success(job_id);
|
||||
// Deliver response to configured channel
|
||||
cron_deliver_response(
|
||||
&kernel,
|
||||
agent_id,
|
||||
&result.response,
|
||||
&delivery,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
let err_msg = format!("{e}");
|
||||
@@ -4167,6 +4186,74 @@ fn shared_memory_agent_id() -> AgentId {
|
||||
]))
|
||||
}
|
||||
|
||||
/// Deliver a cron job's agent response to the configured delivery target.
|
||||
async fn cron_deliver_response(
|
||||
kernel: &OpenFangKernel,
|
||||
agent_id: AgentId,
|
||||
response: &str,
|
||||
delivery: &openfang_types::scheduler::CronDelivery,
|
||||
) {
|
||||
use openfang_types::scheduler::CronDelivery;
|
||||
|
||||
if response.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
match delivery {
|
||||
CronDelivery::None => {}
|
||||
CronDelivery::Channel { channel, to } => {
|
||||
tracing::debug!(channel = %channel, to = %to, "Cron: delivering to channel");
|
||||
// Persist as last channel for this agent (survives restarts)
|
||||
let kv_val = serde_json::json!({"channel": channel, "recipient": to});
|
||||
let _ = kernel
|
||||
.memory
|
||||
.structured_set(agent_id, "delivery.last_channel", kv_val);
|
||||
}
|
||||
CronDelivery::LastChannel => {
|
||||
match kernel
|
||||
.memory
|
||||
.structured_get(agent_id, "delivery.last_channel")
|
||||
{
|
||||
Ok(Some(val)) => {
|
||||
let channel = val["channel"].as_str().unwrap_or("");
|
||||
let recipient = val["recipient"].as_str().unwrap_or("");
|
||||
if !channel.is_empty() && !recipient.is_empty() {
|
||||
tracing::info!(
|
||||
channel = %channel,
|
||||
recipient = %recipient,
|
||||
"Cron: delivering to last channel"
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::debug!("Cron: no last channel found for agent {}", agent_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
CronDelivery::Webhook { url } => {
|
||||
tracing::debug!(url = %url, "Cron: delivering via webhook");
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build();
|
||||
if let Ok(client) = client {
|
||||
let payload = serde_json::json!({
|
||||
"agent_id": agent_id.to_string(),
|
||||
"response": response,
|
||||
"timestamp": chrono::Utc::now().to_rfc3339(),
|
||||
});
|
||||
match client.post(url).json(&payload).send().await {
|
||||
Ok(resp) => {
|
||||
tracing::debug!(status = %resp.status(), "Cron webhook delivered");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Cron webhook delivery failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl KernelHandle for OpenFangKernel {
|
||||
async fn spawn_agent(
|
||||
|
||||
@@ -879,7 +879,7 @@ pub fn builtin_tool_definitions() -> Vec<ToolDefinition> {
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"description": "Action: {\"action\":\"system_event\",\"text\":\"...\"} or {\"action\":\"agent_turn\",\"message\":\"...\",\"timeout_secs\":300}"
|
||||
"description": "Action: {\"kind\":\"system_event\",\"text\":\"...\"} or {\"kind\":\"agent_turn\",\"message\":\"...\",\"timeout_secs\":300}"
|
||||
},
|
||||
"delivery": {
|
||||
"type": "object",
|
||||
|
||||
@@ -32,8 +32,7 @@ async function startConnection() {
|
||||
const pino = (await import('pino')).default || await import('pino');
|
||||
|
||||
const logger = pino({ level: 'warn' });
|
||||
const authDir = new URL('./auth_store/', import.meta.url || `file://${__dirname}/`).pathname
|
||||
|| require('node:path').join(__dirname, 'auth_store');
|
||||
const authDir = require('node:path').join(__dirname, 'auth_store');
|
||||
|
||||
const { state, saveCreds } = await useMultiFileAuthState(
|
||||
require('node:path').join(__dirname, 'auth_store')
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"bin": {
|
||||
"openfang-whatsapp-gateway": "./index.js"
|
||||
},
|
||||
"type": "commonjs",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
|
||||
Reference in New Issue
Block a user