Initial commit
This commit is contained in:
21
src-tauri/src/db/entities/agent_setting.rs
Normal file
21
src-tauri/src/db/entities/agent_setting.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "agent_setting")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub agent_type: String,
|
||||
pub registry_id: String,
|
||||
pub enabled: bool,
|
||||
pub sort_order: i32,
|
||||
pub installed_version: Option<String>,
|
||||
pub env_json: Option<String>,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
19
src-tauri/src/db/entities/app_metadata.rs
Normal file
19
src-tauri/src/db/entities/app_metadata.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "app_metadata")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
#[sea_orm(unique)]
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub deleted_at: Option<DateTimeUtc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
53
src-tauri/src/db/entities/conversation.rs
Normal file
53
src-tauri/src/db/entities/conversation.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
|
||||
#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ConversationStatus {
|
||||
#[sea_orm(string_value = "in_progress")]
|
||||
InProgress,
|
||||
#[sea_orm(string_value = "pending_review")]
|
||||
PendingReview,
|
||||
#[sea_orm(string_value = "completed")]
|
||||
Completed,
|
||||
#[sea_orm(string_value = "cancelled")]
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "conversation")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub folder_id: i32,
|
||||
pub title: Option<String>,
|
||||
pub agent_type: String,
|
||||
pub status: ConversationStatus,
|
||||
pub model: Option<String>,
|
||||
pub git_branch: Option<String>,
|
||||
pub external_id: Option<String>,
|
||||
pub parent_id: Option<i32>,
|
||||
pub message_count: i32,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub deleted_at: Option<DateTimeUtc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::folder::Entity",
|
||||
from = "Column::FolderId",
|
||||
to = "super::folder::Column::Id"
|
||||
)]
|
||||
Folder,
|
||||
}
|
||||
|
||||
impl Related<super::folder::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Folder.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
51
src-tauri/src/db/entities/folder.rs
Normal file
51
src-tauri/src/db/entities/folder.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "folder")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
#[sea_orm(unique)]
|
||||
pub path: String,
|
||||
pub git_branch: Option<String>,
|
||||
pub default_agent_type: Option<String>,
|
||||
pub last_opened_at: DateTimeUtc,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
pub deleted_at: Option<DateTimeUtc>,
|
||||
pub is_open: bool,
|
||||
pub parent_branch: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::conversation::Entity")]
|
||||
Conversations,
|
||||
|
||||
#[sea_orm(has_many = "super::folder_opened_conversation::Entity")]
|
||||
OpenedConversations,
|
||||
|
||||
#[sea_orm(has_many = "super::folder_command::Entity")]
|
||||
FolderCommands,
|
||||
}
|
||||
|
||||
impl Related<super::conversation::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Conversations.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::folder_opened_conversation::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::OpenedConversations.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::folder_command::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::FolderCommands.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
32
src-tauri/src/db/entities/folder_command.rs
Normal file
32
src-tauri/src/db/entities/folder_command.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "folder_command")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub folder_id: i32,
|
||||
pub name: String,
|
||||
pub command: String,
|
||||
pub sort_order: i32,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::folder::Entity",
|
||||
from = "Column::FolderId",
|
||||
to = "super::folder::Column::Id"
|
||||
)]
|
||||
Folder,
|
||||
}
|
||||
|
||||
impl Related<super::folder::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Folder.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
47
src-tauri/src/db/entities/folder_opened_conversation.rs
Normal file
47
src-tauri/src/db/entities/folder_opened_conversation.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "folder_opened_conversation")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub folder_id: i32,
|
||||
pub conversation_id: i32,
|
||||
pub position: i32,
|
||||
pub is_active: bool,
|
||||
pub is_pinned: bool,
|
||||
pub agent_type: String,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::folder::Entity",
|
||||
from = "Column::FolderId",
|
||||
to = "super::folder::Column::Id"
|
||||
)]
|
||||
Folder,
|
||||
|
||||
#[sea_orm(
|
||||
belongs_to = "super::conversation::Entity",
|
||||
from = "Column::ConversationId",
|
||||
to = "super::conversation::Column::Id"
|
||||
)]
|
||||
Conversation,
|
||||
}
|
||||
|
||||
impl Related<super::folder::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Folder.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::conversation::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Conversation.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
7
src-tauri/src/db/entities/mod.rs
Normal file
7
src-tauri/src/db/entities/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod agent_setting;
|
||||
pub mod app_metadata;
|
||||
pub mod conversation;
|
||||
pub mod folder;
|
||||
pub mod folder_command;
|
||||
pub mod folder_opened_conversation;
|
||||
pub mod prelude;
|
||||
8
src-tauri/src/db/entities/prelude.rs
Normal file
8
src-tauri/src/db/entities/prelude.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
#![allow(unused_imports)]
|
||||
|
||||
pub use super::agent_setting::Entity as AgentSetting;
|
||||
pub use super::app_metadata::Entity as AppMetadata;
|
||||
pub use super::conversation::Entity as Conversation;
|
||||
pub use super::folder::Entity as Folder;
|
||||
pub use super::folder_command::Entity as FolderCommand;
|
||||
pub use super::folder_opened_conversation::Entity as FolderOpenedConversation;
|
||||
23
src-tauri/src/db/error.rs
Normal file
23
src-tauri/src/db/error.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DbError {
|
||||
#[error("database error: {0}")]
|
||||
Database(#[from] sea_orm::DbErr),
|
||||
#[error("migration error: {0}")]
|
||||
Migration(String),
|
||||
#[allow(dead_code)]
|
||||
#[error("database not initialized")]
|
||||
NotInitialized,
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
impl Serialize for DbError {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
378
src-tauri/src/db/migration/m20260211_000001_init.rs
Normal file
378
src-tauri/src/db/migration/m20260211_000001_init.rs
Normal file
@@ -0,0 +1,378 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// 1. app_metadata table
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(AppMetadata::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(AppMetadata::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(AppMetadata::Key)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(ColumnDef::new(AppMetadata::Value).string().not_null())
|
||||
.col(
|
||||
ColumnDef::new(AppMetadata::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(AppMetadata::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(AppMetadata::DeletedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 2. folder table
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Folder::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Folder::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Folder::Name).string().not_null())
|
||||
.col(
|
||||
ColumnDef::new(Folder::Path)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Folder::GitBranch).string().null())
|
||||
.col(ColumnDef::new(Folder::DefaultAgentType).string().null())
|
||||
.col(
|
||||
ColumnDef::new(Folder::LastOpenedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Folder::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Folder::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Folder::DeletedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 3. conversation table
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Conversation::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Conversation::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Conversation::FolderId).integer().not_null())
|
||||
.col(ColumnDef::new(Conversation::Title).string().null())
|
||||
.col(ColumnDef::new(Conversation::AgentType).string().not_null())
|
||||
.col(
|
||||
ColumnDef::new(Conversation::Status)
|
||||
.string()
|
||||
.not_null()
|
||||
.default("in_progress"),
|
||||
)
|
||||
.col(ColumnDef::new(Conversation::Model).string().null())
|
||||
.col(ColumnDef::new(Conversation::GitBranch).string().null())
|
||||
.col(ColumnDef::new(Conversation::ExternalId).string().null())
|
||||
.col(ColumnDef::new(Conversation::ParentId).integer().null())
|
||||
.col(
|
||||
ColumnDef::new(Conversation::MessageCount)
|
||||
.integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Conversation::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Conversation::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Conversation::DeletedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.null(),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_conversation_folder")
|
||||
.from(Conversation::Table, Conversation::FolderId)
|
||||
.to(Folder::Table, Folder::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 4. folder_opened_conversation table
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(FolderOpenedConversation::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(FolderOpenedConversation::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(FolderOpenedConversation::FolderId)
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(FolderOpenedConversation::ConversationId)
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(FolderOpenedConversation::Position)
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(FolderOpenedConversation::IsActive)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(FolderOpenedConversation::IsPinned)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(true),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(FolderOpenedConversation::AgentType)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(FolderOpenedConversation::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(FolderOpenedConversation::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_foc_folder")
|
||||
.from(
|
||||
FolderOpenedConversation::Table,
|
||||
FolderOpenedConversation::FolderId,
|
||||
)
|
||||
.to(Folder::Table, Folder::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_foc_conversation")
|
||||
.from(
|
||||
FolderOpenedConversation::Table,
|
||||
FolderOpenedConversation::ConversationId,
|
||||
)
|
||||
.to(Conversation::Table, Conversation::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 6. indexes
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_folder_deleted_last_opened")
|
||||
.table(Folder::Table)
|
||||
.col(Folder::DeletedAt)
|
||||
.col(Folder::LastOpenedAt)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_conversation_folder_id")
|
||||
.table(Conversation::Table)
|
||||
.col(Conversation::FolderId)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_conversation_deleted_created")
|
||||
.table(Conversation::Table)
|
||||
.col(Conversation::DeletedAt)
|
||||
.col(Conversation::CreatedAt)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_conversation_external_agent")
|
||||
.table(Conversation::Table)
|
||||
.col(Conversation::ExternalId)
|
||||
.col(Conversation::AgentType)
|
||||
.unique()
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_foc_folder_position")
|
||||
.table(FolderOpenedConversation::Table)
|
||||
.col(FolderOpenedConversation::FolderId)
|
||||
.col(FolderOpenedConversation::Position)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_foc_folder_conversation")
|
||||
.table(FolderOpenedConversation::Table)
|
||||
.col(FolderOpenedConversation::FolderId)
|
||||
.col(FolderOpenedConversation::ConversationId)
|
||||
.unique()
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(
|
||||
Table::drop()
|
||||
.table(FolderOpenedConversation::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(Conversation::Table).to_owned())
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(Folder::Table).to_owned())
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(AppMetadata::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum AppMetadata {
|
||||
Table,
|
||||
Id,
|
||||
Key,
|
||||
Value,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
DeletedAt,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Folder {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
Path,
|
||||
GitBranch,
|
||||
DefaultAgentType,
|
||||
LastOpenedAt,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
DeletedAt,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Conversation {
|
||||
Table,
|
||||
Id,
|
||||
FolderId,
|
||||
Title,
|
||||
AgentType,
|
||||
Status,
|
||||
Model,
|
||||
GitBranch,
|
||||
ExternalId,
|
||||
ParentId,
|
||||
MessageCount,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
DeletedAt,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum FolderOpenedConversation {
|
||||
Table,
|
||||
Id,
|
||||
FolderId,
|
||||
ConversationId,
|
||||
Position,
|
||||
IsActive,
|
||||
IsPinned,
|
||||
AgentType,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(FolderCommand::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(FolderCommand::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(FolderCommand::FolderId).integer().not_null())
|
||||
.col(ColumnDef::new(FolderCommand::Name).string().not_null())
|
||||
.col(ColumnDef::new(FolderCommand::Command).string().not_null())
|
||||
.col(
|
||||
ColumnDef::new(FolderCommand::SortOrder)
|
||||
.integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(FolderCommand::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(FolderCommand::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_folder_command_folder")
|
||||
.from(FolderCommand::Table, FolderCommand::FolderId)
|
||||
.to(Folder::Table, Folder::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_folder_command_folder_id")
|
||||
.table(FolderCommand::Table)
|
||||
.col(FolderCommand::FolderId)
|
||||
.col(FolderCommand::SortOrder)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(FolderCommand::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum FolderCommand {
|
||||
Table,
|
||||
Id,
|
||||
FolderId,
|
||||
Name,
|
||||
Command,
|
||||
SortOrder,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Folder {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Folder::Table)
|
||||
.add_column(
|
||||
ColumnDef::new(Folder::IsOpen)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Folder::Table)
|
||||
.drop_column(Folder::IsOpen)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Folder {
|
||||
Table,
|
||||
IsOpen,
|
||||
}
|
||||
90
src-tauri/src/db/migration/m20260226_000001_agent_setting.rs
Normal file
90
src-tauri/src/db/migration/m20260226_000001_agent_setting.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(AgentSetting::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(AgentSetting::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(AgentSetting::AgentType)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(ColumnDef::new(AgentSetting::RegistryId).string().not_null())
|
||||
.col(
|
||||
ColumnDef::new(AgentSetting::Enabled)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(true),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(AgentSetting::SortOrder)
|
||||
.integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(AgentSetting::InstalledVersion)
|
||||
.string()
|
||||
.null(),
|
||||
)
|
||||
.col(ColumnDef::new(AgentSetting::EnvJson).text().null())
|
||||
.col(
|
||||
ColumnDef::new(AgentSetting::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(AgentSetting::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_agent_setting_sort_order")
|
||||
.table(AgentSetting::Table)
|
||||
.col(AgentSetting::SortOrder)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(AgentSetting::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum AgentSetting {
|
||||
Table,
|
||||
Id,
|
||||
AgentType,
|
||||
RegistryId,
|
||||
Enabled,
|
||||
SortOrder,
|
||||
InstalledVersion,
|
||||
EnvJson,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Folder::Table)
|
||||
.add_column(ColumnDef::new(Folder::ParentBranch).string().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Folder::Table)
|
||||
.drop_column(Folder::ParentBranch)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Folder {
|
||||
Table,
|
||||
ParentBranch,
|
||||
}
|
||||
21
src-tauri/src/db/migration/mod.rs
Normal file
21
src-tauri/src/db/migration/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20260211_000001_init;
|
||||
mod m20260219_000001_folder_command;
|
||||
mod m20260221_000001_folder_is_open;
|
||||
mod m20260226_000001_agent_setting;
|
||||
mod m20260227_000001_folder_parent_branch;
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![
|
||||
Box::new(m20260211_000001_init::Migration),
|
||||
Box::new(m20260219_000001_folder_command::Migration),
|
||||
Box::new(m20260221_000001_folder_is_open::Migration),
|
||||
Box::new(m20260226_000001_agent_setting::Migration),
|
||||
Box::new(m20260227_000001_folder_parent_branch::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
78
src-tauri/src/db/mod.rs
Normal file
78
src-tauri/src/db/mod.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
pub mod entities;
|
||||
pub mod error;
|
||||
pub mod migration;
|
||||
pub mod service;
|
||||
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use sea_orm::{
|
||||
ConnectOptions, ConnectionTrait, Database, DatabaseConnection, DbBackend, Statement,
|
||||
};
|
||||
use sea_orm_migration::MigratorTrait;
|
||||
|
||||
use error::DbError;
|
||||
use migration::Migrator;
|
||||
|
||||
pub struct AppDatabase {
|
||||
#[allow(dead_code)]
|
||||
pub conn: DatabaseConnection,
|
||||
}
|
||||
|
||||
pub async fn init_database(
|
||||
app_data_dir: impl AsRef<Path>,
|
||||
app_version: &str,
|
||||
) -> Result<AppDatabase, DbError> {
|
||||
let app_data_dir = app_data_dir.as_ref();
|
||||
std::fs::create_dir_all(app_data_dir)?;
|
||||
|
||||
let db_path = app_data_dir.join("codeg.db");
|
||||
let db_url = format!(
|
||||
"sqlite:{}?mode=rwc",
|
||||
urlencoding::encode(&db_path.to_string_lossy())
|
||||
);
|
||||
|
||||
let mut opts = ConnectOptions::new(db_url);
|
||||
opts.max_connections(5)
|
||||
.min_connections(1)
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.idle_timeout(Duration::from_secs(300))
|
||||
.sqlx_logging(false);
|
||||
|
||||
let conn = Database::connect(opts).await?;
|
||||
|
||||
// SQLite performance and reliability pragmas
|
||||
conn.execute(Statement::from_string(
|
||||
DbBackend::Sqlite,
|
||||
"PRAGMA journal_mode=WAL;".to_owned(),
|
||||
))
|
||||
.await?;
|
||||
conn.execute(Statement::from_string(
|
||||
DbBackend::Sqlite,
|
||||
"PRAGMA busy_timeout=5000;".to_owned(),
|
||||
))
|
||||
.await?;
|
||||
conn.execute(Statement::from_string(
|
||||
DbBackend::Sqlite,
|
||||
"PRAGMA synchronous=NORMAL;".to_owned(),
|
||||
))
|
||||
.await?;
|
||||
conn.execute(Statement::from_string(
|
||||
DbBackend::Sqlite,
|
||||
"PRAGMA foreign_keys=ON;".to_owned(),
|
||||
))
|
||||
.await?;
|
||||
conn.execute(Statement::from_string(
|
||||
DbBackend::Sqlite,
|
||||
"PRAGMA cache_size=-8000;".to_owned(),
|
||||
))
|
||||
.await?;
|
||||
|
||||
Migrator::up(&conn, None)
|
||||
.await
|
||||
.map_err(|e| DbError::Migration(e.to_string()))?;
|
||||
|
||||
service::app_metadata_service::update_app_version(&conn, app_version).await?;
|
||||
|
||||
Ok(AppDatabase { conn })
|
||||
}
|
||||
207
src-tauri/src/db/service/agent_setting_service.rs
Normal file
207
src-tauri/src/db/service/agent_setting_service.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::Utc;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, ConnectionTrait, DbBackend, EntityTrait,
|
||||
IntoActiveModel, QueryFilter, QueryOrder, Set, Statement,
|
||||
};
|
||||
|
||||
use crate::db::entities::agent_setting;
|
||||
use crate::db::error::DbError;
|
||||
use crate::models::agent::AgentType;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentDefaultInput {
|
||||
pub agent_type: AgentType,
|
||||
pub registry_id: String,
|
||||
pub default_sort_order: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentSettingsUpdate {
|
||||
pub enabled: bool,
|
||||
pub env_json: Option<String>,
|
||||
}
|
||||
|
||||
fn default_enabled(agent_type: AgentType) -> bool {
|
||||
matches!(
|
||||
agent_type,
|
||||
AgentType::ClaudeCode
|
||||
| AgentType::Codex
|
||||
| AgentType::Gemini
|
||||
| AgentType::OpenCode
|
||||
| AgentType::OpenClaw
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn ensure_defaults(
|
||||
conn: &DatabaseConnection,
|
||||
defaults: &[AgentDefaultInput],
|
||||
) -> Result<(), DbError> {
|
||||
for default in defaults {
|
||||
let agent_type = serde_json::to_string(&default.agent_type)
|
||||
.map_err(|e| DbError::Migration(format!("agent_type serialize failed: {e}")))?;
|
||||
let existing = agent_setting::Entity::find()
|
||||
.filter(agent_setting::Column::AgentType.eq(agent_type.clone()))
|
||||
.one(conn)
|
||||
.await?;
|
||||
|
||||
if let Some(model) = existing {
|
||||
if model.registry_id != default.registry_id {
|
||||
let mut active = model.into_active_model();
|
||||
active.registry_id = Set(default.registry_id.clone());
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.update(conn).await?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
let active = agent_setting::ActiveModel {
|
||||
id: NotSet,
|
||||
agent_type: Set(agent_type),
|
||||
registry_id: Set(default.registry_id.clone()),
|
||||
enabled: Set(default_enabled(default.agent_type)),
|
||||
sort_order: Set(default.default_sort_order),
|
||||
installed_version: Set(None),
|
||||
env_json: Set(None),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
};
|
||||
match active.insert(conn).await {
|
||||
Ok(_) => {}
|
||||
Err(e) if e.to_string().contains("UNIQUE constraint failed") => {
|
||||
// Another concurrent call already inserted this row — safe to ignore.
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list(conn: &DatabaseConnection) -> Result<Vec<agent_setting::Model>, DbError> {
|
||||
let rows = agent_setting::Entity::find()
|
||||
.order_by_asc(agent_setting::Column::SortOrder)
|
||||
.all(conn)
|
||||
.await?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub async fn list_map_by_agent_type(
|
||||
conn: &DatabaseConnection,
|
||||
) -> Result<HashMap<AgentType, agent_setting::Model>, DbError> {
|
||||
let rows = list(conn).await?;
|
||||
let mut map = HashMap::new();
|
||||
for row in rows {
|
||||
if let Ok(agent_type) = serde_json::from_str::<AgentType>(&row.agent_type) {
|
||||
map.insert(agent_type, row);
|
||||
}
|
||||
}
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
pub async fn get_by_agent_type(
|
||||
conn: &DatabaseConnection,
|
||||
agent_type: AgentType,
|
||||
) -> Result<Option<agent_setting::Model>, DbError> {
|
||||
let agent_type_str = serde_json::to_string(&agent_type)
|
||||
.map_err(|e| DbError::Migration(format!("agent_type serialize failed: {e}")))?;
|
||||
let model = agent_setting::Entity::find()
|
||||
.filter(agent_setting::Column::AgentType.eq(agent_type_str))
|
||||
.one(conn)
|
||||
.await?;
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
conn: &DatabaseConnection,
|
||||
agent_type: AgentType,
|
||||
patch: AgentSettingsUpdate,
|
||||
) -> Result<(), DbError> {
|
||||
let agent_type_str = serde_json::to_string(&agent_type)
|
||||
.map_err(|e| DbError::Migration(format!("agent_type serialize failed: {e}")))?;
|
||||
let model = agent_setting::Entity::find()
|
||||
.filter(agent_setting::Column::AgentType.eq(agent_type_str.clone()))
|
||||
.one(conn)
|
||||
.await?
|
||||
.ok_or_else(|| DbError::Migration(format!("agent setting not found: {agent_type_str}")))?;
|
||||
|
||||
let mut active = model.into_active_model();
|
||||
active.enabled = Set(patch.enabled);
|
||||
active.env_json = Set(patch.env_json);
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.update(conn).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_installed_version(
|
||||
conn: &DatabaseConnection,
|
||||
agent_type: AgentType,
|
||||
installed_version: Option<String>,
|
||||
) -> Result<(), DbError> {
|
||||
let agent_type_str = serde_json::to_string(&agent_type)
|
||||
.map_err(|e| DbError::Migration(format!("agent_type serialize failed: {e}")))?;
|
||||
if let Some(model) = agent_setting::Entity::find()
|
||||
.filter(agent_setting::Column::AgentType.eq(agent_type_str))
|
||||
.one(conn)
|
||||
.await?
|
||||
{
|
||||
let mut active = model.into_active_model();
|
||||
active.installed_version = Set(installed_version);
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.update(conn).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reorder(conn: &DatabaseConnection, agent_types: &[AgentType]) -> Result<(), DbError> {
|
||||
if agent_types.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match reorder_once(conn, agent_types).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if is_sqlite_full_error(&err) => {
|
||||
// Try truncating WAL once to reclaim space and retry.
|
||||
conn.execute(Statement::from_string(
|
||||
DbBackend::Sqlite,
|
||||
"PRAGMA wal_checkpoint(TRUNCATE);".to_owned(),
|
||||
))
|
||||
.await?;
|
||||
reorder_once(conn, agent_types).await
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn reorder_once(conn: &DatabaseConnection, agent_types: &[AgentType]) -> Result<(), DbError> {
|
||||
let now = Utc::now();
|
||||
for (index, agent_type) in agent_types.iter().enumerate() {
|
||||
let agent_type_str = serde_json::to_string(agent_type)
|
||||
.map_err(|e| DbError::Migration(format!("agent_type serialize failed: {e}")))?;
|
||||
|
||||
if let Some(model) = agent_setting::Entity::find()
|
||||
.filter(agent_setting::Column::AgentType.eq(agent_type_str))
|
||||
.one(conn)
|
||||
.await?
|
||||
{
|
||||
// Skip unchanged rows to reduce write pressure when repeatedly dragging.
|
||||
if model.sort_order == index as i32 {
|
||||
continue;
|
||||
}
|
||||
let mut active = model.into_active_model();
|
||||
active.sort_order = Set(index as i32);
|
||||
active.updated_at = Set(now.clone());
|
||||
active.update(conn).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_sqlite_full_error(err: &DbError) -> bool {
|
||||
let message = err.to_string();
|
||||
message.contains("database or disk is full") || message.contains("(code: 13)")
|
||||
}
|
||||
84
src-tauri/src/db/service/app_metadata_service.rs
Normal file
84
src-tauri/src/db/service/app_metadata_service.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use chrono::Utc;
|
||||
use sea_orm::sea_query::OnConflict;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use sea_orm::{ActiveValue::NotSet, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||
|
||||
use crate::db::entities::app_metadata;
|
||||
use crate::db::error::DbError;
|
||||
|
||||
pub async fn upsert_value(
|
||||
conn: &DatabaseConnection,
|
||||
key: &str,
|
||||
value: &str,
|
||||
) -> Result<(), DbError> {
|
||||
let now = Utc::now();
|
||||
|
||||
app_metadata::Entity::insert(app_metadata::ActiveModel {
|
||||
id: NotSet,
|
||||
key: Set(key.to_string()),
|
||||
value: Set(value.to_string()),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
deleted_at: NotSet,
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::column(app_metadata::Column::Key)
|
||||
.update_columns([app_metadata::Column::Value, app_metadata::Column::UpdatedAt])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_value(conn: &DatabaseConnection, key: &str) -> Result<Option<String>, DbError> {
|
||||
let model = app_metadata::Entity::find()
|
||||
.filter(app_metadata::Column::Key.eq(key))
|
||||
.filter(app_metadata::Column::DeletedAt.is_null())
|
||||
.one(conn)
|
||||
.await?;
|
||||
Ok(model.map(|m| m.value))
|
||||
}
|
||||
|
||||
pub async fn update_app_version(
|
||||
conn: &DatabaseConnection,
|
||||
app_version: &str,
|
||||
) -> Result<(), DbError> {
|
||||
let now = Utc::now();
|
||||
|
||||
app_metadata::Entity::insert(app_metadata::ActiveModel {
|
||||
id: NotSet,
|
||||
key: Set("app_version".to_string()),
|
||||
value: Set(app_version.to_string()),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
deleted_at: NotSet,
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::column(app_metadata::Column::Key)
|
||||
.update_columns([app_metadata::Column::Value, app_metadata::Column::UpdatedAt])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(conn)
|
||||
.await?;
|
||||
|
||||
app_metadata::Entity::insert(app_metadata::ActiveModel {
|
||||
id: NotSet,
|
||||
key: Set("db_initialized_at".to_string()),
|
||||
value: Set(now.to_rfc3339()),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
deleted_at: NotSet,
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::column(app_metadata::Column::Key)
|
||||
.do_nothing()
|
||||
.to_owned(),
|
||||
)
|
||||
.do_nothing()
|
||||
.exec(conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
187
src-tauri/src/db/service/conversation_service.rs
Normal file
187
src-tauri/src/db/service/conversation_service.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
use chrono::Utc;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, DatabaseConnection, EntityTrait,
|
||||
QueryFilter, QueryOrder, Set,
|
||||
};
|
||||
|
||||
use crate::db::entities::conversation;
|
||||
use crate::db::error::DbError;
|
||||
use crate::models::{AgentType, DbConversationSummary};
|
||||
|
||||
pub async fn create(
|
||||
conn: &DatabaseConnection,
|
||||
folder_id: i32,
|
||||
agent_type: AgentType,
|
||||
title: Option<String>,
|
||||
git_branch: Option<String>,
|
||||
) -> Result<conversation::Model, DbError> {
|
||||
let at_str = serde_json::to_value(agent_type)
|
||||
.ok()
|
||||
.and_then(|v| v.as_str().map(String::from))
|
||||
.unwrap_or_default();
|
||||
let now = Utc::now();
|
||||
let model = conversation::ActiveModel {
|
||||
id: NotSet,
|
||||
folder_id: Set(folder_id),
|
||||
title: Set(title),
|
||||
agent_type: Set(at_str),
|
||||
status: Set(conversation::ConversationStatus::InProgress),
|
||||
model: Set(None),
|
||||
git_branch: Set(git_branch),
|
||||
external_id: Set(None),
|
||||
parent_id: Set(None),
|
||||
message_count: Set(0),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
deleted_at: Set(None),
|
||||
};
|
||||
Ok(model.insert(conn).await?)
|
||||
}
|
||||
|
||||
pub async fn update_status(
|
||||
conn: &DatabaseConnection,
|
||||
conversation_id: i32,
|
||||
status: conversation::ConversationStatus,
|
||||
) -> Result<(), DbError> {
|
||||
let conv = conversation::Entity::find_by_id(conversation_id)
|
||||
.one(conn)
|
||||
.await?
|
||||
.ok_or_else(|| DbError::Migration(format!("Conversation not found: {conversation_id}")))?;
|
||||
let mut active: conversation::ActiveModel = conv.into();
|
||||
active.status = Set(status);
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.update(conn).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_title(
|
||||
conn: &DatabaseConnection,
|
||||
conversation_id: i32,
|
||||
title: String,
|
||||
) -> Result<(), DbError> {
|
||||
let conv = conversation::Entity::find_by_id(conversation_id)
|
||||
.one(conn)
|
||||
.await?
|
||||
.ok_or_else(|| DbError::Migration(format!("Conversation not found: {conversation_id}")))?;
|
||||
let mut active: conversation::ActiveModel = conv.into();
|
||||
active.title = Set(Some(title));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.update(conn).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_external_id(
|
||||
conn: &DatabaseConnection,
|
||||
conversation_id: i32,
|
||||
external_id: String,
|
||||
) -> Result<(), DbError> {
|
||||
let conv = conversation::Entity::find_by_id(conversation_id)
|
||||
.one(conn)
|
||||
.await?
|
||||
.ok_or_else(|| DbError::Migration(format!("Conversation not found: {conversation_id}")))?;
|
||||
let mut active: conversation::ActiveModel = conv.into();
|
||||
active.external_id = Set(Some(external_id));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.update(conn).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn soft_delete(conn: &DatabaseConnection, conversation_id: i32) -> Result<(), DbError> {
|
||||
let conv = conversation::Entity::find_by_id(conversation_id)
|
||||
.filter(conversation::Column::DeletedAt.is_null())
|
||||
.one(conn)
|
||||
.await?
|
||||
.ok_or_else(|| DbError::Migration(format!("Conversation not found: {conversation_id}")))?;
|
||||
let mut active: conversation::ActiveModel = conv.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
active.update(conn).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_agent_type(s: &str) -> AgentType {
|
||||
serde_json::from_value(serde_json::Value::String(s.to_string()))
|
||||
.unwrap_or(AgentType::ClaudeCode)
|
||||
}
|
||||
|
||||
fn conv_to_summary(r: conversation::Model) -> DbConversationSummary {
|
||||
let status = serde_json::to_value(&r.status)
|
||||
.ok()
|
||||
.and_then(|v| v.as_str().map(String::from))
|
||||
.unwrap_or_else(|| format!("{:?}", r.status));
|
||||
DbConversationSummary {
|
||||
id: r.id,
|
||||
folder_id: r.folder_id,
|
||||
title: r.title,
|
||||
agent_type: parse_agent_type(&r.agent_type),
|
||||
status,
|
||||
model: r.model,
|
||||
git_branch: r.git_branch,
|
||||
external_id: r.external_id,
|
||||
message_count: r.message_count as u32,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_by_id(
|
||||
conn: &DatabaseConnection,
|
||||
conversation_id: i32,
|
||||
) -> Result<DbConversationSummary, DbError> {
|
||||
let conv = conversation::Entity::find_by_id(conversation_id)
|
||||
.filter(conversation::Column::DeletedAt.is_null())
|
||||
.one(conn)
|
||||
.await?
|
||||
.ok_or_else(|| DbError::Migration(format!("Conversation not found: {conversation_id}")))?;
|
||||
|
||||
Ok(conv_to_summary(conv))
|
||||
}
|
||||
|
||||
pub async fn list_by_folder(
|
||||
conn: &DatabaseConnection,
|
||||
folder_id: i32,
|
||||
agent_type: Option<AgentType>,
|
||||
search: Option<String>,
|
||||
sort_by: Option<String>,
|
||||
status: Option<String>,
|
||||
) -> Result<Vec<DbConversationSummary>, DbError> {
|
||||
let mut query = conversation::Entity::find()
|
||||
.filter(conversation::Column::FolderId.eq(folder_id))
|
||||
.filter(conversation::Column::DeletedAt.is_null());
|
||||
|
||||
// Filter by agent_type
|
||||
if let Some(ref at) = agent_type {
|
||||
let at_str = serde_json::to_value(at)
|
||||
.ok()
|
||||
.and_then(|v| v.as_str().map(String::from))
|
||||
.unwrap_or_default();
|
||||
query = query.filter(conversation::Column::AgentType.eq(at_str));
|
||||
}
|
||||
|
||||
// Search by title
|
||||
if let Some(ref s) = search {
|
||||
if !s.is_empty() {
|
||||
query = query.filter(conversation::Column::Title.contains(s));
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if let Some(ref st) = status {
|
||||
if let Ok(status_enum) = serde_json::from_value::<conversation::ConversationStatus>(
|
||||
serde_json::Value::String(st.clone()),
|
||||
) {
|
||||
query = query.filter(conversation::Column::Status.eq(status_enum));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort
|
||||
query = match sort_by.as_deref() {
|
||||
Some("oldest") => query.order_by_asc(conversation::Column::CreatedAt),
|
||||
_ => query.order_by_desc(conversation::Column::CreatedAt),
|
||||
};
|
||||
|
||||
let rows = query.all(conn).await?;
|
||||
|
||||
let summaries: Vec<DbConversationSummary> = rows.into_iter().map(conv_to_summary).collect();
|
||||
|
||||
Ok(summaries)
|
||||
}
|
||||
171
src-tauri/src/db/service/folder_command_service.rs
Normal file
171
src-tauri/src/db/service/folder_command_service.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use chrono::Utc;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, ConnectionTrait, DbBackend, EntityTrait,
|
||||
IntoActiveModel, QueryFilter, QueryOrder, Set, Statement,
|
||||
};
|
||||
|
||||
use crate::db::entities::folder_command;
|
||||
use crate::db::error::DbError;
|
||||
use crate::models::FolderCommandInfo;
|
||||
|
||||
fn to_info(m: folder_command::Model) -> FolderCommandInfo {
|
||||
FolderCommandInfo {
|
||||
id: m.id,
|
||||
folder_id: m.folder_id,
|
||||
name: m.name,
|
||||
command: m.command,
|
||||
sort_order: m.sort_order,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_by_folder(
|
||||
conn: &DatabaseConnection,
|
||||
folder_id: i32,
|
||||
) -> Result<Vec<FolderCommandInfo>, DbError> {
|
||||
let rows = folder_command::Entity::find()
|
||||
.filter(folder_command::Column::FolderId.eq(folder_id))
|
||||
.order_by_asc(folder_command::Column::SortOrder)
|
||||
.all(conn)
|
||||
.await?;
|
||||
|
||||
Ok(rows.into_iter().map(to_info).collect())
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
conn: &DatabaseConnection,
|
||||
folder_id: i32,
|
||||
name: &str,
|
||||
command: &str,
|
||||
) -> Result<FolderCommandInfo, DbError> {
|
||||
let now = Utc::now();
|
||||
|
||||
// Get next sort_order
|
||||
let max_order = folder_command::Entity::find()
|
||||
.filter(folder_command::Column::FolderId.eq(folder_id))
|
||||
.order_by_desc(folder_command::Column::SortOrder)
|
||||
.one(conn)
|
||||
.await?
|
||||
.map(|m| m.sort_order)
|
||||
.unwrap_or(-1);
|
||||
|
||||
let active = folder_command::ActiveModel {
|
||||
id: NotSet,
|
||||
folder_id: Set(folder_id),
|
||||
name: Set(name.to_string()),
|
||||
command: Set(command.to_string()),
|
||||
sort_order: Set(max_order + 1),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
};
|
||||
|
||||
let model = active.insert(conn).await?;
|
||||
Ok(to_info(model))
|
||||
}
|
||||
|
||||
pub async fn create_many(
|
||||
conn: &DatabaseConnection,
|
||||
folder_id: i32,
|
||||
items: &[(String, String)],
|
||||
) -> Result<(), DbError> {
|
||||
if items.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
let max_order = folder_command::Entity::find()
|
||||
.filter(folder_command::Column::FolderId.eq(folder_id))
|
||||
.order_by_desc(folder_command::Column::SortOrder)
|
||||
.one(conn)
|
||||
.await?
|
||||
.map(|m| m.sort_order)
|
||||
.unwrap_or(-1);
|
||||
|
||||
let active_models = items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, (name, command))| folder_command::ActiveModel {
|
||||
id: NotSet,
|
||||
folder_id: Set(folder_id),
|
||||
name: Set(name.clone()),
|
||||
command: Set(command.clone()),
|
||||
sort_order: Set(max_order + idx as i32 + 1),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
folder_command::Entity::insert_many(active_models)
|
||||
.exec(conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
conn: &DatabaseConnection,
|
||||
id: i32,
|
||||
name: Option<String>,
|
||||
command: Option<String>,
|
||||
sort_order: Option<i32>,
|
||||
) -> Result<FolderCommandInfo, DbError> {
|
||||
let row = folder_command::Entity::find_by_id(id)
|
||||
.one(conn)
|
||||
.await?
|
||||
.ok_or_else(|| DbError::Migration(format!("FolderCommand {} not found", id)))?;
|
||||
|
||||
let mut active = row.into_active_model();
|
||||
if let Some(n) = name {
|
||||
active.name = Set(n);
|
||||
}
|
||||
if let Some(c) = command {
|
||||
active.command = Set(c);
|
||||
}
|
||||
if let Some(s) = sort_order {
|
||||
active.sort_order = Set(s);
|
||||
}
|
||||
active.updated_at = Set(Utc::now());
|
||||
|
||||
let model = active.update(conn).await?;
|
||||
Ok(to_info(model))
|
||||
}
|
||||
|
||||
pub async fn delete(conn: &DatabaseConnection, id: i32) -> Result<(), DbError> {
|
||||
folder_command::Entity::delete_by_id(id).exec(conn).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reorder(
|
||||
conn: &DatabaseConnection,
|
||||
folder_id: i32,
|
||||
ids: Vec<i32>,
|
||||
) -> Result<(), DbError> {
|
||||
if ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
let now_str = now.format("%Y-%m-%d %H:%M:%S %:z").to_string();
|
||||
let case_expr = ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, id)| format!("WHEN {} THEN {}", id, idx))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let id_list = ids
|
||||
.iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let sql = format!(
|
||||
"UPDATE folder_command SET sort_order = CASE id {case_expr} END, updated_at = '{now_str}' WHERE folder_id = {folder_id} AND id IN ({id_list})"
|
||||
);
|
||||
conn.execute(Statement::from_string(DbBackend::Sqlite, sql))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
246
src-tauri/src/db/service/folder_service.rs
Normal file
246
src-tauri/src/db/service/folder_service.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
use chrono::Utc;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, ConnectionTrait, DbBackend, EntityTrait,
|
||||
IntoActiveModel, QueryFilter, QueryOrder, Set, Statement,
|
||||
};
|
||||
|
||||
use crate::db::entities::{folder, folder_opened_conversation};
|
||||
use crate::db::error::DbError;
|
||||
use crate::models::agent::AgentType;
|
||||
use crate::models::{FolderDetail, FolderHistoryEntry, OpenedConversation};
|
||||
|
||||
fn to_entry(m: folder::Model) -> FolderHistoryEntry {
|
||||
FolderHistoryEntry {
|
||||
id: m.id,
|
||||
path: m.path,
|
||||
name: m.name,
|
||||
last_opened_at: m.last_opened_at,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_agent_type(s: &Option<String>) -> Option<AgentType> {
|
||||
s.as_deref()
|
||||
.and_then(|v| serde_json::from_value(serde_json::Value::String(v.to_string())).ok())
|
||||
}
|
||||
|
||||
fn to_detail(m: folder::Model, opened: Vec<OpenedConversation>) -> FolderDetail {
|
||||
let default_agent_type = parse_agent_type(&m.default_agent_type);
|
||||
FolderDetail {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
path: m.path,
|
||||
git_branch: m.git_branch,
|
||||
parent_branch: m.parent_branch,
|
||||
default_agent_type,
|
||||
last_opened_at: m.last_opened_at,
|
||||
opened_conversations: opened,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_folder_by_id(
|
||||
conn: &DatabaseConnection,
|
||||
folder_id: i32,
|
||||
) -> Result<Option<FolderDetail>, DbError> {
|
||||
let row = folder::Entity::find_by_id(folder_id)
|
||||
.filter(folder::Column::DeletedAt.is_null())
|
||||
.one(conn)
|
||||
.await?;
|
||||
|
||||
match row {
|
||||
None => Ok(None),
|
||||
Some(folder_model) => {
|
||||
let opened = load_opened_conversations(conn, folder_model.id).await?;
|
||||
Ok(Some(to_detail(folder_model, opened)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_folder(
|
||||
conn: &DatabaseConnection,
|
||||
path: &str,
|
||||
) -> Result<FolderHistoryEntry, DbError> {
|
||||
let now = Utc::now();
|
||||
let name = std::path::Path::new(path)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| path.to_string());
|
||||
|
||||
let existing = folder::Entity::find()
|
||||
.filter(folder::Column::Path.eq(path))
|
||||
.one(conn)
|
||||
.await?;
|
||||
|
||||
let model = if let Some(row) = existing {
|
||||
let mut active = row.into_active_model();
|
||||
active.name = Set(name);
|
||||
active.last_opened_at = Set(now);
|
||||
active.updated_at = Set(now);
|
||||
active.deleted_at = Set(None);
|
||||
active.is_open = Set(true);
|
||||
active.update(conn).await?
|
||||
} else {
|
||||
let active = folder::ActiveModel {
|
||||
id: NotSet,
|
||||
name: Set(name),
|
||||
path: Set(path.to_string()),
|
||||
git_branch: Set(None),
|
||||
parent_branch: Set(None),
|
||||
default_agent_type: Set(None),
|
||||
last_opened_at: Set(now),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
deleted_at: Set(None),
|
||||
is_open: Set(true),
|
||||
};
|
||||
active.insert(conn).await?
|
||||
};
|
||||
|
||||
Ok(to_entry(model))
|
||||
}
|
||||
|
||||
pub async fn list_folders(conn: &DatabaseConnection) -> Result<Vec<FolderHistoryEntry>, DbError> {
|
||||
let rows = folder::Entity::find()
|
||||
.filter(folder::Column::DeletedAt.is_null())
|
||||
.order_by_desc(folder::Column::LastOpenedAt)
|
||||
.all(conn)
|
||||
.await?;
|
||||
|
||||
Ok(rows.into_iter().map(to_entry).collect())
|
||||
}
|
||||
|
||||
pub async fn remove_folder(conn: &DatabaseConnection, path: &str) -> Result<(), DbError> {
|
||||
let now = Utc::now();
|
||||
let row = folder::Entity::find()
|
||||
.filter(folder::Column::Path.eq(path))
|
||||
.filter(folder::Column::DeletedAt.is_null())
|
||||
.one(conn)
|
||||
.await?;
|
||||
|
||||
if let Some(row) = row {
|
||||
let mut active = row.into_active_model();
|
||||
active.deleted_at = Set(Some(now));
|
||||
active.updated_at = Set(now);
|
||||
active.update(conn).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save_opened_conversations(
|
||||
conn: &DatabaseConnection,
|
||||
folder_id: i32,
|
||||
items: Vec<OpenedConversation>,
|
||||
) -> Result<(), DbError> {
|
||||
// Delete all existing opened conversations for this folder
|
||||
folder_opened_conversation::Entity::delete_many()
|
||||
.filter(folder_opened_conversation::Column::FolderId.eq(folder_id))
|
||||
.exec(conn)
|
||||
.await?;
|
||||
|
||||
if items.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Batch insert with raw SQL for efficiency
|
||||
let now = Utc::now();
|
||||
let now_str = now.format("%Y-%m-%d %H:%M:%S %:z").to_string();
|
||||
|
||||
let mut values = Vec::with_capacity(items.len());
|
||||
for item in &items {
|
||||
let agent_str = serde_json::to_value(item.agent_type)
|
||||
.ok()
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string()))
|
||||
.unwrap_or_default();
|
||||
values.push(format!(
|
||||
"({}, {}, {}, {}, {}, '{}', '{}', '{}')",
|
||||
folder_id,
|
||||
item.conversation_id,
|
||||
item.position,
|
||||
item.is_active as i32,
|
||||
item.is_pinned as i32,
|
||||
agent_str,
|
||||
now_str,
|
||||
now_str,
|
||||
));
|
||||
}
|
||||
|
||||
let sql = format!(
|
||||
"INSERT INTO folder_opened_conversation (folder_id, conversation_id, position, is_active, is_pinned, agent_type, created_at, updated_at) VALUES {}",
|
||||
values.join(", ")
|
||||
);
|
||||
|
||||
conn.execute(Statement::from_string(DbBackend::Sqlite, sql))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_opened_conversations(
|
||||
conn: &DatabaseConnection,
|
||||
folder_id: i32,
|
||||
) -> Result<Vec<OpenedConversation>, DbError> {
|
||||
let rows = folder_opened_conversation::Entity::find()
|
||||
.filter(folder_opened_conversation::Column::FolderId.eq(folder_id))
|
||||
.order_by_asc(folder_opened_conversation::Column::Position)
|
||||
.all(conn)
|
||||
.await?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.filter_map(|r| {
|
||||
let agent_type = parse_agent_type(&Some(r.agent_type))?;
|
||||
Some(OpenedConversation {
|
||||
conversation_id: r.conversation_id,
|
||||
agent_type,
|
||||
position: r.position,
|
||||
is_active: r.is_active,
|
||||
is_pinned: r.is_pinned,
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn set_folder_parent_branch(
|
||||
conn: &DatabaseConnection,
|
||||
folder_id: i32,
|
||||
parent_branch: Option<String>,
|
||||
) -> Result<(), DbError> {
|
||||
let row = folder::Entity::find_by_id(folder_id).one(conn).await?;
|
||||
|
||||
if let Some(row) = row {
|
||||
let mut active = row.into_active_model();
|
||||
active.parent_branch = Set(parent_branch);
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.update(conn).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_folder_open(
|
||||
conn: &DatabaseConnection,
|
||||
folder_id: i32,
|
||||
is_open: bool,
|
||||
) -> Result<(), DbError> {
|
||||
let row = folder::Entity::find_by_id(folder_id).one(conn).await?;
|
||||
|
||||
if let Some(row) = row {
|
||||
let mut active = row.into_active_model();
|
||||
active.is_open = Set(is_open);
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.update(conn).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_open_folders(
|
||||
conn: &DatabaseConnection,
|
||||
) -> Result<Vec<FolderHistoryEntry>, DbError> {
|
||||
let rows = folder::Entity::find()
|
||||
.filter(folder::Column::DeletedAt.is_null())
|
||||
.filter(folder::Column::IsOpen.eq(true))
|
||||
.order_by_desc(folder::Column::LastOpenedAt)
|
||||
.all(conn)
|
||||
.await?;
|
||||
|
||||
Ok(rows.into_iter().map(to_entry).collect())
|
||||
}
|
||||
99
src-tauri/src/db/service/import_service.rs
Normal file
99
src-tauri/src/db/service/import_service.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use chrono::Utc;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, DatabaseConnection, EntityTrait,
|
||||
QueryFilter, Set,
|
||||
};
|
||||
|
||||
use crate::db::entities::conversation;
|
||||
use crate::db::error::DbError;
|
||||
use crate::models::{AgentType, ImportResult};
|
||||
use crate::parsers::claude::ClaudeParser;
|
||||
use crate::parsers::codex::CodexParser;
|
||||
use crate::parsers::gemini::GeminiParser;
|
||||
use crate::parsers::opencode::OpenCodeParser;
|
||||
use crate::parsers::{path_eq_for_matching, AgentParser};
|
||||
|
||||
pub async fn import_local_conversations(
|
||||
conn: &DatabaseConnection,
|
||||
folder_id: i32,
|
||||
folder_path: &str,
|
||||
) -> Result<ImportResult, DbError> {
|
||||
let path = folder_path.to_string();
|
||||
|
||||
// Run parsers in blocking task since they do filesystem I/O
|
||||
let summaries = tokio::task::spawn_blocking(move || {
|
||||
let parsers: Vec<(AgentType, Box<dyn AgentParser>)> = vec![
|
||||
(AgentType::ClaudeCode, Box::new(ClaudeParser::new())),
|
||||
(AgentType::Codex, Box::new(CodexParser::new())),
|
||||
(AgentType::OpenCode, Box::new(OpenCodeParser::new())),
|
||||
(AgentType::Gemini, Box::new(GeminiParser::new())),
|
||||
];
|
||||
|
||||
let mut matched = Vec::new();
|
||||
for (at, parser) in &parsers {
|
||||
match parser.list_conversations() {
|
||||
Ok(convs) => {
|
||||
for c in convs {
|
||||
if c.folder_path
|
||||
.as_deref()
|
||||
.map(|p| path_eq_for_matching(p, path.as_str()))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
matched.push((*at, c));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error listing {} conversations: {}", at, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
matched
|
||||
})
|
||||
.await
|
||||
.map_err(|e| DbError::Migration(e.to_string()))?;
|
||||
|
||||
let mut imported = 0u32;
|
||||
let mut skipped = 0u32;
|
||||
|
||||
for (agent_type, summary) in &summaries {
|
||||
let at_str = serde_json::to_value(agent_type)
|
||||
.ok()
|
||||
.and_then(|v| v.as_str().map(String::from))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Check if already imported
|
||||
let exists = conversation::Entity::find()
|
||||
.filter(conversation::Column::ExternalId.eq(&summary.id))
|
||||
.filter(conversation::Column::AgentType.eq(&at_str))
|
||||
.one(conn)
|
||||
.await?;
|
||||
|
||||
if exists.is_some() {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
let conv = conversation::ActiveModel {
|
||||
id: NotSet,
|
||||
folder_id: Set(folder_id),
|
||||
title: Set(summary.title.clone()),
|
||||
agent_type: Set(at_str.clone()),
|
||||
status: Set(conversation::ConversationStatus::Completed),
|
||||
model: Set(summary.model.clone()),
|
||||
git_branch: Set(summary.git_branch.clone()),
|
||||
external_id: Set(Some(summary.id.clone())),
|
||||
parent_id: Set(None),
|
||||
message_count: Set(summary.message_count as i32),
|
||||
created_at: Set(summary.started_at),
|
||||
updated_at: Set(now),
|
||||
deleted_at: Set(None),
|
||||
};
|
||||
conv.insert(conn).await?;
|
||||
|
||||
imported += 1;
|
||||
}
|
||||
|
||||
Ok(ImportResult { imported, skipped })
|
||||
}
|
||||
6
src-tauri/src/db/service/mod.rs
Normal file
6
src-tauri/src/db/service/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod agent_setting_service;
|
||||
pub mod app_metadata_service;
|
||||
pub mod conversation_service;
|
||||
pub mod folder_command_service;
|
||||
pub mod folder_service;
|
||||
pub mod import_service;
|
||||
Reference in New Issue
Block a user