fixxxeeess

This commit is contained in:
pavel 2026-02-10 20:57:03 +01:00
commit c4d9ca19ce
8 changed files with 142 additions and 54 deletions

View file

@ -16,7 +16,8 @@ const state = {
selectedRunId: null, selectedRunId: null,
currentView: 'dashboard', // 'dashboard' or 'task' currentView: 'dashboard', // 'dashboard' or 'task'
isEditing: false, isEditing: false,
token: localStorage.getItem('auth_token') token: localStorage.getItem('auth_token'),
refreshToken: localStorage.getItem('refresh_token')
}; };
// DOM elements // DOM elements
@ -67,7 +68,24 @@ async function fetchWithAuth(url, options = {}) {
'Authorization': `Bearer ${state.token}` 'Authorization': `Bearer ${state.token}`
}; };
const response = await fetch(url, { ...options, headers }); let response = await fetch(url, { ...options, headers });
if (response.status === 401 && state.refreshToken) {
// Try to refresh token
try {
const success = await attemptTokenRefresh();
if (success) {
// Retry original request with new token
const newHeaders = {
...options.headers,
'Authorization': `Bearer ${state.token}`
};
response = await fetch(url, { ...options, headers: newHeaders });
}
} catch (error) {
console.error('Token refresh failed:', error);
}
}
if (response.status === 401) { if (response.status === 401) {
logout(); logout();
@ -77,6 +95,32 @@ async function fetchWithAuth(url, options = {}) {
return response; return response;
} }
async function attemptTokenRefresh() {
try {
const response = await fetch(`${API_URL}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: state.refreshToken })
});
if (response.ok) {
const data = await response.json();
if (data.access_token) {
state.token = data.access_token;
localStorage.setItem('auth_token', data.access_token);
if (data.refresh_token) {
state.refreshToken = data.refresh_token;
localStorage.setItem('refresh_token', data.refresh_token);
}
return true;
}
}
} catch (error) {
console.error('Error during token refresh:', error);
}
return false;
}
async function fetchTasks() { async function fetchTasks() {
try { try {
const response = await fetchWithAuth(`${API_URL}/tasks`); const response = await fetchWithAuth(`${API_URL}/tasks`);
@ -134,10 +178,10 @@ async function fetchRecentRuns() {
function renderTaskList() { function renderTaskList() {
const sortedTasks = [...state.tasks].sort((a, b) => { const sortedTasks = [...state.tasks].sort((a, b) => {
const aDate = a.runs && a.runs.length > 0 const aDate = a.runs && a.runs.length > 0
? new Date(a.runs[a.runs.length - 1].created_at) ? new Date(a.runs[0].created_at)
: new Date(a.created_at); : new Date(a.created_at);
const bDate = b.runs && b.runs.length > 0 const bDate = b.runs && b.runs.length > 0
? new Date(b.runs[b.runs.length - 1].created_at) ? new Date(b.runs[0].created_at)
: new Date(b.created_at); : new Date(b.created_at);
return bDate - aDate; return bDate - aDate;
}); });
@ -145,7 +189,7 @@ function renderTaskList() {
taskListEl.innerHTML = sortedTasks taskListEl.innerHTML = sortedTasks
.map((task) => { .map((task) => {
const latestRun = task.runs && task.runs.length > 0 const latestRun = task.runs && task.runs.length > 0
? task.runs[task.runs.length - 1] ? task.runs[0]
: null; : null;
const status = latestRun ? latestRun.status : 'pending'; const status = latestRun ? latestRun.status : 'pending';
const date = latestRun ? new Date(latestRun.created_at) : new Date(task.created_at); const date = latestRun ? new Date(latestRun.created_at) : new Date(task.created_at);
@ -192,8 +236,6 @@ function selectTask(id, runId = null) {
function renderRunHistory(task) { function renderRunHistory(task) {
runListEl.innerHTML = task.runs runListEl.innerHTML = task.runs
.slice()
.reverse()
.map( .map(
(run, index) => ` (run, index) => `
<li class="run-item ${state.selectedRunId === run.id ? 'active' : ''}" data-id="${run.id}"> <li class="run-item ${state.selectedRunId === run.id ? 'active' : ''}" data-id="${run.id}">
@ -250,7 +292,7 @@ function showTaskView(task) {
dashboardViewEl.classList.add('hidden'); dashboardViewEl.classList.add('hidden');
taskViewEl.classList.remove('hidden'); taskViewEl.classList.remove('hidden');
const run = task.runs.find(r => r.id === state.selectedRunId) || task.runs[task.runs.length - 1]; const run = task.runs.find(r => r.id === state.selectedRunId) || task.runs[0];
viewGoalEl.textContent = task.goal; viewGoalEl.textContent = task.goal;
@ -473,7 +515,9 @@ async function showLogin() {
async function logout() { async function logout() {
state.token = null; state.token = null;
state.refreshToken = null;
localStorage.removeItem('auth_token'); localStorage.removeItem('auth_token');
localStorage.removeItem('refresh_token');
showLogin(); showLogin();
} }
@ -493,6 +537,10 @@ async function handleCallback() {
if (data.access_token) { if (data.access_token) {
state.token = data.access_token; state.token = data.access_token;
localStorage.setItem('auth_token', data.access_token); localStorage.setItem('auth_token', data.access_token);
if (data.refresh_token) {
state.refreshToken = data.refresh_token;
localStorage.setItem('refresh_token', data.refresh_token);
}
callbackOverlay.classList.add('hidden'); callbackOverlay.classList.add('hidden');
appEl.classList.remove('hidden'); appEl.classList.remove('hidden');
initializeApp(); initializeApp();

View file

@ -87,7 +87,7 @@ body {
.dashboard-content { .dashboard-content {
flex: 1; flex: 1;
overflow: hidden; overflow-y: auto;
border-radius: 12px; border-radius: 12px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -246,7 +246,7 @@ body {
display: flex; display: flex;
gap: 24px; gap: 24px;
flex: 1; flex: 1;
overflow: hidden; min-height: 0;
} }
.run-history { .run-history {
@ -272,6 +272,7 @@ body {
#run-list { #run-list {
list-style: none; list-style: none;
flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 8px; padding: 8px;
} }

View file

@ -19,13 +19,13 @@ impl Agent {
zen_api_key: Option<String>, zen_api_key: Option<String>,
tavily_api_key: Option<String>, tavily_api_key: Option<String>,
initial_message: String, initial_message: String,
) -> Self { ) -> Result<Self, Box<dyn std::error::Error>> {
let intro = format!( let intro = format!(
"You are an autonomous agent. You have access to tools that can help "You are an autonomous agent. You have access to tools that can help
you achieve your goals. Use them wisely. The user is unable to respond to you you achieve your goals. Use them wisely. The user is unable to respond to you
so do not ask for clarification and use the so do not ask for clarification and use the
answer tool once you to give your final answer. current date is {}", answer tool once you to give your final answer. current date is {}",
Utc::now().to_rfc3339() Utc::now().format("%B %d, %Y %H:%M:%S UTC").to_string()
); );
println!("initial_message: {}", intro); println!("initial_message: {}", intro);
let messages = vec![ let messages = vec![
@ -45,8 +45,12 @@ impl Agent {
let tools = Some(tools::get_tools()); let tools = Some(tools::get_tools());
Self { let client = reqwest::Client::builder()
client: reqwest::Client::new(), .timeout(std::time::Duration::from_secs(60))
.build()?;
Ok(Self {
client,
url: "https://opencode.ai/zen/v1/chat/completions".to_string(), url: "https://opencode.ai/zen/v1/chat/completions".to_string(),
zen_api_key, zen_api_key,
tavily_api_key, tavily_api_key,
@ -54,7 +58,7 @@ impl Agent {
tools, tools,
logs: String::new(), logs: String::new(),
answer: None, answer: None,
} })
} }
fn log(&mut self, message: &str) { fn log(&mut self, message: &str) {

View file

@ -72,7 +72,9 @@ pub async fn perform_search(
query: &str, query: &str,
api_key: &str, api_key: &str,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
let client = reqwest::Client::new(); let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()?;
let response = client let response = client
.post("https://api.tavily.com/search") .post("https://api.tavily.com/search")
.json(&serde_json::json!({ .json(&serde_json::json!({

View file

@ -15,11 +15,13 @@ pub struct Claims {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct Jwk { struct Jwk {
kty: String, #[serde(rename = "kty")]
_kty: String,
kid: String, kid: String,
n: String, n: String,
e: String, e: String,
alg: Option<String>, #[serde(rename = "alg")]
_alg: Option<String>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -146,4 +148,27 @@ impl Authenticator {
Ok(res) Ok(res)
} }
pub async fn refresh_token(
&self,
refresh_token: String,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
let params = [
("grant_type", "refresh_token"),
("refresh_token", &refresh_token),
("client_id", &self.client_id),
("client_secret", &self.client_secret),
];
let res = self
.client
.post(&self.token_url)
.form(&params)
.send()
.await?
.json()
.await?;
Ok(res)
}
} }

View file

@ -95,7 +95,7 @@ impl Scheduler {
run.insert(&db).await?; run.insert(&db).await?;
// Start agent in background // Start agent in background
let mut agent = Agent::new(zen_key, tavily_key, task.goal.clone()); let mut agent = Agent::new(zen_key, tavily_key, task.goal.clone())?;
tokio::spawn(async move { tokio::spawn(async move {
let (logs, answer) = match agent.run().await { let (logs, answer) = match agent.run().await {

View file

@ -127,6 +127,7 @@ pub async fn start(db_url: &str) -> Result<(), Box<dyn std::error::Error>> {
.route("/tasks/:id/runs", post(rerun_task)) .route("/tasks/:id/runs", post(rerun_task))
.route("/runs/recent", get(get_recent_runs)) .route("/runs/recent", get(get_recent_runs))
.route("/auth/callback", get(auth_callback)) .route("/auth/callback", get(auth_callback))
.route("/auth/refresh", post(auth_refresh))
.layer(cors) .layer(cors)
.with_state(state); .with_state(state);
@ -150,7 +151,9 @@ async fn list_tasks(
let response = tasks let response = tasks
.into_iter() .into_iter()
.map(|(t, runs)| TaskResponse { .map(|(t, mut runs)| {
runs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
TaskResponse {
id: t.id, id: t.id,
goal: t.goal, goal: t.goal,
cron: t.cron, cron: t.cron,
@ -165,6 +168,7 @@ async fn list_tasks(
created_at: r.created_at, created_at: r.created_at,
}) })
.collect(), .collect(),
}
}) })
.collect(); .collect();
@ -240,7 +244,8 @@ async fn execute_agent_run(
state.zen_api_key.clone(), state.zen_api_key.clone(),
state.tavily_api_key.clone(), state.tavily_api_key.clone(),
goal.clone(), goal.clone(),
); )
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let (logs, answer) = match agent.run().await { let (logs, answer) = match agent.run().await {
Ok((logs, answer)) => (logs, answer), Ok((logs, answer)) => (logs, answer),
@ -298,6 +303,7 @@ async fn update_task(
get_task_inner(id, &state).await.map(Json) get_task_inner(id, &state).await.map(Json)
} }
#[allow(dead_code)]
pub struct AuthenticatedUser(pub crate::auth::Claims); pub struct AuthenticatedUser(pub crate::auth::Claims);
#[axum::async_trait] #[axum::async_trait]
@ -342,6 +348,23 @@ pub struct AuthCallbackQuery {
pub redirect_uri: String, pub redirect_uri: String,
} }
#[derive(Deserialize)]
struct RefreshRequest {
refresh_token: String,
}
async fn auth_refresh(
State(state): State<Arc<AppState>>,
Json(payload): Json<RefreshRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
state
.authenticator
.refresh_token(payload.refresh_token)
.await
.map(Json)
.map_err(|e| (StatusCode::UNAUTHORIZED, e.to_string()))
}
async fn auth_callback( async fn auth_callback(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Query(query): Query<AuthCallbackQuery>, Query(query): Query<AuthCallbackQuery>,
@ -374,11 +397,13 @@ async fn get_task_inner(id: Uuid, state: &AppState) -> Result<TaskResponse, (Sta
.await .await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let (t, runs) = results let (t, mut runs) = results
.into_iter() .into_iter()
.next() .next()
.ok_or((StatusCode::NOT_FOUND, "Task not found".to_string()))?; .ok_or((StatusCode::NOT_FOUND, "Task not found".to_string()))?;
runs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
Ok(TaskResponse { Ok(TaskResponse {
id: t.id, id: t.id,
goal: t.goal, goal: t.goal,

View file

@ -44,7 +44,6 @@ pub async fn handle_tool_call(
tool_call: &ToolCall, tool_call: &ToolCall,
tavily_api_key: &Option<String>, tavily_api_key: &Option<String>,
) -> Result<(Message, bool, Option<String>), Box<dyn std::error::Error>> { ) -> Result<(Message, bool, Option<String>), Box<dyn std::error::Error>> {
let mut file_written = false;
let mut answer = None; let mut answer = None;
let name = &tool_call.function.name; let name = &tool_call.function.name;
@ -66,30 +65,14 @@ pub async fn handle_tool_call(
"Error: TAVILY_API_KEY is not set. Cannot perform real search.".to_string() "Error: TAVILY_API_KEY is not set. Cannot perform real search.".to_string()
}; };
(search_result, false) (search_result, false)
} else if name == "write_file" {
let args: HashMap<String, String> = serde_json::from_str(&tool_call.function.arguments)?;
let path = args.get("path").ok_or("Missing path argument")?;
let content = args.get("content").ok_or("Missing content argument")?;
println!("--- Executing tool: write_file(path: \"{}\") ---", path);
let write_result = match std::fs::write(path, content) {
Ok(_) => {
file_written = true;
format!("Successfully wrote content to {}", path)
}
Err(e) => format!("Error writing to {}: {}", path, e),
};
(write_result, file_written)
} else if name == "finish" { } else if name == "finish" {
let args: HashMap<String, String> = serde_json::from_str(&tool_call.function.arguments)?; let args: HashMap<String, String> = serde_json::from_str(&tool_call.function.arguments)?;
let result = args.get("result").ok_or("Missing result argument")?; let result = args.get("result").ok_or("Missing result argument")?;
println!("--- Finishing task: {}", result); println!("--- Finishing task: {}", result);
file_written = true;
answer = Some(result.clone()); answer = Some(result.clone());
(result.clone(), file_written) (result.clone(), true)
} else { } else {
(format!("Error: Unknown tool {}", name), false) (format!("Error: Unknown tool {}", name), false)
}; };