WIP: Local changes before merging master into main
This commit is contained in:
parent
bbdf243c86
commit
8f495c75ec
30 changed files with 1630 additions and 265 deletions
|
|
@ -63,14 +63,14 @@ sha256 = ""
|
|||
|
||||
[components.directory]
|
||||
name = "Zitadel Identity Provider"
|
||||
url = "https://github.com/zitadel/zitadel/releases/download/v2.70.4/zitadel-linux-amd64.tar.gz"
|
||||
url = "https://github.com/zitadel/zitadel/releases/download/v4.11.1/zitadel-linux-amd64.tar.gz"
|
||||
filename = "zitadel-linux-amd64.tar.gz"
|
||||
sha256 = ""
|
||||
|
||||
[components.alm]
|
||||
name = "Forgejo Git Server"
|
||||
url = "https://codeberg.org/forgejo/forgejo/releases/download/v10.0.2/forgejo-10.0.2-linux-amd64"
|
||||
filename = "forgejo-10.0.2-linux-amd64"
|
||||
url = "https://codeberg.org/forgejo/forgejo/releases/download/v14.0.2/forgejo-14.0.2-linux-amd64"
|
||||
filename = "forgejo-14.0.2-linux-amd64"
|
||||
sha256 = ""
|
||||
|
||||
[components.alm_ci]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use chrono::Utc;
|
|||
use diesel::prelude::*;
|
||||
use diesel::sql_query;
|
||||
use diesel::sql_types::{Text, Uuid as DieselUuid};
|
||||
use log::{error, info, trace};
|
||||
use log::{error, info, trace, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -413,8 +413,13 @@ pub async fn classify_intent_handler(
|
|||
let session = match get_current_session(&state) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
warn!("No active session for classify, bootstrapping default: {}", e);
|
||||
match bootstrap_default_session(&state) {
|
||||
Ok(s) => s,
|
||||
Err(e2) => {
|
||||
error!("Failed to bootstrap session: {}", e2);
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ClassifyIntentResponse {
|
||||
success: false,
|
||||
intent_type: "UNKNOWN".to_string(),
|
||||
|
|
@ -423,10 +428,12 @@ pub async fn classify_intent_handler(
|
|||
requires_clarification: false,
|
||||
clarification_question: None,
|
||||
result: None,
|
||||
error: Some(format!("Authentication error: {}", e)),
|
||||
error: Some(format!("No session available: {e2}")),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let classifier = IntentClassifier::new(Arc::clone(&state));
|
||||
|
|
@ -1364,6 +1371,49 @@ fn get_current_session(
|
|||
Ok(session)
|
||||
}
|
||||
|
||||
fn bootstrap_default_session(
|
||||
state: &Arc<AppState>,
|
||||
) -> Result<crate::core::shared::models::UserSession, Box<dyn std::error::Error + Send + Sync>> {
|
||||
use diesel::prelude::*;
|
||||
|
||||
let mut conn = state
|
||||
.conn
|
||||
.get()
|
||||
.map_err(|e| format!("DB connection error: {}", e))?;
|
||||
|
||||
#[derive(QueryableByName)]
|
||||
struct BotRow {
|
||||
#[diesel(sql_type = diesel::sql_types::Uuid)]
|
||||
id: uuid::Uuid,
|
||||
}
|
||||
|
||||
let bots: Vec<BotRow> = diesel::sql_query("SELECT id FROM bots LIMIT 1")
|
||||
.get_results(&mut conn)
|
||||
.unwrap_or_default();
|
||||
|
||||
let bot_id = bots
|
||||
.first()
|
||||
.map(|b| b.id)
|
||||
.unwrap_or_else(uuid::Uuid::nil);
|
||||
|
||||
let session_id = uuid::Uuid::new_v4();
|
||||
let user_id = uuid::Uuid::nil();
|
||||
|
||||
diesel::sql_query(
|
||||
"INSERT INTO user_sessions (id, bot_id, user_id, channel, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, 'vibe', NOW(), NOW())
|
||||
ON CONFLICT DO NOTHING"
|
||||
)
|
||||
.bind::<diesel::sql_types::Uuid, _>(session_id)
|
||||
.bind::<diesel::sql_types::Uuid, _>(bot_id)
|
||||
.bind::<diesel::sql_types::Uuid, _>(user_id)
|
||||
.execute(&mut conn)
|
||||
.map_err(|e| format!("Failed to create bootstrap session: {}", e))?;
|
||||
|
||||
get_current_session(state)
|
||||
}
|
||||
|
||||
|
||||
fn create_auto_task_from_plan(
|
||||
_state: &Arc<AppState>,
|
||||
session: &crate::core::shared::models::UserSession,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::auto_task::app_generator::AppGenerator;
|
||||
use crate::auto_task::intent_compiler::IntentCompiler;
|
||||
use crate::basic::ScriptService;
|
||||
|
||||
|
|
@ -10,7 +9,7 @@ use chrono::{DateTime, Utc};
|
|||
use diesel::prelude::*;
|
||||
use diesel::sql_query;
|
||||
use diesel::sql_types::{Text, Uuid as DieselUuid};
|
||||
use log::{error, info, trace, warn};
|
||||
use log::{info, trace, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -512,112 +511,44 @@ Respond with JSON only:
|
|||
session: &UserSession,
|
||||
task_id: Option<String>,
|
||||
) -> Result<IntentResult, Box<dyn std::error::Error + Send + Sync>> {
|
||||
info!("Handling APP_CREATE intent");
|
||||
info!("Handling APP_CREATE intent via Orchestrator pipeline");
|
||||
|
||||
// [AGENT MODE] Initialize the LXC container session for real terminal output
|
||||
let t_id = task_id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||
let mut executor = crate::auto_task::AgentExecutor::new(self.state.clone(), &session.id.to_string(), &t_id);
|
||||
|
||||
if let Err(e) = executor.initialize().await {
|
||||
log::warn!("Failed to initialize LXC container for agent: {}", e);
|
||||
let mut orchestrator = if let Some(tid) = task_id {
|
||||
crate::auto_task::orchestrator::Orchestrator::with_task_id(
|
||||
self.state.clone(),
|
||||
tid,
|
||||
)
|
||||
} else {
|
||||
executor.broadcast_thought("Analyzing the user prompt and setting up a dedicated LXC workspace...");
|
||||
let _ = executor.execute_shell_command("echo 'Initializing Agent Workspace...' && date && mkdir -p /root/app").await;
|
||||
}
|
||||
|
||||
let mut app_generator = if let Some(tid) = task_id {
|
||||
AppGenerator::with_task_id(self.state.clone(), tid)
|
||||
} else {
|
||||
AppGenerator::new(self.state.clone())
|
||||
crate::auto_task::orchestrator::Orchestrator::new(self.state.clone())
|
||||
};
|
||||
|
||||
match app_generator
|
||||
.generate_app(&classification.original_text, session)
|
||||
.await
|
||||
{
|
||||
Ok(app) => {
|
||||
let mut resources = Vec::new();
|
||||
let result = orchestrator
|
||||
.execute_pipeline(classification, session)
|
||||
.await?;
|
||||
|
||||
// Track created tables
|
||||
for table in &app.tables {
|
||||
resources.push(CreatedResource {
|
||||
resource_type: "table".to_string(),
|
||||
name: table.name.clone(),
|
||||
path: Some("tables.bas".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
for page in &app.pages {
|
||||
resources.push(CreatedResource {
|
||||
resource_type: "page".to_string(),
|
||||
name: page.filename.clone(),
|
||||
path: Some(page.filename.clone()),
|
||||
});
|
||||
}
|
||||
|
||||
for tool in &app.tools {
|
||||
resources.push(CreatedResource {
|
||||
resource_type: "tool".to_string(),
|
||||
name: tool.filename.clone(),
|
||||
path: Some(tool.filename.clone()),
|
||||
});
|
||||
}
|
||||
|
||||
let app_url = format!("/apps/{}", app.name.to_lowercase().replace(' ', "-"));
|
||||
|
||||
let res = Ok(IntentResult {
|
||||
success: true,
|
||||
Ok(IntentResult {
|
||||
success: result.success,
|
||||
intent_type: IntentType::AppCreate,
|
||||
message: format!(
|
||||
"Done:\n{}\nApp available at {}",
|
||||
resources
|
||||
.iter()
|
||||
.filter(|r| r.resource_type == "table")
|
||||
.map(|r| format!("{} table created", r.name))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
app_url
|
||||
),
|
||||
created_resources: resources,
|
||||
app_url: Some(app_url),
|
||||
task_id: None,
|
||||
message: result.message,
|
||||
created_resources: result
|
||||
.created_resources
|
||||
.into_iter()
|
||||
.map(|r| CreatedResource {
|
||||
resource_type: r.resource_type,
|
||||
name: r.name,
|
||||
path: r.path,
|
||||
})
|
||||
.collect(),
|
||||
app_url: result.app_url,
|
||||
task_id: Some(result.task_id),
|
||||
schedule_id: None,
|
||||
tool_triggers: Vec::new(),
|
||||
next_steps: vec![
|
||||
"Open the app to start using it".to_string(),
|
||||
"Use Designer to customize the app".to_string(),
|
||||
],
|
||||
error: None,
|
||||
});
|
||||
|
||||
// [AGENT MODE] Cleanup the LXC container
|
||||
executor.broadcast_thought("Generation complete. Terminating LXC sandbox.");
|
||||
executor.cleanup().await;
|
||||
|
||||
res
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to generate app: {e}");
|
||||
let res = Ok(IntentResult {
|
||||
success: false,
|
||||
intent_type: IntentType::AppCreate,
|
||||
message: "Failed to create the application".to_string(),
|
||||
created_resources: Vec::new(),
|
||||
app_url: None,
|
||||
task_id: None,
|
||||
schedule_id: None,
|
||||
tool_triggers: Vec::new(),
|
||||
next_steps: vec!["Try again with more details".to_string()],
|
||||
error: Some(e.to_string()),
|
||||
});
|
||||
|
||||
// [AGENT MODE] Cleanup the LXC container on error
|
||||
if let Err(_) = executor.execute_shell_command("echo 'Build failed' >&2").await {}
|
||||
executor.cleanup().await;
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
error: result.error,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_todo(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ pub mod autotask_api;
|
|||
pub mod designer_ai;
|
||||
pub mod intent_classifier;
|
||||
pub mod intent_compiler;
|
||||
pub mod orchestrator;
|
||||
pub mod safety_layer;
|
||||
pub mod task_manifest;
|
||||
pub mod task_types;
|
||||
|
|
|
|||
1146
src/auto_task/orchestrator.rs
Normal file
1146
src/auto_task/orchestrator.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
use log::info;
|
||||
use log::{info, trace};
|
||||
|
||||
pub fn convert_mail_line_with_substitution(line: &str) -> String {
|
||||
let mut result = String::new();
|
||||
|
|
@ -68,7 +68,7 @@ pub fn convert_mail_line_with_substitution(line: &str) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
info!("Converted mail line: '{}' → '{}'", line, result);
|
||||
trace!("Converted mail line: '{}' → '{}'", line, result);
|
||||
result
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +138,6 @@ pub fn convert_mail_block(recipient: &str, lines: &[String]) -> String {
|
|||
};
|
||||
result.push_str(&format!("send_mail({}, \"{}\", {}, []);\n", recipient_expr, subject, body_expr));
|
||||
|
||||
info!("Converted MAIL block → {}", result);
|
||||
trace!("Converted MAIL block → {}", result);
|
||||
result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ pub mod talk;
|
|||
pub use mail::convert_mail_block;
|
||||
pub use talk::convert_talk_block;
|
||||
|
||||
use log::info;
|
||||
use log::{info, trace};
|
||||
|
||||
pub fn convert_begin_blocks(script: &str) -> String {
|
||||
let mut result = String::new();
|
||||
|
|
@ -23,14 +23,14 @@ pub fn convert_begin_blocks(script: &str) -> String {
|
|||
}
|
||||
|
||||
if upper == "BEGIN TALK" {
|
||||
info!("Converting BEGIN TALK statement");
|
||||
trace!("Converting BEGIN TALK statement");
|
||||
in_talk_block = true;
|
||||
talk_block_lines.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
if upper == "END TALK" {
|
||||
info!("Converting END TALK statement, processing {} lines", talk_block_lines.len());
|
||||
trace!("Converting END TALK statement, processing {} lines", talk_block_lines.len());
|
||||
in_talk_block = false;
|
||||
let converted = convert_talk_block(&talk_block_lines);
|
||||
result.push_str(&converted);
|
||||
|
|
@ -45,7 +45,7 @@ pub fn convert_begin_blocks(script: &str) -> String {
|
|||
|
||||
if upper.starts_with("BEGIN MAIL ") {
|
||||
let recipient = &trimmed[11..].trim();
|
||||
info!("Converting BEGIN MAIL statement: recipient='{}'", recipient);
|
||||
trace!("Converting BEGIN MAIL statement: recipient='{}'", recipient);
|
||||
mail_recipient = recipient.to_string();
|
||||
in_mail_block = true;
|
||||
mail_block_lines.clear();
|
||||
|
|
@ -53,7 +53,7 @@ pub fn convert_begin_blocks(script: &str) -> String {
|
|||
}
|
||||
|
||||
if upper == "END MAIL" {
|
||||
info!("Converting END MAIL statement, processing {} lines", mail_block_lines.len());
|
||||
trace!("Converting END MAIL statement, processing {} lines", mail_block_lines.len());
|
||||
in_mail_block = false;
|
||||
let converted = convert_mail_block(&mail_recipient, &mail_block_lines);
|
||||
result.push_str(&converted);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use log::info;
|
||||
use log::{info, trace};
|
||||
|
||||
pub fn convert_talk_line_with_substitution(line: &str) -> String {
|
||||
let mut result = String::new();
|
||||
|
|
@ -109,7 +109,7 @@ pub fn convert_talk_line_with_substitution(line: &str) -> String {
|
|||
result = "TALK \"\"".to_string();
|
||||
}
|
||||
|
||||
info!("Converted TALK line: '{}' → '{}'", line, result);
|
||||
trace!("Converted TALK line: '{}' → '{}'", line, result);
|
||||
result
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -521,7 +521,7 @@ impl BasicCompiler {
|
|||
{
|
||||
log::error!("Failed to register WEBHOOK during preprocessing: {}", e);
|
||||
} else {
|
||||
log::info!(
|
||||
log::trace!(
|
||||
"Registered webhook endpoint {} for script {} during preprocessing",
|
||||
endpoint,
|
||||
script_name
|
||||
|
|
@ -550,7 +550,7 @@ impl BasicCompiler {
|
|||
{
|
||||
log::error!("Failed to register USE_WEBSITE during preprocessing: {}", e);
|
||||
} else {
|
||||
log::info!(
|
||||
log::trace!(
|
||||
"Registered website {} for crawling during preprocessing (refresh: {})",
|
||||
url, refresh
|
||||
);
|
||||
|
|
@ -676,7 +676,7 @@ impl BasicCompiler {
|
|||
let table_name = table_name.trim_matches('"');
|
||||
|
||||
// Debug log to see what we're querying
|
||||
log::info!("Converting SAVE for table: '{}' (original: '{}')", table_name, &parts[0]);
|
||||
log::trace!("Converting SAVE for table: '{}' (original: '{}')", table_name, &parts[0]);
|
||||
|
||||
// Get column names from TABLE definition (preserves order from .bas file)
|
||||
let column_names = self.get_table_columns_for_save(table_name, bot_id)?;
|
||||
|
|
@ -685,7 +685,7 @@ impl BasicCompiler {
|
|||
let values: Vec<&String> = parts.iter().skip(1).collect();
|
||||
let mut map_pairs = Vec::new();
|
||||
|
||||
log::info!("Matching {} variables to {} columns", values.len(), column_names.len());
|
||||
log::trace!("Matching {} variables to {} columns", values.len(), column_names.len());
|
||||
|
||||
for value_var in values.iter() {
|
||||
// Find the column that matches this variable (case-insensitive)
|
||||
|
|
@ -753,7 +753,7 @@ impl BasicCompiler {
|
|||
// Try to parse TABLE definition from the bot's .bas files to get correct field order
|
||||
if let Ok(columns) = self.get_columns_from_table_definition(table_name, bot_id) {
|
||||
if !columns.is_empty() {
|
||||
log::info!("Using TABLE definition for '{}': {} columns", table_name, columns.len());
|
||||
log::trace!("Using TABLE definition for '{}': {} columns", table_name, columns.len());
|
||||
return Ok(columns);
|
||||
}
|
||||
}
|
||||
|
|
@ -880,7 +880,7 @@ impl BasicCompiler {
|
|||
|
||||
match sql_query(&bot_query).load(&mut *bot_conn) {
|
||||
Ok(bot_cols) => {
|
||||
log::info!("Found {} columns for table '{}' in bot database", bot_cols.len(), table_name);
|
||||
log::trace!("Found {} columns for table '{}' in bot database", bot_cols.len(), table_name);
|
||||
bot_cols.into_iter()
|
||||
.map(|c: ColumnRow| c.column_name)
|
||||
.collect()
|
||||
|
|
@ -895,7 +895,7 @@ impl BasicCompiler {
|
|||
Vec::new()
|
||||
}
|
||||
} else {
|
||||
log::info!("Found {} columns for table '{}' in main database", cols.len(), table_name);
|
||||
log::trace!("Found {} columns for table '{}' in main database", cols.len(), table_name);
|
||||
cols.into_iter()
|
||||
.map(|c: ColumnRow| c.column_name)
|
||||
.collect()
|
||||
|
|
@ -919,7 +919,7 @@ impl BasicCompiler {
|
|||
|
||||
match sql_query(&bot_query).load(&mut *bot_conn) {
|
||||
Ok(cols) => {
|
||||
log::info!("Found {} columns for table '{}' in bot database", cols.len(), table_name);
|
||||
log::trace!("Found {} columns for table '{}' in bot database", cols.len(), table_name);
|
||||
cols.into_iter()
|
||||
.filter(|c: &ColumnRow| c.column_name != "id")
|
||||
.map(|c: ColumnRow| c.column_name)
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ impl Default for McpConnection {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
connection_type: ConnectionType::Http,
|
||||
url: "http://localhost:9000".to_string(),
|
||||
url: "http://localhost:8080".to_string(),
|
||||
port: None,
|
||||
timeout_seconds: 30,
|
||||
max_retries: 3,
|
||||
|
|
|
|||
|
|
@ -388,7 +388,7 @@ impl McpCsvLoader {
|
|||
.first()
|
||||
.map(|s| (*s).to_string())
|
||||
.unwrap_or_else(|| "localhost".to_string());
|
||||
let port: u16 = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(9000);
|
||||
let port: u16 = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(8080);
|
||||
McpConnection {
|
||||
connection_type: ConnectionType::Tcp,
|
||||
url: host,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use crate::basic::keywords::switch_case::switch_keyword;
|
|||
use crate::core::shared::models::UserSession;
|
||||
use crate::core::shared::state::AppState;
|
||||
use diesel::prelude::*;
|
||||
use log::info;
|
||||
use log::{info, trace};
|
||||
use rhai::{Dynamic, Engine, EvalAltResult, Scope};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -560,7 +560,7 @@ impl ScriptService {
|
|||
}
|
||||
pub fn compile(&self, script: &str) -> Result<rhai::AST, Box<EvalAltResult>> {
|
||||
let processed_script = self.preprocess_basic_script(script);
|
||||
info!("Processed Script:\n{}", processed_script);
|
||||
trace!("Processed Script:\n{}", processed_script);
|
||||
match self.engine.compile(&processed_script) {
|
||||
Ok(ast) => Ok(ast),
|
||||
Err(parse_error) => Err(Box::new(parse_error.into())),
|
||||
|
|
@ -587,7 +587,7 @@ impl ScriptService {
|
|||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
info!("Filtered tool metadata: {} -> {} chars", script.len(), executable_script.len());
|
||||
trace!("Filtered tool metadata: {} -> {} chars", script.len(), executable_script.len());
|
||||
|
||||
// Apply minimal preprocessing for tools (skip variable normalization to avoid breaking multi-line strings)
|
||||
let script = preprocess_switch(&executable_script);
|
||||
|
|
@ -597,7 +597,7 @@ impl ScriptService {
|
|||
// let script = Self::convert_format_syntax(&script);
|
||||
// Skip normalize_variables_to_lowercase for tools - it breaks multi-line strings
|
||||
|
||||
info!("Preprocessed tool script for Rhai compilation");
|
||||
trace!("Preprocessed tool script for Rhai compilation");
|
||||
// Convert SAVE statements with field lists to map-based SAVE (simplified version for tools)
|
||||
let script = Self::convert_save_for_tools(&script);
|
||||
// Convert BEGIN TALK and BEGIN MAIL blocks to single calls
|
||||
|
|
@ -986,7 +986,7 @@ impl ScriptService {
|
|||
};
|
||||
result.push_str(&format!("send_mail({}, \"{}\", {}, []);\n", recipient_expr, subject, body_expr));
|
||||
|
||||
log::info!("Converted MAIL block → {}", result);
|
||||
log::trace!("Converted MAIL block → {}", result);
|
||||
result
|
||||
}
|
||||
|
||||
|
|
@ -1086,7 +1086,7 @@ impl ScriptService {
|
|||
let mut mail_block_lines: Vec<String> = Vec::new();
|
||||
let mut in_line_continuation = false;
|
||||
|
||||
log::info!("Converting IF/THEN syntax, input has {} lines", script.lines().count());
|
||||
log::trace!("Converting IF/THEN syntax, input has {} lines", script.lines().count());
|
||||
|
||||
for line in script.lines() {
|
||||
let trimmed = line.trim();
|
||||
|
|
@ -1119,7 +1119,7 @@ impl ScriptService {
|
|||
} else {
|
||||
condition.to_string()
|
||||
};
|
||||
log::info!("Converting IF statement: condition='{}'", condition);
|
||||
log::trace!("Converting IF statement: condition='{}'", condition);
|
||||
result.push_str("if ");
|
||||
result.push_str(&condition);
|
||||
result.push_str(" {\n");
|
||||
|
|
@ -1129,7 +1129,7 @@ impl ScriptService {
|
|||
|
||||
// Handle ELSE
|
||||
if upper == "ELSE" {
|
||||
log::info!("Converting ELSE statement");
|
||||
log::trace!("Converting ELSE statement");
|
||||
result.push_str("} else {\n");
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1152,7 +1152,7 @@ impl ScriptService {
|
|||
} else {
|
||||
condition.to_string()
|
||||
};
|
||||
log::info!("Converting ELSEIF statement: condition='{}'", condition);
|
||||
log::trace!("Converting ELSEIF statement: condition='{}'", condition);
|
||||
result.push_str("} else if ");
|
||||
result.push_str(&condition);
|
||||
result.push_str(" {\n");
|
||||
|
|
@ -1161,7 +1161,7 @@ impl ScriptService {
|
|||
|
||||
// Handle END IF
|
||||
if upper == "END IF" {
|
||||
log::info!("Converting END IF statement");
|
||||
log::trace!("Converting END IF statement");
|
||||
if if_stack.pop().is_some() {
|
||||
result.push_str("}\n");
|
||||
}
|
||||
|
|
@ -1171,7 +1171,7 @@ impl ScriptService {
|
|||
// Handle WITH ... END WITH (BASIC object creation)
|
||||
if upper.starts_with("WITH ") {
|
||||
let object_name = &trimmed[5..].trim();
|
||||
log::info!("Converting WITH statement: object='{}'", object_name);
|
||||
log::trace!("Converting WITH statement: object='{}'", object_name);
|
||||
// Convert WITH obj → let obj = #{ (start object literal)
|
||||
result.push_str("let ");
|
||||
result.push_str(object_name);
|
||||
|
|
@ -1181,7 +1181,7 @@ impl ScriptService {
|
|||
}
|
||||
|
||||
if upper == "END WITH" {
|
||||
log::info!("Converting END WITH statement");
|
||||
log::trace!("Converting END WITH statement");
|
||||
result.push_str("};\n");
|
||||
in_with_block = false;
|
||||
continue;
|
||||
|
|
@ -1189,14 +1189,14 @@ impl ScriptService {
|
|||
|
||||
// Handle BEGIN TALK ... END TALK (multi-line TALK with ${} substitution)
|
||||
if upper == "BEGIN TALK" {
|
||||
log::info!("Converting BEGIN TALK statement");
|
||||
log::trace!("Converting BEGIN TALK statement");
|
||||
in_talk_block = true;
|
||||
talk_block_lines.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
if upper == "END TALK" {
|
||||
log::info!("Converting END TALK statement, processing {} lines", talk_block_lines.len());
|
||||
log::trace!("Converting END TALK statement, processing {} lines", talk_block_lines.len());
|
||||
in_talk_block = false;
|
||||
|
||||
// Split into multiple TALK statements to avoid expression complexity limit
|
||||
|
|
@ -1239,7 +1239,7 @@ impl ScriptService {
|
|||
// Handle BEGIN MAIL ... END MAIL (multi-line email with ${} substitution)
|
||||
if upper.starts_with("BEGIN MAIL ") {
|
||||
let recipient = &trimmed[11..].trim(); // Skip "BEGIN MAIL "
|
||||
log::info!("Converting BEGIN MAIL statement: recipient='{}'", recipient);
|
||||
log::trace!("Converting BEGIN MAIL statement: recipient='{}'", recipient);
|
||||
mail_recipient = recipient.to_string();
|
||||
in_mail_block = true;
|
||||
mail_block_lines.clear();
|
||||
|
|
@ -1247,7 +1247,7 @@ impl ScriptService {
|
|||
}
|
||||
|
||||
if upper == "END MAIL" {
|
||||
log::info!("Converting END MAIL statement, processing {} lines", mail_block_lines.len());
|
||||
log::trace!("Converting END MAIL statement, processing {} lines", mail_block_lines.len());
|
||||
in_mail_block = false;
|
||||
|
||||
// Process the mail block and convert to SEND EMAIL
|
||||
|
|
@ -1287,11 +1287,11 @@ impl ScriptService {
|
|||
|
||||
// Handle SAVE table, field1, field2, ... → INSERT "table", #{field1: value1, field2: value2, ...}
|
||||
if upper.starts_with("SAVE") && upper.contains(',') {
|
||||
log::info!("Processing SAVE line: '{}'", trimmed);
|
||||
log::trace!("Processing SAVE line: '{}'", trimmed);
|
||||
// Extract the part after "SAVE"
|
||||
let after_save = &trimmed[4..].trim(); // Skip "SAVE"
|
||||
let parts: Vec<&str> = after_save.split(',').collect();
|
||||
log::info!("SAVE parts: {:?}", parts);
|
||||
log::trace!("SAVE parts: {:?}", parts);
|
||||
|
||||
if parts.len() >= 2 {
|
||||
// First part is the table name (in quotes)
|
||||
|
|
@ -1301,7 +1301,7 @@ impl ScriptService {
|
|||
if parts.len() == 2 {
|
||||
let object_name = parts[1].trim().trim_end_matches(';');
|
||||
let converted = format!("INSERT \"{}\", {};\n", table, object_name);
|
||||
log::info!("Converted SAVE to INSERT (old syntax): '{}'", converted);
|
||||
log::trace!("Converted SAVE to INSERT (old syntax): '{}'", converted);
|
||||
result.push_str(&converted);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1310,7 +1310,7 @@ impl ScriptService {
|
|||
// The runtime SAVE handler will match them to database columns by position
|
||||
let values = parts[1..].join(", ");
|
||||
let converted = format!("SAVE \"{}\", {};\n", table, values);
|
||||
log::info!("Keeping SAVE syntax (modern): '{}'", converted);
|
||||
log::trace!("Keeping SAVE syntax (modern): '{}'", converted);
|
||||
result.push_str(&converted);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1319,17 +1319,17 @@ impl ScriptService {
|
|||
// Handle SEND EMAIL → send_mail (function call style)
|
||||
// Syntax: SEND EMAIL to, subject, body → send_mail(to, subject, body, [])
|
||||
if upper.starts_with("SEND EMAIL") {
|
||||
log::info!("Processing SEND EMAIL line: '{}'", trimmed);
|
||||
log::trace!("Processing SEND EMAIL line: '{}'", trimmed);
|
||||
let after_send = &trimmed[11..].trim(); // Skip "SEND EMAIL " (10 chars + space = 11)
|
||||
let parts: Vec<&str> = after_send.split(',').collect();
|
||||
log::info!("SEND EMAIL parts: {:?}", parts);
|
||||
log::trace!("SEND EMAIL parts: {:?}", parts);
|
||||
if parts.len() == 3 {
|
||||
let to = parts[0].trim();
|
||||
let subject = parts[1].trim();
|
||||
let body = parts[2].trim().trim_end_matches(';');
|
||||
// Convert to send_mail(to, subject, body, []) function call
|
||||
let converted = format!("send_mail({}, {}, {}, []);\n", to, subject, body);
|
||||
log::info!("Converted SEND EMAIL to: '{}'", converted);
|
||||
log::trace!("Converted SEND EMAIL to: '{}'", converted);
|
||||
result.push_str(&converted);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1397,7 +1397,7 @@ impl ScriptService {
|
|||
}
|
||||
}
|
||||
|
||||
log::info!("IF/THEN conversion complete, output has {} lines", result.lines().count());
|
||||
log::trace!("IF/THEN conversion complete, output has {} lines", result.lines().count());
|
||||
|
||||
// Convert BASIC <> (not equal) to Rhai != globally
|
||||
|
||||
|
|
@ -1417,7 +1417,7 @@ impl ScriptService {
|
|||
let lines: Vec<&str> = script.lines().collect();
|
||||
let mut i = 0;
|
||||
|
||||
log::info!("Converting SELECT/CASE syntax to if-else chains");
|
||||
log::trace!("Converting SELECT/CASE syntax to if-else chains");
|
||||
|
||||
// Helper function to strip 'let ' from the beginning of a line
|
||||
// This is needed because convert_if_then_syntax adds 'let' to all assignments,
|
||||
|
|
@ -1441,7 +1441,7 @@ impl ScriptService {
|
|||
if upper.starts_with("SELECT ") && !upper.contains(" THEN") {
|
||||
// Extract the variable being selected
|
||||
let select_var = trimmed[7..].trim(); // Skip "SELECT "
|
||||
log::info!("Converting SELECT statement for variable: '{}'", select_var);
|
||||
log::trace!("Converting SELECT statement for variable: '{}'", select_var);
|
||||
|
||||
// Skip the SELECT line
|
||||
i += 1;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ pub fn get_processes_to_kill() -> Vec<(&'static str, Vec<&'static str>)> {
|
|||
("botserver-stack/bin/meeting", vec!["-9", "-f"]),
|
||||
("botserver-stack/bin/vector_db", vec!["-9", "-f"]),
|
||||
("botserver-stack/bin/zitadel", vec!["-9", "-f"]),
|
||||
("botserver-stack/bin/alm", vec!["-9", "-f"]),
|
||||
("forgejo", vec!["-9", "-f"]),
|
||||
("caddy", vec!["-9", "-f"]),
|
||||
("postgres", vec!["-9", "-f"]),
|
||||
("minio", vec!["-9", "-f"]),
|
||||
|
|
@ -201,3 +203,25 @@ pub enum BotExistsResult {
|
|||
}
|
||||
|
||||
|
||||
|
||||
/// Check if Zitadel directory is healthy
|
||||
pub fn zitadel_health_check() -> bool {
|
||||
// Check if Zitadel is responding on port 9000
|
||||
if let Ok(output) = Command::new("curl")
|
||||
.args(["-f", "-s", "--connect-timeout", "2", "http://localhost:9000/debug/ready"])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: just check if port 9000 is listening
|
||||
match Command::new("nc")
|
||||
.args(["-z", "-w", "1", "127.0.0.1", "9000"])
|
||||
.output()
|
||||
{
|
||||
Ok(output) => output.status.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ impl AppConfig {
|
|||
let port = std::env::var("PORT")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u16>().ok())
|
||||
.unwrap_or_else(|| get_u16("server_port", 9000));
|
||||
.unwrap_or_else(|| get_u16("server_port", 8080));
|
||||
|
||||
Ok(Self {
|
||||
drive,
|
||||
|
|
@ -302,7 +302,7 @@ impl AppConfig {
|
|||
server: ServerConfig {
|
||||
host: get_str("server_host", "0.0.0.0"),
|
||||
port,
|
||||
base_url: config_map.get("server_base_url").cloned().unwrap_or_else(|| "http://localhost:9000".to_string()),
|
||||
base_url: config_map.get("server_base_url").cloned().unwrap_or_else(|| "http://localhost:8080".to_string()),
|
||||
},
|
||||
site_path: {
|
||||
ConfigManager::new(pool.clone()).get_config(
|
||||
|
|
@ -332,7 +332,7 @@ impl AppConfig {
|
|||
let port = std::env::var("PORT")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u16>().ok())
|
||||
.unwrap_or(9000);
|
||||
.unwrap_or(8080);
|
||||
|
||||
Ok(Self {
|
||||
drive: minio,
|
||||
|
|
@ -340,7 +340,7 @@ impl AppConfig {
|
|||
server: ServerConfig {
|
||||
host: "0.0.0.0".to_string(),
|
||||
port,
|
||||
base_url: "http://localhost:9000".to_string(),
|
||||
base_url: "http://localhost:8080".to_string(),
|
||||
},
|
||||
|
||||
site_path: "./botserver-stack/sites".to_string(),
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ pub async fn provision_user_handler(
|
|||
.config
|
||||
.as_ref()
|
||||
.map(|c| c.server.base_url.clone())
|
||||
.unwrap_or_else(|| "http://localhost:8300".to_string());
|
||||
.unwrap_or_else(|| "http://localhost:9000".to_string());
|
||||
|
||||
let provisioning = UserProvisioningService::new(state.conn.clone(), s3_client, base_url);
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ pub async fn deprovision_user_handler(
|
|||
.config
|
||||
.as_ref()
|
||||
.map(|c| c.server.base_url.clone())
|
||||
.unwrap_or_else(|| "http://localhost:8300".to_string());
|
||||
.unwrap_or_else(|| "http://localhost:9000".to_string());
|
||||
|
||||
let provisioning = UserProvisioningService::new(state.conn.clone(), s3_client, base_url);
|
||||
|
||||
|
|
@ -257,7 +257,7 @@ pub async fn check_services_status(State(state): State<Arc<AppState>>) -> impl I
|
|||
|
||||
let client = create_tls_client(Some(2));
|
||||
|
||||
if let Ok(response) = client.get("https://localhost:8300/healthz").send().await {
|
||||
if let Ok(response) = client.get("https://localhost:9000/healthz").send().await {
|
||||
status.directory = response.status().is_success();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -439,7 +439,7 @@ async fn get_bot_config(state: &AppState) -> HashMap<String, String> {
|
|||
|
||||
fn get_base_url(state: &AppState) -> String {
|
||||
let _ = state;
|
||||
"http://localhost:8300".to_string()
|
||||
"http://localhost:9000".to_string()
|
||||
}
|
||||
|
||||
async fn create_or_get_oauth_user(
|
||||
|
|
|
|||
|
|
@ -443,7 +443,7 @@ impl PackageManager {
|
|||
"directory".to_string(),
|
||||
ComponentConfig {
|
||||
name: "directory".to_string(),
|
||||
ports: vec![8300],
|
||||
ports: vec![9000],
|
||||
dependencies: vec!["tables".to_string()],
|
||||
linux_packages: vec![],
|
||||
macos_packages: vec![],
|
||||
|
|
@ -455,6 +455,8 @@ impl PackageManager {
|
|||
"mkdir -p {{LOGS_PATH}}".to_string(),
|
||||
],
|
||||
post_install_cmds_linux: vec![
|
||||
"cat > {{CONF_PATH}}/directory/steps.yaml << 'EOF'\n---\nDatabase:\n postgres:\n Host: localhost\n Port: 5432\n Database: zitadel\n User:\n Username: zitadel\n Password: zitadel\n SSL:\n Mode: disable\n Admin:\n Username: gbuser\n Password: {{DB_PASSWORD}}\n SSL:\n Mode: disable\nEOF".to_string(),
|
||||
"cat > {{CONF_PATH}}/directory/zitadel.yaml << 'EOF'\nLog:\n Level: info\n\nDatabase:\n postgres:\n Host: localhost\n Port: 5432\n Database: zitadel\n User:\n Username: zitadel\n Password: zitadel\n SSL:\n Mode: disable\n Admin:\n Username: gbuser\n Password: {{DB_PASSWORD}}\n SSL:\n Mode: disable\n\nMachine:\n Identification:\n Hostname: localhost\n WebhookAddress: http://localhost:8080\n\nPort: 8300\nExternalDomain: localhost\nExternalPort: 8300\nExternalSecure: false\n\nTLS:\n Enabled: false\nEOF".to_string(),
|
||||
|
||||
|
||||
|
||||
|
|
@ -471,12 +473,12 @@ impl PackageManager {
|
|||
env_vars: HashMap::from([
|
||||
("ZITADEL_EXTERNALSECURE".to_string(), "false".to_string()),
|
||||
("ZITADEL_EXTERNALDOMAIN".to_string(), "localhost".to_string()),
|
||||
("ZITADEL_EXTERNALPORT".to_string(), "8300".to_string()),
|
||||
("ZITADEL_EXTERNALPORT".to_string(), "9000".to_string()),
|
||||
("ZITADEL_TLS_ENABLED".to_string(), "false".to_string()),
|
||||
]),
|
||||
data_download_list: Vec::new(),
|
||||
exec_cmd: "ZITADEL_MASTERKEY=$(VAULT_ADDR=https://localhost:8200 VAULT_CACERT={{CONF_PATH}}/system/certificates/ca/ca.crt vault kv get -field=masterkey secret/gbo/directory 2>/dev/null || echo 'MasterkeyNeedsToHave32Characters') nohup {{BIN_PATH}}/zitadel start --config {{CONF_PATH}}/directory/zitadel.yaml --masterkeyFromEnv --tlsMode disabled > {{LOGS_PATH}}/zitadel.log 2>&1 &".to_string(),
|
||||
check_cmd: "curl -f --connect-timeout 2 -m 5 http://localhost:8300/healthz >/dev/null 2>&1".to_string(),
|
||||
check_cmd: "curl -f --connect-timeout 2 -m 5 http://localhost:9000/healthz >/dev/null 2>&1".to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ pub mod directory_setup;
|
|||
pub mod email_setup;
|
||||
pub mod vector_db_setup;
|
||||
|
||||
pub use directory_setup::{DirectorySetup, DefaultUser, CreateUserParams};
|
||||
pub use directory_setup::{DirectorySetup, DirectoryConfig, DefaultUser, CreateUserParams};
|
||||
pub use email_setup::EmailSetup;
|
||||
pub use vector_db_setup::VectorDbSetup;
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ impl SecretsManager {
|
|||
Ok((
|
||||
s.get("url")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "http://localhost:8300".into()),
|
||||
.unwrap_or_else(|| "http://localhost:9000".into()),
|
||||
s.get("project_id").cloned().unwrap_or_default(),
|
||||
s.get("client_id").cloned().unwrap_or_default(),
|
||||
s.get("client_secret").cloned().unwrap_or_default(),
|
||||
|
|
@ -338,7 +338,7 @@ impl SecretsManager {
|
|||
secrets.insert("password".into(), "changeme".into());
|
||||
}
|
||||
SecretPaths::DIRECTORY => {
|
||||
secrets.insert("url".into(), "http://localhost:8300".into());
|
||||
secrets.insert("url".into(), "http://localhost:9000".into());
|
||||
secrets.insert("project_id".into(), String::new());
|
||||
secrets.insert("client_id".into(), String::new());
|
||||
secrets.insert("client_secret".into(), String::new());
|
||||
|
|
|
|||
|
|
@ -482,7 +482,7 @@ impl InternalUrls {
|
|||
pub const DIRECTORY_BASE: &'static str = "http://localhost:9000";
|
||||
pub const DATABASE: &'static str = "postgres://localhost:5432";
|
||||
pub const CACHE: &'static str = "redis://localhost:6379";
|
||||
pub const DRIVE: &'static str = "https://localhost:9000";
|
||||
pub const DRIVE: &'static str = "https://localhost:9100";
|
||||
pub const EMAIL: &'static str = "http://localhost:8025";
|
||||
pub const LLM: &'static str = "http://localhost:8081";
|
||||
pub const EMBEDDING: &'static str = "http://localhost:8082";
|
||||
|
|
|
|||
|
|
@ -147,21 +147,30 @@ pub async fn login(
|
|||
)
|
||||
})?;
|
||||
|
||||
// Try to get admin token: first PAT file, then OAuth client credentials
|
||||
let pat_path = std::path::Path::new("./botserver-stack/conf/directory/admin-pat.txt");
|
||||
let admin_token = std::fs::read_to_string(pat_path)
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
if admin_token.is_empty() {
|
||||
error!("Admin PAT token not found");
|
||||
let admin_token = if admin_token.is_empty() {
|
||||
info!("Admin PAT token not found, using OAuth client credentials flow");
|
||||
match get_oauth_token(&http_client, &client).await {
|
||||
Ok(token) => token,
|
||||
Err(e) => {
|
||||
error!("Failed to get OAuth token: {}", e);
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: "Authentication service not configured".to_string(),
|
||||
details: None,
|
||||
details: Some("OAuth client credentials not available".to_string()),
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
admin_token
|
||||
};
|
||||
|
||||
let search_url = format!("{}/v2/users", client.api_url());
|
||||
let search_body = serde_json::json!({
|
||||
|
|
@ -765,3 +774,43 @@ async fn create_organization(
|
|||
Err(format!("Failed to create organization: {}", error_text))
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_oauth_token(
|
||||
http_client: &reqwest::Client,
|
||||
client: &crate::directory::client::ZitadelClient,
|
||||
) -> Result<String, String> {
|
||||
let token_url = format!("{}/oauth/v2/token", client.api_url());
|
||||
|
||||
let params = [
|
||||
("grant_type", "client_credentials".to_string()),
|
||||
("client_id", client.client_id().to_string()),
|
||||
("client_secret", client.client_secret().to_string()),
|
||||
("scope", "openid profile email urn:zitadel:iam:org:project:id:zitadel:aud".to_string()),
|
||||
];
|
||||
|
||||
let response = http_client
|
||||
.post(&token_url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to request OAuth token: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
return Err(format!("OAuth token request failed: {}", error_text));
|
||||
}
|
||||
|
||||
let token_data: serde_json::Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse OAuth token response: {}", e))?;
|
||||
|
||||
let access_token = token_data
|
||||
.get("access_token")
|
||||
.and_then(|t| t.as_str())
|
||||
.ok_or_else(|| "No access_token in OAuth response".to_string())?
|
||||
.to_string();
|
||||
|
||||
info!("Successfully obtained OAuth access token via client credentials");
|
||||
Ok(access_token)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,6 +144,23 @@ async fn create_bootstrap_admin(client: &ZitadelClient) -> Result<BootstrapResul
|
|||
}
|
||||
};
|
||||
|
||||
// Create a PAT token for the admin user for API access
|
||||
// Note: This requires OAuth client credentials. If not available, PAT creation
|
||||
// will be skipped, but admin user can still login with username/password.
|
||||
info!("Creating admin PAT token for API access...");
|
||||
match client.create_pat(&user_id, "Admin API Token", None).await {
|
||||
Ok(pat_token) => {
|
||||
info!("Admin PAT token created successfully");
|
||||
save_admin_pat_token(&pat_token);
|
||||
}
|
||||
Err(e) => {
|
||||
// PAT creation failure is not critical - user can still login
|
||||
// This happens when Zitadel doesn't have OAuth client configured yet
|
||||
warn!("Failed to create admin PAT token (non-critical): {}", e);
|
||||
info!("Admin user can still login with username/password. PAT can be created later via UI.");
|
||||
}
|
||||
}
|
||||
|
||||
let base_url = client.api_url();
|
||||
let setup_url = format!("{}/ui/login", base_url);
|
||||
|
||||
|
|
@ -250,7 +267,7 @@ fn save_setup_credentials(result: &BootstrapResult) {
|
|||
║ Password: {:<46}║
|
||||
║ Email: {:<46}║
|
||||
║ ║
|
||||
║ 🌐 LOGIN NOW: http://localhost:9000/suite/login ║
|
||||
║ 🌐 LOGIN NOW: http://localhost:8080/suite/login ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════════════════╝
|
||||
|
||||
|
|
@ -280,6 +297,32 @@ fn save_setup_credentials(result: &BootstrapResult) {
|
|||
}
|
||||
}
|
||||
|
||||
fn save_admin_pat_token(pat_token: &str) {
|
||||
// Create directory if it doesn't exist
|
||||
let pat_dir = std::path::Path::new("./botserver-stack/conf/directory");
|
||||
if let Err(e) = fs::create_dir_all(pat_dir) {
|
||||
error!("Failed to create PAT directory: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
let pat_path = pat_dir.join("admin-pat.txt");
|
||||
|
||||
match fs::write(&pat_path, pat_token) {
|
||||
Ok(_) => {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if let Err(e) = fs::set_permissions(&pat_path, fs::Permissions::from_mode(0o600)) {
|
||||
warn!("Failed to set PAT file permissions: {}", e);
|
||||
}
|
||||
}
|
||||
info!("Admin PAT token saved to: {}", pat_path.display());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to save admin PAT token: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_bootstrap_credentials(result: &BootstrapResult) {
|
||||
let separator = "═".repeat(60);
|
||||
|
||||
|
|
@ -313,7 +356,7 @@ fn print_bootstrap_credentials(result: &BootstrapResult) {
|
|||
println!("║{:^60}║", "");
|
||||
println!("║ {:56}║", "🌐 LOGIN NOW:");
|
||||
println!("║{:^60}║", "");
|
||||
println!("║ {:56}║", "http://localhost:9000/suite/login");
|
||||
println!("║ {:56}║", "http://localhost:8080/suite/login");
|
||||
println!("║{:^60}║", "");
|
||||
println!("╠{}╣", separator);
|
||||
println!("║{:^60}║", "");
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ pub struct ZitadelClient {
|
|||
http_client: reqwest::Client,
|
||||
access_token: Arc<RwLock<Option<String>>>,
|
||||
pat_token: Option<String>,
|
||||
/// Username and password for password grant OAuth flow
|
||||
password_credentials: Option<(String, String)>,
|
||||
}
|
||||
|
||||
impl ZitadelClient {
|
||||
|
|
@ -35,6 +37,28 @@ impl ZitadelClient {
|
|||
http_client,
|
||||
access_token: Arc::new(RwLock::new(None)),
|
||||
pat_token: None,
|
||||
password_credentials: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a client that uses password grant OAuth flow
|
||||
/// This is used for initial bootstrap with Zitadel's default admin user
|
||||
pub fn with_password_grant(
|
||||
config: ZitadelConfig,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> Result<Self> {
|
||||
let http_client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
http_client,
|
||||
access_token: Arc::new(RwLock::new(None)),
|
||||
pat_token: None,
|
||||
password_credentials: Some((username, password)),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +73,7 @@ impl ZitadelClient {
|
|||
http_client,
|
||||
access_token: Arc::new(RwLock::new(None)),
|
||||
pat_token: Some(pat_token),
|
||||
password_credentials: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -108,13 +133,24 @@ impl ZitadelClient {
|
|||
let token_url = format!("{}/oauth/v2/token", self.config.api_url);
|
||||
log::info!("Requesting access token from: {}", token_url);
|
||||
|
||||
let params = [
|
||||
("grant_type", "client_credentials"),
|
||||
("client_id", &self.config.client_id),
|
||||
("client_secret", &self.config.client_secret),
|
||||
("scope", "openid profile email"),
|
||||
// Build params dynamically based on auth method
|
||||
let mut params: Vec<(&str, String)> = vec![
|
||||
("client_id", self.config.client_id.clone()),
|
||||
("client_secret", self.config.client_secret.clone()),
|
||||
];
|
||||
|
||||
if let Some((username, password)) = &self.password_credentials {
|
||||
// Use password grant flow
|
||||
params.push(("grant_type", "password".to_string()));
|
||||
params.push(("username", username.clone()));
|
||||
params.push(("password", password.clone()));
|
||||
params.push(("scope", "openid profile email urn:zitadel:iam:org:project:id:zitadel:aud".to_string()));
|
||||
} else {
|
||||
// Use client credentials flow
|
||||
params.push(("grant_type", "client_credentials".to_string()));
|
||||
params.push(("scope", "openid profile email".to_string()));
|
||||
}
|
||||
|
||||
let response = self
|
||||
.http_client
|
||||
.post(&token_url)
|
||||
|
|
@ -533,4 +569,47 @@ impl ZitadelClient {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_pat(&self, user_id: &str, display_name: &str, expiration_date: Option<&str>) -> Result<String> {
|
||||
let token = self.get_access_token().await?;
|
||||
let url = format!("{}/v2/users/{}/pat", self.config.api_url, user_id);
|
||||
|
||||
let body = if let Some(expiry) = expiration_date {
|
||||
serde_json::json!({
|
||||
"displayName": display_name,
|
||||
"expirationDate": expiry
|
||||
})
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"displayName": display_name
|
||||
})
|
||||
};
|
||||
|
||||
let response = self
|
||||
.http_client
|
||||
.post(&url)
|
||||
.bearer_auth(&token)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to create PAT: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
return Err(anyhow!("Failed to create PAT: {}", error_text));
|
||||
}
|
||||
|
||||
let data: serde_json::Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to parse PAT response: {}", e))?;
|
||||
|
||||
let pat_token = data
|
||||
.get("token")
|
||||
.and_then(|t| t.as_str())
|
||||
.ok_or_else(|| anyhow!("No token in PAT response"))?
|
||||
.to_string();
|
||||
|
||||
Ok(pat_token)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ impl DriveMonitor {
|
|||
let mut file_states = self.file_states.write().await;
|
||||
let count = states.len();
|
||||
*file_states = states;
|
||||
info!(
|
||||
trace!(
|
||||
"[DRIVE_MONITOR] Loaded {} file states from disk for bot {}",
|
||||
count,
|
||||
self.bot_id
|
||||
|
|
@ -236,7 +236,7 @@ impl DriveMonitor {
|
|||
.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
|
||||
trace!("start_monitoring: calling check_for_changes...");
|
||||
info!("Calling initial check_for_changes...");
|
||||
trace!("Calling initial check_for_changes...");
|
||||
|
||||
match tokio::time::timeout(Duration::from_secs(300), self.check_for_changes()).await {
|
||||
Ok(Ok(_)) => {
|
||||
|
|
@ -262,15 +262,15 @@ impl DriveMonitor {
|
|||
|
||||
// Force enable periodic monitoring regardless of initial check result
|
||||
self.is_processing.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
info!("Forced is_processing to true for periodic monitoring");
|
||||
trace!("Forced is_processing to true for periodic monitoring");
|
||||
|
||||
let self_clone = self.clone(); // Don't wrap in Arc::new - that creates a copy
|
||||
tokio::spawn(async move {
|
||||
let mut consecutive_processing_failures = 0;
|
||||
info!("Starting periodic monitoring loop for bot {}", self_clone.bot_id);
|
||||
trace!("Starting periodic monitoring loop for bot {}", self_clone.bot_id);
|
||||
|
||||
let is_processing_state = self_clone.is_processing.load(std::sync::atomic::Ordering::SeqCst);
|
||||
info!("is_processing state at loop start: {} for bot {}", is_processing_state, self_clone.bot_id);
|
||||
trace!("is_processing state at loop start: {} for bot {}", is_processing_state, self_clone.bot_id);
|
||||
|
||||
while self_clone
|
||||
.is_processing
|
||||
|
|
@ -304,7 +304,7 @@ impl DriveMonitor {
|
|||
self_clone.consecutive_failures.swap(0, Ordering::Relaxed);
|
||||
consecutive_processing_failures = 0;
|
||||
if prev_failures > 0 {
|
||||
info!("S3/MinIO recovered for bucket {} after {} failures",
|
||||
trace!("S3/MinIO recovered for bucket {} after {} failures",
|
||||
self_clone.bucket_name, prev_failures);
|
||||
}
|
||||
}
|
||||
|
|
@ -336,15 +336,15 @@ impl DriveMonitor {
|
|||
}
|
||||
}
|
||||
|
||||
info!("Monitoring loop ended for bot {}", self_clone.bot_id);
|
||||
trace!("Monitoring loop ended for bot {}", self_clone.bot_id);
|
||||
});
|
||||
|
||||
info!("DriveMonitor started for bot {}", self.bot_id);
|
||||
trace!("DriveMonitor started for bot {}", self.bot_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop_monitoring(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
info!("Stopping DriveMonitor for bot {}", self.bot_id);
|
||||
trace!("Stopping DriveMonitor for bot {}", self.bot_id);
|
||||
|
||||
self.is_processing
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
|
|
@ -352,12 +352,12 @@ impl DriveMonitor {
|
|||
self.file_states.write().await.clear();
|
||||
self.consecutive_failures.store(0, Ordering::Relaxed);
|
||||
|
||||
info!("DriveMonitor stopped for bot {}", self.bot_id);
|
||||
trace!("DriveMonitor stopped for bot {}", self.bot_id);
|
||||
Ok(())
|
||||
}
|
||||
pub fn spawn(self: Arc<Self>) -> tokio::task::JoinHandle<()> {
|
||||
tokio::spawn(async move {
|
||||
info!(
|
||||
trace!(
|
||||
"Drive Monitor service started for bucket: {}",
|
||||
self.bucket_name
|
||||
);
|
||||
|
|
@ -387,7 +387,7 @@ impl DriveMonitor {
|
|||
Ok(_) => {
|
||||
let prev_failures = self.consecutive_failures.swap(0, Ordering::Relaxed);
|
||||
if prev_failures > 0 {
|
||||
info!("S3/MinIO recovered for bucket {} after {} failures",
|
||||
trace!("S3/MinIO recovered for bucket {} after {} failures",
|
||||
self.bucket_name, prev_failures);
|
||||
}
|
||||
}
|
||||
|
|
@ -634,7 +634,7 @@ impl DriveMonitor {
|
|||
let normalized_new_value = Self::normalize_config_value(new_value);
|
||||
|
||||
if normalized_old_value != normalized_new_value {
|
||||
info!(
|
||||
trace!(
|
||||
"Detected change in {} (old: {}, new: {})",
|
||||
key, normalized_old_value, normalized_new_value
|
||||
);
|
||||
|
|
@ -656,7 +656,7 @@ impl DriveMonitor {
|
|||
}
|
||||
|
||||
if llm_url_changed {
|
||||
info!("Broadcasting LLM configuration refresh");
|
||||
trace!("Broadcasting LLM configuration refresh");
|
||||
let effective_url = if !new_llm_url.is_empty() {
|
||||
new_llm_url
|
||||
} else {
|
||||
|
|
@ -672,7 +672,7 @@ impl DriveMonitor {
|
|||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
info!(
|
||||
trace!(
|
||||
"LLM configuration changed to: URL={}, Model={}",
|
||||
effective_url, effective_model
|
||||
);
|
||||
|
|
@ -697,7 +697,7 @@ impl DriveMonitor {
|
|||
Some(effective_endpoint_path),
|
||||
)
|
||||
.await;
|
||||
info!("Dynamic LLM provider updated with new configuration");
|
||||
trace!("Dynamic LLM provider updated with new configuration");
|
||||
} else {
|
||||
warn!("Dynamic LLM provider not available - config change ignored");
|
||||
}
|
||||
|
|
@ -788,7 +788,7 @@ impl DriveMonitor {
|
|||
client: &Client,
|
||||
file_path: &str,
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
info!(
|
||||
trace!(
|
||||
"Fetching object from Drive: bucket={}, key={}",
|
||||
&self.bucket_name, file_path
|
||||
);
|
||||
|
|
@ -800,7 +800,7 @@ impl DriveMonitor {
|
|||
.await
|
||||
{
|
||||
Ok(res) => {
|
||||
info!(
|
||||
trace!(
|
||||
"Successfully fetched object from Drive: bucket={}, key={}, size={}",
|
||||
&self.bucket_name,
|
||||
file_path,
|
||||
|
|
@ -838,22 +838,26 @@ impl DriveMonitor {
|
|||
let tool_name_clone = tool_name.clone();
|
||||
let source_content_clone = source_content.clone();
|
||||
let bot_id = self.bot_id;
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let elapsed_ms = tokio::task::spawn_blocking(move || {
|
||||
std::fs::create_dir_all(&work_dir_clone)?;
|
||||
let local_source_path = format!("{}/{}.bas", work_dir_clone, tool_name_clone);
|
||||
std::fs::write(&local_source_path, &source_content_clone)?;
|
||||
let mut compiler = BasicCompiler::new(state_clone, bot_id);
|
||||
let start_time = std::time::Instant::now();
|
||||
let result = compiler.compile_file(&local_source_path, &work_dir_str)?;
|
||||
let elapsed = start_time.elapsed().as_millis();
|
||||
if let Some(mcp_tool) = result.mcp_tool {
|
||||
info!(
|
||||
trace!(
|
||||
"MCP tool definition generated with {} parameters",
|
||||
mcp_tool.input_schema.properties.len()
|
||||
);
|
||||
}
|
||||
Ok::<(), Box<dyn Error + Send + Sync>>(())
|
||||
Ok::<u128, Box<dyn Error + Send + Sync>>(elapsed)
|
||||
})
|
||||
.await??;
|
||||
|
||||
info!("Successfully compiled {} in {} ms", tool_name, elapsed_ms);
|
||||
|
||||
// Check for USE WEBSITE commands and trigger immediate crawling
|
||||
if source_content.contains("USE WEBSITE") {
|
||||
self.trigger_immediate_website_crawl(&source_content).await?;
|
||||
|
|
@ -892,7 +896,7 @@ impl DriveMonitor {
|
|||
|
||||
let refresh_str = cap.get(2).map(|m| m.as_str()).unwrap_or("1m");
|
||||
|
||||
info!("Found USE WEBSITE command for {}, checking if crawl needed", url_str);
|
||||
trace!("Found USE WEBSITE command for {}, checking if crawl needed", url_str);
|
||||
|
||||
// Check if crawl is already in progress or recently completed
|
||||
let mut conn = self.state.conn.get()
|
||||
|
|
@ -1098,13 +1102,13 @@ impl DriveMonitor {
|
|||
if is_new || is_modified {
|
||||
if path.to_lowercase().ends_with(".pdf") {
|
||||
pdf_files_found += 1;
|
||||
info!(
|
||||
trace!(
|
||||
"Detected {} PDF in .gbkb: {} (will extract text for vectordb)",
|
||||
if is_new { "new" } else { "changed" },
|
||||
path
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
trace!(
|
||||
"Detected {} in .gbkb: {}",
|
||||
if is_new { "new file" } else { "change" },
|
||||
path
|
||||
|
|
@ -1148,7 +1152,7 @@ impl DriveMonitor {
|
|||
#[cfg(any(feature = "research", feature = "llm"))]
|
||||
{
|
||||
if !is_embedding_server_ready() {
|
||||
info!("Embedding server not ready, deferring KB indexing for {}", kb_folder_path.display());
|
||||
trace!("Embedding server not ready, deferring KB indexing for {}", kb_folder_path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -1178,7 +1182,7 @@ impl DriveMonitor {
|
|||
let kb_key_owned = kb_key.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
info!(
|
||||
trace!(
|
||||
"Triggering KB indexing for folder: {} (PDF text extraction enabled)",
|
||||
kb_folder_owned.display()
|
||||
);
|
||||
|
|
@ -1246,7 +1250,7 @@ impl DriveMonitor {
|
|||
}
|
||||
|
||||
if files_processed > 0 {
|
||||
info!(
|
||||
trace!(
|
||||
"Processed {} .gbkb files (including {} PDFs for text extraction)",
|
||||
files_processed, pdf_files_found
|
||||
);
|
||||
|
|
@ -1264,7 +1268,7 @@ impl DriveMonitor {
|
|||
});
|
||||
|
||||
for path in paths_to_remove {
|
||||
info!("Detected deletion in .gbkb: {}", path);
|
||||
trace!("Detected deletion in .gbkb: {}", path);
|
||||
file_states.remove(&path);
|
||||
|
||||
let path_parts: Vec<&str> = path.split('/').collect();
|
||||
|
|
@ -1321,7 +1325,7 @@ impl DriveMonitor {
|
|||
let bytes = response.body.collect().await?.into_bytes();
|
||||
tokio::fs::write(&local_path, bytes).await?;
|
||||
|
||||
info!(
|
||||
trace!(
|
||||
"Downloaded .gbkb file {} to {}",
|
||||
file_path,
|
||||
local_path.display()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::basic::compiler::BasicCompiler;
|
||||
use crate::core::shared::state::AppState;
|
||||
use diesel::prelude::*;
|
||||
use log::{debug, error, info, warn};
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -38,7 +38,7 @@ impl LocalFileMonitor {
|
|||
// Use /opt/gbo/data as the base directory for source files
|
||||
let data_dir = PathBuf::from("/opt/gbo/data");
|
||||
|
||||
info!("Initializing with data_dir: {:?}, work_root: {:?}", data_dir, work_root);
|
||||
trace!("Initializing with data_dir: {:?}, work_root: {:?}", data_dir, work_root);
|
||||
|
||||
Self {
|
||||
state,
|
||||
|
|
@ -50,7 +50,7 @@ impl LocalFileMonitor {
|
|||
}
|
||||
|
||||
pub async fn start_monitoring(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
info!("Starting local file monitor for /opt/gbo/data/*.gbai directories");
|
||||
trace!("Starting local file monitor for /opt/gbo/data/*.gbai directories");
|
||||
|
||||
// Create data directory if it doesn't exist
|
||||
if let Err(e) = tokio::fs::create_dir_all(&self.data_dir).await {
|
||||
|
|
@ -68,12 +68,12 @@ impl LocalFileMonitor {
|
|||
monitor.monitoring_loop().await;
|
||||
});
|
||||
|
||||
info!("Local file monitor started");
|
||||
trace!("Local file monitor started");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn monitoring_loop(&self) {
|
||||
info!("Starting monitoring loop");
|
||||
trace!("Starting monitoring loop");
|
||||
|
||||
// Try to create a file system watcher
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(100);
|
||||
|
|
@ -105,7 +105,7 @@ impl LocalFileMonitor {
|
|||
return;
|
||||
}
|
||||
|
||||
info!("Watching directory: {:?}", self.data_dir);
|
||||
trace!("Watching directory: {:?}", self.data_dir);
|
||||
|
||||
while self.is_processing.load(Ordering::SeqCst) {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
|
|
@ -116,7 +116,7 @@ impl LocalFileMonitor {
|
|||
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Any => {
|
||||
for path in &event.paths {
|
||||
if self.is_gbdialog_file(path) {
|
||||
info!("Detected change: {:?}", path);
|
||||
trace!("Detected change: {:?}", path);
|
||||
if let Err(e) = self.compile_local_file(path).await {
|
||||
error!("Failed to compile {:?}: {}", path, e);
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ impl LocalFileMonitor {
|
|||
EventKind::Remove(_) => {
|
||||
for path in &event.paths {
|
||||
if self.is_gbdialog_file(path) {
|
||||
info!("File removed: {:?}", path);
|
||||
trace!("File removed: {:?}", path);
|
||||
self.remove_file_state(path).await;
|
||||
}
|
||||
}
|
||||
|
|
@ -141,11 +141,11 @@ impl LocalFileMonitor {
|
|||
}
|
||||
}
|
||||
|
||||
info!("Monitoring loop ended");
|
||||
trace!("Monitoring loop ended");
|
||||
}
|
||||
|
||||
async fn polling_loop(&self) {
|
||||
info!("Using polling fallback (checking every 10s)");
|
||||
trace!("Using polling fallback (checking every 10s)");
|
||||
|
||||
while self.is_processing.load(Ordering::SeqCst) {
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
|
|
@ -231,7 +231,7 @@ impl LocalFileMonitor {
|
|||
};
|
||||
|
||||
if should_compile {
|
||||
info!("Compiling: {:?}", path);
|
||||
trace!("Compiling: {:?}", path);
|
||||
if let Err(e) = self.compile_local_file(&path).await {
|
||||
error!("Failed to compile {:?}: {}", path, e);
|
||||
}
|
||||
|
|
@ -285,7 +285,7 @@ impl LocalFileMonitor {
|
|||
.map_err(|e| format!("Failed to get bot_id for '{}': {}", bot_name_clone, e))?
|
||||
};
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let elapsed_ms = tokio::task::spawn_blocking(move || {
|
||||
std::fs::create_dir_all(&work_dir_clone)?;
|
||||
let local_source_path = work_dir_clone.join(format!("{}.bas", tool_name_clone));
|
||||
std::fs::write(&local_source_path, &source_content_clone)?;
|
||||
|
|
@ -294,19 +294,21 @@ impl LocalFileMonitor {
|
|||
.ok_or_else(|| "Invalid UTF-8 in local source path".to_string())?;
|
||||
let work_dir_str = work_dir_clone.to_str()
|
||||
.ok_or_else(|| "Invalid UTF-8 in work directory path".to_string())?;
|
||||
let start_time = std::time::Instant::now();
|
||||
let result = compiler.compile_file(local_source_str, work_dir_str)?;
|
||||
let elapsed_ms = start_time.elapsed().as_millis();
|
||||
if let Some(mcp_tool) = result.mcp_tool {
|
||||
info!(
|
||||
trace!(
|
||||
"[LOCAL_MONITOR] MCP tool generated with {} parameters for bot {}",
|
||||
mcp_tool.input_schema.properties.len(),
|
||||
bot_name_clone
|
||||
);
|
||||
}
|
||||
Ok::<(), Box<dyn Error + Send + Sync>>(())
|
||||
Ok::<u128, Box<dyn Error + Send + Sync>>(elapsed_ms)
|
||||
})
|
||||
.await??;
|
||||
|
||||
info!("Successfully compiled: {:?}", file_path);
|
||||
info!("Successfully compiled: {:?} in {} ms", file_path, elapsed_ms);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -317,7 +319,7 @@ impl LocalFileMonitor {
|
|||
}
|
||||
|
||||
pub async fn stop_monitoring(&self) {
|
||||
info!("Stopping local file monitor");
|
||||
trace!("Stopping local file monitor");
|
||||
self.is_processing.store(false, Ordering::SeqCst);
|
||||
self.file_states.write().await.clear();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use crate::core::config::ConfigManager;
|
||||
use crate::core::kb::embedding_generator::set_embedding_server_ready;
|
||||
use crate::core::shared::memory_monitor::{log_jemalloc_stats, MemoryStats};
|
||||
use crate::security::command_guard::SafeCommand;
|
||||
use crate::core::shared::models::schema::bots::dsl::*;
|
||||
|
|
@ -7,7 +6,6 @@ use crate::core::shared::state::AppState;
|
|||
use diesel::prelude::*;
|
||||
use log::{error, info, trace, warn};
|
||||
use reqwest;
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
use tokio;
|
||||
|
||||
|
|
@ -95,9 +93,10 @@ pub async fn ensure_llama_servers_running(
|
|||
};
|
||||
|
||||
// For llama-server startup, use path relative to botserver root
|
||||
// The models are in ./data/llm/ and the llama-server runs from botserver root
|
||||
let llm_model_path = format!("./data/llm/{}", llm_model);
|
||||
let embedding_model_path = format!("./data/llm/{}", embedding_model);
|
||||
// The models are in <stack_path>/data/llm/ and the llama-server runs from botserver root
|
||||
let stack_path = std::env::var("BOTSERVER_STACK_PATH").unwrap_or_else(|_| "./botserver-stack".to_string());
|
||||
let llm_model_path = format!("{stack_path}/data/llm/{}", llm_model);
|
||||
let embedding_model_path = format!("{stack_path}/data/llm/{}", embedding_model);
|
||||
if !llm_server_enabled {
|
||||
info!("Local LLM server management disabled (llm-server=false). Using external endpoints.");
|
||||
info!(" LLM URL: {llm_url}");
|
||||
|
|
@ -188,6 +187,37 @@ pub async fn ensure_llama_servers_running(
|
|||
} else if embedding_model.is_empty() {
|
||||
info!("EMBEDDING_MODEL not set, skipping Embedding server");
|
||||
}
|
||||
// Start servers in background - don't block HTTP server startup
|
||||
if !tasks.is_empty() {
|
||||
info!("LLM servers starting in background (non-blocking mode)");
|
||||
tokio::spawn(async move {
|
||||
for task in tasks {
|
||||
if let Err(e) = task.await {
|
||||
error!("LLM server task failed: {}", e);
|
||||
}
|
||||
}
|
||||
info!("LLM server startup tasks completed");
|
||||
});
|
||||
}
|
||||
|
||||
// Return immediately - don't wait for servers to be ready
|
||||
info!("LLM server initialization initiated (will start in background)");
|
||||
info!("HTTP server can start without waiting for LLM servers");
|
||||
trace!("ensure_llama_servers_running returning early (non-blocking)");
|
||||
|
||||
let end_mem = MemoryStats::current();
|
||||
trace!(
|
||||
"[LLM_LOCAL] ensure_llama_servers_running END (non-blocking), RSS={} (total delta={})",
|
||||
MemoryStats::format_bytes(end_mem.rss_bytes),
|
||||
MemoryStats::format_bytes(end_mem.rss_bytes.saturating_sub(start_mem.rss_bytes))
|
||||
);
|
||||
log_jemalloc_stats();
|
||||
|
||||
trace!("ensure_llama_servers_running EXIT OK (non-blocking)");
|
||||
return Ok(());
|
||||
|
||||
// OLD BLOCKING CODE - REMOVED TO PREVENT HTTP SERVER BLOCKING
|
||||
/*
|
||||
for task in tasks {
|
||||
task.await??;
|
||||
}
|
||||
|
|
@ -202,7 +232,7 @@ pub async fn ensure_llama_servers_running(
|
|||
let mut llm_ready = llm_running || llm_model.is_empty();
|
||||
let mut embedding_ready = embedding_running || embedding_model.is_empty();
|
||||
let mut attempts = 0;
|
||||
let max_attempts = 120;
|
||||
let max_attempts = 15; // Reduced from 120 to 15 (30 seconds instead of 240)
|
||||
while attempts < max_attempts && (!llm_ready || !embedding_ready) {
|
||||
trace!("Wait loop iteration {}", attempts);
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
|
@ -301,6 +331,7 @@ pub async fn ensure_llama_servers_running(
|
|||
}
|
||||
Err(error_msg.into())
|
||||
}
|
||||
*/ // END OF OLD BLOCKING CODE
|
||||
}
|
||||
pub async fn is_server_running(url: &str) -> bool {
|
||||
let client = reqwest::Client::builder()
|
||||
|
|
@ -377,8 +408,8 @@ pub fn start_llm_server(
|
|||
|
||||
let gpu_layers = config_manager
|
||||
.get_config(&default_bot_id, "llm-server-gpu-layers", None)
|
||||
.unwrap_or_else(|_| "20".to_string());
|
||||
let gpu_layers = if gpu_layers.is_empty() { "20".to_string() } else { gpu_layers };
|
||||
.unwrap_or_else(|_| "0".to_string());
|
||||
let gpu_layers = if gpu_layers.is_empty() { "0".to_string() } else { gpu_layers };
|
||||
|
||||
let reasoning_format = config_manager
|
||||
.get_config(&default_bot_id, "llm-server-reasoning-format", None)
|
||||
|
|
@ -501,7 +532,7 @@ pub async fn start_embedding_server(
|
|||
.arg("--host").arg("0.0.0.0")
|
||||
.arg("--port").arg(port)
|
||||
.arg("--embedding")
|
||||
.arg("--n-gpu-layers").arg("99")
|
||||
.arg("--n-gpu-layers").arg("0")
|
||||
.arg("--verbose");
|
||||
|
||||
if !cfg!(windows) {
|
||||
|
|
|
|||
|
|
@ -607,8 +607,11 @@ pub async fn create_app_state(
|
|||
fn init_directory_service() -> Result<(Arc<Mutex<crate::directory::AuthService>>, crate::directory::ZitadelConfig), std::io::Error> {
|
||||
let zitadel_config = {
|
||||
// Try to load from directory_config.json first
|
||||
let config_path = "./config/directory_config.json";
|
||||
if let Ok(content) = std::fs::read_to_string(config_path) {
|
||||
// Use same path as DirectorySetup saves to (BOTSERVER_STACK_PATH/conf/system/directory_config.json)
|
||||
let stack_path = std::env::var("BOTSERVER_STACK_PATH")
|
||||
.unwrap_or_else(|_| "./botserver-stack".to_string());
|
||||
let config_path = format!("{}/conf/system/directory_config.json", stack_path);
|
||||
if let Ok(content) = std::fs::read_to_string(&config_path) {
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
|
||||
let base_url = json
|
||||
.get("base_url")
|
||||
|
|
|
|||
|
|
@ -101,14 +101,14 @@ impl CorsConfig {
|
|||
Self {
|
||||
allowed_origins: vec![
|
||||
"http://localhost:3000".to_string(),
|
||||
"http://localhost:8080".to_string(),
|
||||
"http://localhost:9000".to_string(),
|
||||
"http://localhost:8300".to_string(),
|
||||
"http://127.0.0.1:3000".to_string(),
|
||||
"http://127.0.0.1:8080".to_string(),
|
||||
"http://127.0.0.1:9000".to_string(),
|
||||
"http://127.0.0.1:8300".to_string(),
|
||||
"https://localhost:3000".to_string(),
|
||||
"https://localhost:8080".to_string(),
|
||||
"https://localhost:9000".to_string(),
|
||||
"https://localhost:8300".to_string(),
|
||||
],
|
||||
allowed_methods: vec![
|
||||
Method::GET,
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ impl TlsIntegration {
|
|||
services.insert(
|
||||
"api".to_string(),
|
||||
ServiceUrls {
|
||||
original: "http://localhost:9000".to_string(),
|
||||
original: "http://localhost:8080".to_string(),
|
||||
secure: "https://localhost:8443".to_string(),
|
||||
port: 9000,
|
||||
port: 8080,
|
||||
tls_port: 8443,
|
||||
},
|
||||
);
|
||||
|
|
@ -95,10 +95,10 @@ impl TlsIntegration {
|
|||
services.insert(
|
||||
"minio".to_string(),
|
||||
ServiceUrls {
|
||||
original: "https://localhost:9000".to_string(),
|
||||
secure: "https://localhost:9000".to_string(),
|
||||
port: 9000,
|
||||
tls_port: 9000,
|
||||
original: "https://localhost:9100".to_string(),
|
||||
secure: "https://localhost:9100".to_string(),
|
||||
port: 9100,
|
||||
tls_port: 9100,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ pub struct ZitadelAuthConfig {
|
|||
impl Default for ZitadelAuthConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
issuer_url: "http://localhost:8300".to_string(),
|
||||
api_url: "http://localhost:8300".to_string(),
|
||||
issuer_url: "http://localhost:9000".to_string(),
|
||||
api_url: "http://localhost:9000".to_string(),
|
||||
client_id: String::new(),
|
||||
client_secret: String::new(),
|
||||
project_id: String::new(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue