From 37c3ae76c1286d71f727c5edc33d5300d649b6f1 Mon Sep 17 00:00:00 2001 From: Gabriel Bao Date: Fri, 15 May 2026 17:18:18 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(base/db/platform):=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E5=BA=95=E5=B1=82=E5=9F=BA=E5=BA=A7=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=AE=8C=E5=96=84=E3=80=81=E8=B7=A8=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E4=BC=98=E5=8C=96=E5=92=8C=E5=85=B6=E4=BB=96?= =?UTF-8?q?=E5=BA=95=E5=B1=82=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + Cargo.toml | 11 +- locales/en.yml | 3 + locales/zh-CN.yml | 3 + src/core/db/db_init.rs | 285 ++++++++++++++++++++ src/core/db/mod.rs | 390 ++++++++++++++++++++++++++++ src/db/mod.rs | 6 +- src/db/models/agent_arena.rs | 102 +++++++- src/db/models/agent_info.rs | 121 ++++++++- src/db/models/agent_mcp_rel.rs | 100 ++++++- src/db/models/ai_provider.rs | 118 ++++++++- src/db/models/chat_message.rs | 123 ++++++++- src/db/models/chat_session.rs | 107 +++++++- src/db/models/config.rs | 12 - src/db/models/llm_model.rs | 132 +++++++++- src/db/models/mcp_provider.rs | 121 ++++++++- src/db/models/mcp_tool.rs | 129 ++++++++- src/db/models/memory_info.rs | 116 ++++++++- src/db/models/mod.rs | 31 ++- src/db/models/project_info.rs | 110 +++++++- src/db/models/prompt_info.rs | 108 +++++++- src/db/models/prompt_placeholder.rs | 113 +++++++- src/db/models/system_api_log.rs | 117 ++++++++- src/db/models/system_config.rs | 162 +++++++++++- src/lib.rs | 17 +- src/main.rs | 24 +- src/platform/mod.rs | 44 +++- src/ui/layout.rs | 4 +- 28 files changed, 2463 insertions(+), 147 deletions(-) create mode 100644 src/core/db/db_init.rs delete mode 100644 src/db/models/config.rs diff --git a/Cargo.lock b/Cargo.lock index bec2645..407d4d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1436,6 +1436,7 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ + "cc", "pkg-config", "vcpkg", ] diff --git a/Cargo.toml b/Cargo.toml index 51dc8c7..c5a9033 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,13 @@ name = "OmegaCode" version = "0.1.0-alpha" edition = "2024" +[lib] +path = "src/lib.rs" + +[[bin]] +name = "OmegaCode" +path = "src/main.rs" + [dependencies] clap = "4.6.1" anyhow = "1.0.102" @@ -12,10 +19,10 @@ indicatif = "0.18.4" reqwest = "0.13.3" tokio-stream = "0.1.18" futures-util = "0.3.32" -serde = "1.0.228" +serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" rmcp = "1.7.0" -rusqlite = "0.39.0" +rusqlite = { version = "0.39.0", features = ["bundled"] } ratatui = "0.30.0" crossterm = "0.29.0" log = "0.4.29" diff --git a/locales/en.yml b/locales/en.yml index c6dbe9f..8b6bd1b 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -2,3 +2,6 @@ _version: 1 test_message: "Hello, %{name}" logger_is_initialized: "Logger is initialized." current_locale: "Current locale: %{locale_name}" +database_path: "Connecting to database at: %{database_path}" +database_initialized: "Database initialized." +database_table_create: "Database table %{table_name} is created." \ No newline at end of file diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 84fe8f3..7a724f9 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -2,3 +2,6 @@ _version: 1 test_message: "你好,%{name}" logger_is_initialized: "日志已经初始化了喵~" current_locale: "当前语言: %{locale_name}" +database_path: "数据库位置: %{database_path}" +database_initialized: "数据库初始化成功" +database_table_create: "数据表 %{table_name} 已创建" \ No newline at end of file diff --git a/src/core/db/db_init.rs b/src/core/db/db_init.rs new file mode 100644 index 0000000..115f1bd --- /dev/null +++ b/src/core/db/db_init.rs @@ -0,0 +1,285 @@ +use rusqlite::Connection; +use anyhow::{Context, Result}; +use log::{info}; +use rust_i18n::t; +use crate::core::db::Model; +use crate::db::models::{ + ai_provider::AiProvider, + llm_model::LLMModel, + prompt_info::PromptInfo, + agent_info::AgentInfo, + prompt_placeholder::PromptPlaceholder, + agent_arena::AgentArena, + chat_session::ChatSession, + chat_message::ChatMessage, + memory_info::MemoryInfo, + mcp_provider::McpProvider, + mcp_tool::McpTool, + agent_mcp_rel::AgentMcpRel, + project_info::ProjectInfo, + system_api_log::SystemApiLog, + system_config::SystemConfig +}; + +pub fn init_all_tables(conn: &Connection) -> Result<()> { + info!("Initializing database tables..."); + + create_table_ai_provider(conn) + .context("Failed to create ai_provider table")?; + create_table_llm_model(conn) + .context("Failed to create llm_model table")?; + create_table_prompt_info(conn) + .context("Failed to create prompt_info table")?; + create_table_agent_info(conn) + .context("Failed to create agent_info table")?; + create_table_prompt_placeholder(conn) + .context("Failed to create prompt_placeholder table")?; + create_table_agent_arena(conn) + .context("Failed to create agent_arena table")?; + create_table_chat_session(conn) + .context("Failed to create chat_session table")?; + create_table_chat_message(conn) + .context("Failed to create chat_message table")?; + create_table_memory_info(conn) + .context("Failed to create memory_info table")?; + create_table_mcp_provider(conn) + .context("Failed to create mcp_provider table")?; + create_table_mcp_tool(conn) + .context("Failed to create mcp_tool table")?; + create_table_agent_mcp_rel(conn) + .context("Failed to create agent_mcp_rel table")?; + create_table_project_info(conn) + .context("Failed to create project_info table")?; + create_table_system_api_log(conn) + .context("Failed to create system_api_log table")?; + create_table_system_config(conn) + .context("Failed to create system_config table")?; + create_indexes(conn) + .context("Failed to create indexes")?; + + info!("Database tables initialized successfully"); + Ok(()) +} + +// ============================================================ +// Create table +// ============================================================ +fn create_table_ai_provider(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='ai_provider'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(AiProvider::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "ai_provider")); + } + Ok(()) +} + +fn create_table_llm_model(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='llm_model'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(LLMModel::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "llm_model")); + } + Ok(()) +} + +fn create_table_prompt_info(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='prompt_info'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(PromptInfo::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "prompt_info")); + } + Ok(()) +} + +fn create_table_agent_info(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='agent_info'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(AgentInfo::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "agent_info")); + } + Ok(()) +} + +fn create_table_prompt_placeholder(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='prompt_placeholder'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(PromptPlaceholder::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "prompt_placeholder")); + } + Ok(()) +} + +fn create_table_agent_arena(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='agent_arena'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(AgentArena::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "agent_arena")); + } + Ok(()) +} + +fn create_table_chat_session(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='chat_session'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(ChatSession::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "chat_session")); + } + Ok(()) +} + +fn create_table_chat_message(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='chat_message'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(ChatMessage::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "chat_message")); + } + Ok(()) +} + +fn create_table_memory_info(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='memory_info'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(MemoryInfo::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "memory_info")); + } + Ok(()) +} + +fn create_table_mcp_provider(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='mcp_provider'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(McpProvider::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "mcp_provider")); + } + Ok(()) +} + +fn create_table_mcp_tool(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='mcp_tool'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(McpTool::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "mcp_tool")); + } + Ok(()) +} + +fn create_table_agent_mcp_rel(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='agent_mcp_rel'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(AgentMcpRel::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "agent_mcp_rel")); + } + Ok(()) +} + +fn create_table_project_info(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='project_info'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(ProjectInfo::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "project_info")); + } + Ok(()) +} + +fn create_table_system_api_log(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='system_api_log'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(SystemApiLog::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "system_api_log")); + } + Ok(()) +} + +fn create_table_system_config(conn: &Connection) -> Result<()> { + let exists: bool = conn.query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='system_config'", + [], + |row| row.get(0), + )?; + if !exists { + conn.execute_batch(SystemConfig::create_table_sql())?; + info!("{}", t!("database_table_create", table_name = "system_config")); + } + Ok(()) +} + +// ============================================================ +// Index +// ============================================================ + +fn create_indexes(conn: &Connection) -> Result<()> { + conn.execute_batch( + " + CREATE INDEX IF NOT EXISTS idx_chat_session_chat_uuid ON chat_session(chat_uuid); + CREATE INDEX IF NOT EXISTS idx_chat_message_session_id ON chat_message(session_id); + CREATE INDEX IF NOT EXISTS idx_agent_arena_agent_id ON agent_arena(agent_id); + CREATE INDEX IF NOT EXISTS idx_agent_arena_arena_id ON agent_arena(arena_id); + CREATE INDEX IF NOT EXISTS idx_memory_info_session_id ON memory_info(session_id); + CREATE INDEX IF NOT EXISTS idx_agent_mcp_rel_agent_id ON agent_mcp_rel(agent_id); + CREATE INDEX IF NOT EXISTS idx_agent_mcp_rel_mcp_id ON agent_mcp_rel(mcp_id); + CREATE INDEX IF NOT EXISTS idx_prompt_placeholder_prompt_id ON prompt_placeholder(prompt_id); + CREATE INDEX IF NOT EXISTS idx_llm_model_provider_id ON llm_model(provider_id); + CREATE INDEX IF NOT EXISTS idx_mcp_tool_provider_id ON mcp_tool(provider_id); + CREATE INDEX IF NOT EXISTS idx_agent_info_model_id ON agent_info(model_id); + CREATE INDEX IF NOT EXISTS idx_agent_info_prompt_id ON agent_info(prompt_id); + ", + )?; + info!("{}", t!("database_table_create", table_name = "table_indexes")); + Ok(()) +} diff --git a/src/core/db/mod.rs b/src/core/db/mod.rs index e69de29..3e9645d 100644 --- a/src/core/db/mod.rs +++ b/src/core/db/mod.rs @@ -0,0 +1,390 @@ +use anyhow::{Context, Result}; +use log::{debug, error, info}; +use rusqlite::{ + Connection, + OptionalExtension, + Result as RusqliteResult, + Row, + ToSql, +}; +use rust_i18n::t; + +use crate::platform::get_database_path; + +mod db_init; + +/// Model Trait +pub trait Model: Sized + Clone { + fn table_name() -> &'static str; + + fn create_table_sql() -> &'static str; + + fn from_row(row: &Row) -> RusqliteResult; + + fn insert_columns() -> &'static str; + + fn insert_values(&self) -> Vec>; + + fn update_set_clause(&self) -> String; + + fn update_values(&self) -> Vec>; + + fn primary_key_column() -> &'static str { + "id" + } + + fn primary_key_value(&self) -> Box; +} + +/// DatabaseManager +pub struct DatabaseManager { + pub conn: Connection, +} + +impl DatabaseManager { + /// init database + pub fn new() -> Result { + let db_path = get_database_path()?; + + let conn = Connection::open(&db_path) + .with_context(|| format!("Failed to open database: {}", db_path))?; + + info!( + "{}", + t!("database_path", database_path = &db_path) + ); + + db_init::init_all_tables(&conn) + .context("Failed to initialize database")?; + + info!("{}", t!("database_initialized")); + + Ok(Self { conn }) + } + + /// health + pub fn health_check(&self) -> Result<()> { + info!("HEALTH CHECK starting"); + + let sql = "SELECT 1"; + debug!("SQL => {}", sql); + + self.conn.query_row(sql, [], |row| { + let val: i32 = row.get(0)?; + Ok(val) + })?; + + let version_sql = "SELECT sqlite_version()"; + debug!("SQL => {}", version_sql); + + let sqlite_version: String = self.conn.query_row(version_sql, [], |row| row.get(0))?; + + info!( + "HEALTH CHECK success - SQLite version: {}", + sqlite_version + ); + + Ok(()) + } + + /// create + pub fn create(&self, model: T) -> Result + where + T: Model, + { + let values = model.insert_values(); + + let placeholders = + vec!["?"; values.len()].join(", "); + + let sql = format!( + "INSERT INTO {} ({}) VALUES ({})", + T::table_name(), + T::insert_columns(), + placeholders + ); + + debug!("SQL => {}", sql); + + let params: Vec<&dyn ToSql> = + values.iter().map(|v| v.as_ref()).collect(); + + match self.conn.execute(&sql, params.as_slice()) { + Ok(_) => { + let id = self.conn.last_insert_rowid(); + + info!( + "INSERT success table={} id={}", + T::table_name(), + id + ); + + Ok(id) + } + + Err(err) => { + error!( + "INSERT failed table={} error={}", + T::table_name(), + err + ); + + Err(err.into()) + } + } + } + + /// read by id + pub fn read_by_id(&self, id: i32) -> Result> + where + T: Model, + { + let sql = format!( + "SELECT * FROM {} WHERE {} = ?1", + T::table_name(), + T::primary_key_column() + ); + + debug!("SQL => {}", sql); + + match self + .conn + .query_row(&sql, [id], T::from_row) + .optional() + { + Ok(data) => { + info!( + "SELECT success table={} id={}", + T::table_name(), + id + ); + + Ok(data) + } + + Err(err) => { + error!( + "SELECT failed table={} id={} error={}", + T::table_name(), + id, + err + ); + + Err(err.into()) + } + } + } + + /// read all + pub fn read_all(&self) -> Result> + where + T: Model, + { + let sql = format!( + "SELECT * FROM {}", + T::table_name() + ); + + debug!("SQL => {}", sql); + + let mut stmt = self.conn.prepare(&sql)?; + + let iter = stmt.query_map([], T::from_row)?; + + let mut result = vec![]; + + for item in iter { + result.push(item?); + } + + info!( + "SELECT ALL success table={} count={}", + T::table_name(), + result.len() + ); + + Ok(result) + } + + /// pagination + pub fn paginate( + &self, + page: Option, + page_size: Option, + ) -> Result<(Vec, usize)> + where + T: Model, + { + let page = page.unwrap_or(1); + + let page_size = page_size.unwrap_or(10); + + let offset = (page - 1) * page_size; + + let count_sql = format!( + "SELECT COUNT(*) FROM {}", + T::table_name() + ); + + debug!("SQL => {}", count_sql); + + let total: i64 = + self.conn.query_row(&count_sql, [], |row| { + row.get(0) + })?; + + let total = total as usize; + + let sql = format!( + "SELECT * FROM {} LIMIT ? OFFSET ?", + T::table_name() + ); + + debug!("SQL => {}", sql); + + let mut stmt = self.conn.prepare(&sql)?; + + let iter = stmt.query_map( + [ + &(page_size as i64) as &dyn ToSql, + &(offset as i64), + ], + T::from_row, + )?; + + let mut result = vec![]; + + for item in iter { + result.push(item?); + } + + info!( + "PAGINATE success table={} total={} page={} size={}", + T::table_name(), + total, + page, + page_size + ); + + Ok((result, total)) + } + + /// where query + pub fn find_where( + &self, + where_sql: &str, + params: &[&dyn ToSql], + ) -> Result> + where + T: Model, + { + let sql = format!( + "SELECT * FROM {} WHERE {}", + T::table_name(), + where_sql + ); + + debug!("SQL => {}", sql); + + let mut stmt = self.conn.prepare(&sql)?; + + let iter = stmt.query_map( + params, + T::from_row, + )?; + + let mut result = vec![]; + + for item in iter { + result.push(item?); + } + + info!( + "WHERE QUERY success table={} count={}", + T::table_name(), + result.len() + ); + + Ok(result) + } + + /// update + pub fn update(&self, model: T) -> Result + where + T: Model, + { + let mut values = model.update_values(); + + values.push(model.primary_key_value()); + + let sql = format!( + "UPDATE {} SET {} WHERE {} = ?", + T::table_name(), + model.update_set_clause(), + T::primary_key_column() + ); + + debug!("SQL => {}", sql); + + let params: Vec<&dyn ToSql> = + values.iter().map(|v| v.as_ref()).collect(); + + match self.conn.execute(&sql, params.as_slice()) { + Ok(rows) => { + info!( + "UPDATE success table={} affected={}", + T::table_name(), + rows + ); + + Ok(rows) + } + + Err(err) => { + error!( + "UPDATE failed table={} error={}", + T::table_name(), + err + ); + + Err(err.into()) + } + } + } + + /// delete + pub fn delete(&self, id: i32) -> Result + where + T: Model, + { + let sql = format!( + "DELETE FROM {} WHERE {} = ?1", + T::table_name(), + T::primary_key_column() + ); + + debug!("SQL => {}", sql); + + match self.conn.execute(&sql, [id]) { + Ok(rows) => { + info!( + "DELETE success table={} affected={}", + T::table_name(), + rows + ); + + Ok(rows) + } + + Err(err) => { + error!( + "DELETE failed table={} error={}", + T::table_name(), + err + ); + + Err(err.into()) + } + } + } +} \ No newline at end of file diff --git a/src/db/mod.rs b/src/db/mod.rs index 8002c18..33f612f 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,3 +1,3 @@ -mod models; -mod store; -mod r#enum; \ No newline at end of file +pub mod models; +pub mod store; +pub mod r#enum; \ No newline at end of file diff --git a/src/db/models/agent_arena.rs b/src/db/models/agent_arena.rs index c9af86d..7ba42ac 100644 --- a/src/db/models/agent_arena.rs +++ b/src/db/models/agent_arena.rs @@ -1,14 +1,102 @@ use chrono::{DateTime, Utc}; +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] pub struct AgentArena { pub id: i32, - pub chat_uuid: String, - pub round_uuid: String, pub agent_id: i32, - pub vote_num: i8, - pub vote_type: i8, - pub comment: Option, - pub status: i8, + pub arena_id: i32, pub created_at: DateTime, pub updated_at: DateTime, pub remark: Option, -} \ No newline at end of file +} + +impl Model for AgentArena { + fn table_name() -> &'static str { + "agent_arena" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS agent_arena ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + agent_id INTEGER NOT NULL, + arena_id INTEGER NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at: String = row.get("created_at")?; + let updated_at: String = row.get("updated_at")?; + + Ok(Self { + id: row.get("id")?, + agent_id: row.get("agent_id")?, + arena_id: row.get("arena_id")?, + created_at: DateTime::parse_from_rfc3339(&created_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + updated_at: DateTime::parse_from_rfc3339(&updated_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "agent_id, arena_id, remark" + } + + fn insert_values(&self) -> Vec> { + vec![ + Box::new(self.agent_id), + Box::new(self.arena_id), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + agent_id = ?, + arena_id = ?, + updated_at = CURRENT_TIMESTAMP, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values(&self) -> Vec> { + vec![ + Box::new(self.agent_id), + Box::new(self.arena_id), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_value(&self) -> Box { + Box::new(self.id) + } +} diff --git a/src/db/models/agent_info.rs b/src/db/models/agent_info.rs index e8a5087..29c8e22 100644 --- a/src/db/models/agent_info.rs +++ b/src/db/models/agent_info.rs @@ -1,17 +1,120 @@ use chrono::{DateTime, Utc}; +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] pub struct AgentInfo { pub id: i32, - pub llm_id: i32, pub name: String, - pub description: Option, - pub prompt_id: i32, pub avatar: String, - pub call_count: i64,// default 0 - pub avg_score: f32,// default 0.0 - pub temperature: f32, // default 0.7 - pub top_p: f32, // default 0.95 - pub status: i8,// default 1 + pub model_id: i32, + pub prompt_id: i32, + pub status: i8, // default 1 pub created_at: DateTime, pub updated_at: DateTime, pub remark: Option, -} \ No newline at end of file +} + +impl Model for AgentInfo { + fn table_name() -> &'static str { + "agent_info" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS agent_info ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + avatar TEXT NOT NULL, + model_id INTEGER NOT NULL, + prompt_id INTEGER NOT NULL, + status INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at: String = row.get("created_at")?; + let updated_at: String = row.get("updated_at")?; + + Ok(Self { + id: row.get("id")?, + name: row.get("name")?, + avatar: row.get("avatar")?, + model_id: row.get("model_id")?, + prompt_id: row.get("prompt_id")?, + status: row.get("status")?, + created_at: DateTime::parse_from_rfc3339(&created_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + updated_at: DateTime::parse_from_rfc3339(&updated_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "name, avatar, model_id, prompt_id, status, remark" + } + + fn insert_values(&self) -> Vec> { + vec![ + Box::new(self.name.clone()), + Box::new(self.avatar.clone()), + Box::new(self.model_id), + Box::new(self.prompt_id), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + name = ?, + avatar = ?, + model_id = ?, + prompt_id = ?, + status = ?, + updated_at = CURRENT_TIMESTAMP, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values(&self) -> Vec> { + vec![ + Box::new(self.name.clone()), + Box::new(self.avatar.clone()), + Box::new(self.model_id), + Box::new(self.prompt_id), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_value(&self) -> Box { + Box::new(self.id) + } +} diff --git a/src/db/models/agent_mcp_rel.rs b/src/db/models/agent_mcp_rel.rs index 9840ee5..0b6c1a6 100644 --- a/src/db/models/agent_mcp_rel.rs +++ b/src/db/models/agent_mcp_rel.rs @@ -1,10 +1,102 @@ use chrono::{DateTime, Utc}; -pub struct AgentMCPRel { +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] +pub struct AgentMcpRel { pub id: i32, pub agent_id: i32, - pub mcp_tool_id: i32, - pub status: i8,// default 1 + pub mcp_id: i32, pub created_at: DateTime, pub updated_at: DateTime, pub remark: Option, -} \ No newline at end of file +} + +impl Model for AgentMcpRel { + fn table_name() -> &'static str { + "agent_mcp_rel" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS agent_mcp_rel ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + agent_id INTEGER NOT NULL, + mcp_id INTEGER NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at: String = row.get("created_at")?; + let updated_at: String = row.get("updated_at")?; + + Ok(Self { + id: row.get("id")?, + agent_id: row.get("agent_id")?, + mcp_id: row.get("mcp_id")?, + created_at: DateTime::parse_from_rfc3339(&created_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + updated_at: DateTime::parse_from_rfc3339(&updated_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "agent_id, mcp_id, remark" + } + + fn insert_values(&self) -> Vec> { + vec![ + Box::new(self.agent_id), + Box::new(self.mcp_id), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + agent_id = ?, + mcp_id = ?, + updated_at = CURRENT_TIMESTAMP, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values(&self) -> Vec> { + vec![ + Box::new(self.agent_id), + Box::new(self.mcp_id), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_value(&self) -> Box { + Box::new(self.id) + } +} diff --git a/src/db/models/ai_provider.rs b/src/db/models/ai_provider.rs index e0c4a7d..b49f9d1 100644 --- a/src/db/models/ai_provider.rs +++ b/src/db/models/ai_provider.rs @@ -1,13 +1,127 @@ use chrono::{DateTime, Utc}; +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] pub struct AiProvider { pub id: i32, pub name: String, pub avatar: String, pub api: String, pub key: Option, - pub timeout: i32,//ms default 5000 - pub status: i8,// default 1 + pub timeout: i32, + pub status: i8, pub created_at: DateTime, pub updated_at: DateTime, pub remark: Option, +} + +impl Model for AiProvider { + fn table_name() -> &'static str { + "ai_provider" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS ai_provider ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + avatar TEXT NOT NULL, + api TEXT NOT NULL, + key TEXT, + timeout INTEGER NOT NULL DEFAULT 5000, + status INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at: String = row.get("created_at")?; + + let updated_at: String = row.get("updated_at")?; + + Ok(Self { + id: row.get("id")?, + name: row.get("name")?, + avatar: row.get("avatar")?, + api: row.get("api")?, + key: row.get("key")?, + timeout: row.get("timeout")?, + status: row.get("status")?, + created_at: DateTime::parse_from_rfc3339(&created_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + updated_at: DateTime::parse_from_rfc3339(&updated_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "name, avatar, api, key, timeout, status, remark" + } + + fn insert_values(&self) -> Vec> { + vec![ + Box::new(self.name.clone()), + Box::new(self.avatar.clone()), + Box::new(self.api.clone()), + Box::new(self.key.clone()), + Box::new(self.timeout), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + name = ?, + avatar = ?, + api = ?, + key = ?, + timeout = ?, + status = ?, + updated_at = CURRENT_TIMESTAMP, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values(&self) -> Vec> { + vec![ + Box::new(self.name.clone()), + Box::new(self.avatar.clone()), + Box::new(self.api.clone()), + Box::new(self.key.clone()), + Box::new(self.timeout), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_value(&self) -> Box { + Box::new(self.id) + } } \ No newline at end of file diff --git a/src/db/models/chat_message.rs b/src/db/models/chat_message.rs index 17b5ac4..d5d6627 100644 --- a/src/db/models/chat_message.rs +++ b/src/db/models/chat_message.rs @@ -1,17 +1,120 @@ use chrono::{DateTime, Utc}; -use crate::db::r#enum::PromptRole; +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] pub struct ChatMessage { pub id: i32, - pub chat_uuid: String, - pub round_uuid: String, - pub role: PromptRole, + pub session_id: String, + pub role: String, pub content: String, - pub input_tokens: i32, // default 0 - pub output_tokens: i32,// default 0 - pub duration: i32,// default 0 - pub model_config: String, - pub status: i8,// default 1 + pub model: Option, + pub status: i8, // default 1 pub created_at: DateTime, pub updated_at: DateTime, pub remark: Option, -} \ No newline at end of file +} + +impl Model for ChatMessage { + fn table_name() -> &'static str { + "chat_message" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS chat_message ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL, + model TEXT, + status INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at: String = row.get("created_at")?; + let updated_at: String = row.get("updated_at")?; + + Ok(Self { + id: row.get("id")?, + session_id: row.get("session_id")?, + role: row.get("role")?, + content: row.get("content")?, + model: row.get("model")?, + status: row.get("status")?, + created_at: DateTime::parse_from_rfc3339(&created_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + updated_at: DateTime::parse_from_rfc3339(&updated_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "session_id, role, content, model, status, remark" + } + + fn insert_values(&self) -> Vec> { + vec![ + Box::new(self.session_id.clone()), + Box::new(self.role.clone()), + Box::new(self.content.clone()), + Box::new(self.model.clone()), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + session_id = ?, + role = ?, + content = ?, + model = ?, + status = ?, + updated_at = CURRENT_TIMESTAMP, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values(&self) -> Vec> { + vec![ + Box::new(self.session_id.clone()), + Box::new(self.role.clone()), + Box::new(self.content.clone()), + Box::new(self.model.clone()), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_value(&self) -> Box { + Box::new(self.id) + } +} diff --git a/src/db/models/chat_session.rs b/src/db/models/chat_session.rs index eb7cb40..dcefd5a 100644 --- a/src/db/models/chat_session.rs +++ b/src/db/models/chat_session.rs @@ -1,11 +1,114 @@ use chrono::{DateTime, Utc}; +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] pub struct ChatSession { pub id: i32, pub chat_uuid: String, pub title: Option, pub llm_id: i32, - pub status: i8,// default 1 + pub status: i8, // default 1 pub created_at: DateTime, pub updated_at: DateTime, pub remark: Option, -} \ No newline at end of file +} + +impl Model for ChatSession { + fn table_name() -> &'static str { + "chat_session" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS chat_session ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chat_uuid TEXT NOT NULL, + title TEXT, + llm_id INTEGER NOT NULL, + status INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at: String = row.get("created_at")?; + let updated_at: String = row.get("updated_at")?; + + Ok(Self { + id: row.get("id")?, + chat_uuid: row.get("chat_uuid")?, + title: row.get("title")?, + llm_id: row.get("llm_id")?, + status: row.get("status")?, + created_at: DateTime::parse_from_rfc3339(&created_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + updated_at: DateTime::parse_from_rfc3339(&updated_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "chat_uuid, title, llm_id, status, remark" + } + + fn insert_values(&self) -> Vec> { + vec![ + Box::new(self.chat_uuid.clone()), + Box::new(self.title.clone()), + Box::new(self.llm_id), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + chat_uuid = ?, + title = ?, + llm_id = ?, + status = ?, + updated_at = CURRENT_TIMESTAMP, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values(&self) -> Vec> { + vec![ + Box::new(self.chat_uuid.clone()), + Box::new(self.title.clone()), + Box::new(self.llm_id), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_value(&self) -> Box { + Box::new(self.id) + } +} diff --git a/src/db/models/config.rs b/src/db/models/config.rs deleted file mode 100644 index 46e5ea4..0000000 --- a/src/db/models/config.rs +++ /dev/null @@ -1,12 +0,0 @@ -use chrono::{DateTime, Utc}; -pub struct Config { - pub key: String, - pub value: String, - pub description: String, - pub key_type: String, - pub tag: String, - pub sort: i32,// default 0 - pub created_at: DateTime, - pub updated_at: DateTime, - pub remark: Option, -} diff --git a/src/db/models/llm_model.rs b/src/db/models/llm_model.rs index d0b83dd..1be9e4b 100644 --- a/src/db/models/llm_model.rs +++ b/src/db/models/llm_model.rs @@ -1,4 +1,13 @@ use chrono::{DateTime, Utc}; +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] pub struct LLMModel { pub id: i32, pub provider_id: i32, @@ -9,8 +18,127 @@ pub struct LLMModel { pub max_response_tokens: i32, // default 0 pub weight: i32, // default 0 pub call_count: i64, // default 0 - pub status: i8,// default 1 + pub status: i8, // default 1 pub created_at: DateTime, pub updated_at: DateTime, pub remark: Option, -} \ No newline at end of file +} + +impl Model for LLMModel { + fn table_name() -> &'static str { + "llm_model" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS llm_model ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + provider_id INTEGER NOT NULL, + name TEXT, + avatar TEXT NOT NULL, + model_code TEXT NOT NULL, + max_tokens INTEGER NOT NULL DEFAULT 0, + max_response_tokens INTEGER NOT NULL DEFAULT 0, + weight INTEGER NOT NULL DEFAULT 0, + call_count INTEGER NOT NULL DEFAULT 0, + status INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at: String = row.get("created_at")?; + let updated_at: String = row.get("updated_at")?; + + Ok(Self { + id: row.get("id")?, + provider_id: row.get("provider_id")?, + name: row.get("name")?, + avatar: row.get("avatar")?, + model_code: row.get("model_code")?, + max_tokens: row.get("max_tokens")?, + max_response_tokens: row.get("max_response_tokens")?, + weight: row.get("weight")?, + call_count: row.get("call_count")?, + status: row.get("status")?, + created_at: DateTime::parse_from_rfc3339(&created_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + updated_at: DateTime::parse_from_rfc3339(&updated_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "provider_id, name, avatar, model_code, max_tokens, max_response_tokens, weight, call_count, status, remark" + } + + fn insert_values(&self) -> Vec> { + vec![ + Box::new(self.provider_id), + Box::new(self.name.clone()), + Box::new(self.avatar.clone()), + Box::new(self.model_code.clone()), + Box::new(self.max_tokens), + Box::new(self.max_response_tokens), + Box::new(self.weight), + Box::new(self.call_count), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + provider_id = ?, + name = ?, + avatar = ?, + model_code = ?, + max_tokens = ?, + max_response_tokens = ?, + weight = ?, + call_count = ?, + status = ?, + updated_at = CURRENT_TIMESTAMP, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values(&self) -> Vec> { + vec![ + Box::new(self.provider_id), + Box::new(self.name.clone()), + Box::new(self.avatar.clone()), + Box::new(self.model_code.clone()), + Box::new(self.max_tokens), + Box::new(self.max_response_tokens), + Box::new(self.weight), + Box::new(self.call_count), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_value(&self) -> Box { + Box::new(self.id) + } +} diff --git a/src/db/models/mcp_provider.rs b/src/db/models/mcp_provider.rs index 24dae7e..60719e4 100644 --- a/src/db/models/mcp_provider.rs +++ b/src/db/models/mcp_provider.rs @@ -1,13 +1,126 @@ use chrono::{DateTime, Utc}; -pub struct MCPProvider { +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] +pub struct McpProvider { pub id: i32, pub name: String, pub url: String, pub auth_key: Option, - pub timeout: i32,// ms default 5000 + pub timeout: i32, // ms default 5000 pub health_api: Option, - pub status: i8,// default 1 + pub status: i8, // default 1 pub created_at: DateTime, pub updated_at: DateTime, pub remark: Option, -} \ No newline at end of file +} + +impl Model for McpProvider { + fn table_name() -> &'static str { + "mcp_provider" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS mcp_provider ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + url TEXT NOT NULL, + auth_key TEXT, + timeout INTEGER NOT NULL DEFAULT 5000, + health_api TEXT, + status INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at: String = row.get("created_at")?; + let updated_at: String = row.get("updated_at")?; + + Ok(Self { + id: row.get("id")?, + name: row.get("name")?, + url: row.get("url")?, + auth_key: row.get("auth_key")?, + timeout: row.get("timeout")?, + health_api: row.get("health_api")?, + status: row.get("status")?, + created_at: DateTime::parse_from_rfc3339(&created_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + updated_at: DateTime::parse_from_rfc3339(&updated_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "name, url, auth_key, timeout, health_api, status, remark" + } + + fn insert_values(&self) -> Vec> { + vec![ + Box::new(self.name.clone()), + Box::new(self.url.clone()), + Box::new(self.auth_key.clone()), + Box::new(self.timeout), + Box::new(self.health_api.clone()), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + name = ?, + url = ?, + auth_key = ?, + timeout = ?, + health_api = ?, + status = ?, + updated_at = CURRENT_TIMESTAMP, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values(&self) -> Vec> { + vec![ + Box::new(self.name.clone()), + Box::new(self.url.clone()), + Box::new(self.auth_key.clone()), + Box::new(self.timeout), + Box::new(self.health_api.clone()), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_value(&self) -> Box { + Box::new(self.id) + } +} diff --git a/src/db/models/mcp_tool.rs b/src/db/models/mcp_tool.rs index 3be4a5c..2f98812 100644 --- a/src/db/models/mcp_tool.rs +++ b/src/db/models/mcp_tool.rs @@ -1,15 +1,132 @@ use chrono::{DateTime, Utc}; -pub struct MCPTool { +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] +pub struct McpTool { pub id: i32, pub provider_id: i32, pub name: String, pub tool_code: String, + pub tool_type: String, pub description: String, - pub tag: String, - pub parameters: String, - pub response_format: String, - pub status: i8,// default 1 + pub input_schema: String, + pub status: i8, // default 1 pub created_at: DateTime, pub updated_at: DateTime, pub remark: Option, -} \ No newline at end of file +} + +impl Model for McpTool { + fn table_name() -> &'static str { + "mcp_tool" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS mcp_tool ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + provider_id INTEGER NOT NULL, + name TEXT NOT NULL, + tool_code TEXT NOT NULL, + tool_type TEXT NOT NULL, + description TEXT NOT NULL, + input_schema TEXT NOT NULL, + status INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at: String = row.get("created_at")?; + let updated_at: String = row.get("updated_at")?; + + Ok(Self { + id: row.get("id")?, + provider_id: row.get("provider_id")?, + name: row.get("name")?, + tool_code: row.get("tool_code")?, + tool_type: row.get("tool_type")?, + description: row.get("description")?, + input_schema: row.get("input_schema")?, + status: row.get("status")?, + created_at: DateTime::parse_from_rfc3339(&created_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + updated_at: DateTime::parse_from_rfc3339(&updated_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "provider_id, name, tool_code, tool_type, description, input_schema, status, remark" + } + + fn insert_values(&self) -> Vec> { + vec![ + Box::new(self.provider_id), + Box::new(self.name.clone()), + Box::new(self.tool_code.clone()), + Box::new(self.tool_type.clone()), + Box::new(self.description.clone()), + Box::new(self.input_schema.clone()), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + provider_id = ?, + name = ?, + tool_code = ?, + tool_type = ?, + description = ?, + input_schema = ?, + status = ?, + updated_at = CURRENT_TIMESTAMP, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values(&self) -> Vec> { + vec![ + Box::new(self.provider_id), + Box::new(self.name.clone()), + Box::new(self.tool_code.clone()), + Box::new(self.tool_type.clone()), + Box::new(self.description.clone()), + Box::new(self.input_schema.clone()), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_value(&self) -> Box { + Box::new(self.id) + } +} diff --git a/src/db/models/memory_info.rs b/src/db/models/memory_info.rs index 7d66458..9882f21 100644 --- a/src/db/models/memory_info.rs +++ b/src/db/models/memory_info.rs @@ -1,14 +1,114 @@ use chrono::{DateTime, Utc}; +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] pub struct MemoryInfo { pub id: i32, - pub memory_uuid: String, - pub name: String, - pub memory_type: i8, // default 0 - pub is_vector: i8, // default 0 - pub vector_id: Option, - pub keywords: Option, - pub status: i8,// default 1 + pub session_id: String, + pub content: String, + pub memory_type: String, + pub status: i8, // default 1 pub created_at: DateTime, pub updated_at: DateTime, pub remark: Option, -} \ No newline at end of file +} + +impl Model for MemoryInfo { + fn table_name() -> &'static str { + "memory_info" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS memory_info ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + content TEXT NOT NULL, + memory_type TEXT NOT NULL, + status INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at: String = row.get("created_at")?; + let updated_at: String = row.get("updated_at")?; + + Ok(Self { + id: row.get("id")?, + session_id: row.get("session_id")?, + content: row.get("content")?, + memory_type: row.get("memory_type")?, + status: row.get("status")?, + created_at: DateTime::parse_from_rfc3339(&created_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + updated_at: DateTime::parse_from_rfc3339(&updated_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "session_id, content, memory_type, status, remark" + } + + fn insert_values(&self) -> Vec> { + vec![ + Box::new(self.session_id.clone()), + Box::new(self.content.clone()), + Box::new(self.memory_type.clone()), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + session_id = ?, + content = ?, + memory_type = ?, + status = ?, + updated_at = CURRENT_TIMESTAMP, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values(&self) -> Vec> { + vec![ + Box::new(self.session_id.clone()), + Box::new(self.content.clone()), + Box::new(self.memory_type.clone()), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_value(&self) -> Box { + Box::new(self.id) + } +} diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index f7e25cf..34128e6 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -1,16 +1,15 @@ -mod config; -mod ai_provider; -mod llm_model; -mod agent_info; -mod prompt_info; -mod prompt_placeholder; -mod agent_arena; -mod chat_session; -mod chat_message; -mod memory_info; -mod mcp_provider; -mod mcp_tool; -mod agent_mcp_rel; -mod project_info; -mod system_api_log; -mod system_config; \ No newline at end of file +pub mod ai_provider; +pub mod llm_model; +pub mod agent_info; +pub mod prompt_info; +pub mod prompt_placeholder; +pub mod agent_arena; +pub mod chat_session; +pub mod chat_message; +pub mod memory_info; +pub mod mcp_provider; +pub mod mcp_tool; +pub mod agent_mcp_rel; +pub mod project_info; +pub mod system_api_log; +pub mod system_config; \ No newline at end of file diff --git a/src/db/models/project_info.rs b/src/db/models/project_info.rs index eede05f..9987e2e 100644 --- a/src/db/models/project_info.rs +++ b/src/db/models/project_info.rs @@ -1,10 +1,114 @@ use chrono::{DateTime, Utc}; +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] pub struct ProjectInfo { pub id: i32, pub name: String, - pub path: String, - pub is_deleted: i8, // default 0 + pub icon: String, + pub project_type: String, + pub status: i8, // default 1 pub created_at: DateTime, pub updated_at: DateTime, pub remark: Option, -} \ No newline at end of file +} + +impl Model for ProjectInfo { + fn table_name() -> &'static str { + "project_info" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS project_info ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + icon TEXT NOT NULL, + project_type TEXT NOT NULL, + status INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at: String = row.get("created_at")?; + let updated_at: String = row.get("updated_at")?; + + Ok(Self { + id: row.get("id")?, + name: row.get("name")?, + icon: row.get("icon")?, + project_type: row.get("project_type")?, + status: row.get("status")?, + created_at: DateTime::parse_from_rfc3339(&created_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + updated_at: DateTime::parse_from_rfc3339(&updated_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "name, icon, project_type, status, remark" + } + + fn insert_values(&self) -> Vec> { + vec![ + Box::new(self.name.clone()), + Box::new(self.icon.clone()), + Box::new(self.project_type.clone()), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + name = ?, + icon = ?, + project_type = ?, + status = ?, + updated_at = CURRENT_TIMESTAMP, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values(&self) -> Vec> { + vec![ + Box::new(self.name.clone()), + Box::new(self.icon.clone()), + Box::new(self.project_type.clone()), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_value(&self) -> Box { + Box::new(self.id) + } +} diff --git a/src/db/models/prompt_info.rs b/src/db/models/prompt_info.rs index 9cde327..4ac944b 100644 --- a/src/db/models/prompt_info.rs +++ b/src/db/models/prompt_info.rs @@ -1,16 +1,108 @@ use chrono::{DateTime, Utc}; -use crate::db::r#enum::PromptRole; +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] pub struct PromptInfo { pub id: i32, pub name: String, pub content: String, - pub category: String, - pub role: PromptRole, - pub version: String, - pub version_desc: String, - pub tag: String, - pub status: i8,// default 1 + pub status: i8, // default 1 pub created_at: DateTime, pub updated_at: DateTime, pub remark: Option, -} \ No newline at end of file +} + +impl Model for PromptInfo { + fn table_name() -> &'static str { + "prompt_info" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS prompt_info ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + content TEXT NOT NULL, + status INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at: String = row.get("created_at")?; + let updated_at: String = row.get("updated_at")?; + + Ok(Self { + id: row.get("id")?, + name: row.get("name")?, + content: row.get("content")?, + status: row.get("status")?, + created_at: DateTime::parse_from_rfc3339(&created_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + updated_at: DateTime::parse_from_rfc3339(&updated_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "name, content, status, remark" + } + + fn insert_values(&self) -> Vec> { + vec![ + Box::new(self.name.clone()), + Box::new(self.content.clone()), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + name = ?, + content = ?, + status = ?, + updated_at = CURRENT_TIMESTAMP, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values(&self) -> Vec> { + vec![ + Box::new(self.name.clone()), + Box::new(self.content.clone()), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_value(&self) -> Box { + Box::new(self.id) + } +} diff --git a/src/db/models/prompt_placeholder.rs b/src/db/models/prompt_placeholder.rs index 0129050..fee5b73 100644 --- a/src/db/models/prompt_placeholder.rs +++ b/src/db/models/prompt_placeholder.rs @@ -1,15 +1,114 @@ use chrono::{DateTime, Utc}; +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] pub struct PromptPlaceholder { pub id: i32, pub prompt_id: i32, - pub key: String, - pub label: String, + pub placeholder: String, pub value: String, - pub default: String, - pub input_type: String, - pub required: i8, // default 1 - pub status: i8,// default 1 + pub status: i8, // default 1 pub created_at: DateTime, pub updated_at: DateTime, pub remark: Option, -} \ No newline at end of file +} + +impl Model for PromptPlaceholder { + fn table_name() -> &'static str { + "prompt_placeholder" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS prompt_placeholder ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + prompt_id INTEGER NOT NULL, + placeholder TEXT NOT NULL, + value TEXT NOT NULL, + status INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at: String = row.get("created_at")?; + let updated_at: String = row.get("updated_at")?; + + Ok(Self { + id: row.get("id")?, + prompt_id: row.get("prompt_id")?, + placeholder: row.get("placeholder")?, + value: row.get("value")?, + status: row.get("status")?, + created_at: DateTime::parse_from_rfc3339(&created_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + updated_at: DateTime::parse_from_rfc3339(&updated_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "prompt_id, placeholder, value, status, remark" + } + + fn insert_values(&self) -> Vec> { + vec![ + Box::new(self.prompt_id), + Box::new(self.placeholder.clone()), + Box::new(self.value.clone()), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + prompt_id = ?, + placeholder = ?, + value = ?, + status = ?, + updated_at = CURRENT_TIMESTAMP, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values(&self) -> Vec> { + vec![ + Box::new(self.prompt_id), + Box::new(self.placeholder.clone()), + Box::new(self.value.clone()), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_value(&self) -> Box { + Box::new(self.id) + } +} diff --git a/src/db/models/system_api_log.rs b/src/db/models/system_api_log.rs index 3e2d103..f6c7650 100644 --- a/src/db/models/system_api_log.rs +++ b/src/db/models/system_api_log.rs @@ -1,12 +1,113 @@ use chrono::{DateTime, Utc}; -pub struct SystemAPILog { +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] +pub struct SystemApiLog { pub id: i32, - pub rel_uuid: String, - pub token_cost: i32, - pub duration: i32, - pub content: String, - pub status: i8, // 0: success, 1: failed + pub api_path: String, + pub method: String, + pub status_code: i32, + pub response_time: i64, + pub request_data: String, + pub response_data: String, pub created_at: DateTime, - pub updated_at: DateTime, pub remark: Option, -} \ No newline at end of file +} + +impl Model for SystemApiLog { + fn table_name() -> &'static str { + "system_api_log" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS system_api_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + api_path TEXT NOT NULL, + method TEXT NOT NULL, + status_code INTEGER NOT NULL, + response_time INTEGER NOT NULL, + request_data TEXT NOT NULL, + response_data TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at: String = row.get("created_at")?; + + Ok(Self { + id: row.get("id")?, + api_path: row.get("api_path")?, + method: row.get("method")?, + status_code: row.get("status_code")?, + response_time: row.get("response_time")?, + request_data: row.get("request_data")?, + response_data: row.get("response_data")?, + created_at: DateTime::parse_from_rfc3339(&created_at) + .map(|v| v.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?, + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "api_path, method, status_code, response_time, request_data, response_data, remark" + } + + fn insert_values(&self) -> Vec> { + vec![ + Box::new(self.api_path.clone()), + Box::new(self.method.clone()), + Box::new(self.status_code), + Box::new(self.response_time), + Box::new(self.request_data.clone()), + Box::new(self.response_data.clone()), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + api_path = ?, + method = ?, + status_code = ?, + response_time = ?, + request_data = ?, + response_data = ?, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values(&self) -> Vec> { + vec![ + Box::new(self.api_path.clone()), + Box::new(self.method.clone()), + Box::new(self.status_code), + Box::new(self.response_time), + Box::new(self.request_data.clone()), + Box::new(self.response_data.clone()), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_value(&self) -> Box { + Box::new(self.id) + } +} diff --git a/src/db/models/system_config.rs b/src/db/models/system_config.rs index f5a19aa..a824db9 100644 --- a/src/db/models/system_config.rs +++ b/src/db/models/system_config.rs @@ -1,13 +1,167 @@ -use chrono::{DateTime, Utc}; +use chrono::{DateTime, NaiveDateTime, Utc}; +use rusqlite::{ + Result as RusqliteResult, + Row, + ToSql, +}; + +use crate::core::db::Model; + +#[derive(Clone, Debug)] pub struct SystemConfig { - pub key: i32, + pub key: String, pub value: String, pub key_type: String, pub description: String, pub tag: String, - pub sort: i32, // default 0 - pub status: i8,// default 1 + pub sort: i32, + pub status: i8, pub created_at: DateTime, pub updated_at: DateTime, pub remark: Option, +} + +impl Model for SystemConfig { + fn table_name() -> &'static str { + "system_config" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS system_config ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + key_type TEXT NOT NULL, + description TEXT NOT NULL, + tag TEXT NOT NULL, + sort INTEGER NOT NULL DEFAULT 0, + status INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + remark TEXT + ) + "# + } + + fn from_row(row: &Row) -> RusqliteResult { + let created_at_str: String = + row.get("created_at")?; + + let updated_at_str: String = + row.get("updated_at")?; + + let created_at = + NaiveDateTime::parse_from_str( + &created_at_str, + "%Y-%m-%d %H:%M:%S", + ) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?; + + let updated_at = + NaiveDateTime::parse_from_str( + &updated_at_str, + "%Y-%m-%d %H:%M:%S", + ) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?; + + Ok(Self { + key: row.get("key")?, + + value: row.get("value")?, + + key_type: row.get("key_type")?, + + description: row.get("description")?, + + tag: row.get("tag")?, + + sort: row.get("sort")?, + + status: row.get("status")?, + + created_at: + DateTime::::from_naive_utc_and_offset( + created_at, + Utc, + ), + + updated_at: + DateTime::::from_naive_utc_and_offset( + updated_at, + Utc, + ), + + remark: row.get("remark")?, + }) + } + + fn insert_columns() -> &'static str { + "key, value, key_type, description, tag, sort, status, remark" + } + + fn insert_values( + &self, + ) -> Vec> { + vec![ + Box::new(self.key.clone()), + Box::new(self.value.clone()), + Box::new(self.key_type.clone()), + Box::new(self.description.clone()), + Box::new(self.tag.clone()), + Box::new(self.sort), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn update_set_clause(&self) -> String { + r#" + value = ?, + key_type = ?, + description = ?, + tag = ?, + sort = ?, + status = ?, + updated_at = CURRENT_TIMESTAMP, + remark = ? + "# + .trim() + .to_string() + } + + fn update_values( + &self, + ) -> Vec> { + vec![ + Box::new(self.value.clone()), + Box::new(self.key_type.clone()), + Box::new(self.description.clone()), + Box::new(self.tag.clone()), + Box::new(self.sort), + Box::new(self.status), + Box::new(self.remark.clone()), + ] + } + + fn primary_key_column() -> &'static str { + "key" + } + + fn primary_key_value( + &self, + ) -> Box { + Box::new(self.key.clone()) + } } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index e8d91fe..a649d84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,10 @@ +#[macro_use] +extern crate rust_i18n; + use std::io; +use log::info; +use rust_i18n::t; +use crate::core::db::DatabaseManager; use crate::ui::layout::App; pub mod core; @@ -10,12 +16,17 @@ pub mod components; pub mod health; pub mod platform; pub mod db; -pub fn run() -> io::Result<()> { + +i18n!("locales", fallback = "en"); +pub fn run() -> anyhow::Result<()> { + info!("{}", t!("logger_is_initialized")); + info!("{}", t!("test_message", name = "OmegaCode")); + info!("{}", t!("current_locale", locale_name = "en")); + let db = DatabaseManager::new()?; + db.health_check()?; let mut terminal = ratatui::init(); let mut app = App::default(); - // components::welcome::output_welcome_screen("123","12"); let result = app.run(&mut terminal); ratatui::restore(); result - // Ok(()) } diff --git a/src/main.rs b/src/main.rs index a15a0f0..6015173 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,23 +4,23 @@ extern crate OmegaCode; extern crate log; #[macro_use] extern crate flexi_logger; -#[macro_use] -extern crate rust_i18n; use flexi_logger::{ Cleanup, Criterion, Duplicate, FileSpec, Logger, Naming, WriteMode, detailed_format, }; -use log::{debug, error, info, trace, warn}; -use rust_i18n::{set_locale, t}; -rust_i18n::i18n!("locales"); fn main() { - set_locale("en"); init_logger(); - info!("{}", t!("logger_is_initialized")); - info!("{}", t!("test_message", name = "OmegaCode")); - info!("{}", t!("current_locale", locale_name = "en")); - OmegaCode::run(); + if let Err(e) = OmegaCode::run() { + eprintln!("Application error: {}", e); + // Print full error chain + let mut current = e.source(); + while let Some(cause) = current { + eprintln!("Caused by: {}", cause); + current = cause.source(); + } + std::process::exit(1); + } } fn init_logger() { @@ -28,7 +28,7 @@ fn init_logger() { .unwrap() .log_to_file( FileSpec::default() - .directory(app_dir!()) + .directory(app_dir!() + "/logs") .basename("omega") .suffix("log"), ) @@ -37,7 +37,7 @@ fn init_logger() { Naming::Numbers, Cleanup::KeepLogFiles(3), ) - .write_mode(WriteMode::BufferAndFlush) + .write_mode(WriteMode::Direct) .duplicate_to_stderr(Duplicate::Debug) // Logger level, production env should be Info level .format_for_files(detailed_format) .start() diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 6a4085b..f23be0d 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -1,26 +1,44 @@ -pub fn get_platform_user_dir() -> String { +use anyhow::Result; + +pub fn get_platform_user_dir() -> Result { #[cfg(target_os = "windows")] - let user_dir = std::env::var("USERPROFILE").unwrap_or_default(); + let user_dir = std::env::var("USERPROFILE") + .map_err(|e| anyhow::anyhow!("Failed to get USERPROFILE environment variable: {}", e))?; #[cfg(target_os = "linux")] - let user_dir = std::env::var("HOME").unwrap_or_default(); + let user_dir = std::env::var("HOME") + .map_err(|e| anyhow::anyhow!("Failed to get HOME environment variable: {}", e))?; #[cfg(target_os = "macos")] - let user_dir = std::env::var("HOME").unwrap_or_default(); + let user_dir = std::env::var("HOME") + .map_err(|e| anyhow::anyhow!("Failed to get HOME environment variable: {}", e))?; + + Ok(user_dir) +} - user_dir +pub fn get_platform_app_dir() -> Result { + let user_dir = get_platform_user_dir()?; + let app_dir = format!("{}/.omega", user_dir); + std::fs::create_dir_all(&app_dir) + .map_err(|e| anyhow::anyhow!("Failed to create app directory '{}': {}", app_dir, e))?; + Ok(app_dir) } -pub fn get_platform_app_dir() -> String { - let user_dir = get_platform_user_dir(); - let app_dir = format!("{}/.OmegaCode", user_dir); - std::fs::create_dir_all(&app_dir).unwrap(); - app_dir +pub fn get_database_path() -> Result { + let app_dir = get_platform_app_dir()?; + let db_path = format!("{}/omega_code.db", app_dir); + Ok(db_path) } #[macro_export] macro_rules! app_dir { - () => { - $crate::platform::get_platform_app_dir() - }; + () => {{ + match $crate::platform::get_platform_app_dir() { + Ok(dir) => dir, + Err(e) => { + eprintln!("Failed to get app directory: {}", e); + std::process::exit(1); + } + } + }}; } \ No newline at end of file diff --git a/src/ui/layout.rs b/src/ui/layout.rs index 2e3402a..b228ccd 100644 --- a/src/ui/layout.rs +++ b/src/ui/layout.rs @@ -18,7 +18,7 @@ pub struct App { impl App { /// 运行主循环直到用户退出 - pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { + pub fn run(&mut self, terminal: &mut DefaultTerminal) -> anyhow::Result<()> { while !self.exit { terminal.draw(|frame| self.draw(frame))?; self.handle_events()?; @@ -168,7 +168,7 @@ impl App { } // 标准主函数(极简、规范) -fn main() -> io::Result<()> { +fn main() -> anyhow::Result<()> { let mut terminal = ratatui::init(); let mut app = App::default(); let result = app.run(&mut terminal); From e7f13855e9838bebb6a5293d27f44662af37842b Mon Sep 17 00:00:00 2001 From: Gabriel Bao Date: Fri, 15 May 2026 17:38:11 +0800 Subject: [PATCH 2/3] docs: add README.md --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..f97140f --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Omega Code +## 开发指南 +1. `Fork` 仓库 +2. `Clone` 你已经 `Fork` 的仓库 + ```bash + git clone https://github.com/Your-username/Omega-Code.git + ``` +3. 创建特性分支(`feature`/`fix`/`docs`/`chore`...) +4. 编写代码,提交 `commit`(需要细化) +5. 提交 `pr` 到 `origin/dev` +6. 等待审查 + +## 说明 +前端位置:`console/console-ui/` + +后端位置:`console/console-panel/` + +MCP Server 位置:`mcp_server/` + +cli 位置:`src/` + +数据库位置: +1. Windows:`C:/Users/{电脑用户名}/.omega` +2. Linux: `/home/{电脑用户名}/.omega` +3. macOS: `/Users/{电脑用户名}/.omega` From 1db83d536df2761f5c02b071ae3609bf64f1f310 Mon Sep 17 00:00:00 2001 From: Gabriel Bao Date: Sat, 16 May 2026 14:04:41 +0800 Subject: [PATCH 3/3] chore: Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ea8c4bf..cada1bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/.trae \ No newline at end of file