From 37c3ae76c1286d71f727c5edc33d5300d649b6f1 Mon Sep 17 00:00:00 2001 From: Gabriel Bao Date: Fri, 15 May 2026 17:18:18 +0800 Subject: [PATCH 1/5] =?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/5] 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/5] 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 From 7d6dfe262a7303bef6cc66b7b70dcf63b9a1699a Mon Sep 17 00:00:00 2001 From: Gabriel Bao Date: Sun, 17 May 2026 22:28:05 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat(component):=20welcome=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E9=AA=A8=E6=9E=B6=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 286 ++++++++++++++++-- Cargo.toml | 6 +- locales/en.yml | 9 +- locales/zh-CN.yml | 10 +- src/components/input_bar.rs | 67 ++++ src/components/mod.rs | 2 + .../route/mod.rs => components/status_bar.rs} | 0 src/components/welcome.rs | 272 +++++++++++++++-- src/core/mod.rs | 2 - src/lib.rs | 22 +- src/pages/chat.rs | 10 + src/pages/context.rs | 0 src/{ui => pages}/layout.rs | 0 src/pages/mod.rs | 4 + src/pages/welcome.rs | 13 + src/router/mod.rs | 10 + src/{core => }/state/mod.rs | 0 src/{core => }/state/state.rs | 2 +- src/ui/mod.rs | 1 - 19 files changed, 651 insertions(+), 65 deletions(-) create mode 100644 src/components/input_bar.rs rename src/{core/route/mod.rs => components/status_bar.rs} (100%) create mode 100644 src/pages/chat.rs create mode 100644 src/pages/context.rs rename src/{ui => pages}/layout.rs (100%) create mode 100644 src/pages/mod.rs create mode 100644 src/pages/welcome.rs create mode 100644 src/router/mod.rs rename src/{core => }/state/mod.rs (100%) rename src/{core => }/state/state.rs (61%) delete mode 100644 src/ui/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 407d4d4..a82f148 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,13 +10,15 @@ dependencies = [ "chrono", "clap", "colored", - "crossterm", + "crossterm 0.29.0", "flexi_logger", + "futures", "futures-util", "indicatif", "log", "once_cell", - "ratatui", + "ratatui 0.30.0", + "ratatui-kit", "reqwest", "rmcp", "rusqlite", @@ -101,6 +103,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "any_key" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d21bb2cdab8087ed9d69411dd99c608dbede1df847c255b4d609f0399a3cb452" +dependencies = [ + "debugit", + "mopa", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -246,6 +258,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "castaway" version = "0.2.4" @@ -354,6 +372,20 @@ dependencies = [ "memchr", ] +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "compact_str" version = "0.9.0" @@ -376,7 +408,7 @@ checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", - "unicode-width", + "unicode-width 0.2.0", "windows-sys 0.61.2", ] @@ -449,6 +481,23 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.11.1", + "crossterm_winapi", + "futures-core", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm" version = "0.29.0" @@ -461,7 +510,7 @@ dependencies = [ "document-features", "mio", "parking_lot", - "rustix", + "rustix 1.1.4", "signal-hook", "signal-hook-mio", "winapi", @@ -530,6 +579,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "debugit" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63c2f7e3034df2b09f750327e23c1adfe33301e6b7388f05bb4fcc0fa46825e3" +dependencies = [ + "version_check 0.1.5", +] + [[package]] name = "deltae" version = "0.3.2" @@ -840,6 +898,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generational-box" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a673cf4fb0ea6a91aa86c08695756dfe875277a912cdbf33db9a9f62d47ed82b" +dependencies = [ + "parking_lot", + "tracing", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -847,7 +915,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", - "version_check", + "version_check 0.9.5", ] [[package]] @@ -945,6 +1013,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.1.5", ] @@ -1261,7 +1331,7 @@ checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" dependencies = [ "console", "portable-atomic", - "unicode-width", + "unicode-width 0.2.0", "unit-prefix", "web-time", ] @@ -1309,6 +1379,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -1450,6 +1529,12 @@ dependencies = [ "bitflags 2.11.1", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1483,6 +1568,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "lru" version = "0.16.4" @@ -1553,6 +1647,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "mopa" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a785740271256c230f57462d3b83e52f998433a7062fc18f96d5999474a9f915" + [[package]] name = "nix" version = "0.29.0" @@ -1679,6 +1779,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pastey" version = "0.2.2" @@ -1968,6 +2074,27 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.11.1", + "cassowary", + "compact_str 0.8.1", + "crossterm 0.28.1", + "indoc", + "instability", + "itertools 0.13.0", + "lru 0.12.5", + "paste", + "strum 0.26.3", + "unicode-segmentation", + "unicode-truncate 1.1.0", + "unicode-width 0.2.0", +] + [[package]] name = "ratatui" version = "0.30.0" @@ -1989,17 +2116,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" dependencies = [ "bitflags 2.11.1", - "compact_str", + "compact_str 0.9.0", "hashbrown 0.16.1", "indoc", "itertools 0.14.0", "kasuari", - "lru", - "strum", + "lru 0.16.4", + "strum 0.27.2", "thiserror 2.0.18", "unicode-segmentation", - "unicode-truncate", - "unicode-width", + "unicode-truncate 2.0.1", + "unicode-width 0.2.0", ] [[package]] @@ -2009,11 +2136,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" dependencies = [ "cfg-if", - "crossterm", + "crossterm 0.29.0", "instability", "ratatui-core", ] +[[package]] +name = "ratatui-kit" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9bd1a956e29a6d59ae8212a5d3499f964b8d2a2a8d878614d182d34191280c6" +dependencies = [ + "any_key", + "crossterm 0.28.1", + "futures", + "generational-box", + "ratatui 0.29.0", + "ratatui-kit-macros", + "regex", + "tui-input", + "tui-textarea", + "tui-tree-widget", +] + +[[package]] +name = "ratatui-kit-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22bf3a8e830d8a0db564c642d017597c912580049a7569e79708007acef9de73" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "uuid", +] + [[package]] name = "ratatui-macros" version = "0.7.0" @@ -2047,10 +2204,10 @@ dependencies = [ "itertools 0.14.0", "line-clipping", "ratatui-core", - "strum", + "strum 0.27.2", "time", "unicode-segmentation", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] @@ -2289,6 +2446,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -2298,7 +2468,7 @@ dependencies = [ "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -2666,13 +2836,35 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + [[package]] name = "strum" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros", + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", ] [[package]] @@ -2914,7 +3106,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -3099,6 +3293,37 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tui-input" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911e93158bf80bbc94bad533b2b16e3d711e1132d69a6a6980c3920a63422c19" +dependencies = [ + "ratatui 0.29.0", + "unicode-width 0.2.0", +] + +[[package]] +name = "tui-textarea" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae" +dependencies = [ + "crossterm 0.28.1", + "ratatui 0.29.0", + "unicode-width 0.2.0", +] + +[[package]] +name = "tui-tree-widget" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c14c4488e071617f5b5922222193cdf6725835e492c6229557af85d3c1a4e903" +dependencies = [ + "ratatui 0.29.0", + "unicode-width 0.2.0", +] + [[package]] name = "typenum" version = "1.20.0" @@ -3123,6 +3348,17 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + [[package]] name = "unicode-truncate" version = "2.0.1" @@ -3131,14 +3367,20 @@ checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ "itertools 0.14.0", "unicode-segmentation", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] name = "unicode-width" -version = "0.2.2" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unicode-xid" @@ -3206,6 +3448,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index c5a9033..c5591dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,12 @@ path = "src/main.rs" [dependencies] clap = "4.6.1" anyhow = "1.0.102" -tokio = "1.52.3" +tokio = { version = "1.52.3", features = ["full"] } colored = "3.1.1" indicatif = "0.18.4" reqwest = "0.13.3" tokio-stream = "0.1.18" +futures = "0.3.32" futures-util = "0.3.32" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" @@ -29,4 +30,5 @@ log = "0.4.29" flexi_logger = "0.31.8" chrono = "0.4.44" rust-i18n = "4.0.0" -once_cell = "1.21.4" \ No newline at end of file +once_cell = "1.21.4" +ratatui-kit = { version = "0.5.9", features = ["full"] } diff --git a/locales/en.yml b/locales/en.yml index 8b6bd1b..2f8c1e8 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -4,4 +4,11 @@ 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 +database_table_create: "Database table %{table_name} is created." + +welcome.version: "Version: %{version}" +welcome.text: "Welcome to Omega Code!" +welcome.menu: "Quick Menu(use ↑/↓ keys to navigate, press enter to select an item):" +welcome.menu_items.0: "Open Omega Code Console Panel, to configure the application." +welcome.menu_items.1: "Start a new project." +welcome.menu_items.2: "Quit" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 7a724f9..e12d17a 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -4,4 +4,12 @@ logger_is_initialized: "日志已经初始化了喵~" current_locale: "当前语言: %{locale_name}" database_path: "数据库位置: %{database_path}" database_initialized: "数据库初始化成功" -database_table_create: "数据表 %{table_name} 已创建" \ No newline at end of file +database_table_create: "数据表 %{table_name} 已创建" + +welcome.version: "版本: %{version}" +welcome.text: "欢迎来到 Omega Code !" +welcome.menu: "快速菜单(使用 ↑/↓ 键导航,按 enter 选择项):" +menu_items: + - "打开 Omega Code 控制台面板,配置应用程序" + - "开始新项目" + - "退出" diff --git a/src/components/input_bar.rs b/src/components/input_bar.rs new file mode 100644 index 0000000..647ab51 --- /dev/null +++ b/src/components/input_bar.rs @@ -0,0 +1,67 @@ +use ratatui_kit::{ + crossterm::event::{Event, KeyCode, KeyEventKind}, + prelude::*, + ratatui::{ + layout::Constraint, + style::{Style, Stylize}, + text::Line, + }, +}; + +#[component] +pub fn InputBar(mut hooks: Hooks) -> impl Into> { + let mut value = hooks.use_state(String::new); + + let mut should_exit = hooks.use_state(|| false); + + let mut system_ctx = hooks.use_context_mut::(); + let insert_before = hooks.use_insert_before(); + + if should_exit.get() { + system_ctx.exit(); + } + + hooks.use_events(move |event| { + if let Event::Key(key_event) = event { + if key_event.kind == KeyEventKind::Press { + match key_event.code { + KeyCode::Esc => { + should_exit.set(true); + } + KeyCode::Enter => { + if !value.read().is_empty() { + insert_before + .render_before(Line::from(format!("message: {value}")), 1) + .finish(); + + value.set(String::new()); + } + } + _ => {} + } + } + } + }); + + element!(Border( + height: Constraint::Length(4), + style: Style::default().green(), + bottom_title: Line::styled( + "Press 'Enter' to submit, 'Esc' to exit", + Style::default().yellow(), + ).centered(), + ) { + TextArea( + value: value.read().to_string(), + is_focus: true, + on_change: move |new_value: String| { + value.set(new_value); + }, + multiline: false, + cursor_style: Style::default().on_green(), + placeholder: Some("Type something...".to_string()), + placeholder_style: Style::default().green(), + ) + + }) +} \ No newline at end of file diff --git a/src/components/mod.rs b/src/components/mod.rs index 3d8b2be..ee26743 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1 +1,3 @@ pub mod welcome; +pub(crate) mod input_bar; +mod status_bar; diff --git a/src/core/route/mod.rs b/src/components/status_bar.rs similarity index 100% rename from src/core/route/mod.rs rename to src/components/status_bar.rs diff --git a/src/components/welcome.rs b/src/components/welcome.rs index decc579..0ef5f9e 100644 --- a/src/components/welcome.rs +++ b/src/components/welcome.rs @@ -1,36 +1,248 @@ -pub fn output_welcome_screen(version: &str, version_info: &str) { - println!(" - ██████╗ ███╗ ███╗███████╗ ██████╗ █████╗ -██╔═══██╗████╗ ████║██╔════╝██╔════╝ ██╔══██╗ -██║ ██║██╔████╔██║█████╗ ██║ ███╗███████║ -██║ ██║██║╚██╔╝██║██╔══╝ ██║ ██║██╔══██║ -╚██████╔╝██║ ╚═╝ ██║███████╗╚██████╔╝██║ ██║ - ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ - - ██████╗ ██████╗ ██████╗ ███████╗ -██╔════╝██╔═══██╗██╔══██╗██╔════╝ -██║ ██║ ██║██║ ██║█████╗ -██║ ██║ ██║██║ ██║██╔══╝ -╚██████╗╚██████╔╝██████╔╝███████╗ - ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ -Omega Code v{} ({})", - &version, &version_info); -} +use ratatui_kit::{ + Component, + ComponentDrawer, + ComponentUpdater, + Hooks, + Props, + UseEvents, + UseState, +}; + +use ratatui_kit::crossterm::event::{Event, KeyCode}; -pub fn output_main_screen() { - println!(" +use ratatui_kit::ratatui::{ + style::{Modifier, Style}, + text::{Line, Span}, + widgets::{Paragraph, Widget}, +}; +#[derive(Props)] +pub struct WelcomeProps { + pub version: Option, + pub style: Style, + pub menu_style: Style, + pub show_menu: bool, +} + +impl Default for WelcomeProps { + fn default() -> Self { + Self { + version: None, + style: Style::default(), + menu_style: Style::default(), + show_menu: true, + } + } +} - ▄████▄ - ██▀ ▀██ - ██ ██ - ██ ██ - ▄██ ██▄ - ▀▀▀ ▀▀▀ +pub struct Welcome { + selected_index: usize, - -") + // props cache + style: Style, + menu_style: Style, + version: Option, + show_menu: bool, } -fn main() { - output_welcome_screen("123","12"); + +impl Component for Welcome { + type Props<'a> = WelcomeProps; + + fn new(props: &Self::Props<'_>) -> Self { + Self { + selected_index: 0, + + style: props.style, + menu_style: props.menu_style, + version: props.version.clone(), + show_menu: props.show_menu, + } + } + + fn update( + &mut self, + props: &mut Self::Props<'_>, + mut hooks: Hooks, + _updater: &mut ComponentUpdater, + ) { + // sync props + self.style = props.style; + self.menu_style = props.menu_style; + self.version = props.version.clone(); + self.show_menu = props.show_menu; + + let mut state_index = hooks.use_state(|| self.selected_index); + + hooks.use_events(move |event| { + if let Event::Key(key) = event { + match key.code { + KeyCode::Up => { + state_index.set( + state_index + .get() + .saturating_sub(1), + ); + } + + KeyCode::Down => { + let current = state_index.get(); + + state_index.set((current + 1).min(2)); + } + + KeyCode::Enter => { + match state_index.get() { + 0 => { + todo!("打开配置"); + } + + 1 => { + todo!("开启新项目"); + } + + 2 => { + todo!("退出程序"); + } + + _ => {} + } + } + + _ => {} + } + } + }); + + self.selected_index = state_index.get(); + } + + fn draw(&mut self, drawer: &mut ComponentDrawer<'_, '_>) { + let area = drawer.area; + let buf = drawer.buffer_mut(); + + // ASCII LOGO + let ascii_lines = [ + " ██████╗ ███╗ ███╗███████╗ ██████╗ █████╗", + "██╔═══██╗████╗ ████║██╔════╝██╔════╝ ██╔══██╗", + "██║ ██║██╔████╔██║█████╗ ██║ ███╗███████║", + "██║ ██║██║╚██╔╝██║██╔══╝ ██║ ██║██╔══██║", + "╚██████╔╝██║ ╚═╝ ██║███████╗╚██████╔╝██║ ██║", + " ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝", + "", + " ██████╗ ██████╗ ██████╗ ███████╗", + "██╔════╝██╔═══██╗██╔══██╗██╔════╝", + "██║ ██║ ██║██║ ██║█████╗ ", + "██║ ██║ ██║██║ ██║██╔══╝ ", + "╚██████╗╚██████╔╝██████╔╝███████╗", + " ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝", + ]; + + let version = self + .version + .clone() + .unwrap_or_else(|| { + env!("CARGO_PKG_VERSION").to_string() + }); + + let welcome_text = t!("welcome.text"); + + let menu_items = [ + t!("welcome.menu_items.0"), + t!("welcome.menu_items.1"), + t!("welcome.menu_items.2"), + ]; + + let mut lines = Vec::new(); + + // logo + for line in ascii_lines { + lines.push( + Line::from( + Span::styled( + line, + self.style, + ) + ) + ); + } + + lines.push(Line::from("")); + + // version + lines.push( + Line::from( + Span::styled( + t!("welcome.version", version = version), + self.style.add_modifier(Modifier::DIM), + ) + ) + ); + + lines.push(Line::from("")); + + // welcome text + lines.push( + Line::from( + Span::styled( + welcome_text, + self.style, + ) + ) + ); + + lines.push(Line::from("")); + + // menu title + lines.push( + Line::from( + Span::styled( + t!("welcome.menu"), + self.style.add_modifier(Modifier::BOLD), + ) + ) + ); + + lines.push(Line::from("")); + + // menu + if self.show_menu { + let max_index = menu_items + .len() + .saturating_sub(1); + + self.selected_index = + self.selected_index.min(max_index); + + for (i, item) in menu_items.iter().enumerate() { + let selected = + i == self.selected_index; + + let prefix = if selected { + "→ " + } else { + " " + }; + + let style = if selected { + self.menu_style.add_modifier( + Modifier::BOLD + | Modifier::REVERSED, + ) + } else { + self.style + }; + + lines.push( + Line::from( + Span::styled( + format!("{prefix}• {item}"), + style, + ) + ) + ); + } + } + + Paragraph::new(lines).render(area, buf); + } } \ No newline at end of file diff --git a/src/core/mod.rs b/src/core/mod.rs index 6582b9b..f5562e2 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,6 +1,4 @@ pub mod action; -pub mod state; -pub mod route; pub mod event; pub mod update; pub mod context; diff --git a/src/lib.rs b/src/lib.rs index a649d84..d59f23f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,30 +3,36 @@ extern crate rust_i18n; use std::io; use log::info; +use ratatui_kit::{element, ElementExt}; +use ratatui_kit::prelude::RouterProvider; use rust_i18n::t; use crate::core::db::DatabaseManager; -use crate::ui::layout::App; +use crate::pages::layout::App; pub mod core; pub mod runtime; pub mod services; -pub mod ui; +pub mod pages; pub mod utils; pub mod components; pub mod health; pub mod platform; pub mod db; +pub mod state; +pub mod router; + +// use crate::pages::chat::chat_page; +use crate::pages::welcome::WelcomePage; i18n!("locales", fallback = "en"); -pub fn run() -> anyhow::Result<()> { + +#[tokio::main] +pub async 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(); - let result = app.run(&mut terminal); - ratatui::restore(); - result + element!(WelcomePage()).fullscreen().await.expect("Failed to render welcome page"); + Ok(()) } diff --git a/src/pages/chat.rs b/src/pages/chat.rs new file mode 100644 index 0000000..3518cd6 --- /dev/null +++ b/src/pages/chat.rs @@ -0,0 +1,10 @@ +use ratatui_kit::{component, element, AnyElement, ElementExt, Hooks}; +use ratatui_kit::components::View; +use ratatui_kit::ratatui::{TerminalOptions, Viewport}; +use ratatui_kit::ratatui::layout::Direction; +use crate::components::input_bar::InputBar; + +#[component] +pub fn ChatPage(mut hooks: Hooks) -> impl Into> { + element!(ratatui_kit::components::textarea::TextArea) +} \ No newline at end of file diff --git a/src/pages/context.rs b/src/pages/context.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/layout.rs b/src/pages/layout.rs similarity index 100% rename from src/ui/layout.rs rename to src/pages/layout.rs diff --git a/src/pages/mod.rs b/src/pages/mod.rs new file mode 100644 index 0000000..4c2aff3 --- /dev/null +++ b/src/pages/mod.rs @@ -0,0 +1,4 @@ +pub mod layout; +pub mod welcome; +pub mod chat; +pub mod context; diff --git a/src/pages/welcome.rs b/src/pages/welcome.rs new file mode 100644 index 0000000..ce612ea --- /dev/null +++ b/src/pages/welcome.rs @@ -0,0 +1,13 @@ +use ratatui_kit::{component, element, AnyElement}; +use ratatui_kit::prelude::View; +use ratatui_kit::ratatui::prelude::Direction; +use crate::components::welcome::Welcome; + +#[component] +pub fn WelcomePage() -> impl Into> { + element!(View( + flex_direction: Direction::Horizontal, + ) { + Welcome() + }) +} diff --git a/src/router/mod.rs b/src/router/mod.rs new file mode 100644 index 0000000..7c2cbff --- /dev/null +++ b/src/router/mod.rs @@ -0,0 +1,10 @@ +use ratatui_kit::prelude::*; +use crate::pages::welcome::WelcomePage; +// use crate::pages::chat::chat_page; +// use crate::pages::context::context_page; +pub fn app_routes() -> Vec { + let routes = routes!{ + "/welcome" => WelcomePage, + }; + routes +} \ No newline at end of file diff --git a/src/core/state/mod.rs b/src/state/mod.rs similarity index 100% rename from src/core/state/mod.rs rename to src/state/mod.rs diff --git a/src/core/state/state.rs b/src/state/state.rs similarity index 61% rename from src/core/state/state.rs rename to src/state/state.rs index c6f8e30..cd5bccf 100644 --- a/src/core/state/state.rs +++ b/src/state/state.rs @@ -1,4 +1,4 @@ // pub struct AppState { -// pub route: Route; +// pub router: Route; // pub // } \ No newline at end of file diff --git a/src/ui/mod.rs b/src/ui/mod.rs deleted file mode 100644 index 0cc99f7..0000000 --- a/src/ui/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod layout; \ No newline at end of file From 0eed9daf14687558fc7b2c6ca90569af9cb39663 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 20:32:22 +0800 Subject: [PATCH 5/5] deps(npm): bump @vitejs/plugin-vue from 6.0.6 to 6.0.7 in /console/console-ui (#14) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- console/console-ui/package.json | 2 +- console/console-ui/pnpm-lock.yaml | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/console/console-ui/package.json b/console/console-ui/package.json index 3020465..7c2a024 100644 --- a/console/console-ui/package.json +++ b/console/console-ui/package.json @@ -35,7 +35,7 @@ }, "devDependencies": { "@types/node": "^25.8.0", - "@vitejs/plugin-vue": "^6.0.6", + "@vitejs/plugin-vue": "^6.0.7", "@vue/tsconfig": "^0.9.1", "tw-animate-css": "^1.4.0", "typescript": "~6.0.2", diff --git a/console/console-ui/pnpm-lock.yaml b/console/console-ui/pnpm-lock.yaml index 9fd752d..f1a679e 100644 --- a/console/console-ui/pnpm-lock.yaml +++ b/console/console-ui/pnpm-lock.yaml @@ -82,8 +82,8 @@ importers: specifier: ^25.8.0 version: 25.8.0 '@vitejs/plugin-vue': - specifier: ^6.0.6 - version: 6.0.6(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)) + specifier: ^6.0.7 + version: 6.0.7(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)) '@vue/tsconfig': specifier: ^0.9.1 version: 0.9.1(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)) @@ -415,9 +415,6 @@ packages: cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.13': - resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==} - '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} @@ -717,8 +714,8 @@ packages: '@unovis/ts': 1.6.5 vue: ^3 - '@vitejs/plugin-vue@6.0.6': - resolution: {integrity: sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==} + '@vitejs/plugin-vue@6.0.7': + resolution: {integrity: sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2115,8 +2112,6 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.0.1': optional: true - '@rolldown/pluginutils@1.0.0-rc.13': {} - '@rolldown/pluginutils@1.0.1': {} '@swc/helpers@0.5.21': @@ -2496,9 +2491,9 @@ snapshots: '@unovis/ts': 1.6.5 vue: 3.5.34(typescript@6.0.3) - '@vitejs/plugin-vue@6.0.6(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))': + '@vitejs/plugin-vue@6.0.7(vite@8.0.13(@types/node@25.8.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))': dependencies: - '@rolldown/pluginutils': 1.0.0-rc.13 + '@rolldown/pluginutils': 1.0.1 vite: 8.0.13(@types/node@25.8.0)(jiti@2.7.0)(yaml@2.9.0) vue: 3.5.34(typescript@6.0.3)